diff --git a/.github/workflows/netflify.yaml b/.github/workflows/netlify.yaml similarity index 100% rename from .github/workflows/netflify.yaml rename to .github/workflows/netlify.yaml diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d65a524d1..6f71c1414c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,76 @@ -Changes in [3.27.0](https://github.com/vector-im/element-desktop/releases/tag/v3.27.0) (2021-07-02) +Changes in [3.28.1](https://github.com/vector-im/element-desktop/releases/tag/v3.28.1) (2021-08-17) +=================================================================================================== + +## 🐛 Bug Fixes + * Fix multiple VoIP regressions ([matrix-org/matrix-js-sdk#1860](https://github.com/matrix-org/matrix-js-sdk/pull/1860)). + +Changes in [3.28.0](https://github.com/vector-im/element-desktop/releases/tag/v3.28.0) (2021-08-16) +=================================================================================================== + +## ✨ Features + * Show how long a call was on call tiles ([\#6570](https://github.com/matrix-org/matrix-react-sdk/pull/6570)). Fixes vector-im/element-web#18405. Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Add regional indicators to emoji picker ([\#6490](https://github.com/matrix-org/matrix-react-sdk/pull/6490)). Fixes vector-im/element-web#14963. Contributed by [robintown](https://github.com/robintown). + * Make call control buttons accessible to screen reader users ([\#6181](https://github.com/matrix-org/matrix-react-sdk/pull/6181)). Fixes vector-im/element-web#18358. Contributed by [pvagner](https://github.com/pvagner). + * Skip sending a thumbnail if it is not a sufficient saving over the original ([\#6559](https://github.com/matrix-org/matrix-react-sdk/pull/6559)). Fixes vector-im/element-web#17906. + * Increase PiP snapping speed ([\#6539](https://github.com/matrix-org/matrix-react-sdk/pull/6539)). Fixes vector-im/element-web#18371. Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Improve and move the incoming call toast ([\#6470](https://github.com/matrix-org/matrix-react-sdk/pull/6470)). Fixes vector-im/element-web#17912. Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Allow all of the URL schemes that Firefox allows ([\#6457](https://github.com/matrix-org/matrix-react-sdk/pull/6457)). Contributed by [aaronraimist](https://github.com/aaronraimist). + * Improve bubble layout colors ([\#6452](https://github.com/matrix-org/matrix-react-sdk/pull/6452)). Fixes vector-im/element-web#18081. Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Spaces let users switch between Home and All Rooms behaviours ([\#6497](https://github.com/matrix-org/matrix-react-sdk/pull/6497)). Fixes vector-im/element-web#18093. + * Support for MSC2285 (hidden read receipts) ([\#6390](https://github.com/matrix-org/matrix-react-sdk/pull/6390)). Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Group pinned message events with MELS ([\#6349](https://github.com/matrix-org/matrix-react-sdk/pull/6349)). Fixes vector-im/element-web#17938. Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Make version copiable ([\#6227](https://github.com/matrix-org/matrix-react-sdk/pull/6227)). Fixes vector-im/element-web#17603 and vector-im/element-web#18329. Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Improve voice messages uploading state ([\#6530](https://github.com/matrix-org/matrix-react-sdk/pull/6530)). Fixes vector-im/element-web#18226 and vector-im/element-web#18224. + * Add surround with feature ([\#5510](https://github.com/matrix-org/matrix-react-sdk/pull/5510)). Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Improve call event tile wording ([\#6545](https://github.com/matrix-org/matrix-react-sdk/pull/6545)). Fixes vector-im/element-web#18376. Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Show an avatar/a turned off microphone icon for muted users ([\#6486](https://github.com/matrix-org/matrix-react-sdk/pull/6486)). Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Prompt user to leave rooms/subspaces in a space when leaving space ([\#6424](https://github.com/matrix-org/matrix-react-sdk/pull/6424)). Fixes vector-im/element-web#18071. + * Add customisation point to override widget variables ([\#6455](https://github.com/matrix-org/matrix-react-sdk/pull/6455)). Fixes vector-im/element-web#18035. + * Add support for screen sharing in 1:1 calls ([\#5992](https://github.com/matrix-org/matrix-react-sdk/pull/5992)). Contributed by [SimonBrandner](https://github.com/SimonBrandner). + +## 🐛 Bug Fixes + * [Release] Fix glare related regressions ([\#6622](https://github.com/matrix-org/matrix-react-sdk/pull/6622)). Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * [Release] Fix PiP of held calls ([\#6612](https://github.com/matrix-org/matrix-react-sdk/pull/6612)). Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * [Release] Fix toast colors ([\#6607](https://github.com/matrix-org/matrix-react-sdk/pull/6607)). Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Fix [object Object] in Widget Permissions ([\#6560](https://github.com/matrix-org/matrix-react-sdk/pull/6560)). Fixes vector-im/element-web#18384. Contributed by [Palid](https://github.com/Palid). + * Fix right margin for events on IRC layout ([\#6542](https://github.com/matrix-org/matrix-react-sdk/pull/6542)). Fixes vector-im/element-web#18354. + * Mirror only usermedia feeds ([\#6512](https://github.com/matrix-org/matrix-react-sdk/pull/6512)). Fixes vector-im/element-web#5633. Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Fix LogoutDialog warning + TypeScript migration ([\#6533](https://github.com/matrix-org/matrix-react-sdk/pull/6533)). + * Fix the wrong font being used in the room topic field ([\#6527](https://github.com/matrix-org/matrix-react-sdk/pull/6527)). Fixes vector-im/element-web#18339. Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Fix inconsistent styling for links on hover ([\#6513](https://github.com/matrix-org/matrix-react-sdk/pull/6513)). Contributed by [janogarcia](https://github.com/janogarcia). + * Fix incorrect height for encoded placeholder images ([\#6514](https://github.com/matrix-org/matrix-react-sdk/pull/6514)). Contributed by [Palid](https://github.com/Palid). + * Fix call events layout for message bubble ([\#6465](https://github.com/matrix-org/matrix-react-sdk/pull/6465)). Fixes vector-im/element-web#18144. + * Improve subspaces and some utilities around room/space creation ([\#6458](https://github.com/matrix-org/matrix-react-sdk/pull/6458)). Fixes vector-im/element-web#18090 vector-im/element-web#18091 and vector-im/element-web#17256. + * Restore pointer cursor for SenderProfile in message bubbles ([\#6501](https://github.com/matrix-org/matrix-react-sdk/pull/6501)). Fixes vector-im/element-web#18249. + * Fix issues with the Call View ([\#6472](https://github.com/matrix-org/matrix-react-sdk/pull/6472)). Fixes vector-im/element-web#18221. Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Align event list summary read receipts when using message bubbles ([\#6500](https://github.com/matrix-org/matrix-react-sdk/pull/6500)). Fixes vector-im/element-web#18143. + * Better positioning for unbubbled events in timeline ([\#6477](https://github.com/matrix-org/matrix-react-sdk/pull/6477)). Fixes vector-im/element-web#18132. + * Realign reactions row with messages in modern layout ([\#6491](https://github.com/matrix-org/matrix-react-sdk/pull/6491)). Fixes vector-im/element-web#18118. Contributed by [robintown](https://github.com/robintown). + * Fix CreateRoomDialog exploding when making public room outside of a space ([\#6492](https://github.com/matrix-org/matrix-react-sdk/pull/6492)). Fixes vector-im/element-web#18275. + * Fix call crashing because `element` was undefined ([\#6488](https://github.com/matrix-org/matrix-react-sdk/pull/6488)). Fixes vector-im/element-web#18270. Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Upscale thumbnails to the container size ([\#6589](https://github.com/matrix-org/matrix-react-sdk/pull/6589)). Fixes vector-im/element-web#18307. + * Fix create room dialog in spaces no longer adding to the space ([\#6587](https://github.com/matrix-org/matrix-react-sdk/pull/6587)). Fixes vector-im/element-web#18465. + * Don't show a modal on call reject/user hangup ([\#6580](https://github.com/matrix-org/matrix-react-sdk/pull/6580)). Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Fade Call View Buttons after `componentDidMount` ([\#6581](https://github.com/matrix-org/matrix-react-sdk/pull/6581)). Fixes vector-im/element-web#18439. Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Fix missing expand button on codeblocks ([\#6565](https://github.com/matrix-org/matrix-react-sdk/pull/6565)). Fixes vector-im/element-web#18388. Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * allow customizing the bubble layout colors ([\#6568](https://github.com/matrix-org/matrix-react-sdk/pull/6568)). Fixes vector-im/element-web#18408. Contributed by [benneti](https://github.com/benneti). + * Don't flash "Missed call" when accepting a call ([\#6567](https://github.com/matrix-org/matrix-react-sdk/pull/6567)). Fixes vector-im/element-web#18404. Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Fix clicking whitespaces on replies ([\#6571](https://github.com/matrix-org/matrix-react-sdk/pull/6571)). Fixes vector-im/element-web#18327. Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Fix disabled state for voice messages + send button tooltip ([\#6562](https://github.com/matrix-org/matrix-react-sdk/pull/6562)). Fixes vector-im/element-web#18413. + * Fix voice feed being cut-off ([\#6550](https://github.com/matrix-org/matrix-react-sdk/pull/6550)). Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Fix sizing issues of the screen picker ([\#6498](https://github.com/matrix-org/matrix-react-sdk/pull/6498)). Fixes vector-im/element-web#18281. Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Stop voice messages that are playing when starting a recording ([\#6563](https://github.com/matrix-org/matrix-react-sdk/pull/6563)). Fixes vector-im/element-web#18410. + * Properly set style attribute on shared usercontent iframe ([\#6561](https://github.com/matrix-org/matrix-react-sdk/pull/6561)). Fixes vector-im/element-web#18414. + * Null guard space inviter to prevent the app exploding ([\#6558](https://github.com/matrix-org/matrix-react-sdk/pull/6558)). + * Make the ringing sound mutable/disablable ([\#6534](https://github.com/matrix-org/matrix-react-sdk/pull/6534)). Fixes vector-im/element-web#15591. Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Fix wrong cursor being used in PiP ([\#6551](https://github.com/matrix-org/matrix-react-sdk/pull/6551)). Fixes vector-im/element-web#18383. Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Re-pin Jitsi if the widget already exists ([\#6226](https://github.com/matrix-org/matrix-react-sdk/pull/6226)). Fixes vector-im/element-web#17679. Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Fix broken call notification regression ([\#6526](https://github.com/matrix-org/matrix-react-sdk/pull/6526)). Fixes vector-im/element-web#18335. Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * createRoom, only send join rule event if we have a join rule to put in it ([\#6516](https://github.com/matrix-org/matrix-react-sdk/pull/6516)). Fixes vector-im/element-web#18301. + * Fix clicking pills inside replies ([\#6508](https://github.com/matrix-org/matrix-react-sdk/pull/6508)). Fixes vector-im/element-web#18283. Contributed by [SimonBrandner](https://github.com/SimonBrandner). + * Fix grecaptcha regression ([\#6503](https://github.com/matrix-org/matrix-react-sdk/pull/6503)). Fixes vector-im/element-web#18284. Contributed by [Palid](https://github.com/Palid). + +Changes in [3.27.0](https://github.com/vector-im/element-desktop/releases/tag/v3.27.0) (2021-08-02) =================================================================================================== ## 🔒 SECURITY FIXES diff --git a/package.json b/package.json index 2445e3c973..e0883f5556 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "3.27.0", + "version": "3.28.1", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { @@ -55,6 +55,8 @@ }, "dependencies": { "@babel/runtime": "^7.12.5", + "@sentry/browser": "^6.11.0", + "@sentry/tracing": "^6.11.0", "await-lock": "^2.1.0", "blurhash": "^1.1.3", "browser-encrypt-attachment": "^0.3.0", @@ -80,7 +82,7 @@ "katex": "^0.12.0", "linkifyjs": "^2.1.9", "lodash": "^4.17.20", - "matrix-js-sdk": "12.2.0", + "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", "matrix-widget-api": "^0.1.0-beta.15", "minimist": "^1.2.5", "opus-recorder": "^8.0.3", @@ -149,7 +151,7 @@ "@typescript-eslint/eslint-plugin": "^4.17.0", "@typescript-eslint/parser": "^4.17.0", "@wojtekmaj/enzyme-adapter-react-17": "^0.6.1", - "allchange": "github:matrix-org/allchange", + "allchange": "^1.0.0", "babel-jest": "^26.6.3", "chokidar": "^3.5.1", "concurrently": "^5.3.0", diff --git a/res/css/_common.scss b/res/css/_common.scss index 6c82fdbbdb..5fcc10add0 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -168,7 +168,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { // it has the appearance of a text box so the controls // appear to be part of the input -.mx_Dialog, .mx_MatrixChat { +.mx_Dialog, .mx_MatrixChat_wrapper { .mx_textinput > input[type=text], .mx_textinput > input[type=search] { border: none; @@ -381,6 +381,11 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { font-size: $font-14px; color: $primary-content; word-wrap: break-word; + + a { + color: $accent-color; + cursor: pointer; + } } .mx_Dialog_buttons { diff --git a/res/css/_components.scss b/res/css/_components.scss index 035caec36a..566b84a7c8 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -17,6 +17,7 @@ @import "./structures/_LeftPanelWidget.scss"; @import "./structures/_MainSplit.scss"; @import "./structures/_MatrixChat.scss"; +@import "./structures/_BackdropPanel.scss"; @import "./structures/_MyGroups.scss"; @import "./structures/_NonUrgentToastContainer.scss"; @import "./structures/_NotificationPanel.scss"; @@ -27,8 +28,8 @@ @import "./structures/_RoomView.scss"; @import "./structures/_ScrollPanel.scss"; @import "./structures/_SearchBox.scss"; +@import "./structures/_SpaceHierarchy.scss"; @import "./structures/_SpacePanel.scss"; -@import "./structures/_SpaceRoomDirectory.scss"; @import "./structures/_SpaceRoomView.scss"; @import "./structures/_TabbedView.scss"; @import "./structures/_ToastContainer.scss"; diff --git a/res/css/structures/_BackdropPanel.scss b/res/css/structures/_BackdropPanel.scss new file mode 100644 index 0000000000..6c41ed7a09 --- /dev/null +++ b/res/css/structures/_BackdropPanel.scss @@ -0,0 +1,35 @@ +/* +Copyright 2021 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. +*/ + +.mx_BackdropPanel { + position: absolute; + left: 0; + top: 0; + height: 100vh; + width: 100%; + overflow: hidden; + filter: blur(var(--lp-background-blur)); +} + +.mx_BackdropPanel--image { + position: absolute; + top: 0; + left: 0; + min-height: 100%; + z-index: 0; + pointer-events: none; + overflow: hidden; +} diff --git a/res/css/structures/_GroupFilterPanel.scss b/res/css/structures/_GroupFilterPanel.scss index 6923ebeb5e..cc0e760031 100644 --- a/res/css/structures/_GroupFilterPanel.scss +++ b/res/css/structures/_GroupFilterPanel.scss @@ -14,10 +14,27 @@ See the License for the specific language governing permissions and limitations under the License. */ +$groupFilterPanelWidth: 56px; // only applies in this file, used for calculations + +.mx_GroupFilterPanelContainer { + flex-grow: 0; + flex-shrink: 0; + width: $groupFilterPanelWidth; + height: 100%; + + // Create another flexbox so the GroupFilterPanel fills the container + display: flex; + flex-direction: column; + + // GroupFilterPanel handles its own CSS +} + .mx_GroupFilterPanel { - flex: 1; + z-index: 1; background-color: $groupFilterPanel-bg-color; + flex: 1; cursor: pointer; + position: relative; display: flex; flex-direction: column; diff --git a/res/css/structures/_LeftPanel.scss b/res/css/structures/_LeftPanel.scss index 738b77684f..c376ee74f8 100644 --- a/res/css/structures/_LeftPanel.scss +++ b/res/css/structures/_LeftPanel.scss @@ -14,31 +14,38 @@ See the License for the specific language governing permissions and limitations under the License. */ -$groupFilterPanelWidth: 56px; // only applies in this file, used for calculations $roomListCollapsedWidth: 68px; +.mx_MatrixChat--with-avatar { + .mx_LeftPanel, + .mx_LeftPanel .mx_LeftPanel_roomListContainer { + background-color: transparent; + } +} + +.mx_LeftPanel_wrapper { + display: flex; + max-width: 50%; + + .mx_LeftPanel_wrapper--user { + background-color: $roomlist-bg-color; + display: flex; + overflow: hidden; + position: relative; + } +} + + + .mx_LeftPanel { background-color: $roomlist-bg-color; // 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 display: flex; contain: content; - - .mx_LeftPanel_GroupFilterPanelContainer { - flex-grow: 0; - flex-shrink: 0; - flex-basis: $groupFilterPanelWidth; - height: 100%; - - // Create another flexbox so the GroupFilterPanel fills the container - display: flex; - flex-direction: column; - - // GroupFilterPanel handles its own CSS - } + position: relative; // Note: The 'room list' in this context is actually everything that isn't the tag // panel, such as the menu options, breadcrumbs, filtering, etc diff --git a/res/css/structures/_MatrixChat.scss b/res/css/structures/_MatrixChat.scss index 63efa92ebc..fdf5cb1a03 100644 --- a/res/css/structures/_MatrixChat.scss +++ b/res/css/structures/_MatrixChat.scss @@ -29,8 +29,6 @@ limitations under the License. .mx_MatrixChat_wrapper { display: flex; - flex-direction: column; - width: 100%; height: 100%; } @@ -42,13 +40,12 @@ limitations under the License. } .mx_MatrixChat { + position: relative; width: 100%; height: 100%; display: flex; - order: 2; - flex: 1; min-height: 0; } @@ -66,7 +63,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_SpacePanel):not(.mx_ResizeHandle) { +.mx_MatrixChat > :not(.mx_LeftPanel):not(.mx_SpacePanel):not(.mx_ResizeHandle):not(.mx_LeftPanel_wrapper) { background-color: $background; flex: 1 1 0; diff --git a/res/css/structures/_RoomView.scss b/res/css/structures/_RoomView.scss index cc61b65d96..5f18838ead 100644 --- a/res/css/structures/_RoomView.scss +++ b/res/css/structures/_RoomView.scss @@ -14,10 +14,20 @@ See the License for the specific language governing permissions and limitations under the License. */ +.mx_RoomView_wrapper { + display: flex; + flex-direction: column; + flex: 1; + position: relative; + justify-content: center; +} + .mx_RoomView { word-wrap: break-word; display: flex; flex-direction: column; + flex: 1; + position: relative; } diff --git a/res/css/structures/_SpaceRoomDirectory.scss b/res/css/structures/_SpaceHierarchy.scss similarity index 85% rename from res/css/structures/_SpaceRoomDirectory.scss rename to res/css/structures/_SpaceHierarchy.scss index 88676d06ac..a5d589f9c2 100644 --- a/res/css/structures/_SpaceRoomDirectory.scss +++ b/res/css/structures/_SpaceHierarchy.scss @@ -14,21 +14,6 @@ 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-content; - word-break: break-word; - display: flex; - flex-direction: column; -} - -.mx_SpaceRoomDirectory, .mx_SpaceRoomView_landing { .mx_Dialog_title { display: flex; @@ -68,7 +53,7 @@ limitations under the License. margin: 24px 0 16px; } - .mx_SpaceRoomDirectory_noResults { + .mx_SpaceHierarchy_noResults { text-align: center; > div { @@ -78,13 +63,19 @@ limitations under the License. } } - .mx_SpaceRoomDirectory_listHeader { + .mx_SpaceHierarchy_listHeader { display: flex; min-height: 32px; align-items: center; font-size: $font-15px; line-height: $font-24px; color: $primary-content; + margin-bottom: 12px; + + > h4 { + font-weight: $font-semi-bold; + margin: 0; + } .mx_AccessibleButton { padding: 4px 12px; @@ -105,7 +96,7 @@ limitations under the License. } } - .mx_SpaceRoomDirectory_error { + .mx_SpaceHierarchy_error { position: relative; font-weight: $font-semi-bold; color: $notice-primary-color; @@ -124,13 +115,14 @@ limitations under the License. background-image: url("$(res)/img/element-icons/warning-badge.svg"); } } -} -.mx_SpaceRoomDirectory_list { - margin-top: 16px; - padding-bottom: 40px; + .mx_SpaceHierarchy_list { + list-style: none; + padding: 0; + margin: 0; + } - .mx_SpaceRoomDirectory_roomCount { + .mx_SpaceHierarchy_roomCount { > h3 { display: inline; font-weight: $font-semi-bold; @@ -147,13 +139,13 @@ limitations under the License. } } - .mx_SpaceRoomDirectory_subspace { + .mx_SpaceHierarchy_subspace { .mx_BaseAvatar_image { border-radius: 8px; } } - .mx_SpaceRoomDirectory_subspace_toggle { + .mx_SpaceHierarchy_subspace_toggle { position: absolute; left: -1px; top: 10px; @@ -177,17 +169,17 @@ limitations under the License. mask-image: url('$(res)/img/feather-customised/chevron-down.svg'); } - &.mx_SpaceRoomDirectory_subspace_toggle_shown::before { + &.mx_SpaceHierarchy_subspace_toggle_shown::before { transform: rotate(0deg); } } - .mx_SpaceRoomDirectory_subspace_children { + .mx_SpaceHierarchy_subspace_children { position: relative; padding-left: 12px; } - .mx_SpaceRoomDirectory_roomTile { + .mx_SpaceHierarchy_roomTile { position: relative; padding: 8px 16px; border-radius: 8px; @@ -204,7 +196,7 @@ limitations under the License. grid-column: 1; } - .mx_SpaceRoomDirectory_roomTile_name { + .mx_SpaceHierarchy_roomTile_name { font-weight: $font-semi-bold; font-size: $font-15px; line-height: $font-18px; @@ -232,7 +224,7 @@ limitations under the License. } } - .mx_SpaceRoomDirectory_roomTile_info { + .mx_SpaceHierarchy_roomTile_info { font-size: $font-14px; line-height: $font-18px; color: $secondary-content; @@ -244,7 +236,7 @@ limitations under the License. overflow: hidden; } - .mx_SpaceRoomDirectory_actions { + .mx_SpaceHierarchy_actions { text-align: right; margin-left: 20px; grid-column: 3; @@ -278,12 +270,12 @@ limitations under the License. } } - li.mx_SpaceRoomDirectory_roomTileWrapper { + li.mx_SpaceHierarchy_roomTileWrapper { list-style: none; } - .mx_SpaceRoomDirectory_roomTile, - .mx_SpaceRoomDirectory_subspace_children { + .mx_SpaceHierarchy_roomTile, + .mx_SpaceHierarchy_subspace_children { &::before { content: ""; position: absolute; @@ -295,8 +287,8 @@ limitations under the License. } } - .mx_SpaceRoomDirectory_actions { - .mx_SpaceRoomDirectory_actionsText { + .mx_SpaceHierarchy_actions { + .mx_SpaceHierarchy_actionsText { font-weight: normal; font-size: $font-12px; line-height: $font-15px; @@ -311,7 +303,7 @@ limitations under the License. margin: 20px 0; } - .mx_SpaceRoomDirectory_createRoom { + .mx_SpaceHierarchy_createRoom { display: block; margin: 16px auto 0; width: max-content; diff --git a/res/css/structures/_SpacePanel.scss b/res/css/structures/_SpacePanel.scss index 977c1c3670..be740df46e 100644 --- a/res/css/structures/_SpacePanel.scss +++ b/res/css/structures/_SpacePanel.scss @@ -23,10 +23,13 @@ $activeBackgroundColor: $roomtile-selected-bg-color; $activeBorderColor: $secondary-content; .mx_SpacePanel { - flex: 0 0 auto; background-color: $groupFilterPanel-bg-color; + flex: 0 0 auto; padding: 0; margin: 0; + position: relative; + // Fix for the blurred avatar-background + z-index: 1; // Create another flexbox so the Panel fills the container display: flex; diff --git a/res/css/structures/_SpaceRoomView.scss b/res/css/structures/_SpaceRoomView.scss index 49d6b61f49..39eabe2e07 100644 --- a/res/css/structures/_SpaceRoomView.scss +++ b/res/css/structures/_SpaceRoomView.scss @@ -62,6 +62,9 @@ $SpaceRoomViewInnerWidth: 428px; } .mx_SpaceRoomView { + overflow-y: auto; + flex: 1; + .mx_MainSplit > div:first-child { padding: 80px 60px; flex-grow: 1; @@ -248,6 +251,7 @@ $SpaceRoomViewInnerWidth: 428px; .mx_SpaceRoomView_landing { display: flex; flex-direction: column; + min-width: 0; > .mx_BaseAvatar_image, > .mx_BaseAvatar > .mx_BaseAvatar_image { @@ -359,11 +363,6 @@ $SpaceRoomViewInnerWidth: 428px; width: max-content; margin: 0 0 -40px auto; // collapse its own height to not push other components down } - - .mx_SpaceRoomDirectory_list { - // we don't want this container to get forced into the flexbox layout - display: contents; - } } .mx_SpaceRoomView_privateScope { diff --git a/res/css/views/messages/_CallEvent.scss b/res/css/views/messages/_CallEvent.scss index f641cd44a5..7934f8f3c2 100644 --- a/res/css/views/messages/_CallEvent.scss +++ b/res/css/views/messages/_CallEvent.scss @@ -14,126 +14,23 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_CallEvent { +.mx_CallEvent_wrapper { display: flex; - flex-direction: row; - align-items: center; - justify-content: space-between; + width: 100%; - background-color: $dark-panel-bg-color; - border-radius: 8px; - margin: 10px auto; - width: 75%; - box-sizing: border-box; - height: 60px; - - &.mx_CallEvent_voice { - .mx_CallEvent_type_icon::before, - .mx_CallEvent_content_button_callBack span::before, - .mx_CallEvent_content_button_answer span::before { - mask-image: url('$(res)/img/element-icons/call/voice-call.svg'); - } - } - - &.mx_CallEvent_video { - .mx_CallEvent_type_icon::before, - .mx_CallEvent_content_button_callBack span::before, - .mx_CallEvent_content_button_answer span::before { - mask-image: url('$(res)/img/element-icons/call/video-call.svg'); - } - } - - &.mx_CallEvent_voice.mx_CallEvent_missed .mx_CallEvent_type_icon::before { - mask-image: url('$(res)/img/voip/missed-voice.svg'); - } - - &.mx_CallEvent_video.mx_CallEvent_missed .mx_CallEvent_type_icon::before { - mask-image: url('$(res)/img/voip/missed-video.svg'); - } - - .mx_CallEvent_info { + .mx_CallEvent { + position: relative; display: flex; flex-direction: row; align-items: center; - margin-left: 12px; + justify-content: space-between; - .mx_CallEvent_info_basic { - display: flex; - flex-direction: column; - margin-left: 10px; // To match mx_CallEvent - - .mx_CallEvent_sender { - font-weight: 600; - font-size: 1.5rem; - line-height: 1.8rem; - margin-bottom: 3px; - } - - .mx_CallEvent_type { - font-weight: 400; - color: $secondary-content; - font-size: 1.2rem; - line-height: $font-13px; - display: flex; - align-items: center; - - .mx_CallEvent_type_icon { - height: 13px; - width: 13px; - margin-right: 5px; - - &::before { - content: ''; - position: absolute; - height: 13px; - width: 13px; - background-color: $tertiary-content; - mask-repeat: no-repeat; - mask-size: contain; - } - } - } - } - } - - .mx_CallEvent_content { - display: flex; - flex-direction: row; - align-items: center; - color: $secondary-content; - margin-right: 16px; - - .mx_CallEvent_content_button { - height: 24px; - padding: 0px 12px; - margin-left: 8px; - - span { - padding: 8px 0; - display: flex; - align-items: center; - - &::before { - content: ''; - display: inline-block; - background-color: $button-fg-color; - mask-position: center; - mask-repeat: no-repeat; - mask-size: 16px; - width: 16px; - height: 16px; - margin-right: 8px; - } - } - } - - .mx_CallEvent_content_button_reject span::before { - mask-image: url('$(res)/img/element-icons/call/hangup.svg'); - } - - .mx_CallEvent_content_tooltip { - margin-right: 5px; - } + background-color: $dark-panel-bg-color; + border-radius: 8px; + width: 65%; + box-sizing: border-box; + height: 60px; + margin: 4px 0; .mx_CallEvent_iconButton { display: inline-flex; @@ -144,7 +41,7 @@ limitations under the License. height: 16px; width: 16px; - background-color: $tertiary-content; + background-color: $secondary-content; mask-repeat: no-repeat; mask-size: contain; mask-position: center; @@ -158,5 +55,165 @@ limitations under the License. .mx_CallEvent_unSilence::before { mask-image: url('$(res)/img/voip/un-silence.svg'); } + + &.mx_CallEvent_voice { + .mx_CallEvent_type_icon::before, + .mx_CallEvent_content_button_callBack span::before, + .mx_CallEvent_content_button_answer span::before { + mask-image: url('$(res)/img/element-icons/call/voice-call.svg'); + } + } + + &.mx_CallEvent_video { + .mx_CallEvent_type_icon::before, + .mx_CallEvent_content_button_callBack span::before, + .mx_CallEvent_content_button_answer span::before { + mask-image: url('$(res)/img/element-icons/call/video-call.svg'); + } + } + + &.mx_CallEvent_voice.mx_CallEvent_missed .mx_CallEvent_type_icon::before { + mask-image: url('$(res)/img/voip/missed-voice.svg'); + } + + &.mx_CallEvent_video.mx_CallEvent_missed .mx_CallEvent_type_icon::before { + mask-image: url('$(res)/img/voip/missed-video.svg'); + } + + &.mx_CallEvent_voice.mx_CallEvent_rejected .mx_CallEvent_type_icon::before, + &.mx_CallEvent_voice.mx_CallEvent_noAnswer .mx_CallEvent_type_icon::before { + mask-image: url('$(res)/img/voip/declined-voice.svg'); + } + + &.mx_CallEvent_video.mx_CallEvent_rejected .mx_CallEvent_type_icon::before, + &.mx_CallEvent_video.mx_CallEvent_noAnswer .mx_CallEvent_type_icon::before { + mask-image: url('$(res)/img/voip/declined-video.svg'); + } + + .mx_CallEvent_info { + display: flex; + flex-direction: row; + align-items: center; + margin-left: 12px; + min-width: 0; + + .mx_CallEvent_info_basic { + display: flex; + flex-direction: column; + margin-left: 10px; // To match mx_CallEvent + min-width: 0; + + .mx_CallEvent_sender { + font-weight: 600; + font-size: 1.5rem; + line-height: 1.8rem; + margin-bottom: 3px; + + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + + .mx_CallEvent_type { + font-weight: 400; + color: $secondary-content; + font-size: 1.2rem; + line-height: $font-13px; + display: flex; + align-items: center; + + .mx_CallEvent_type_icon { + height: 13px; + width: 13px; + margin-right: 5px; + + &::before { + content: ''; + position: absolute; + height: 13px; + width: 13px; + background-color: $secondary-content; + mask-repeat: no-repeat; + mask-size: contain; + } + } + } + } + } + + .mx_CallEvent_content { + display: flex; + flex-direction: row; + align-items: center; + color: $secondary-content; + margin-right: 16px; + gap: 8px; + min-width: max-content; + + .mx_CallEvent_content_button { + padding: 0px 12px; + + span { + padding: 1px 0; + display: flex; + align-items: center; + + &::before { + content: ''; + display: inline-block; + background-color: $button-fg-color; + mask-position: center; + mask-repeat: no-repeat; + mask-size: 16px; + width: 16px; + height: 16px; + margin-right: 8px; + + flex-shrink: 0; + } + } + } + + .mx_CallEvent_content_button_reject span::before { + mask-image: url('$(res)/img/element-icons/call/hangup.svg'); + } + + .mx_CallEvent_content_tooltip { + margin-right: 5px; + } + } + + &.mx_CallEvent_narrow { + height: unset; + width: 290px; + flex-direction: column; + align-items: unset; + gap: 16px; + + .mx_CallEvent_iconButton { + position: absolute; + margin-right: 0; + top: 12px; + right: 12px; + height: 16px; + width: 16px; + display: flex; + } + + .mx_CallEvent_info { + align-items: unset; + margin-top: 12px; + margin-right: 12px; + + .mx_CallEvent_sender { + margin-bottom: 8px; + } + } + + .mx_CallEvent_content { + margin-left: 54px; // mx_CallEvent margin (12px) + avatar (32px) + mx_CallEvent_info_basic margin (10px) + margin-bottom: 16px; + } + } } } diff --git a/res/css/views/messages/_MImageBody.scss b/res/css/views/messages/_MImageBody.scss index ab7ab14772..ddbacba386 100644 --- a/res/css/views/messages/_MImageBody.scss +++ b/res/css/views/messages/_MImageBody.scss @@ -36,6 +36,10 @@ $timelineImageBorderRadius: 4px; animation: mx--anim-pulse 1.75s infinite cubic-bezier(.4, 0, .6, 1); border-radius: $timelineImageBorderRadius; } + + .mx_no-image-placeholder { + background-color: $primary-bg-color; + } } .mx_MImageBody_thumbnail_container { diff --git a/res/css/views/rooms/_Autocomplete.scss b/res/css/views/rooms/_Autocomplete.scss index 9514faa292..529936b2b2 100644 --- a/res/css/views/rooms/_Autocomplete.scss +++ b/res/css/views/rooms/_Autocomplete.scss @@ -7,13 +7,15 @@ background: $background; border-bottom: none; border-radius: 8px 8px 0 0; - max-height: 50vh; - overflow: auto; + max-height: 35vh; + overflow: clip; + display: flex; box-shadow: 0px -16px 32px $composer-shadow-color; } .mx_Autocomplete_ProviderSection { border-bottom: 1px solid $primary-hairline-color; + width: 100%; } /* a "block" completion takes up a whole line */ @@ -59,8 +61,8 @@ .mx_Autocomplete_Completion_container_pill { margin: 12px; - display: flex; - flex-flow: wrap; + height: 100%; + overflow-y: scroll; } .mx_Autocomplete_Completion_container_truncate { @@ -68,7 +70,6 @@ .mx_Autocomplete_Completion_subtitle, .mx_Autocomplete_Completion_description { /* Ellipsis for long names/subtitles/descriptions */ - max-width: 150px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; diff --git a/res/css/views/rooms/_EventBubbleTile.scss b/res/css/views/rooms/_EventBubbleTile.scss index c6170bf7c0..8c02affbed 100644 --- a/res/css/views/rooms/_EventBubbleTile.scss +++ b/res/css/views/rooms/_EventBubbleTile.scss @@ -105,6 +105,8 @@ limitations under the License. .mx_ReplyTile .mx_SenderProfile { display: block; + top: unset; + left: unset; } .mx_ReactionsRow { @@ -188,8 +190,6 @@ limitations under the License. } .mx_ReplyThread { - margin: 0 calc(-1 * var(--gutterSize)); - .mx_EventTile_reply { max-width: 90%; padding: 0; @@ -223,11 +223,6 @@ limitations under the License. margin-left: -9px; } - .mx_ReplyThread { - border-left-width: 2px; - border-left-color: $eventbubble-reply-color; - } - /* Special layout scenario for "Unable To Decrypt (UTD)" events */ &.mx_EventTile_bad > .mx_EventTile_line { display: grid; @@ -264,6 +259,7 @@ limitations under the License. } .mx_EventTile.mx_EventTile_bubbleContainer[data-layout=bubble], +.mx_EventTile.mx_EventTile_leftAlignedBubble[data-layout=bubble], .mx_EventTile.mx_EventTile_info[data-layout=bubble], .mx_EventListSummary[data-layout=bubble][data-expanded=false] { --backgroundColor: transparent; diff --git a/res/css/views/settings/tabs/user/_HelpUserSettingsTab.scss b/res/css/views/settings/tabs/user/_HelpUserSettingsTab.scss index fbbe9909e7..3e61e80a9d 100644 --- a/res/css/views/settings/tabs/user/_HelpUserSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_HelpUserSettingsTab.scss @@ -36,6 +36,7 @@ limitations under the License. margin-top: 10px; padding: 10px; width: max-content; + max-width: 100%; .mx_HelpUserSettingsTab_copyButton { flex-shrink: 0; diff --git a/res/css/views/toasts/_IncomingCallToast.scss b/res/css/views/toasts/_IncomingCallToast.scss index 98e9121f3a..cb05b1a977 100644 --- a/res/css/views/toasts/_IncomingCallToast.scss +++ b/res/css/views/toasts/_IncomingCallToast.scss @@ -30,7 +30,14 @@ limitations under the License. font-size: $font-15px; line-height: $font-18px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + margin-top: 2px; + margin-right: 6px; + + max-width: 200px; } .mx_CallEvent_type { diff --git a/res/css/views/voip/_CallContainer.scss b/res/css/views/voip/_CallContainer.scss index d11ab9bf9f..a0137b18e8 100644 --- a/res/css/views/voip/_CallContainer.scss +++ b/res/css/views/voip/_CallContainer.scss @@ -26,19 +26,6 @@ limitations under the License. // different level. pointer-events: none; - .mx_CallPreview { - pointer-events: initial; // restore pointer events so the user can leave/interact - - .mx_VideoFeed_remote.mx_VideoFeed_voice { - min-height: 150px; - } - - .mx_VideoFeed_local { - border-radius: 8px; - overflow: hidden; - } - } - .mx_AppTile_persistedWrapper div { min-width: 350px; } diff --git a/res/css/views/voip/_CallPreview.scss b/res/css/views/voip/_CallPreview.scss index 92348fb465..0fd97d4676 100644 --- a/res/css/views/voip/_CallPreview.scss +++ b/res/css/views/voip/_CallPreview.scss @@ -18,4 +18,15 @@ limitations under the License. position: fixed; left: 0; top: 0; + + pointer-events: initial; // restore pointer events so the user can leave/interact + + .mx_VideoFeed_remote.mx_VideoFeed_voice { + min-height: 150px; + } + + .mx_VideoFeed_local { + border-radius: 8px; + overflow: hidden; + } } diff --git a/res/css/views/voip/_CallView.scss b/res/css/views/voip/_CallView.scss index df961d852b..63ca91267f 100644 --- a/res/css/views/voip/_CallView.scss +++ b/res/css/views/voip/_CallView.scss @@ -43,6 +43,7 @@ limitations under the License. box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.20); border-radius: 8px; + .mx_CallView_video_hold, .mx_CallView_voice { height: 180px; } diff --git a/res/img/voip/declined-video.svg b/res/img/voip/declined-video.svg new file mode 100644 index 0000000000..509ffa8fd1 --- /dev/null +++ b/res/img/voip/declined-video.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/voip/declined-voice.svg b/res/img/voip/declined-voice.svg new file mode 100644 index 0000000000..78e8d90cdf --- /dev/null +++ b/res/img/voip/declined-voice.svg @@ -0,0 +1,4 @@ + + + + diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index 84c5ab4618..d37233a046 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -222,9 +222,10 @@ $voice-playback-button-fg-color: $message-body-panel-icon-fg-color; // Appearance tab colors $appearance-tab-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; +// blur amounts for left left panel (only for element theme) +:root { + --lp-background-blur: 45px; +} $composer-shadow-color: rgba(0, 0, 0, 0.28); diff --git a/res/themes/dark/css/dark.scss b/res/themes/dark/css/dark.scss index 600cfd528a..df83d6db88 100644 --- a/res/themes/dark/css/dark.scss +++ b/res/themes/dark/css/dark.scss @@ -2,10 +2,6 @@ @import "../../light/css/_paths.scss"; @import "../../light/css/_fonts.scss"; @import "../../light/css/_light.scss"; -// important this goes before _mods, -// as $groupFilterPanel-background-blur-amount and -// $roomlist-background-blur-amount -// are overridden in _dark.scss @import "_dark.scss"; @import "../../light/css/_mods.scss"; @import "../../../../res/css/_components.scss"; diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index 08b3b57677..262a6c48eb 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -350,10 +350,10 @@ $voice-playback-button-fg-color: $message-body-panel-icon-fg-color; // FontSlider colors $appearance-tab-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; - +// blur amounts for left left panel (only for element theme) +:root { + --lp-background-blur: 30px; +} $composer-shadow-color: rgba(0, 0, 0, 0.04); // Bubble tiles diff --git a/res/themes/light/css/_mods.scss b/res/themes/light/css/_mods.scss index fbca58dfb1..15f6d4b0fe 100644 --- a/res/themes/light/css/_mods.scss +++ b/res/themes/light/css/_mods.scss @@ -4,27 +4,6 @@ // set the user avatar (if any) as a background so // it can be blurred by the tag panel and room list -@supports (backdrop-filter: none) { - .mx_LeftPanel { - background-image: var(--avatar-url, unset); - background-repeat: no-repeat; - background-size: cover; - background-position: left top; - } - - .mx_GroupFilterPanel { - 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); - } -} - .mx_RoomSublist_showNButton { background-color: transparent !important; } diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index 41571666c3..e11e828864 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -86,7 +86,7 @@ import { randomUppercaseString, randomLowercaseString } from "matrix-js-sdk/src/ import EventEmitter from 'events'; import SdkConfig from './SdkConfig'; import { ensureDMExists, findDMForUser } from './createRoom'; -import { IPushRule, RuleId, TweakName, Tweaks } from "matrix-js-sdk/src/@types/PushRules"; +import { RuleId, TweakName, Tweaks } from "matrix-js-sdk/src/@types/PushRules"; import { PushProcessor } from 'matrix-js-sdk/src/pushprocessor'; import { WidgetLayoutStore, Container } from './stores/widgets/WidgetLayoutStore'; import { getIncomingCallToastKey } from './toasts/IncomingCallToast'; @@ -464,85 +464,7 @@ export default class CallHandler extends EventEmitter { this.removeCallForRoom(mappedRoomId); }); call.on(CallEvent.State, (newState: CallState, oldState: CallState) => { - if (!this.matchesCallForThisRoom(call)) return; - - this.setCallState(call, newState); - - switch (oldState) { - case CallState.Ringing: - this.pause(AudioID.Ring); - break; - case CallState.InviteSent: - this.pause(AudioID.Ringback); - break; - } - - if (newState !== CallState.Ringing) { - this.silencedCalls.delete(call.callId); - } - - switch (newState) { - case CallState.Ringing: { - const incomingCallPushRule = ( - new PushProcessor(MatrixClientPeg.get()).getPushRuleById(RuleId.IncomingCall) as IPushRule - ); - const pushRuleEnabled = incomingCallPushRule?.enabled; - const tweakSetToRing = incomingCallPushRule?.actions.some((action: Tweaks) => ( - action.set_tweak === TweakName.Sound && - action.value === "ring" - )); - - if (pushRuleEnabled && tweakSetToRing) { - this.play(AudioID.Ring); - } else { - this.silenceCall(call.callId); - } - break; - } - case CallState.InviteSent: { - this.play(AudioID.Ringback); - break; - } - case CallState.Ended: { - const hangupReason = call.hangupReason; - Analytics.trackEvent('voip', 'callEnded', 'hangupReason', hangupReason); - this.removeCallForRoom(mappedRoomId); - if (oldState === CallState.InviteSent && call.hangupParty === CallParty.Remote) { - this.play(AudioID.Busy); - - // Don't show a modal when we got rejected/the call was hung up - if (!hangupReason || [CallErrorCode.UserHangup, "user hangup"].includes(hangupReason)) break; - - let title; - let description; - // TODO: We should either do away with these or figure out a copy for each code (expect user_hangup...) - if (call.hangupReason === CallErrorCode.UserBusy) { - title = _t("User Busy"); - description = _t("The user you called is busy."); - } else { - title = _t("Call Failed"); - description = _t("The call could not be established"); - } - - Modal.createTrackedDialog('Call Handler', 'Call Failed', ErrorDialog, { - title, description, - }); - } else if ( - hangupReason === CallErrorCode.AnsweredElsewhere && oldState === CallState.Connecting - ) { - Modal.createTrackedDialog('Call Handler', 'Call Failed', ErrorDialog, { - title: _t("Answered Elsewhere"), - description: _t("The call was answered on another device."), - }); - } 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; - } - } + this.onCallStateChanged(newState, oldState, call); }); call.on(CallEvent.Replaced, (newCall: MatrixCall) => { if (!this.matchesCallForThisRoom(call)) return; @@ -598,6 +520,89 @@ export default class CallHandler extends EventEmitter { }); } + private onCallStateChanged = (newState: CallState, oldState: CallState, call: MatrixCall): void => { + if (!this.matchesCallForThisRoom(call)) return; + + const mappedRoomId = this.roomIdForCall(call); + this.setCallState(call, newState); + + switch (oldState) { + case CallState.Ringing: + this.pause(AudioID.Ring); + break; + case CallState.InviteSent: + this.pause(AudioID.Ringback); + break; + } + + if (newState !== CallState.Ringing) { + this.silencedCalls.delete(call.callId); + } + + switch (newState) { + case CallState.Ringing: { + const incomingCallPushRule = ( + new PushProcessor(MatrixClientPeg.get()).getPushRuleById(RuleId.IncomingCall) + ); + const pushRuleEnabled = incomingCallPushRule?.enabled; + const tweakSetToRing = incomingCallPushRule?.actions.some((action: Tweaks) => ( + action.set_tweak === TweakName.Sound && + action.value === "ring" + )); + + if (pushRuleEnabled && tweakSetToRing) { + this.play(AudioID.Ring); + } else { + this.silenceCall(call.callId); + } + break; + } + case CallState.InviteSent: { + this.play(AudioID.Ringback); + break; + } + case CallState.Ended: { + const hangupReason = call.hangupReason; + Analytics.trackEvent('voip', 'callEnded', 'hangupReason', hangupReason); + this.removeCallForRoom(mappedRoomId); + if (oldState === CallState.InviteSent && call.hangupParty === CallParty.Remote) { + this.play(AudioID.Busy); + + // Don't show a modal when we got rejected/the call was hung up + if (!hangupReason || [CallErrorCode.UserHangup, "user hangup"].includes(hangupReason)) break; + + let title; + let description; + // TODO: We should either do away with these or figure out a copy for each code (expect user_hangup...) + if (call.hangupReason === CallErrorCode.UserBusy) { + title = _t("User Busy"); + description = _t("The user you called is busy."); + } else { + title = _t("Call Failed"); + description = _t("The call could not be established"); + } + + Modal.createTrackedDialog('Call Handler', 'Call Failed', ErrorDialog, { + title, description, + }); + } else if ( + hangupReason === CallErrorCode.AnsweredElsewhere && oldState === CallState.Connecting + ) { + Modal.createTrackedDialog('Call Handler', 'Call Failed', ErrorDialog, { + title: _t("Answered Elsewhere"), + description: _t("The call was answered on another device."), + }); + } 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; + } + } + }; + private async logCallStats(call: MatrixCall, mappedRoomId: string) { const stats = await call.getCurrentCallStats(); logger.debug( @@ -861,6 +866,8 @@ export default class CallHandler extends EventEmitter { this.calls.set(mappedRoomId, call); this.emit(CallHandlerEvent.CallsChanged, this.calls); this.setCallListeners(call); + // Explicitly handle first state change + this.onCallStateChanged(call.state, null, 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 diff --git a/src/KeyBindingsDefaults.ts b/src/KeyBindingsDefaults.ts index b2f70abff7..6169f431f4 100644 --- a/src/KeyBindingsDefaults.ts +++ b/src/KeyBindingsDefaults.ts @@ -161,31 +161,29 @@ const messageComposerBindings = (): KeyBinding[] => { const autocompleteBindings = (): KeyBinding[] => { return [ { - action: AutocompleteAction.CompleteOrNextSelection, + action: AutocompleteAction.ForceComplete, keyCombo: { key: Key.TAB, }, }, { - action: AutocompleteAction.CompleteOrNextSelection, + action: AutocompleteAction.ForceComplete, keyCombo: { key: Key.TAB, ctrlKey: true, }, }, { - action: AutocompleteAction.CompleteOrPrevSelection, + action: AutocompleteAction.Complete, keyCombo: { - key: Key.TAB, - shiftKey: true, + key: Key.ENTER, }, }, { - action: AutocompleteAction.CompleteOrPrevSelection, + action: AutocompleteAction.Complete, keyCombo: { - key: Key.TAB, + key: Key.ENTER, ctrlKey: true, - shiftKey: true, }, }, { diff --git a/src/KeyBindingsManager.ts b/src/KeyBindingsManager.ts index 4225d2f449..3a893e2ec8 100644 --- a/src/KeyBindingsManager.ts +++ b/src/KeyBindingsManager.ts @@ -52,13 +52,11 @@ export enum MessageComposerAction { /** 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', + /** Accepts chosen autocomplete selection */ + Complete = 'Complete', + /** Accepts chosen autocomplete selection or, + * if the autocompletion window is not shown, open the window and select the first selection */ + ForceComplete = 'ForceComplete', /** Move to the previous autocomplete selection */ PrevSelection = 'PrevSelection', /** Move to the next autocomplete selection */ diff --git a/src/PasswordReset.js b/src/PasswordReset.ts similarity index 89% rename from src/PasswordReset.js rename to src/PasswordReset.ts index 88ae00d088..76f54de245 100644 --- a/src/PasswordReset.js +++ b/src/PasswordReset.ts @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { createClient } from 'matrix-js-sdk/src/matrix'; +import { createClient, IRequestTokenResponse, MatrixClient } from 'matrix-js-sdk/src/matrix'; import { _t } from './languageHandler'; /** @@ -26,12 +26,18 @@ import { _t } from './languageHandler'; * API on the homeserver in question with the new password. */ export default class PasswordReset { + private client: MatrixClient; + private clientSecret: string; + private identityServerDomain: string; + private password: string; + private sessionId: string; + /** * Configure the endpoints for password resetting. * @param {string} homeserverUrl The URL to the HS which has the account to reset. * @param {string} identityUrl The URL to the IS which has linked the email -> mxid mapping. */ - constructor(homeserverUrl, identityUrl) { + constructor(homeserverUrl: string, identityUrl: string) { this.client = createClient({ baseUrl: homeserverUrl, idBaseUrl: identityUrl, @@ -47,7 +53,7 @@ export default class PasswordReset { * @param {string} newPassword The new password for the account. * @return {Promise} Resolves when the email has been sent. Then call checkEmailLinkClicked(). */ - resetPassword(emailAddress, newPassword) { + public resetPassword(emailAddress: string, newPassword: string): Promise { this.password = newPassword; return this.client.requestPasswordEmailToken(emailAddress, this.clientSecret, 1).then((res) => { this.sessionId = res.sid; @@ -69,7 +75,7 @@ export default class PasswordReset { * with a "message" property which contains a human-readable message detailing why * the reset failed, e.g. "There is no mapped matrix user ID for the given email address". */ - async checkEmailLinkClicked() { + public async checkEmailLinkClicked(): Promise { const creds = { sid: this.sessionId, client_secret: this.clientSecret, diff --git a/src/autocomplete/AutocompleteProvider.tsx b/src/autocomplete/AutocompleteProvider.tsx index 51ab2e2cf7..2d82a9f591 100644 --- a/src/autocomplete/AutocompleteProvider.tsx +++ b/src/autocomplete/AutocompleteProvider.tsx @@ -27,11 +27,11 @@ export interface ICommand { }; } -export default class AutocompleteProvider { +export default abstract class AutocompleteProvider { commandRegex: RegExp; forcedCommandRegex: RegExp; - constructor(commandRegex?: RegExp, forcedCommandRegex?: RegExp) { + protected constructor(commandRegex?: RegExp, forcedCommandRegex?: RegExp) { if (commandRegex) { if (!commandRegex.global) { throw new Error('commandRegex must have global flag set'); @@ -93,23 +93,16 @@ export default class AutocompleteProvider { }; } - async getCompletions( + abstract getCompletions( query: string, selection: ISelectionRange, - force = false, - limit = -1, - ): Promise { - return []; - } + force: boolean, + limit: number, + ): Promise; - getName(): string { - return 'Default Provider'; - } + abstract getName(): string; - renderCompletions(completions: React.ReactNode[]): React.ReactNode | null { - console.error('stub; should be implemented in subclasses'); - return null; - } + abstract renderCompletions(completions: React.ReactNode[]): React.ReactNode | null; // Whether we should provide completions even if triggered forcefully, without a sigil. shouldForceComplete(): boolean { diff --git a/src/autocomplete/CommandProvider.tsx b/src/autocomplete/CommandProvider.tsx index e9a7742dee..d56adc026c 100644 --- a/src/autocomplete/CommandProvider.tsx +++ b/src/autocomplete/CommandProvider.tsx @@ -96,7 +96,7 @@ export default class CommandProvider extends AutocompleteProvider { return (
{ completions } diff --git a/src/autocomplete/CommunityProvider.tsx b/src/autocomplete/CommunityProvider.tsx index de99675b4b..4b42f4c64e 100644 --- a/src/autocomplete/CommunityProvider.tsx +++ b/src/autocomplete/CommunityProvider.tsx @@ -116,7 +116,7 @@ export default class CommunityProvider extends AutocompleteProvider { return (
{ completions } diff --git a/src/autocomplete/DuckDuckGoProvider.tsx b/src/autocomplete/DuckDuckGoProvider.tsx index 08750493d3..c41a91b97f 100644 --- a/src/autocomplete/DuckDuckGoProvider.tsx +++ b/src/autocomplete/DuckDuckGoProvider.tsx @@ -105,7 +105,7 @@ export default class DuckDuckGoProvider extends AutocompleteProvider { return (
{ completions } diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx index d3175edbdb..326651e037 100644 --- a/src/autocomplete/EmojiProvider.tsx +++ b/src/autocomplete/EmojiProvider.tsx @@ -67,7 +67,7 @@ export default class EmojiProvider extends AutocompleteProvider { constructor() { super(EMOJI_REGEX); this.matcher = new QueryMatcher(SORTED_EMOJI, { - keys: ['emoji.emoticon'], + keys: [], funcs: [o => o.emoji.shortcodes.map(s => `:${s}:`)], // For matching against ascii equivalents shouldMatchWordsOnly: false, @@ -91,7 +91,8 @@ export default class EmojiProvider extends AutocompleteProvider { let completions = []; const { command, range } = this.getCurrentCommand(query, selection); - if (command) { + + if (command && command[0].length > 2) { const matchedString = command[0]; completions = this.matcher.match(matchedString, limit); @@ -139,7 +140,7 @@ export default class EmojiProvider extends AutocompleteProvider { return (
{ completions } diff --git a/src/autocomplete/NotifProvider.tsx b/src/autocomplete/NotifProvider.tsx index 31b834ccfe..aa4f1174dc 100644 --- a/src/autocomplete/NotifProvider.tsx +++ b/src/autocomplete/NotifProvider.tsx @@ -70,7 +70,7 @@ export default class NotifProvider extends AutocompleteProvider { return (
{ completions } diff --git a/src/autocomplete/RoomProvider.tsx b/src/autocomplete/RoomProvider.tsx index 37ddf2c387..00bfe6be5c 100644 --- a/src/autocomplete/RoomProvider.tsx +++ b/src/autocomplete/RoomProvider.tsx @@ -134,7 +134,7 @@ export default class RoomProvider extends AutocompleteProvider { return (
{ completions } diff --git a/src/autocomplete/UserProvider.tsx b/src/autocomplete/UserProvider.tsx index 182743abb3..48854657de 100644 --- a/src/autocomplete/UserProvider.tsx +++ b/src/autocomplete/UserProvider.tsx @@ -181,7 +181,7 @@ export default class UserProvider extends AutocompleteProvider { return (
{ completions } diff --git a/src/components/structures/BackdropPanel.tsx b/src/components/structures/BackdropPanel.tsx new file mode 100644 index 0000000000..08f6c33738 --- /dev/null +++ b/src/components/structures/BackdropPanel.tsx @@ -0,0 +1,44 @@ +/* +Copyright 2021 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 React, { CSSProperties } from "react"; + +interface IProps { + backgroundImage?: string; + blurMultiplier?: number; +} + +export const BackdropPanel: React.FC = ({ backgroundImage, blurMultiplier }) => { + if (!backgroundImage) return null; + + const styles: CSSProperties = {}; + if (blurMultiplier) { + const rootStyle = getComputedStyle(document.documentElement); + const blurValue = rootStyle.getPropertyValue('--lp-background-blur'); + const pixelsValue = blurValue.replace('px', ''); + const parsed = parseInt(pixelsValue, 10); + if (!isNaN(parsed)) { + styles.filter = `blur(${parsed * blurMultiplier}px)`; + } + } + return
+ +
; +}; +export default BackdropPanel; diff --git a/src/components/structures/GroupFilterPanel.js b/src/components/structures/GroupFilterPanel.tsx similarity index 71% rename from src/components/structures/GroupFilterPanel.js rename to src/components/structures/GroupFilterPanel.tsx index 5d1be64f25..3e7c6e9b17 100644 --- a/src/components/structures/GroupFilterPanel.js +++ b/src/components/structures/GroupFilterPanel.tsx @@ -15,6 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +import type { EventSubscription } from "fbemitter"; import React from 'react'; import GroupFilterOrderStore from '../../stores/GroupFilterOrderStore'; @@ -30,22 +31,43 @@ import AutoHideScrollbar from "./AutoHideScrollbar"; import SettingsStore from "../../settings/SettingsStore"; import UserTagTile from "../views/elements/UserTagTile"; import { replaceableComponent } from "../../utils/replaceableComponent"; +import UIStore from "../../stores/UIStore"; + +interface IGroupFilterPanelProps { + +} + +// FIXME: Properly type this after migrating GroupFilterOrderStore.js to Typescript +type OrderedTagsTemporaryType = Array<{}>; +// FIXME: Properly type this after migrating GroupFilterOrderStore.js to Typescript +type SelectedTagsTemporaryType = Array<{}>; + +interface IGroupFilterPanelState { + // FIXME: Properly type this after migrating GroupFilterOrderStore.js to Typescript + orderedTags: OrderedTagsTemporaryType; + // FIXME: Properly type this after migrating GroupFilterOrderStore.js to Typescript + selectedTags: SelectedTagsTemporaryType; +} @replaceableComponent("structures.GroupFilterPanel") -class GroupFilterPanel extends React.Component { - static contextType = MatrixClientContext; +class GroupFilterPanel extends React.Component { + public static contextType = MatrixClientContext; - state = { + public state = { orderedTags: [], selectedTags: [], }; - componentDidMount() { - this.unmounted = false; - this.context.on("Group.myMembership", this._onGroupMyMembership); - this.context.on("sync", this._onClientSync); + private ref = React.createRef(); + private unmounted = false; + private groupFilterOrderStoreToken?: EventSubscription; - this._groupFilterOrderStoreToken = GroupFilterOrderStore.addListener(() => { + public componentDidMount() { + this.unmounted = false; + this.context.on("Group.myMembership", this.onGroupMyMembership); + this.context.on("sync", this.onClientSync); + + this.groupFilterOrderStoreToken = GroupFilterOrderStore.addListener(() => { if (this.unmounted) { return; } @@ -56,23 +78,25 @@ class GroupFilterPanel extends React.Component { }); // This could be done by anything with a matrix client dis.dispatch(GroupActions.fetchJoinedGroups(this.context)); + UIStore.instance.trackElementDimensions("GroupPanel", this.ref.current); } - componentWillUnmount() { + public componentWillUnmount() { this.unmounted = true; - this.context.removeListener("Group.myMembership", this._onGroupMyMembership); - this.context.removeListener("sync", this._onClientSync); - if (this._groupFilterOrderStoreToken) { - this._groupFilterOrderStoreToken.remove(); + this.context.removeListener("Group.myMembership", this.onGroupMyMembership); + this.context.removeListener("sync", this.onClientSync); + if (this.groupFilterOrderStoreToken) { + this.groupFilterOrderStoreToken.remove(); } + UIStore.instance.stopTrackingElementDimensions("GroupPanel"); } - _onGroupMyMembership = () => { + private onGroupMyMembership = () => { if (this.unmounted) return; dis.dispatch(GroupActions.fetchJoinedGroups(this.context)); }; - _onClientSync = (syncState, prevState) => { + private onClientSync = (syncState, prevState) => { // Consider the client reconnected if there is no error with syncing. // This means the state could be RECONNECTING, SYNCING, PREPARED or CATCHUP. const reconnected = syncState !== "ERROR" && prevState !== syncState; @@ -82,18 +106,18 @@ class GroupFilterPanel extends React.Component { } }; - onClick = e => { + private onClick = e => { // only dispatch if its not a no-op if (this.state.selectedTags.length > 0) { dis.dispatch({ action: 'deselect_tags' }); } }; - onClearFilterClick = ev => { + private onClearFilterClick = ev => { dis.dispatch({ action: 'deselect_tags' }); }; - renderGlobalIcon() { + private renderGlobalIcon() { if (!SettingsStore.getValue("feature_communities_v2_prototypes")) return null; return ( @@ -104,7 +128,7 @@ class GroupFilterPanel extends React.Component { ); } - render() { + public render() { const DNDTagTile = sdk.getComponent('elements.DNDTagTile'); const ActionButton = sdk.getComponent('elements.ActionButton'); @@ -147,7 +171,7 @@ class GroupFilterPanel extends React.Component { ); } - return
+ return
{ - this._authLogic.poll(); - }, 2000); - } - - this._stageComponent = createRef(); - } - - // TODO: [REACT-WARNING] Replace component with real class, use constructor for refs - UNSAFE_componentWillMount() { // eslint-disable-line camelcase - this._authLogic.attemptAuth().then((result) => { - const extra = { - emailSid: this._authLogic.getEmailSid(), - clientSecret: this._authLogic.getClientSecret(), - }; - this.props.onAuthFinished(true, result, extra); - }).catch((error) => { - this.props.onAuthFinished(false, error); - console.error("Error during user-interactive auth:", error); - if (this._unmounted) { - return; - } - - const msg = error.message || error.toString(); - this.setState({ - errorText: msg, - }); - }); - } - - componentWillUnmount() { - this._unmounted = true; - - if (this._intervalId !== null) { - clearInterval(this._intervalId); - } - } - - _requestEmailToken = async (...args) => { - this.setState({ - busy: true, - }); - try { - return await this.props.requestEmailToken(...args); - } finally { - this.setState({ - busy: false, - }); - } - }; - - tryContinue = () => { - if (this._stageComponent.current && this._stageComponent.current.tryContinue) { - this._stageComponent.current.tryContinue(); - } - }; - - _authStateUpdated = (stageType, stageState) => { - const oldStage = this.state.authStage; - this.setState({ - busy: false, - authStage: stageType, - stageState: stageState, - errorText: stageState.error, - }, () => { - if (oldStage !== stageType) { - this._setFocus(); - } else if ( - !stageState.error && this._stageComponent.current && - this._stageComponent.current.attemptFailed - ) { - this._stageComponent.current.attemptFailed(); - } - }); - }; - - _requestCallback = (auth) => { - // This wrapper just exists because the js-sdk passes a second - // 'busy' param for backwards compat. This throws the tests off - // so discard it here. - return this.props.makeRequest(auth); - }; - - _onBusyChanged = (busy) => { - // if we've started doing stuff, reset the error messages - if (busy) { - this.setState({ - busy: true, - errorText: null, - stageErrorText: null, - }); - } - // The JS SDK eagerly reports itself as "not busy" right after any - // immediate work has completed, but that's not really what we want at - // the UI layer, so we ignore this signal and show a spinner until - // there's a new screen to show the user. This is implemented by setting - // `busy: false` in `_authStateUpdated`. - // See also https://github.com/vector-im/element-web/issues/12546 - }; - - _setFocus() { - if (this._stageComponent.current && this._stageComponent.current.focus) { - this._stageComponent.current.focus(); - } - } - - _submitAuthDict = authData => { - this._authLogic.submitAuthDict(authData); - }; - - _onPhaseChange = newPhase => { - if (this.props.onStagePhaseChange) { - this.props.onStagePhaseChange(this.state.authStage, newPhase || 0); - } - }; - - _onStageCancel = () => { - this.props.onAuthFinished(false, ERROR_USER_CANCELLED); - }; - - _renderCurrentStage() { - const stage = this.state.authStage; - if (!stage) { - if (this.state.busy) { - const Loader = sdk.getComponent("elements.Spinner"); - return ; - } else { - return null; - } - } - - const StageComponent = getEntryComponentForLoginType(stage); - return ( - - ); - } - - _onAuthStageFailed = e => { - this.props.onAuthFinished(false, e); - }; - - _setEmailSid = sid => { - this._authLogic.setEmailSid(sid); - }; - - render() { - let error = null; - if (this.state.errorText) { - error = ( -
- { this.state.errorText } -
- ); - } - - return ( -
-
- { this._renderCurrentStage() } - { error } -
-
- ); - } -} diff --git a/src/components/structures/InteractiveAuth.tsx b/src/components/structures/InteractiveAuth.tsx new file mode 100644 index 0000000000..869cd29cba --- /dev/null +++ b/src/components/structures/InteractiveAuth.tsx @@ -0,0 +1,300 @@ +/* +Copyright 2017 - 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 { + AuthType, + IAuthData, + IAuthDict, + IInputs, + InteractiveAuth, + IStageStatus, +} from "matrix-js-sdk/src/interactive-auth"; +import { MatrixClient } from "matrix-js-sdk/src/client"; +import React, { createRef } from 'react'; + +import getEntryComponentForLoginType, { IStageComponent } from '../views/auth/InteractiveAuthEntryComponents'; +import Spinner from "../views/elements/Spinner"; +import { replaceableComponent } from "../../utils/replaceableComponent"; + +export const ERROR_USER_CANCELLED = new Error("User cancelled auth session"); + +interface IProps { + // matrix client to use for UI auth requests + matrixClient: MatrixClient; + // response from initial request. If not supplied, will do a request on mount. + authData?: IAuthData; + // Inputs provided by the user to the auth process + // and used by various stages. As passed to js-sdk + // interactive-auth + inputs?: IInputs; + sessionId?: string; + clientSecret?: string; + emailSid?: string; + // If true, poll to see if the auth flow has been completed out-of-band + poll?: boolean; + // If true, components will be told that the 'Continue' button + // is managed by some other party and should not be managed by + // the component itself. + continueIsManaged?: boolean; + // continueText and continueKind are passed straight through to the AuthEntryComponent. + continueText?: string; + continueKind?: string; + // callback + makeRequest(auth: IAuthData): Promise; + // callback called when the auth process has finished, + // successfully or unsuccessfully. + // @param {boolean} status True if the operation requiring + // auth was completed successfully, false if canceled. + // @param {object} result The result of the authenticated call + // if successful, otherwise the error object. + // @param {object} extra Additional information about the UI Auth + // process: + // * emailSid {string} If email auth was performed, the sid of + // the auth session. + // * clientSecret {string} The client secret used in auth + // sessions with the ID server. + onAuthFinished( + status: boolean, + result: IAuthData | Error, + extra?: { emailSid?: string, clientSecret?: string }, + ): void; + // As js-sdk interactive-auth + requestEmailToken?(email: string, secret: string, attempt: number, session: string): Promise<{ sid: string }>; + // Called when the stage changes, or the stage's phase changes. First + // argument is the stage, second is the phase. Some stages do not have + // phases and will be counted as 0 (numeric). + onStagePhaseChange?(stage: string, phase: string | number): void; +} + +interface IState { + authStage?: AuthType; + stageState?: IStageStatus; + busy: boolean; + errorText?: string; + stageErrorText?: string; + submitButtonEnabled: boolean; +} + +@replaceableComponent("structures.InteractiveAuthComponent") +export default class InteractiveAuthComponent extends React.Component { + private readonly authLogic: InteractiveAuth; + private readonly intervalId: number = null; + private readonly stageComponent = createRef(); + + private unmounted = false; + + constructor(props) { + super(props); + + this.state = { + authStage: null, + busy: false, + errorText: null, + stageErrorText: null, + submitButtonEnabled: false, + }; + + this.authLogic = new InteractiveAuth({ + authData: this.props.authData, + doRequest: this.requestCallback, + busyChanged: this.onBusyChanged, + inputs: this.props.inputs, + stateUpdated: this.authStateUpdated, + matrixClient: this.props.matrixClient, + sessionId: this.props.sessionId, + clientSecret: this.props.clientSecret, + emailSid: this.props.emailSid, + requestEmailToken: this.requestEmailToken, + }); + + if (this.props.poll) { + this.intervalId = setInterval(() => { + this.authLogic.poll(); + }, 2000); + } + } + + // TODO: [REACT-WARNING] Replace component with real class, use constructor for refs + UNSAFE_componentWillMount() { // eslint-disable-line @typescript-eslint/naming-convention, camelcase + this.authLogic.attemptAuth().then((result) => { + const extra = { + emailSid: this.authLogic.getEmailSid(), + clientSecret: this.authLogic.getClientSecret(), + }; + this.props.onAuthFinished(true, result, extra); + }).catch((error) => { + this.props.onAuthFinished(false, error); + console.error("Error during user-interactive auth:", error); + if (this.unmounted) { + return; + } + + const msg = error.message || error.toString(); + this.setState({ + errorText: msg, + }); + }); + } + + componentWillUnmount() { + this.unmounted = true; + + if (this.intervalId !== null) { + clearInterval(this.intervalId); + } + } + + private requestEmailToken = async ( + email: string, + secret: string, + attempt: number, + session: string, + ): Promise<{sid: string}> => { + this.setState({ + busy: true, + }); + try { + return await this.props.requestEmailToken(email, secret, attempt, session); + } finally { + this.setState({ + busy: false, + }); + } + }; + + private tryContinue = (): void => { + this.stageComponent.current?.tryContinue?.(); + }; + + private authStateUpdated = (stageType: AuthType, stageState: IStageStatus): void => { + const oldStage = this.state.authStage; + this.setState({ + busy: false, + authStage: stageType, + stageState: stageState, + errorText: stageState.error, + }, () => { + if (oldStage !== stageType) { + this.setFocus(); + } else if (!stageState.error) { + this.stageComponent.current?.attemptFailed?.(); + } + }); + }; + + private requestCallback = (auth: IAuthData, background: boolean): Promise => { + // This wrapper just exists because the js-sdk passes a second + // 'busy' param for backwards compat. This throws the tests off + // so discard it here. + return this.props.makeRequest(auth); + }; + + private onBusyChanged = (busy: boolean): void => { + // if we've started doing stuff, reset the error messages + if (busy) { + this.setState({ + busy: true, + errorText: null, + stageErrorText: null, + }); + } + // The JS SDK eagerly reports itself as "not busy" right after any + // immediate work has completed, but that's not really what we want at + // the UI layer, so we ignore this signal and show a spinner until + // there's a new screen to show the user. This is implemented by setting + // `busy: false` in `authStateUpdated`. + // See also https://github.com/vector-im/element-web/issues/12546 + }; + + private setFocus(): void { + this.stageComponent.current?.focus?.(); + } + + private submitAuthDict = (authData: IAuthDict): void => { + this.authLogic.submitAuthDict(authData); + }; + + private onPhaseChange = (newPhase: number): void => { + this.props.onStagePhaseChange?.(this.state.authStage, newPhase || 0); + }; + + private onStageCancel = (): void => { + this.props.onAuthFinished(false, ERROR_USER_CANCELLED); + }; + + private renderCurrentStage(): JSX.Element { + const stage = this.state.authStage; + if (!stage) { + if (this.state.busy) { + return ; + } else { + return null; + } + } + + const StageComponent = getEntryComponentForLoginType(stage); + return ( + + ); + } + + private onAuthStageFailed = (e: Error): void => { + this.props.onAuthFinished(false, e); + }; + + private setEmailSid = (sid: string): void => { + this.authLogic.setEmailSid(sid); + }; + + render() { + let error = null; + if (this.state.errorText) { + error = ( +
+ { this.state.errorText } +
+ ); + } + + return ( +
+
+ { this.renderCurrentStage() } + { error } +
+
+ ); + } +} diff --git a/src/components/structures/LeftPanel.tsx b/src/components/structures/LeftPanel.tsx index ff5d15d44d..d955271249 100644 --- a/src/components/structures/LeftPanel.tsx +++ b/src/components/structures/LeftPanel.tsx @@ -19,8 +19,6 @@ 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 dis from "../../dispatcher/dispatcher"; import { _t } from "../../languageHandler"; import RoomList from "../views/rooms/RoomList"; @@ -33,15 +31,12 @@ import RoomBreadcrumbs from "../views/rooms/RoomBreadcrumbs"; import { BreadcrumbsStore } from "../../stores/BreadcrumbsStore"; 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 IndicatorScrollbar from "../structures/IndicatorScrollbar"; import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton"; -import { OwnProfileStore } from "../../stores/OwnProfileStore"; 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"; import UIStore from "../../stores/UIStore"; @@ -53,7 +48,6 @@ interface IProps { interface IState { showBreadcrumbs: boolean; - showGroupFilterPanel: boolean; activeSpace?: Room; } @@ -70,8 +64,6 @@ const cssClasses = [ export default class LeftPanel extends React.Component { private ref: React.RefObject = createRef(); private listContainerRef: React.RefObject = createRef(); - private groupFilterPanelWatcherRef: string; - private bgImageWatcherRef: string; private focusedElement = null; private isDoingStickyHeaders = false; @@ -80,22 +72,16 @@ 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, () => { - this.setState({ showGroupFilterPanel: SettingsStore.getValue("TagPanel.enableTagPanel") }); - }); } public componentDidMount() { + UIStore.instance.trackElementDimensions("LeftPanel", this.ref.current); UIStore.instance.trackElementDimensions("ListContainer", this.listContainerRef.current); UIStore.instance.on("ListContainer", this.refreshStickyHeaders); // Using the passive option to not block the main thread @@ -104,11 +90,8 @@ export default class LeftPanel extends React.Component { } public componentWillUnmount() { - SettingsStore.unwatchSetting(this.groupFilterPanelWatcherRef); - SettingsStore.unwatchSetting(this.bgImageWatcherRef); 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); UIStore.instance.stopTrackingElementDimensions("ListContainer"); UIStore.instance.removeListener("ListContainer", this.refreshStickyHeaders); @@ -149,23 +132,6 @@ export default class LeftPanel extends React.Component { } }; - private onBackgroundImageUpdate = () => { - // Note: we do this in the LeftPanel as it uses this variable most prominently. - const avatarSize = 32; // arbitrary - let avatarUrl = OwnProfileStore.instance.getHttpAvatarUrl(avatarSize); - const settingBgMxc = SettingsStore.getValue("RoomList.backgroundImage"); - if (settingBgMxc) { - avatarUrl = mediaFromMxc(settingBgMxc).getSquareThumbnailHttp(avatarSize); - } - - const avatarUrlProp = `url(${avatarUrl})`; - if (!avatarUrl) { - document.body.style.removeProperty("--avatar-url"); - } else if (document.body.style.getPropertyValue("--avatar-url") !== avatarUrlProp) { - document.body.style.setProperty("--avatar-url", avatarUrlProp); - } - }; - private handleStickyHeaders(list: HTMLDivElement) { if (this.isDoingStickyHeaders) return; this.isDoingStickyHeaders = true; @@ -440,16 +406,6 @@ export default class LeftPanel extends React.Component { } public render(): React.ReactNode { - let leftLeftPanel; - if (this.state.showGroupFilterPanel) { - leftLeftPanel = ( -
- - { SettingsStore.getValue("feature_custom_tags") ? : null } -
- ); - } - const roomList = { return (
- { leftLeftPanel }