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 43a1494497..c31eedf93b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,165 @@ +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) diff --git a/package.json b/package.json index 2263c7de32..7ed1b272da 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "3.13.1", + "version": "3.15.0", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { @@ -86,7 +86,6 @@ "pako": "^2.0.3", "parse5": "^6.0.1", "png-chunks-extract": "^1.0.0", - "project-name-generator": "^2.1.9", "prop-types": "^15.7.2", "qrcode": "^1.4.4", "qs": "^6.9.6", @@ -102,7 +101,7 @@ "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" }, diff --git a/res/css/_components.scss b/res/css/_components.scss index 006bac09c9..d894688cac 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"; @@ -89,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"; @@ -212,6 +217,7 @@ @import "./views/settings/_DevicesPanel.scss"; @import "./views/settings/_E2eAdvancedPanel.scss"; @import "./views/settings/_EmailAddresses.scss"; +@import "./views/settings/_SpellCheckLanguages.scss"; @import "./views/settings/_IntegrationManager.scss"; @import "./views/settings/_Notifications.scss"; @import "./views/settings/_PhoneNumbers.scss"; @@ -232,6 +238,9 @@ @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"; diff --git a/res/css/structures/_LeftPanel.scss b/res/css/structures/_LeftPanel.scss index 168590502d..f1f27014ee 100644 --- a/res/css/structures/_LeftPanel.scss +++ b/res/css/structures/_LeftPanel.scss @@ -15,6 +15,7 @@ limitations under the License. */ $groupFilterPanelWidth: 56px; // only applies in this file, used for calculations +$roomListCollapsedWidth: 68px; .mx_LeftPanel { background-color: $roomlist-bg-color; @@ -37,18 +38,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; @@ -168,17 +163,10 @@ $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; 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/_SpacePanel.scss b/res/css/structures/_SpacePanel.scss new file mode 100644 index 0000000000..9937117086 --- /dev/null +++ b/res/css/structures/_SpacePanel.scss @@ -0,0 +1,349 @@ +/* +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: 17px; +$activeStripeSize: 4px; +$activeBorderTransparentGap: 2px; + +$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; + } + + .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: 16px 12px 16px 0; + } + + .mx_SpaceButton_toggleCollapse { + cursor: pointer; + } + + .mx_SpaceItem.collapsed { + .mx_SpaceButton { + .mx_NotificationBadge { + right: -4px; + top: -4px; + } + } + + & > .mx_SpaceButton > .mx_SpaceButton_toggleCollapse { + transform: rotate(-90deg); + } + + & > .mx_SpaceTreeLevel { + display: none; + } + } + + .mx_SpaceItem:not(.hasSubSpaces) > .mx_SpaceButton { + margin-left: $gutterSize; + } + + .mx_SpaceButton { + border-radius: 8px; + position: relative; + margin-bottom: 2px; + display: flex; + align-items: center; + padding: 4px; + + &.mx_SpaceButton_active { + &:not(.mx_SpaceButton_narrow) .mx_SpaceButton_selectionWrapper { + background-color: $activeBackgroundColor; + border-radius: 8px; + } + + &.mx_SpaceButton_narrow { + .mx_BaseAvatar, .mx_SpaceButton_avatarPlaceholder { + border: 2px $activeBorderColor solid; + border-radius: 11px; + } + } + } + + .mx_SpaceButton_selectionWrapper { + display: flex; + flex: 1; + align-items: center; + } + + .mx_SpaceButton_name { + flex: 1; + margin-left: 8px; + white-space: nowrap; + display: block; + max-width: 150px; + text-overflow: ellipsis; + overflow: hidden; + padding-right: 8px; + font-size: $font-14px; + line-height: $font-18px; + } + + .mx_SpaceButton_toggleCollapse { + width: calc($gutterSize - $activeStripeSize); + margin-left: 1px; + 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_avatarPlaceholder { + border: $activeBorderTransparentGap transparent solid; + padding: $activeBorderTransparentGap; + } + + &.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 { + /* moving the border-radius to this element from _image + element so we can add a border to it without the initials being displaced */ + overflow: hidden; + border: 2px transparent solid; + padding: $activeBorderTransparentGap; + + .mx_BaseAvatar_initial { + top: $activeBorderTransparentGap; + left: $activeBorderTransparentGap; + } + + .mx_BaseAvatar_image { + border-radius: 8px; + } + } + + .mx_SpaceButton_menuButton { + width: 20px; + min-width: 20px; // yay flex + height: 20px; + margin-top: auto; + margin-bottom: auto; + position: relative; + display: none; + + &::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 { + height: 16px; + // don't set width so that it takes no space when there is no badge to show + margin: auto 0; // vertically align + + // 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 { + position: absolute; + right: 0px; + top: 2px; + } + } + } + + &:not(.collapsed) { + .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_iconHome::before { + mask-image: url('$(res)/img/element-icons/roomlist/home.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/plus.svg'); + } + + .mx_SpacePanel_iconExplore::before { + mask-image: url('$(res)/img/element-icons/roomlist/explore.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..5cb91820cf --- /dev/null +++ b/res/css/structures/_SpaceRoomDirectory.scss @@ -0,0 +1,231 @@ +/* +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: 16px; + } + + .mx_BaseAvatar_image { + border-radius: 8px; + } + + > div { + > h1 { + font-weight: $font-semi-bold; + font-size: $font-18px; + line-height: $font-22px; + margin: 0; + } + + > div { + color: $secondary-fg-color; + font-size: $font-15px; + line-height: $font-24px; + } + } + } + + .mx_Dialog_content { + // TODO fix scrollbar + //display: flex; + //flex-direction: column; + //height: calc(100% - 80px); + + .mx_AccessibleButton_kind_link { + padding: 0; + } + + .mx_SearchBox { + margin: 24px 0 28px; + } + + .mx_SpaceRoomDirectory_listHeader { + display: flex; + font-size: $font-12px; + line-height: $font-15px; + color: $secondary-fg-color; + + .mx_FormButton { + margin-bottom: 8px; + } + + > span { + margin: auto 0 0 auto; + } + } + } +} + +.mx_SpaceRoomDirectory_list { + margin-top: 8px; + + .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 { + margin-top: 8px; + + .mx_SpaceRoomDirectory_subspace_info { + display: flex; + flex-direction: row; + align-items: center; + margin-bottom: 8px; + color: $secondary-fg-color; + font-weight: $font-semi-bold; + font-size: $font-12px; + line-height: $font-15px; + + .mx_BaseAvatar { + margin-right: 12px; + vertical-align: middle; + } + + .mx_BaseAvatar_image { + border-radius: 8px; + } + + .mx_SpaceRoomDirectory_actions { + text-align: right; + height: min-content; + margin-left: auto; + margin-right: 16px; + } + } + + .mx_SpaceRoomDirectory_subspace_children { + margin-left: 12px; + border-left: 2px solid $space-button-outline-color; + padding-left: 24px; + } + } + + .mx_SpaceRoomDirectory_roomTile { + padding: 16px; + border-radius: 8px; + border: 1px solid $space-button-outline-color; + margin: 8px 0 16px; + display: flex; + min-height: 76px; + box-sizing: border-box; + + &.mx_AccessibleButton:hover { + background-color: rgba(141, 151, 165, 0.1); + } + + .mx_BaseAvatar { + margin-right: 16px; + margin-top: 6px; + } + + .mx_SpaceRoomDirectory_roomTile_info { + display: inline-block; + font-size: $font-15px; + flex-grow: 1; + height: min-content; + margin: auto 0; + + .mx_SpaceRoomDirectory_roomTile_name { + font-weight: $font-semi-bold; + line-height: $font-18px; + } + .mx_SpaceRoomDirectory_roomTile_topic { + line-height: $font-24px; + color: $secondary-fg-color; + } + } + + .mx_SpaceRoomDirectory_roomTile_memberCount { + position: relative; + margin: auto 0 auto 24px; + padding: 0 0 0 28px; + line-height: $font-24px; + display: inline-block; + width: 32px; + + &::before { + position: absolute; + content: ''; + width: 24px; + height: 24px; + top: 0; + left: 0; + mask-position: center; + mask-repeat: no-repeat; + mask-size: contain; + background-color: $secondary-fg-color; + mask-image: url('$(res)/img/element-icons/community-members.svg'); + } + } + + .mx_SpaceRoomDirectory_actions { + width: 180px; + text-align: right; + height: min-content; + margin: auto 0 auto 28px; + + .mx_AccessibleButton { + vertical-align: middle; + + & + .mx_AccessibleButton { + margin-left: 24px; + } + } + } + } + + .mx_SpaceRoomDirectory_actions { + .mx_SpaceRoomDirectory_actionsText { + font-weight: normal; + font-size: $font-12px; + line-height: $font-15px; + color: $secondary-fg-color; + } + + .mx_Checkbox { + display: inline-block; + } + } +} diff --git a/res/css/structures/_SpaceRoomView.scss b/res/css/structures/_SpaceRoomView.scss new file mode 100644 index 0000000000..38310d39a9 --- /dev/null +++ b/res/css/structures/_SpaceRoomView.scss @@ -0,0 +1,336 @@ +/* +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; + +.mx_SpaceRoomView { + .mx_MainSplit > div:first-child { + padding: 80px 60px; + flex-grow: 1; + + 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_FormButton { + 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_landing { + overflow-y: auto; + + > .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_memberCount { + position: relative; + margin-left: 24px; + padding: 0 0 0 28px; + line-height: $font-24px; + vertical-align: text-bottom; + + &::before { + position: absolute; + content: ''; + width: 24px; + height: 24px; + top: 0; + left: 0; + mask-position: center; + mask-repeat: no-repeat; + mask-size: contain; + background-color: $accent-color; + mask-image: url('$(res)/img/element-icons/community-members.svg'); + } + } + } + + .mx_SpaceRoomView_landing_topic { + font-size: $font-15px; + } + + .mx_SpaceRoomView_landing_joinButtons { + margin-top: 24px; + + .mx_FormButton { + padding: 8px 22px; + } + } + + .mx_SpaceRoomView_landing_adminButtons { + margin-top: 32px; + + .mx_AccessibleButton { + position: relative; + width: 160px; + height: 124px; + box-sizing: border-box; + padding: 72px 16px 0; + border-radius: 12px; + border: 1px solid $space-button-outline-color; + margin-right: 28px; + margin-bottom: 28px; + 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_inviteButton { + &::before { + background-color: $accent-color; + } + + &::after { + mask-image: url('$(res)/img/element-icons/room/invite.svg'); + } + } + + &.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_SpaceRoomDirectory_list { + max-width: 600px; + + .mx_SpaceRoomDirectory_roomTile_actions { + display: none; + } + } + } + + .mx_SpaceRoomView_privateScope { + .mx_RadioButton { + width: $SpaceRoomViewInnerWidth; + border-radius: 8px; + border: 1px solid $space-button-outline-color; + padding: 16px 16px 16px 72px; + margin-top: 36px; + cursor: pointer; + box-sizing: border-box; + position: relative; + + > div:first-of-type { + // hide radio dot + display: none; + } + + .mx_RadioButton_content { + margin: 0; + + > h3 { + margin: 0 0 4px; + font-size: $font-15px; + font-weight: $font-semi-bold; + line-height: $font-18px; + } + + > div { + color: $secondary-fg-color; + font-size: $font-15px; + line-height: $font-24px; + } + } + + &::before { + content: ""; + position: absolute; + height: 32px; + width: 32px; + top: 24px; + left: 20px; + background-color: $secondary-fg-color; + mask-repeat: no-repeat; + mask-position: center; + mask-size: contain; + } + } + + .mx_RadioButton_checked { + border-color: $accent-color; + + .mx_RadioButton_content { + > div { + color: $primary-fg-color; + } + } + + &::before { + background-color: $accent-color; + } + } + + .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'); + } + } + } +} diff --git a/res/css/structures/_UserMenu.scss b/res/css/structures/_UserMenu.scss index f2577938e5..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 @@ -128,7 +129,7 @@ limitations under the License. } .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 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..0c9d8e3840 --- /dev/null +++ b/res/css/views/dialogs/_AddExistingToSpaceDialog.scss @@ -0,0 +1,185 @@ +/* +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; + + .mx_Dialog_title { + display: flex; + + .mx_BaseAvatar { + display: inline-flex; + margin: 5px 16px 5px 5px; + vertical-align: middle; + } + + .mx_BaseAvatar_image { + border-radius: 8px; + margin: 0; + vertical-align: unset; + } + + > 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; + } + + .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 { + .mx_AddExistingToSpaceDialog_noResults { + 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-12px; + line-height: $font-15px; + + > * { + 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/_SpaceSettingsDialog.scss b/res/css/views/dialogs/_SpaceSettingsDialog.scss new file mode 100644 index 0000000000..c1fa539e9b --- /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_FormButton { + padding: 8px 22px; + } +} diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index da94f914f7..5841cf2853 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -257,17 +257,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; @@ -531,14 +527,14 @@ $left-gutter: 64px; display: inline-block; visibility: hidden; cursor: pointer; - top: 6px; - right: 6px; + top: 8px; + right: 8px; width: 19px; height: 19px; background-color: $message-action-bar-fg-color; } .mx_EventTile_buttonBottom { - top: 31px; + top: 33px; } .mx_EventTile_copyButton { mask-image: url($copy-button-url); diff --git a/res/css/views/rooms/_GroupLayout.scss b/res/css/views/rooms/_GroupLayout.scss index 543e6ed685..903fabc8fd 100644 --- a/res/css/views/rooms/_GroupLayout.scss +++ b/res/css/views/rooms/_GroupLayout.scss @@ -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/_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..dea1b58741 100644 --- a/res/css/views/rooms/_MessageComposer.scss +++ b/res/css/views/rooms/_MessageComposer.scss @@ -227,18 +227,6 @@ 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_emoji::before { mask-image: url('$(res)/img/element-icons/room/composer/emoji.svg'); } @@ -247,6 +235,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/_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 66e1b827d0..d49ed4b736 100644 --- a/res/css/views/rooms/_RoomList.scss +++ b/res/css/views/rooms/_RoomList.scss @@ -19,7 +19,10 @@ 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'); 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/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..2a11ec9f23 --- /dev/null +++ b/res/css/views/spaces/_SpaceCreateMenu.scss @@ -0,0 +1,138 @@ +/* +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. +*/ + +// TODO: the space panel currently does not have a fixed width, +// just the headers at each level have a max-width of 150px +// so this will look slightly off for now. We should probably use css grid for the whole main layout... +$spacePanelWidth: 200px; + +.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 { + position: relative; + padding: 16px 32px 16px 72px; + width: 432px; + box-sizing: border-box; + border-radius: 8px; + border: 1px solid $input-darker-bg-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: 32px; + background-color: $tertiary-fg-color; + } + + &:hover { + border-color: $accent-color; + + &::before { + background-color: $accent-color; + } + + > span { + color: $primary-fg-color; + } + } + } + + .mx_SpaceCreateMenuType_public::before { + mask-image: url('$(res)/img/globe.svg'); + mask-size: 26px; + } + .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_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/_SpacePublicShare.scss b/res/css/views/spaces/_SpacePublicShare.scss new file mode 100644 index 0000000000..9ba0549ae3 --- /dev/null +++ b/res/css/views/spaces/_SpacePublicShare.scss @@ -0,0 +1,60 @@ +/* +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 { + border: 1px solid $space-button-outline-color; + box-sizing: border-box; + border-radius: 8px; + padding: 12px 24px 12px 52px; + margin-top: 16px; + width: $SpaceRoomViewInnerWidth; + font-size: $font-15px; + line-height: $font-24px; + position: relative; + display: flex; + + > span { + color: #368bd6; + margin-left: auto; + } + + &:hover { + background-color: rgba(141, 151, 165, 0.1); + } + + &::before { + content: ""; + position: absolute; + width: 30px; + height: 30px; + mask-repeat: no-repeat; + mask-size: contain; + mask-position: center; + background: $muted-fg-color; + left: 12px; + top: 9px; + } + + &.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/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/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/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/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index 08fe2e9f57..0de5e69782 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -123,6 +123,7 @@ $roomsublist-divider-color: $primary-fg-color; $roomsublist-skeleton-ui-bg: linear-gradient(180deg, #3e444c 0%, #3e444c00 100%); $groupFilterPanel-divider-color: $roomlist-header-color; +$space-button-outline-color: rgba(141, 151, 165, 0.2); $roomtile-preview-color: $secondary-fg-color; $roomtile-default-badge-bg-color: #61708b; @@ -258,6 +259,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..8c5f20178b 100644 --- a/res/themes/legacy-dark/css/_legacy-dark.scss +++ b/res/themes/legacy-dark/css/_legacy-dark.scss @@ -120,6 +120,7 @@ $roomsublist-divider-color: $primary-fg-color; $roomsublist-skeleton-ui-bg: linear-gradient(180deg, #3e444c 0%, #3e444c00 100%); $groupFilterPanel-divider-color: $roomlist-header-color; +$space-button-outline-color: rgba(141, 151, 165, 0.2); $roomtile-preview-color: #9e9e9e; $roomtile-default-badge-bg-color: #61708b; diff --git a/res/themes/legacy-light/css/_legacy-light.scss b/res/themes/legacy-light/css/_legacy-light.scss index a740ba155c..3ba10a68ea 100644 --- a/res/themes/legacy-light/css/_legacy-light.scss +++ b/res/themes/legacy-light/css/_legacy-light.scss @@ -187,6 +187,7 @@ $roomsublist-divider-color: $primary-fg-color; $roomsublist-skeleton-ui-bg: linear-gradient(180deg, #ffffff 0%, #ffffff00 100%); $groupFilterPanel-divider-color: $roomlist-header-color; +$space-button-outline-color: #E3E8F0; $roomtile-preview-color: #9e9e9e; $roomtile-default-badge-bg-color: #61708b; diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index 1c89d83c01..76bf2ddc21 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -181,6 +181,7 @@ $roomsublist-divider-color: $primary-fg-color; $roomsublist-skeleton-ui-bg: linear-gradient(180deg, #ffffff 0%, #ffffff00 100%); $groupFilterPanel-divider-color: $roomlist-header-color; +$space-button-outline-color: #E3E8F0; $roomtile-preview-color: $secondary-fg-color; $roomtile-default-badge-bg-color: #61708b; 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/src/@types/global.d.ts b/src/@types/global.d.ts index 2a28c8e43f..4aa6df5488 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -37,6 +37,8 @@ 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"; declare global { interface Window { @@ -66,6 +68,8 @@ declare global { mxCountlyAnalytics: typeof CountlyAnalytics; mxUserActivity: UserActivity; mxModalWidgetStore: ModalWidgetStore; + mxVoipUserMapper: VoipUserMapper; + mxSpaceStore: SpaceStoreClass; } interface Document { diff --git a/src/Avatar.ts b/src/Avatar.ts index 60bdfdcf75..e2557e21a8 100644 --- a/src/Avatar.ts +++ b/src/Avatar.ts @@ -165,6 +165,9 @@ export function avatarUrlForRoom(room: Room, width: number, height: number, resi return explicitRoomAvatar; } + // 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) { diff --git a/src/BasePlatform.ts b/src/BasePlatform.ts index d0d5e60ce8..9d7077097b 100644 --- a/src/BasePlatform.ts +++ b/src/BasePlatform.ts @@ -131,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. @@ -240,6 +248,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 || ""; diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index 41a5941092..8621f441de 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"; @@ -84,10 +83,19 @@ 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 { roomForVirtualRoom, getOrCreateVirtualRoomForRoom } from './VoipUserMapper'; +import VoipUserMapper from './VoipUserMapper'; import { addManagedHybridWidget, isManagedHybridWidgetEnabled } from './widgets/ManagedHybrid'; +import { randomUppercaseString, randomLowercaseString } from "matrix-js-sdk/src/randomstring"; -const CHECK_PSTN_SUPPORT_ATTEMPTS = 3; +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', @@ -96,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 @@ -126,7 +157,12 @@ export default class CallHandler { 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) { @@ -140,9 +176,9 @@ export default class CallHandler { * 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) { + public static roomIdForCall(call: MatrixCall): string { if (!call) return null; - return roomForVirtualRoom(call.roomId) || call.roomId; + return VoipUserMapper.sharedInstance().nativeRoomForVirtualRoom(call.roomId) || call.roomId; } start() { @@ -163,7 +199,7 @@ export default class CallHandler { MatrixClientPeg.get().on('Call.incoming', this.onCallIncoming); } - this.checkForPstnSupport(CHECK_PSTN_SUPPORT_ATTEMPTS); + this.checkProtocols(CHECK_PROTOCOLS_ATTEMPTS); } stop() { @@ -177,33 +213,73 @@ export default class CallHandler { } } - private async checkForPstnSupport(maxTries) { + private async checkProtocols(maxTries) { try { const protocols = await MatrixClientPeg.get().getThirdpartyProtocols(); - if (protocols['im.vector.protocol.pstn'] !== undefined) { - this.supportsPstnProtocol = protocols['im.vector.protocol.pstn']; - } else if (protocols['m.protocol.pstn'] !== undefined) { - this.supportsPstnProtocol = protocols['m.protocol.pstn']; + + 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 pstn protocol support and no retries remain: assuming no support", e); + console.log("Failed to check for protocol support and no retries remain: assuming no support", e); } else { - console.log("Failed to check for pstn protocol support: will retry", e); + console.log("Failed to check for protocol support: will retry", e); this.pstnSupportCheckTimer = setTimeout(() => { - this.checkForPstnSupport(maxTries - 1); + this.checkProtocols(maxTries - 1); }, 10000); } } } - getSupportsPstnProtocol() { + 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 @@ -550,9 +626,11 @@ export default class CallHandler { Analytics.trackEvent('voip', 'placeCall', 'type', type); CountlyAnalytics.instance.trackStartCall(roomId, type === PlaceCallType.Video, false); - const mappedRoomId = (await getOrCreateVirtualRoomForRoom(roomId)) || 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 + " seconds"); const call = createNewMatrixCall(MatrixClientPeg.get(), mappedRoomId); this.calls.set(roomId, call); @@ -628,6 +706,14 @@ export default class CallHandler { 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; + } + const members = room.getJoinedMembers(); if (members.length <= 1) { Modal.createTrackedDialog('Call Handler', 'Cannot place call with self', ErrorDialog, { @@ -681,6 +767,12 @@ export default class CallHandler { Analytics.trackEvent('voip', 'receiveCall', 'type', call.type); 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': @@ -779,8 +871,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}); @@ -796,6 +889,7 @@ export default class CallHandler { isAudioOnly: type === 'voice', domain: jitsiDomain, auth: jitsiAuth, + roomName: room.name, }; const widgetId = ( diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts index bb4be663b6..98ca446532 100644 --- a/src/MatrixClientPeg.ts +++ b/src/MatrixClientPeg.ts @@ -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, 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/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/RoomInvite.js b/src/RoomInvite.js index 06d3fb04e8..503411d2b3 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, KIND_SPACE_INVITE} from "./components/views/dialogs/InviteDialog"; import CommunityPrototypeInviteDialog from "./components/views/dialogs/CommunityPrototypeInviteDialog"; import {CommunityPrototypeStore} from "./stores/CommunityPrototypeStore"; @@ -50,10 +50,13 @@ export function showStartChatInviteDialog(initialText) { } export function showRoomInviteDialog(roomId) { + const isSpace = MatrixClientPeg.get()?.getRoom(roomId)?.isSpaceRoom(); // 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", isSpace ? "Space" : "Room", InviteDialog, { + kind: isSpace ? KIND_SPACE_INVITE : KIND_INVITE, + roomId, + }, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true, ); } diff --git a/src/SecurityManager.ts b/src/SecurityManager.ts index 220320470a..03cbe88c22 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]) { diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx index a39ad33b04..6b5f261374 100644 --- a/src/SlashCommands.tsx +++ b/src/SlashCommands.tsx @@ -1040,9 +1040,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"); } @@ -1182,7 +1180,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) { 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 index a4f5822065..d919615349 100644 --- a/src/VoipUserMapper.ts +++ b/src/VoipUserMapper.ts @@ -14,66 +14,97 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { ensureDMExists, findDMForUser } from './createRoom'; +import { ensureVirtualRoomExists, findDMForUser } from './createRoom'; import { MatrixClientPeg } from "./MatrixClientPeg"; import DMRoomMap from "./utils/DMRoomMap"; -import SdkConfig from "./SdkConfig"; +import CallHandler, { VIRTUAL_ROOM_EVENT_TYPE } from './CallHandler'; +import { Room } from 'matrix-js-sdk/src/models/room'; -// Functions for mapping users & rooms for the voip_mxid_translate_pattern -// config option +// Functions for mapping virtual users & rooms. Currently the only lookup +// is sip virtual: there could be others in the future. -export function voipUserMapperEnabled(): boolean { - return SdkConfig.get()['voip_mxid_translate_pattern'] !== undefined; -} +export default class VoipUserMapper { + private virtualRoomIdCache = new Set(); -// only exported for tests -export function userToVirtualUser(userId: string, templateString?: string): string { - if (templateString === undefined) templateString = SdkConfig.get()['voip_mxid_translate_pattern']; - if (!templateString) return null; - return templateString.replace('${mxid}', encodeURIComponent(userId).replace(/%/g, '=').toLowerCase()); -} + public static sharedInstance(): VoipUserMapper { + if (window.mxVoipUserMapper === undefined) window.mxVoipUserMapper = new VoipUserMapper(); + return window.mxVoipUserMapper; + } -// only exported for tests -export function virtualUserToUser(userId: string, templateString?: string): string { - if (templateString === undefined) templateString = SdkConfig.get()['voip_mxid_translate_pattern']; - if (!templateString) return null; + private async userToVirtualUser(userId: string): Promise { + const results = await CallHandler.sharedInstance().sipVirtualLookup(userId); + if (results.length === 0) return null; + return results[0].userid; + } - const regexString = templateString.replace('${mxid}', '(.+)'); + public async getOrCreateVirtualRoomForRoom(roomId: string):Promise { + const userId = DMRoomMap.shared().getUserIdForRoomId(roomId); + if (!userId) return null; - const match = userId.match('^' + regexString + '$'); - if (!match) return null; + const virtualUser = await this.userToVirtualUser(userId); + if (!virtualUser) return null; - return decodeURIComponent(match[1].replace(/=/g, '%')); -} + const virtualRoomId = await ensureVirtualRoomExists(MatrixClientPeg.get(), virtualUser, roomId); + MatrixClientPeg.get().setRoomAccountData(virtualRoomId, VIRTUAL_ROOM_EVENT_TYPE, { + native_room: roomId, + }); -async function getOrCreateVirtualRoomForUser(userId: string):Promise { - const virtualUser = userToVirtualUser(userId); - if (!virtualUser) return null; + return virtualRoomId; + } - return await ensureDMExists(MatrixClientPeg.get(), virtualUser); -} + 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; + } -export async function getOrCreateVirtualRoomForRoom(roomId: string):Promise { - const user = DMRoomMap.shared().getUserIdForRoomId(roomId); - if (!user) return null; - return getOrCreateVirtualRoomForUser(user); -} + public isVirtualRoom(room: Room):boolean { + if (this.nativeRoomForVirtualRoom(room.roomId)) return true; -export function roomForVirtualRoom(roomId: string):string { - const virtualUser = DMRoomMap.shared().getUserIdForRoomId(roomId); - if (!virtualUser) return null; - const realUser = virtualUserToUser(virtualUser); - const room = findDMForUser(MatrixClientPeg.get(), realUser); - if (room) { - return room.roomId; - } else { - return null; + 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) { + 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); + } } } - -export function isVirtualRoom(roomId: string):boolean { - const virtualUser = DMRoomMap.shared().getUserIdForRoomId(roomId); - if (!virtualUser) return null; - const realUser = virtualUserToUser(virtualUser); - return Boolean(realUser); -} 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/autocomplete/UserProvider.tsx b/src/autocomplete/UserProvider.tsx index 32eea55b0b..7fc01daef9 100644 --- a/src/autocomplete/UserProvider.tsx +++ b/src/autocomplete/UserProvider.tsx @@ -155,6 +155,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 aab7701f26..726ff547ff 100644 --- a/src/components/structures/ContextMenu.tsx +++ b/src/components/structures/ContextMenu.tsx @@ -76,6 +76,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(); @@ -299,7 +300,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 +366,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 diff --git a/src/components/structures/HostSignupAction.tsx b/src/components/structures/HostSignupAction.tsx index 39185acd05..9cf84a9379 100644 --- a/src/components/structures/HostSignupAction.tsx +++ b/src/components/structures/HostSignupAction.tsx @@ -21,6 +21,7 @@ import { } from "../views/context_menus/IconizedContextMenu"; import { _t } from "../../languageHandler"; import { HostSignupStore } from "../../stores/HostSignupStore"; +import SdkConfig from "../../SdkConfig"; interface IProps {} @@ -32,11 +33,21 @@ export default class HostSignupAction extends React.PureComponent diff --git a/src/components/structures/LeftPanel.tsx b/src/components/structures/LeftPanel.tsx index 4445ff3ff8..82dd9443cc 100644 --- a/src/components/structures/LeftPanel.tsx +++ b/src/components/structures/LeftPanel.tsx @@ -39,6 +39,7 @@ import { OwnProfileStore } from "../../stores/OwnProfileStore"; import { MatrixClientPeg } from "../../MatrixClientPeg"; import RoomListNumResults from "../views/rooms/RoomListNumResults"; import LeftPanelWidget from "./LeftPanelWidget"; +import SpacePanel from "../views/spaces/SpacePanel"; interface IProps { isMinimized: boolean; @@ -388,12 +389,19 @@ 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; + // Currently TagPanel.enableTagPanel is disabled when Legacy Communities are disabled so for now + // ignore it and force the rendering of SpacePanel if that Labs flag is enabled. + if (SettingsStore.getValue("feature_spaces")) { + leftLeftPanel = ; + } else if (this.state.showGroupFilterPanel) { + leftLeftPanel = ( +
+ + {SettingsStore.getValue("feature_custom_tags") ? : null} +
+ ); + } const roomList = { const containerClasses = classNames({ "mx_LeftPanel": true, - "mx_LeftPanel_hasGroupFilterPanel": !!groupFilterPanel, "mx_LeftPanel_minimized": this.props.isMinimized, }); @@ -417,7 +424,7 @@ export default class LeftPanel extends React.Component { return (
- {groupFilterPanel} + {leftLeftPanel}