diff --git a/CHANGELOG.md b/CHANGELOG.md index 73b383d76d..4d65a524d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,122 @@ +Changes in [3.27.0](https://github.com/vector-im/element-desktop/releases/tag/v3.27.0) (2021-07-02) +=================================================================================================== + +## 🔒 SECURITY FIXES + * Sanitize untrusted variables from message previews before translation + Fixes vector-im/element-web#18314 + +## ✨ Features + * Fix editing of `` & ` & `` + [\#6469](https://github.com/matrix-org/matrix-react-sdk/pull/6469) + Fixes vector-im/element-web#18211 + * Zoom images in lightbox to where the cursor points + [\#6418](https://github.com/matrix-org/matrix-react-sdk/pull/6418) + Fixes vector-im/element-web#17870 + * Avoid hitting the settings store from TextForEvent + [\#6205](https://github.com/matrix-org/matrix-react-sdk/pull/6205) + Fixes vector-im/element-web#17650 + * Initial MSC3083 + MSC3244 support + [\#6212](https://github.com/matrix-org/matrix-react-sdk/pull/6212) + Fixes vector-im/element-web#17686 and vector-im/element-web#17661 + * Navigate to the first room with notifications when clicked on space notification dot + [\#5974](https://github.com/matrix-org/matrix-react-sdk/pull/5974) + * Add matrix: to the list of permitted URL schemes + [\#6388](https://github.com/matrix-org/matrix-react-sdk/pull/6388) + * Add "Copy Link" to room context menu + [\#6374](https://github.com/matrix-org/matrix-react-sdk/pull/6374) + * 💭 Message bubble layout + [\#6291](https://github.com/matrix-org/matrix-react-sdk/pull/6291) + Fixes vector-im/element-web#4635, vector-im/element-web#17773 vector-im/element-web#16220 and vector-im/element-web#7687 + * Play only one audio file at a time + [\#6417](https://github.com/matrix-org/matrix-react-sdk/pull/6417) + Fixes vector-im/element-web#17439 + * Move download button for media to the action bar + [\#6386](https://github.com/matrix-org/matrix-react-sdk/pull/6386) + Fixes vector-im/element-web#17943 + * Improved display of one-to-one call history with summary boxes for each call + [\#6121](https://github.com/matrix-org/matrix-react-sdk/pull/6121) + Fixes vector-im/element-web#16409 + * Notification settings UI refresh + [\#6352](https://github.com/matrix-org/matrix-react-sdk/pull/6352) + Fixes vector-im/element-web#17782 + * Fix EventIndex double handling events and erroring + [\#6385](https://github.com/matrix-org/matrix-react-sdk/pull/6385) + Fixes vector-im/element-web#18008 + * Improve reply rendering + [\#3553](https://github.com/matrix-org/matrix-react-sdk/pull/3553) + Fixes vector-im/riot-web#9217, vector-im/riot-web#7633, vector-im/riot-web#7530, vector-im/riot-web#7169, vector-im/riot-web#7151, vector-im/riot-web#6692 vector-im/riot-web#6579 and vector-im/element-web#17440 + +## 🐛 Bug Fixes + * Fix CreateRoomDialog exploding when making public room outside of a space + [\#6493](https://github.com/matrix-org/matrix-react-sdk/pull/6493) + * Fix regression where registration would soft-crash on captcha + [\#6505](https://github.com/matrix-org/matrix-react-sdk/pull/6505) + Fixes vector-im/element-web#18284 + * only send join rule event if we have a join rule to put in it + [\#6517](https://github.com/matrix-org/matrix-react-sdk/pull/6517) + * Improve the new download button's discoverability and interactions. + [\#6510](https://github.com/matrix-org/matrix-react-sdk/pull/6510) + * Fix voice recording UI looking broken while microphone permissions are being requested. + [\#6479](https://github.com/matrix-org/matrix-react-sdk/pull/6479) + Fixes vector-im/element-web#18223 + * Match colors of room and user avatars in DMs + [\#6393](https://github.com/matrix-org/matrix-react-sdk/pull/6393) + Fixes vector-im/element-web#2449 + * Fix onPaste handler to work with copying files from Finder + [\#5389](https://github.com/matrix-org/matrix-react-sdk/pull/5389) + Fixes vector-im/element-web#15536 and vector-im/element-web#16255 + * Fix infinite pagination loop when offline + [\#6478](https://github.com/matrix-org/matrix-react-sdk/pull/6478) + Fixes vector-im/element-web#18242 + * Fix blurhash rounded corners missing regression + [\#6467](https://github.com/matrix-org/matrix-react-sdk/pull/6467) + Fixes vector-im/element-web#18110 + * Fix position of the space hierarchy spinner + [\#6462](https://github.com/matrix-org/matrix-react-sdk/pull/6462) + Fixes vector-im/element-web#18182 + * Fix display of image messages that lack thumbnails + [\#6456](https://github.com/matrix-org/matrix-react-sdk/pull/6456) + Fixes vector-im/element-web#18175 + * Fix crash with large audio files. + [\#6436](https://github.com/matrix-org/matrix-react-sdk/pull/6436) + Fixes vector-im/element-web#18149 + * Make diff colors in codeblocks more pleasant + [\#6355](https://github.com/matrix-org/matrix-react-sdk/pull/6355) + Fixes vector-im/element-web#17939 + * Show the correct audio file duration while loading the file. + [\#6435](https://github.com/matrix-org/matrix-react-sdk/pull/6435) + Fixes vector-im/element-web#18160 + * Fix various timeline settings not applying immediately. + [\#6261](https://github.com/matrix-org/matrix-react-sdk/pull/6261) + Fixes vector-im/element-web#17748 + * Fix issues with room list duplication + [\#6391](https://github.com/matrix-org/matrix-react-sdk/pull/6391) + Fixes vector-im/element-web#14508 + * Fix grecaptcha throwing useless error sometimes + [\#6401](https://github.com/matrix-org/matrix-react-sdk/pull/6401) + Fixes vector-im/element-web#15142 + * Update Emojibase and Twemoji and switch to IamCal (Slack-style) shortcodes + [\#6347](https://github.com/matrix-org/matrix-react-sdk/pull/6347) + Fixes vector-im/element-web#13857 and vector-im/element-web#13334 + * Respect compound emojis in default avatar initial generation + [\#6397](https://github.com/matrix-org/matrix-react-sdk/pull/6397) + Fixes vector-im/element-web#18040 + * Fix bug where the 'other homeserver' field in the server selection dialog would become briefly focus and then unfocus when clicked. + [\#6394](https://github.com/matrix-org/matrix-react-sdk/pull/6394) + Fixes vector-im/element-web#18031 + * Standardise spelling and casing of homeserver, identity server, and integration manager + [\#6365](https://github.com/matrix-org/matrix-react-sdk/pull/6365) + * Fix widgets not receiving decrypted events when they have permission. + [\#6371](https://github.com/matrix-org/matrix-react-sdk/pull/6371) + Fixes vector-im/element-web#17615 + * Prevent client hangs when calculating blurhashes + [\#6366](https://github.com/matrix-org/matrix-react-sdk/pull/6366) + Fixes vector-im/element-web#17945 + * Exclude state events from widgets reading room events + [\#6378](https://github.com/matrix-org/matrix-react-sdk/pull/6378) + * Cache feature_spaces\* flags to improve performance + [\#6381](https://github.com/matrix-org/matrix-react-sdk/pull/6381) + Changes in [3.26.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.26.0) (2021-07-19) ===================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.26.0-rc.1...v3.26.0) diff --git a/README.md b/README.md index b3e96ef001..67e5e12f59 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ All code lands on the `develop` branch - `master` is only used for stable releas **Please file PRs against `develop`!!** Please follow the standard Matrix contributor's guide: -https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.rst +https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.md Please follow the Matrix JS/React code style as per: https://github.com/matrix-org/matrix-react-sdk/blob/master/code_style.md diff --git a/package.json b/package.json index b73462d188..704315ca69 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "3.26.0", + "version": "3.27.0", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { @@ -80,7 +80,7 @@ "katex": "^0.12.0", "linkifyjs": "^2.1.9", "lodash": "^4.17.20", - "matrix-js-sdk": "12.1.0", + "matrix-js-sdk": "12.2.0", "matrix-widget-api": "^0.1.0-beta.15", "minimist": "^1.2.5", "opus-recorder": "^8.0.3", @@ -153,7 +153,7 @@ "enzyme": "^3.11.0", "eslint": "7.18.0", "eslint-config-google": "^0.14.0", - "eslint-plugin-matrix-org": "github:matrix-org/eslint-plugin-matrix-org#main", + "eslint-plugin-matrix-org": "github:matrix-org/eslint-plugin-matrix-org#2306b3d4da4eba908b256014b979f1d3d43d2945", "eslint-plugin-react": "^7.22.0", "eslint-plugin-react-hooks": "^4.2.0", "glob": "^7.1.6", diff --git a/res/css/_common.scss b/res/css/_common.scss index b128a82442..6b4e109b3a 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -104,8 +104,8 @@ a:visited { input[type=text], input[type=search], input[type=password] { + font-family: inherit; padding: 9px; - font-family: $font-family; font-size: $font-14px; font-weight: 600; min-width: 0; @@ -146,7 +146,6 @@ input[type=text], input[type=password], textarea { /* Required by Firefox */ textarea { - font-family: $font-family; color: $primary-fg-color; } diff --git a/res/css/_components.scss b/res/css/_components.scss index 396a5560a6..76551b51f8 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -67,7 +67,6 @@ @import "./views/dialogs/_AddExistingToSpaceDialog.scss"; @import "./views/dialogs/_AddressPickerDialog.scss"; @import "./views/dialogs/_Analytics.scss"; -@import "./views/dialogs/_BetaFeedbackDialog.scss"; @import "./views/dialogs/_BugReportDialog.scss"; @import "./views/dialogs/_ChangelogDialog.scss"; @import "./views/dialogs/_ChatCreateOrReuseChatDialog.scss"; @@ -76,16 +75,21 @@ @import "./views/dialogs/_CreateCommunityPrototypeDialog.scss"; @import "./views/dialogs/_CreateGroupDialog.scss"; @import "./views/dialogs/_CreateRoomDialog.scss"; +@import "./views/dialogs/_CreateSubspaceDialog.scss"; @import "./views/dialogs/_DeactivateAccountDialog.scss"; @import "./views/dialogs/_DevtoolsDialog.scss"; @import "./views/dialogs/_EditCommunityPrototypeDialog.scss"; @import "./views/dialogs/_FeedbackDialog.scss"; @import "./views/dialogs/_ForwardDialog.scss"; +@import "./views/dialogs/_GenericFeatureFeedbackDialog.scss"; @import "./views/dialogs/_GroupAddressPicker.scss"; @import "./views/dialogs/_HostSignupDialog.scss"; @import "./views/dialogs/_IncomingSasDialog.scss"; @import "./views/dialogs/_InviteDialog.scss"; +@import "./views/dialogs/_JoinRuleDropdown.scss"; @import "./views/dialogs/_KeyboardShortcutsDialog.scss"; +@import "./views/dialogs/_LeaveSpaceDialog.scss"; +@import "./views/dialogs/_ManageRestrictedJoinRuleDialog.scss"; @import "./views/dialogs/_MessageEditHistoryDialog.scss"; @import "./views/dialogs/_ModalWidgetDialog.scss"; @import "./views/dialogs/_NewSessionReviewDialog.scss"; @@ -268,6 +272,7 @@ @import "./views/voip/_CallPreview.scss"; @import "./views/voip/_CallView.scss"; @import "./views/voip/_CallViewForRoom.scss"; +@import "./views/voip/_CallViewSidebar.scss"; @import "./views/voip/_DialPad.scss"; @import "./views/voip/_DialPadContextMenu.scss"; @import "./views/voip/_DialPadModal.scss"; diff --git a/res/css/structures/_SpacePanel.scss b/res/css/structures/_SpacePanel.scss index e64057d16c..1dea6332f5 100644 --- a/res/css/structures/_SpacePanel.scss +++ b/res/css/structures/_SpacePanel.scss @@ -297,7 +297,7 @@ $activeBorderColor: $secondary-fg-color; .mx_SpaceButton:hover, .mx_SpaceButton:focus-within, .mx_SpaceButton_hasMenuOpen { - &:not(.mx_SpaceButton_home):not(.mx_SpaceButton_invite) { + &:not(.mx_SpaceButton_invite) { // Hide the badge container on hover because it'll be a menu button .mx_SpacePanel_badgeContainer { width: 0; @@ -368,6 +368,14 @@ $activeBorderColor: $secondary-fg-color; .mx_SpacePanel_iconExplore::before { mask-image: url('$(res)/img/element-icons/roomlist/browse.svg'); } + + .mx_SpacePanel_noIcon { + display: none; + + & + .mx_IconizedContextMenu_label { + padding-left: 5px !important; // override default iconized label style to align with header + } + } } diff --git a/res/css/structures/_SpaceRoomDirectory.scss b/res/css/structures/_SpaceRoomDirectory.scss index 7925686bf1..cb91aa3c7d 100644 --- a/res/css/structures/_SpaceRoomDirectory.scss +++ b/res/css/structures/_SpaceRoomDirectory.scss @@ -61,6 +61,7 @@ limitations under the License. .mx_AccessibleButton_kind_link { padding: 0; + font-size: inherit; } .mx_SearchBox { @@ -190,7 +191,6 @@ limitations under the License. position: relative; padding: 8px 16px; border-radius: 8px; - min-height: 56px; box-sizing: border-box; display: grid; diff --git a/res/css/structures/_SpaceRoomView.scss b/res/css/structures/_SpaceRoomView.scss index 48b565be7f..58a4b426c2 100644 --- a/res/css/structures/_SpaceRoomView.scss +++ b/res/css/structures/_SpaceRoomView.scss @@ -234,6 +234,9 @@ $SpaceRoomViewInnerWidth: 428px; } .mx_SpaceRoomView_landing { + display: flex; + flex-direction: column; + > .mx_BaseAvatar_image, > .mx_BaseAvatar > .mx_BaseAvatar_image { border-radius: 12px; @@ -332,23 +335,22 @@ $SpaceRoomViewInnerWidth: 428px; word-wrap: break-word; } - > hr { - border: none; - height: 1px; - background-color: $groupFilterPanel-bg-color; - } - .mx_SearchBox { margin: 0 0 20px; + flex: 0; } .mx_SpaceFeedbackPrompt { - margin-bottom: 16px; + padding: 7px; // 8px - 1px border + border: 1px solid $menu-border-color; + border-radius: 8px; + width: max-content; + margin: 0 0 -40px auto; // collapse its own height to not push other components down + } - // hide the HR as we have our own - & + hr { - display: none; - } + .mx_SpaceRoomDirectory_list { + // we don't want this container to get forced into the flexbox layout + display: contents; } } @@ -504,66 +506,3 @@ $SpaceRoomViewInnerWidth: 428px; } } } - -.mx_SpaceFeedbackPrompt { - margin-top: 18px; - margin-bottom: 12px; - - > hr { - border: none; - border-top: 1px solid $input-border-color; - margin-bottom: 12px; - } - - > div { - display: flex; - flex-direction: row; - font-size: $font-15px; - line-height: $font-24px; - - > span { - color: $secondary-fg-color; - position: relative; - padding-left: 32px; - font-size: inherit; - line-height: inherit; - margin-right: auto; - - &::before { - content: ''; - position: absolute; - left: 0; - top: 2px; - height: 20px; - width: 20px; - background-color: $secondary-fg-color; - mask-repeat: no-repeat; - mask-size: contain; - mask-image: url('$(res)/img/element-icons/room/room-summary.svg'); - mask-position: center; - } - } - - .mx_AccessibleButton_kind_link { - color: $accent-color; - position: relative; - padding: 0 0 0 24px; - margin-left: 8px; - font-size: inherit; - line-height: inherit; - - &::before { - content: ''; - position: absolute; - left: 0; - height: 16px; - width: 16px; - background-color: $accent-color; - mask-repeat: no-repeat; - mask-size: contain; - mask-image: url('$(res)/img/element-icons/chat-bubbles.svg'); - mask-position: center; - } - } - } -} diff --git a/res/css/views/avatars/_BaseAvatar.scss b/res/css/views/avatars/_BaseAvatar.scss index 65e4493f19..cbddd97e18 100644 --- a/res/css/views/avatars/_BaseAvatar.scss +++ b/res/css/views/avatars/_BaseAvatar.scss @@ -27,7 +27,6 @@ limitations under the License. // https://bugzilla.mozilla.org/show_bug.cgi?id=255139 display: inline-block; user-select: none; - line-height: 1; } .mx_BaseAvatar_initial { diff --git a/res/css/views/context_menus/_IconizedContextMenu.scss b/res/css/views/context_menus/_IconizedContextMenu.scss index 204435995f..ff176eef7e 100644 --- a/res/css/views/context_menus/_IconizedContextMenu.scss +++ b/res/css/views/context_menus/_IconizedContextMenu.scss @@ -99,6 +99,10 @@ limitations under the License. .mx_IconizedContextMenu_icon + .mx_IconizedContextMenu_label { padding-left: 14px; } + + .mx_BetaCard_betaPill { + margin-left: 16px; + } } } @@ -145,12 +149,17 @@ limitations under the License. } } - .mx_IconizedContextMenu_checked { + .mx_IconizedContextMenu_checked, + .mx_IconizedContextMenu_unchecked { margin-left: 16px; margin-right: -5px; + } - &::before { - mask-image: url('$(res)/img/element-icons/roomlist/checkmark.svg'); - } + .mx_IconizedContextMenu_checked::before { + mask-image: url('$(res)/img/element-icons/roomlist/checkmark.svg'); + } + + .mx_IconizedContextMenu_unchecked::before { + content: unset; } } diff --git a/res/css/views/dialogs/_AddExistingToSpaceDialog.scss b/res/css/views/dialogs/_AddExistingToSpaceDialog.scss index 2776c477fc..42e17c8d98 100644 --- a/res/css/views/dialogs/_AddExistingToSpaceDialog.scss +++ b/res/css/views/dialogs/_AddExistingToSpaceDialog.scss @@ -50,64 +50,11 @@ limitations under the License. line-height: $font-15px; } - .mx_AddExistingToSpace_entry { - display: flex; - margin-top: 12px; - - // we can't target .mx_BaseAvatar here as it'll break the decorated avatar styling - .mx_DecoratedRoomAvatar { - margin-right: 12px; - } - - .mx_AddExistingToSpace_entry_name { - font-size: $font-15px; - line-height: 30px; - flex-grow: 1; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - margin-right: 12px; - } - - .mx_Checkbox { - align-items: center; - } - } - } - - .mx_AddExistingToSpace_section_spaces { - .mx_BaseAvatar { - margin-right: 12px; - } - - .mx_BaseAvatar_image { - border-radius: 8px; - } - } - - .mx_AddExistingToSpace_section_experimental { - position: relative; - border-radius: 8px; - margin: 12px 0; - padding: 8px 8px 8px 42px; - background-color: $header-panel-bg-color; - - font-size: $font-12px; - line-height: $font-15px; - color: $secondary-fg-color; - - &::before { - content: ''; - position: absolute; - left: 10px; - top: calc(50% - 8px); // vertical centering - height: 16px; - width: 16px; - background-color: $secondary-fg-color; - mask-repeat: no-repeat; - mask-size: contain; - mask-image: url('$(res)/img/element-icons/room/room-summary.svg'); - mask-position: center; + .mx_AccessibleButton_kind_link { + font-size: $font-12px; + line-height: $font-15px; + margin-top: 8px; + padding: 0; } } @@ -205,77 +152,106 @@ limitations under the License. min-height: 0; height: 80vh; - .mx_Dialog_title { - display: flex; - - .mx_BaseAvatar_image { - border-radius: 8px; - margin: 0; - vertical-align: unset; - } - - .mx_BaseAvatar { - display: inline-flex; - margin: auto 16px auto 5px; - vertical-align: middle; - } - - > div { - > h1 { - font-weight: $font-semi-bold; - font-size: $font-18px; - line-height: $font-22px; - margin: 0; - } - - .mx_AddExistingToSpaceDialog_onlySpace { - color: $secondary-fg-color; - font-size: $font-15px; - line-height: $font-24px; - } - } - - .mx_Dropdown_input { - border: none; - - > .mx_Dropdown_option { - padding-left: 0; - flex: unset; - height: unset; - color: $secondary-fg-color; - font-size: $font-15px; - line-height: $font-24px; - - .mx_BaseAvatar { - display: none; - } - } - - .mx_Dropdown_menu { - .mx_AddExistingToSpaceDialog_dropdownOptionActive { - color: $accent-color; - padding-right: 32px; - position: relative; - - &::before { - content: ''; - width: 20px; - height: 20px; - top: 8px; - right: 0; - position: absolute; - mask-position: center; - mask-size: contain; - mask-repeat: no-repeat; - background-color: $accent-color; - mask-image: url('$(res)/img/element-icons/roomlist/checkmark.svg'); - } - } - } - } - } - .mx_AddExistingToSpace { display: contents; } } + +.mx_SubspaceSelector { + display: flex; + + .mx_BaseAvatar_image { + border-radius: 8px; + margin: 0; + vertical-align: unset; + } + + .mx_BaseAvatar { + display: inline-flex; + margin: auto 16px auto 5px; + vertical-align: middle; + } + + > div { + > h1 { + font-weight: $font-semi-bold; + font-size: $font-18px; + line-height: $font-22px; + margin: 0; + } + } + + .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_SubspaceSelector_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_SubspaceSelector_onlySpace { + color: $secondary-fg-color; + font-size: $font-15px; + line-height: $font-24px; + } +} + +.mx_AddExistingToSpace_entry { + display: flex; + margin-top: 12px; + + .mx_DecoratedRoomAvatar, // we can't target .mx_BaseAvatar here as it'll break the decorated avatar styling + .mx_BaseAvatar.mx_RoomAvatar_isSpaceRoom { + margin-right: 12px; + } + + img.mx_RoomAvatar_isSpaceRoom, + .mx_RoomAvatar_isSpaceRoom img { + border-radius: 8px; + } + + .mx_AddExistingToSpace_entry_name { + font-size: $font-15px; + line-height: 30px; + flex-grow: 1; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + margin-right: 12px; + } + + .mx_Checkbox { + align-items: center; + } +} diff --git a/res/css/views/dialogs/_AddressPickerDialog.scss b/res/css/views/dialogs/_AddressPickerDialog.scss index 136e497994..a1147e6fbc 100644 --- a/res/css/views/dialogs/_AddressPickerDialog.scss +++ b/res/css/views/dialogs/_AddressPickerDialog.scss @@ -29,7 +29,6 @@ limitations under the License. .mx_AddressPickerDialog_input:focus { height: 26px; font-size: $font-14px; - font-family: $font-family; padding-left: 12px; padding-right: 12px; margin: 0 !important; diff --git a/res/css/views/dialogs/_ConfirmUserActionDialog.scss b/res/css/views/dialogs/_ConfirmUserActionDialog.scss index 823f4d1e28..284c171f4e 100644 --- a/res/css/views/dialogs/_ConfirmUserActionDialog.scss +++ b/res/css/views/dialogs/_ConfirmUserActionDialog.scss @@ -34,7 +34,6 @@ limitations under the License. } .mx_ConfirmUserActionDialog_reasonField { - font-family: $font-family; font-size: $font-14px; color: $primary-fg-color; background-color: $primary-bg-color; diff --git a/res/css/views/dialogs/_CreateRoomDialog.scss b/res/css/views/dialogs/_CreateRoomDialog.scss index 2678f7b4ad..e7cfbf6050 100644 --- a/res/css/views/dialogs/_CreateRoomDialog.scss +++ b/res/css/views/dialogs/_CreateRoomDialog.scss @@ -65,7 +65,7 @@ limitations under the License. .mx_CreateRoomDialog_aliasContainer { display: flex; // put margin on container so it can collapse with siblings - margin: 10px 0; + margin: 24px 0 10px; .mx_RoomAliasField { margin: 0; @@ -101,10 +101,6 @@ limitations under the License. margin-left: 30px; } - .mx_CreateRoomDialog_topic { - margin-bottom: 36px; - } - .mx_Dialog_content > .mx_SettingsFlag { margin-top: 24px; } @@ -114,4 +110,3 @@ limitations under the License. font-size: $font-12px; } } - diff --git a/res/css/views/dialogs/_CreateSubspaceDialog.scss b/res/css/views/dialogs/_CreateSubspaceDialog.scss new file mode 100644 index 0000000000..1ec4731ae6 --- /dev/null +++ b/res/css/views/dialogs/_CreateSubspaceDialog.scss @@ -0,0 +1,81 @@ +/* +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_CreateSubspaceDialog_wrapper { + .mx_Dialog { + display: flex; + flex-direction: column; + } +} + +.mx_CreateSubspaceDialog { + width: 480px; + color: $primary-fg-color; + display: flex; + flex-direction: column; + flex-wrap: nowrap; + min-height: 0; + + .mx_CreateSubspaceDialog_content { + flex-grow: 1; + + .mx_CreateSubspaceDialog_betaNotice { + padding: 12px 16px; + border-radius: 8px; + background-color: $header-panel-bg-color; + + .mx_BetaCard_betaPill { + margin-right: 8px; + vertical-align: middle; + } + } + + .mx_JoinRuleDropdown + p { + color: $muted-fg-color; + font-size: $font-12px; + } + } + + .mx_CreateSubspaceDialog_footer { + display: flex; + margin-top: 20px; + + .mx_CreateSubspaceDialog_footer_prompt { + flex-grow: 1; + font-size: $font-12px; + line-height: $font-15px; + color: $secondary-fg-color; + + > * { + vertical-align: middle; + } + } + + .mx_AccessibleButton { + display: inline-block; + align-self: center; + } + + .mx_AccessibleButton_kind_primary { + margin-left: 16px; + padding: 8px 36px; + } + + .mx_AccessibleButton_kind_link { + padding: 0; + } + } +} diff --git a/res/css/views/dialogs/_DevtoolsDialog.scss b/res/css/views/dialogs/_DevtoolsDialog.scss index 8fee740016..4d35e8d569 100644 --- a/res/css/views/dialogs/_DevtoolsDialog.scss +++ b/res/css/views/dialogs/_DevtoolsDialog.scss @@ -55,22 +55,6 @@ limitations under the License. padding-right: 24px; } -.mx_DevTools_inputCell { - display: table-cell; - width: 240px; -} - -.mx_DevTools_inputCell input { - display: inline-block; - border: 0; - border-bottom: 1px solid $input-underline-color; - padding: 0; - width: 240px; - color: $input-fg-color; - font-family: $font-family; - font-size: $font-16px; -} - .mx_DevTools_textarea { font-size: $font-12px; max-width: 684px; @@ -139,7 +123,6 @@ limitations under the License. + .mx_DevTools_tgl-btn { padding: 2px; transition: all .2s ease; - font-family: sans-serif; perspective: 100px; &::after, &::before { diff --git a/res/css/views/dialogs/_BetaFeedbackDialog.scss b/res/css/views/dialogs/_GenericFeatureFeedbackDialog.scss similarity index 90% rename from res/css/views/dialogs/_BetaFeedbackDialog.scss rename to res/css/views/dialogs/_GenericFeatureFeedbackDialog.scss index 9f5f6b512e..f83eed9c53 100644 --- a/res/css/views/dialogs/_BetaFeedbackDialog.scss +++ b/res/css/views/dialogs/_GenericFeatureFeedbackDialog.scss @@ -14,8 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_BetaFeedbackDialog { - .mx_BetaFeedbackDialog_subheading { +.mx_GenericFeatureFeedbackDialog { + .mx_GenericFeatureFeedbackDialog_subheading { color: $primary-fg-color; font-size: $font-14px; line-height: $font-20px; diff --git a/res/css/views/dialogs/_JoinRuleDropdown.scss b/res/css/views/dialogs/_JoinRuleDropdown.scss new file mode 100644 index 0000000000..c48a79af3c --- /dev/null +++ b/res/css/views/dialogs/_JoinRuleDropdown.scss @@ -0,0 +1,67 @@ +/* +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_JoinRuleDropdown { + margin-bottom: 8px; + font-weight: normal; + font-family: $font-family; + font-size: $font-14px; + color: $primary-fg-color; + + .mx_Dropdown_input { + border: 1px solid $input-border-color; + } + + .mx_Dropdown_option { + font-size: $font-14px; + line-height: $font-32px; + height: 32px; + min-height: 32px; + + > div { + padding-left: 30px; + position: relative; + + &::before { + content: ""; + position: absolute; + height: 16px; + width: 16px; + left: 6px; + top: 8px; + mask-repeat: no-repeat; + mask-position: center; + background-color: $secondary-fg-color; + } + } + } + + .mx_JoinRuleDropdown_invite::before { + mask-image: url('$(res)/img/element-icons/lock.svg'); + mask-size: contain; + } + + .mx_JoinRuleDropdown_public::before { + mask-image: url('$(res)/img/globe.svg'); + mask-size: 12px; + } + + .mx_JoinRuleDropdown_restricted::before { + mask-image: url('$(res)/img/element-icons/community-members.svg'); + mask-size: contain; + } +} + diff --git a/res/css/views/dialogs/_LeaveSpaceDialog.scss b/res/css/views/dialogs/_LeaveSpaceDialog.scss new file mode 100644 index 0000000000..c982f50e52 --- /dev/null +++ b/res/css/views/dialogs/_LeaveSpaceDialog.scss @@ -0,0 +1,96 @@ +/* +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_LeaveSpaceDialog_wrapper { + .mx_Dialog { + display: flex; + flex-direction: column; + padding: 24px 32px; + } +} + +.mx_LeaveSpaceDialog { + width: 440px; + display: flex; + flex-direction: column; + flex-wrap: nowrap; + max-height: 520px; + + .mx_Dialog_content { + flex-grow: 1; + margin: 0; + overflow-y: auto; + + .mx_RadioButton + .mx_RadioButton { + margin-top: 16px; + } + + .mx_SearchBox { + // To match the space around the title + margin: 0 0 15px 0; + flex-grow: 0; + border-radius: 8px; + } + + .mx_LeaveSpaceDialog_noResults { + display: block; + margin-top: 24px; + } + + .mx_LeaveSpaceDialog_section { + margin: 16px 0; + } + + .mx_LeaveSpaceDialog_section_warning { + position: relative; + border-radius: 8px; + margin: 12px 0 0; + padding: 12px 8px 12px 42px; + background-color: $header-panel-bg-color; + + font-size: $font-12px; + line-height: $font-15px; + color: $secondary-fg-color; + + &::before { + content: ''; + position: absolute; + left: 10px; + top: calc(50% - 8px); // vertical centering + height: 16px; + width: 16px; + background-color: $secondary-fg-color; + mask-repeat: no-repeat; + mask-size: contain; + mask-image: url('$(res)/img/element-icons/room/room-summary.svg'); + mask-position: center; + } + } + + > p { + color: $primary-fg-color; + } + } + + .mx_Dialog_buttons { + margin-top: 20px; + + .mx_Dialog_primary { + background-color: $notice-primary-color !important; // override default colour + border-color: $notice-primary-color; + } + } +} diff --git a/res/css/views/dialogs/_ManageRestrictedJoinRuleDialog.scss b/res/css/views/dialogs/_ManageRestrictedJoinRuleDialog.scss new file mode 100644 index 0000000000..91df76675a --- /dev/null +++ b/res/css/views/dialogs/_ManageRestrictedJoinRuleDialog.scss @@ -0,0 +1,150 @@ +/* +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_ManageRestrictedJoinRuleDialog_wrapper { + .mx_Dialog { + display: flex; + flex-direction: column; + } +} + +.mx_ManageRestrictedJoinRuleDialog { + width: 480px; + color: $primary-fg-color; + display: flex; + flex-direction: column; + flex-wrap: nowrap; + min-height: 0; + height: 60vh; + + .mx_SearchBox { + // To match the space around the title + margin: 0 0 15px 0; + flex-grow: 0; + } + + .mx_ManageRestrictedJoinRuleDialog_content { + flex-grow: 1; + } + + .mx_ManageRestrictedJoinRuleDialog_noResults { + display: block; + margin-top: 24px; + } + + .mx_ManageRestrictedJoinRuleDialog_section { + &:not(:first-child) { + margin-top: 24px; + } + + > h3 { + margin: 0; + color: $secondary-fg-color; + font-size: $font-12px; + font-weight: $font-semi-bold; + line-height: $font-15px; + } + + .mx_ManageRestrictedJoinRuleDialog_entry { + display: flex; + margin-top: 12px; + + > div { + flex-grow: 1; + } + + img.mx_RoomAvatar_isSpaceRoom, + .mx_RoomAvatar_isSpaceRoom img { + border-radius: 4px; + } + + .mx_ManageRestrictedJoinRuleDialog_entry_name { + margin: 0 8px; + font-size: $font-15px; + line-height: 30px; + flex-grow: 1; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + + .mx_ManageRestrictedJoinRuleDialog_entry_description { + margin-top: 8px; + font-size: $font-12px; + line-height: $font-15px; + color: $tertiary-fg-color; + } + + .mx_Checkbox { + align-items: center; + } + } + } + + .mx_ManageRestrictedJoinRuleDialog_section_spaces { + .mx_BaseAvatar { + margin-right: 12px; + } + + .mx_BaseAvatar_image { + border-radius: 8px; + } + } + + .mx_ManageRestrictedJoinRuleDialog_section_info { + position: relative; + border-radius: 8px; + margin: 12px 0; + padding: 8px 8px 8px 42px; + background-color: $header-panel-bg-color; + + font-size: $font-12px; + line-height: $font-15px; + color: $secondary-fg-color; + + &::before { + content: ''; + position: absolute; + left: 10px; + top: calc(50% - 8px); // vertical centering + height: 16px; + width: 16px; + background-color: $secondary-fg-color; + mask-repeat: no-repeat; + mask-size: contain; + mask-image: url('$(res)/img/element-icons/room/room-summary.svg'); + mask-position: center; + } + } + + .mx_ManageRestrictedJoinRuleDialog_footer { + margin-top: 20px; + + .mx_ManageRestrictedJoinRuleDialog_footer_buttons { + display: flex; + width: max-content; + margin-left: auto; + + .mx_AccessibleButton { + display: inline-block; + + & + .mx_AccessibleButton { + margin-left: 24px; + } + } + } + } +} diff --git a/res/css/views/elements/_DesktopCapturerSourcePicker.scss b/res/css/views/elements/_DesktopCapturerSourcePicker.scss index 69dde5925e..49a0a44417 100644 --- a/res/css/views/elements/_DesktopCapturerSourcePicker.scss +++ b/res/css/views/elements/_DesktopCapturerSourcePicker.scss @@ -16,57 +16,43 @@ limitations under the License. .mx_desktopCapturerSourcePicker { overflow: hidden; -} -.mx_desktopCapturerSourcePicker_tabLabels { - display: flex; - padding: 0 0 8px 0; -} + .mx_desktopCapturerSourcePicker_tab { + display: flex; + flex-wrap: wrap; + justify-content: center; + align-items: flex-start; + height: 500px; + overflow: overlay; + } -.mx_desktopCapturerSourcePicker_tabLabel, -.mx_desktopCapturerSourcePicker_tabLabel_selected { - width: 100%; - text-align: center; - border-radius: 8px; - padding: 8px 0; - font-size: $font-13px; -} + .mx_desktopCapturerSourcePicker_source { + display: flex; + flex-direction: column; + margin: 8px; + } -.mx_desktopCapturerSourcePicker_tabLabel_selected { - background-color: $tab-label-active-bg-color; - color: $tab-label-active-fg-color; -} + .mx_desktopCapturerSourcePicker_source_thumbnail { + margin: 4px; + padding: 4px; + width: 312px; + border-width: 2px; + border-radius: 8px; + border-style: solid; + border-color: transparent; -.mx_desktopCapturerSourcePicker_panel { - display: flex; - flex-wrap: wrap; - justify-content: center; - align-items: flex-start; - height: 500px; - overflow: overlay; -} + &.mx_desktopCapturerSourcePicker_source_thumbnail_selected, + &:hover, + &:focus { + border-color: $accent-color; + } + } -.mx_desktopCapturerSourcePicker_stream_button { - display: flex; - flex-direction: column; - margin: 8px; - border-radius: 4px; -} - -.mx_desktopCapturerSourcePicker_stream_button:hover, -.mx_desktopCapturerSourcePicker_stream_button:focus { - background: $roomtile-selected-bg-color; -} - -.mx_desktopCapturerSourcePicker_stream_thumbnail { - margin: 4px; - width: 312px; -} - -.mx_desktopCapturerSourcePicker_stream_name { - margin: 0 4px; - white-space: nowrap; - text-overflow: ellipsis; - overflow: hidden; - width: 312px; + .mx_desktopCapturerSourcePicker_source_name { + margin: 0 4px; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + width: 312px; + } } diff --git a/res/css/views/elements/_Dropdown.scss b/res/css/views/elements/_Dropdown.scss index 2a2508c17c..3b67e0191e 100644 --- a/res/css/views/elements/_Dropdown.scss +++ b/res/css/views/elements/_Dropdown.scss @@ -27,7 +27,7 @@ limitations under the License. display: flex; align-items: center; position: relative; - border-radius: 3px; + border-radius: 4px; border: 1px solid $strong-input-border-color; font-size: $font-12px; user-select: none; @@ -109,7 +109,7 @@ input.mx_Dropdown_option:focus { z-index: 2; margin: 0; padding: 0px; - border-radius: 3px; + border-radius: 4px; border: 1px solid $input-focused-border-color; background-color: $primary-bg-color; max-height: 200px; diff --git a/res/css/views/elements/_Field.scss b/res/css/views/elements/_Field.scss index f67da6477b..cae81dcc97 100644 --- a/res/css/views/elements/_Field.scss +++ b/res/css/views/elements/_Field.scss @@ -39,7 +39,6 @@ limitations under the License. .mx_Field select, .mx_Field textarea { font-weight: normal; - font-family: $font-family; font-size: $font-14px; border: none; // Even without a border here, we still need this avoid overlapping the rounded diff --git a/res/css/views/messages/_CallEvent.scss b/res/css/views/messages/_CallEvent.scss index 54c7df3e0b..0c1b41ca38 100644 --- a/res/css/views/messages/_CallEvent.scss +++ b/res/css/views/messages/_CallEvent.scss @@ -23,7 +23,7 @@ limitations under the License. background-color: $dark-panel-bg-color; border-radius: 8px; margin: 10px auto; - max-width: 75%; + width: 75%; box-sizing: border-box; height: 60px; @@ -43,6 +43,14 @@ limitations under the License. } } + &.mx_CallEvent_voice.mx_CallEvent_missed .mx_CallEvent_type_icon::before { + mask-image: url('$(res)/img/voip/missed-voice.svg'); + } + + &.mx_CallEvent_video.mx_CallEvent_missed .mx_CallEvent_type_icon::before { + mask-image: url('$(res)/img/voip/missed-video.svg'); + } + .mx_CallEvent_info { display: flex; flex-direction: row; diff --git a/res/css/views/messages/_MFileBody.scss b/res/css/views/messages/_MFileBody.scss index 403f671673..d941a8132f 100644 --- a/res/css/views/messages/_MFileBody.scss +++ b/res/css/views/messages/_MFileBody.scss @@ -1,5 +1,5 @@ /* -Copyright 2015, 2016, 2021 The Matrix.org Foundation C.I.C. +Copyright 2015 - 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. @@ -60,6 +60,8 @@ limitations under the License. } .mx_MFileBody_info { + cursor: pointer; + .mx_MFileBody_info_icon { background-color: $message-body-panel-icon-bg-color; border-radius: 20px; diff --git a/res/css/views/messages/_MImageBody.scss b/res/css/views/messages/_MImageBody.scss index f5d8131e6e..a748435cd8 100644 --- a/res/css/views/messages/_MImageBody.scss +++ b/res/css/views/messages/_MImageBody.scss @@ -16,10 +16,6 @@ limitations under the License. $timelineImageBorderRadius: 4px; -.mx_MImageBody { - display: block; -} - .mx_MImageBody_thumbnail { object-fit: contain; border-radius: $timelineImageBorderRadius; @@ -28,7 +24,7 @@ $timelineImageBorderRadius: 4px; justify-content: center; align-items: center; - > canvas { + > div > canvas { border-radius: $timelineImageBorderRadius; } } diff --git a/res/css/views/messages/_ViewSourceEvent.scss b/res/css/views/messages/_ViewSourceEvent.scss index 66825030e0..b0e40a5152 100644 --- a/res/css/views/messages/_ViewSourceEvent.scss +++ b/res/css/views/messages/_ViewSourceEvent.scss @@ -43,8 +43,10 @@ limitations under the License. margin-bottom: 7px; mask-image: url('$(res)/img/feather-customised/minimise.svg'); } +} - &:hover .mx_ViewSourceEvent_toggle { +.mx_EventTile:hover { + .mx_ViewSourceEvent_toggle { visibility: visible; } } diff --git a/res/css/views/rooms/_EventBubbleTile.scss b/res/css/views/rooms/_EventBubbleTile.scss index cac463d4db..1e25deba26 100644 --- a/res/css/views/rooms/_EventBubbleTile.scss +++ b/res/css/views/rooms/_EventBubbleTile.scss @@ -15,7 +15,7 @@ limitations under the License. */ .mx_EventTile[data-layout=bubble], -.mx_EventTile[data-layout=bubble] ~ .mx_EventListSummary { +.mx_EventListSummary[data-layout=bubble] { --avatarSize: 32px; --gutterSize: 11px; --cornerRadius: 12px; @@ -38,17 +38,22 @@ limitations under the License. padding-top: 0; } - &:hover { + &::before { + content: ''; + position: absolute; + top: -1px; + bottom: -1px; + left: -60px; + right: -60px; + z-index: -1; + border-radius: 4px; + } + + &:hover, + &.mx_EventTile_selected { + &::before { - content: ''; - position: absolute; - top: -1px; - bottom: -1px; - left: -60px; - right: -60px; - z-index: -1; background: $eventbubble-bg-hover; - border-radius: 4px; } .mx_EventTile_avatar { @@ -155,12 +160,24 @@ limitations under the License. position: absolute; top: 0; line-height: 1; + z-index: 9; img { box-shadow: 0 0 0 3px $eventbubble-avatar-outline; border-radius: 50%; } } + &.mx_EventTile_noSender { + .mx_EventTile_avatar { + top: -19px; + } + } + + .mx_BaseAvatar, + .mx_EventTile_avatar { + line-height: 1; + } + &[data-has-reply=true] { > .mx_EventTile_line { flex-direction: column; @@ -211,89 +228,6 @@ limitations under the License. border-left-color: $eventbubble-reply-color; } - &.mx_EventTile_bubbleContainer, - &.mx_EventTile_info, - & ~ .mx_EventListSummary[data-expanded=false] { - --backgroundColor: transparent; - --gutterSize: 0; - - display: flex; - align-items: center; - justify-content: center; - - .mx_EventTile_avatar { - position: static; - order: -1; - margin-right: 5px; - } - } - - & ~ .mx_EventListSummary { - --maxWidth: 80%; - margin-left: calc(var(--avatarSize) + var(--gutterSize)); - margin-right: calc(var(--gutterSize) + var(--avatarSize)); - .mx_EventListSummary_toggle { - float: none; - margin: 0; - order: 9; - margin-left: 5px; - } - .mx_EventListSummary_avatars { - padding-top: 0; - } - - &::after { - content: ""; - clear: both; - } - - .mx_EventTile { - margin: 0 6px; - } - - .mx_EventTile_line { - margin: 0 5px; - > a { - left: auto; - right: 0; - transform: translateX(calc(100% + 5px)); - } - } - - .mx_MessageActionBar { - transform: translate3d(90%, 0, 0); - } - } - - & ~ .mx_EventListSummary[data-expanded=false] { - padding: 0 34px; - } - - /* events that do not require bubble layout */ - & ~ .mx_EventListSummary, - &.mx_EventTile_bad { - .mx_EventTile_line { - background: transparent; - } - - &:hover { - &::before { - background: transparent; - } - } - } - - & + .mx_EventListSummary { - .mx_EventTile { - margin-top: 0; - padding: 0; - } - } - - .mx_EventListSummary_toggle { - margin-right: 55px; - } - /* Special layout scenario for "Unable To Decrypt (UTD)" events */ &.mx_EventTile_bad > .mx_EventTile_line { display: grid; @@ -328,3 +262,93 @@ limitations under the License. max-width: 100%; } } + +.mx_EventTile.mx_EventTile_bubbleContainer[data-layout=bubble], +.mx_EventTile.mx_EventTile_info[data-layout=bubble], +.mx_EventListSummary[data-layout=bubble][data-expanded=false] { + --backgroundColor: transparent; + --gutterSize: 0; + + display: flex; + align-items: center; + justify-content: start; + padding: 5px 0; + + .mx_EventTile_avatar { + position: static; + order: -1; + margin-right: 5px; + } + + .mx_EventTile_line, + .mx_EventTile_info { + min-width: 100%; + } + + .mx_EventTile_e2eIcon { + margin-left: 9px; + } + + .mx_EventTile_line > a { + right: auto; + top: -15px; + left: -68px; + } +} + +.mx_EventListSummary[data-layout=bubble] { + --maxWidth: 70%; + margin-left: calc(var(--avatarSize) + var(--gutterSize)); + margin-right: 94px; + .mx_EventListSummary_toggle { + float: none; + margin: 0; + order: 9; + margin-left: 5px; + margin-right: 55px; + } + .mx_EventListSummary_avatars { + padding-top: 0; + } + + &::after { + content: ""; + clear: both; + } + + .mx_EventTile { + margin: 0 6px; + padding: 2px 0; + } + + .mx_EventTile_line { + margin: 0 5px; + > a { + left: auto; + right: 0; + transform: translateX(calc(100% + 5px)); + } + } + + .mx_MessageActionBar { + transform: translate3d(90%, 0, 0); + } +} + +.mx_EventListSummary[data-expanded=false][data-layout=bubble] { + padding: 0 34px; +} + +/* events that do not require bubble layout */ +.mx_EventListSummary[data-layout=bubble], +.mx_EventTile.mx_EventTile_bad[data-layout=bubble] { + .mx_EventTile_line { + background: transparent; + } + + &:hover { + &::before { + background: transparent; + } + } +} diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index bda4a15f45..6e207d674b 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -59,7 +59,6 @@ $hover-select-border: 4px; font-size: $font-14px; display: inline-block; /* anti-zalgo, with overflow hidden */ overflow: hidden; - cursor: pointer; padding-bottom: 0px; padding-top: 0px; margin: 0px; @@ -132,15 +131,6 @@ $hover-select-border: 4px; } } - &.mx_EventTile_info .mx_EventTile_line, - & ~ .mx_EventListSummary .mx_EventTile_avatar ~ .mx_EventTile_line { - padding-left: calc($left-gutter + 18px); - } - - & ~ .mx_EventListSummary .mx_EventTile_line { - padding-left: calc($left-gutter); - } - &.mx_EventTile_selected.mx_EventTile_info .mx_EventTile_line { padding-left: calc($left-gutter + 18px - $hover-select-border); } @@ -276,10 +266,19 @@ $hover-select-border: 4px; .mx_ReactionsRow { margin: 0; - padding: 6px 60px; + padding: 4px 64px; } } +.mx_EventTile:not([data-layout=bubble]).mx_EventTile_info .mx_EventTile_line, +.mx_EventListSummary:not([data-layout=bubble]) > :not(.mx_EventTile) .mx_EventTile_avatar ~ .mx_EventTile_line { + padding-left: calc($left-gutter + 18px); +} + +.mx_EventListSummary:not([data-layout=bubble]) .mx_EventTile_line { + padding-left: calc($left-gutter); +} + /* all the overflow-y: hidden; are to trap Zalgos - but they introduce an implicit overflow-x: auto. so make that explicitly hidden too to avoid random @@ -322,6 +321,10 @@ $hover-select-border: 4px; // on ELS we need the margin to allow interaction with the expand/collapse button which is normally in the RR gutter } +.mx_SenderProfile { + cursor: pointer; +} + .mx_EventTile_bubbleContainer { display: grid; grid-template-columns: 1fr 100px; @@ -456,8 +459,14 @@ $hover-select-border: 4px; /* Various markdown overrides */ -.mx_EventTile_body pre { - border: 1px solid transparent; +.mx_EventTile_body { + a:hover { + text-decoration: underline; + } + + pre { + border: 1px solid transparent; + } } .mx_EventTile_content .markdown-body { @@ -573,6 +582,12 @@ $hover-select-border: 4px; color: $accent-color-alt; } +.mx_EventTile_content .markdown-body blockquote { + border-left: 2px solid $blockquote-bar-color; + border-radius: 2px; + padding: 0 10px; +} + .mx_EventTile_content .markdown-body .hljs { display: inline !important; } diff --git a/res/css/views/rooms/_IRCLayout.scss b/res/css/views/rooms/_IRCLayout.scss index 97190807ca..578c0325d2 100644 --- a/res/css/views/rooms/_IRCLayout.scss +++ b/res/css/views/rooms/_IRCLayout.scss @@ -116,6 +116,11 @@ $irc-line-height: $font-18px; .mx_EditMessageComposer_buttons { position: relative; } + + .mx_ReactionsRow { + padding-left: 0; + padding-right: 0; + } } .mx_EventTile_emote { diff --git a/res/css/views/rooms/_LinkPreviewWidget.scss b/res/css/views/rooms/_LinkPreviewWidget.scss index b3eff7e960..24900ee14b 100644 --- a/res/css/views/rooms/_LinkPreviewWidget.scss +++ b/res/css/views/rooms/_LinkPreviewWidget.scss @@ -34,7 +34,7 @@ limitations under the License. .mx_LinkPreviewWidget_caption { margin-left: 15px; flex: 1 1 auto; - overflow-x: hidden; // cause it to wrap rather than clip + overflow: hidden; // cause it to wrap rather than clip } .mx_LinkPreviewWidget_title { diff --git a/res/css/views/rooms/_MessageComposer.scss b/res/css/views/rooms/_MessageComposer.scss index e6c0cc3f46..5e2eff4047 100644 --- a/res/css/views/rooms/_MessageComposer.scss +++ b/res/css/views/rooms/_MessageComposer.scss @@ -165,8 +165,6 @@ limitations under the License. font-size: $font-14px; max-height: 120px; overflow: auto; - /* needed for FF */ - font-family: $font-family; } /* hack for FF as vertical alignment of custom placeholder text is broken */ diff --git a/res/css/views/rooms/_ReplyTile.scss b/res/css/views/rooms/_ReplyTile.scss index f3e204e415..fd21e5f348 100644 --- a/res/css/views/rooms/_ReplyTile.scss +++ b/res/css/views/rooms/_ReplyTile.scss @@ -60,8 +60,6 @@ limitations under the License. $reply-lines: 2; $line-height: $font-22px; - pointer-events: none; - text-overflow: ellipsis; display: -webkit-box; -webkit-box-orient: vertical; diff --git a/res/css/views/settings/_ProfileSettings.scss b/res/css/views/settings/_ProfileSettings.scss index 4cbcb8e708..63a5fa7edf 100644 --- a/res/css/views/settings/_ProfileSettings.scss +++ b/res/css/views/settings/_ProfileSettings.scss @@ -16,6 +16,7 @@ limitations under the License. .mx_ProfileSettings_controls_topic { & > textarea { + font-family: inherit; resize: vertical; } } diff --git a/res/css/views/settings/tabs/_SettingsTab.scss b/res/css/views/settings/tabs/_SettingsTab.scss index 892f5fe744..9f40372690 100644 --- a/res/css/views/settings/tabs/_SettingsTab.scss +++ b/res/css/views/settings/tabs/_SettingsTab.scss @@ -36,7 +36,6 @@ limitations under the License. .mx_SettingsTab_subheading { font-size: $font-16px; display: block; - font-family: $font-family; font-weight: 600; color: $primary-fg-color; margin-bottom: 10px; @@ -47,14 +46,14 @@ limitations under the License. color: $settings-subsection-fg-color; font-size: $font-14px; display: block; - margin: 10px 100px 10px 0; // Align with the rest of the view + margin: 10px 80px 10px 0; // Align with the rest of the view } .mx_SettingsTab_section { margin-bottom: 24px; .mx_SettingsFlag { - margin-right: 100px; + margin-right: 80px; margin-bottom: 10px; } @@ -73,6 +72,13 @@ limitations under the License. padding-right: 10px; } +.mx_SettingsTab_section .mx_SettingsFlag .mx_SettingsFlag_microcopy { + margin-top: 4px; + font-size: $font-12px; + line-height: $font-15px; + color: $secondary-fg-color; +} + .mx_SettingsTab_section .mx_SettingsFlag .mx_ToggleSwitch { float: right; } diff --git a/res/css/views/settings/tabs/room/_SecurityRoomSettingsTab.scss b/res/css/views/settings/tabs/room/_SecurityRoomSettingsTab.scss index 23dcc532b2..2aab201352 100644 --- a/res/css/views/settings/tabs/room/_SecurityRoomSettingsTab.scss +++ b/res/css/views/settings/tabs/room/_SecurityRoomSettingsTab.scss @@ -14,6 +14,44 @@ See the License for the specific language governing permissions and limitations under the License. */ +.mx_SecurityRoomSettingsTab { + .mx_SettingsTab_showAdvanced { + padding: 0; + margin-bottom: 16px; + } + + .mx_SecurityRoomSettingsTab_spacesWithAccess { + > h4 { + color: $secondary-fg-color; + font-weight: $font-semi-bold; + font-size: $font-12px; + line-height: $font-15px; + text-transform: uppercase; + } + + > span { + font-weight: 500; + font-size: $font-14px; + line-height: 32px; // matches height of avatar for v-align + color: $secondary-fg-color; + display: inline-block; + + img.mx_RoomAvatar_isSpaceRoom, + .mx_RoomAvatar_isSpaceRoom img { + border-radius: 8px; + } + + .mx_BaseAvatar { + margin-right: 8px; + } + + & + span { + margin-left: 16px; + } + } + } +} + .mx_SecurityRoomSettingsTab_warning { display: block; @@ -26,5 +64,51 @@ limitations under the License. } .mx_SecurityRoomSettingsTab_encryptionSection { - margin-bottom: 25px; + padding-bottom: 24px; + border-bottom: 1px solid $menu-border-color; + margin-bottom: 32px; +} + +.mx_SecurityRoomSettingsTab_upgradeRequired { + margin-left: 16px; + padding: 4px 16px; + border: 1px solid $accent-color; + border-radius: 8px; + color: $accent-color; + font-size: $font-12px; + line-height: $font-15px; +} + +.mx_SecurityRoomSettingsTab_joinRule { + .mx_RadioButton { + padding-top: 16px; + margin-bottom: 8px; + + .mx_RadioButton_content { + margin-left: 14px; + font-weight: $font-semi-bold; + font-size: $font-15px; + line-height: $font-24px; + color: $primary-fg-color; + display: block; + } + } + + > span { + display: inline-block; + margin-left: 34px; + margin-bottom: 16px; + font-size: $font-15px; + line-height: $font-24px; + color: $secondary-fg-color; + + & + .mx_RadioButton { + border-top: 1px solid $menu-border-color; + } + } + + .mx_AccessibleButton_kind_link { + padding: 0; + font-size: inherit; + } } diff --git a/res/css/views/spaces/_SpaceCreateMenu.scss b/res/css/views/spaces/_SpaceCreateMenu.scss index 88b9d8f693..097b2b648e 100644 --- a/res/css/views/spaces/_SpaceCreateMenu.scss +++ b/res/css/views/spaces/_SpaceCreateMenu.scss @@ -43,6 +43,12 @@ $spacePanelWidth: 71px; color: $secondary-fg-color; margin: 0; } + + .mx_SpaceFeedbackPrompt { + border-top: 1px solid $input-border-color; + padding-top: 12px; + margin-top: 16px; + } } // XXX remove this when spaces leaves Beta @@ -99,3 +105,25 @@ $spacePanelWidth: 71px; } } } + +.mx_SpaceFeedbackPrompt { + font-size: $font-15px; + line-height: $font-24px; + + > span { + color: $secondary-fg-color; + position: relative; + font-size: inherit; + line-height: inherit; + margin-right: auto; + } + + .mx_AccessibleButton_kind_link { + color: $accent-color; + position: relative; + padding: 0; + margin-left: 8px; + font-size: inherit; + line-height: inherit; + } +} diff --git a/res/css/views/voip/_CallView.scss b/res/css/views/voip/_CallView.scss index 205d431752..eff865f20c 100644 --- a/res/css/views/voip/_CallView.scss +++ b/res/css/views/voip/_CallView.scss @@ -67,7 +67,32 @@ limitations under the License. .mx_CallView_content { position: relative; display: flex; + justify-content: center; border-radius: 8px; + + > .mx_VideoFeed { + width: 100%; + height: 100%; + + &.mx_VideoFeed_voice { + // We don't want to collide with the call controls that have 52px of height + margin-bottom: 52px; + background-color: $inverted-bg-color; + display: flex; + justify-content: center; + align-items: center; + } + + .mx_VideoFeed_video { + height: 100%; + background-color: #000; + } + + .mx_VideoFeed_mic { + left: 10px; + bottom: 10px; + } + } } .mx_CallView_voice { @@ -260,7 +285,7 @@ limitations under the License. max-width: 240px; } -.mx_CallView_header_phoneIcon { +.mx_CallView_header_callTypeIcon { display: inline-block; margin-right: 6px; height: 16px; @@ -274,12 +299,19 @@ limitations under the License. height: 16px; width: 16px; - background-color: $warning-color; + background-color: $secondary-fg-color; mask-repeat: no-repeat; mask-size: contain; mask-position: center; + } + + &.mx_CallView_header_callTypeIcon_voice::before { mask-image: url('$(res)/img/element-icons/call/voice-call.svg'); } + + &.mx_CallView_header_callTypeIcon_video::before { + mask-image: url('$(res)/img/element-icons/call/video-call.svg'); + } } .mx_CallView_callControls { @@ -287,9 +319,9 @@ limitations under the License. display: flex; justify-content: center; bottom: 5px; - width: 100%; opacity: 1; transition: opacity 0.5s; + z-index: 200; // To be above _all_ feeds } .mx_CallView_callControls_hidden { @@ -297,10 +329,29 @@ limitations under the License. pointer-events: none; } +.mx_CallView_presenting { + opacity: 1; + transition: opacity 0.5s; + + position: absolute; + margin-top: 18px; + padding: 4px 8px; + border-radius: 4px; + + // Same on both themes + color: white; + background-color: #17191c; +} + +.mx_CallView_presenting_hidden { + opacity: 0.001; // opacity 0 can cause a re-layout + pointer-events: none; +} + .mx_CallView_callControls_button { cursor: pointer; - margin-left: 8px; - margin-right: 8px; + margin-left: 2px; + margin-right: 2px; &::before { @@ -317,17 +368,11 @@ limitations under the License. } .mx_CallView_callControls_dialpad { - margin-right: auto; &::before { background-image: url('$(res)/img/voip/dialpad.svg'); } } -.mx_CallView_callControls_button_dialpad_hidden { - margin-right: auto; - cursor: initial; -} - .mx_CallView_callControls_button_micOn { &::before { background-image: url('$(res)/img/voip/mic-on.svg'); @@ -352,6 +397,30 @@ limitations under the License. } } +.mx_CallView_callControls_button_screensharingOn { + &::before { + background-image: url('$(res)/img/voip/screensharing-on.svg'); + } +} + +.mx_CallView_callControls_button_screensharingOff { + &::before { + background-image: url('$(res)/img/voip/screensharing-off.svg'); + } +} + +.mx_CallView_callControls_button_sidebarOn { + &::before { + background-image: url('$(res)/img/voip/sidebar-on.svg'); + } +} + +.mx_CallView_callControls_button_sidebarOff { + &::before { + background-image: url('$(res)/img/voip/sidebar-off.svg'); + } +} + .mx_CallView_callControls_button_hangup { &::before { background-image: url('$(res)/img/voip/hangup.svg'); @@ -359,17 +428,11 @@ limitations under the License. } .mx_CallView_callControls_button_more { - margin-left: auto; &::before { background-image: url('$(res)/img/voip/more.svg'); } } -.mx_CallView_callControls_button_more_hidden { - margin-left: auto; - cursor: initial; -} - .mx_CallView_callControls_button_invisible { visibility: hidden; pointer-events: none; diff --git a/res/css/views/voip/_CallViewSidebar.scss b/res/css/views/voip/_CallViewSidebar.scss new file mode 100644 index 0000000000..892a137a32 --- /dev/null +++ b/res/css/views/voip/_CallViewSidebar.scss @@ -0,0 +1,63 @@ +/* +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_CallViewSidebar { + position: absolute; + right: 16px; + bottom: 16px; + z-index: 100; // To be above the primary feed + + overflow: auto; + + height: calc(100% - 32px); // Subtract the top and bottom padding + width: 20%; + + display: flex; + flex-direction: column-reverse; + justify-content: flex-start; + align-items: flex-end; + gap: 12px; + + > .mx_VideoFeed { + width: 100%; + + &.mx_VideoFeed_voice { + border-radius: 4px; + + display: flex; + align-items: center; + justify-content: center; + + aspect-ratio: 16 / 9; + } + + .mx_VideoFeed_video { + border-radius: 4px; + } + + .mx_VideoFeed_mic { + left: 6px; + bottom: 6px; + } + } + + &.mx_CallViewSidebar_pipMode { + top: 16px; + bottom: unset; + justify-content: flex-end; + gap: 4px; + } +} diff --git a/res/css/views/voip/_DialPadContextMenu.scss b/res/css/views/voip/_DialPadContextMenu.scss index 0019994e72..527d223ffc 100644 --- a/res/css/views/voip/_DialPadContextMenu.scss +++ b/res/css/views/voip/_DialPadContextMenu.scss @@ -69,7 +69,6 @@ limitations under the License. overflow: hidden; max-width: 185px; text-align: left; - direction: rtl; padding: 8px 0px; background-color: rgb(0, 0, 0, 0); } diff --git a/res/css/views/voip/_VideoFeed.scss b/res/css/views/voip/_VideoFeed.scss index 4a3fbdf597..3a0f62636e 100644 --- a/res/css/views/voip/_VideoFeed.scss +++ b/res/css/views/voip/_VideoFeed.scss @@ -14,37 +14,53 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_VideoFeed_voice { - background-color: $inverted-bg-color; -} +.mx_VideoFeed { + overflow: hidden; + position: relative; - -.mx_VideoFeed_remote { - width: 100%; - height: 100%; - display: flex; - justify-content: center; - align-items: center; - - &.mx_VideoFeed_video { - background-color: #000; + &.mx_VideoFeed_voice { + background-color: $inverted-bg-color; } -} -.mx_VideoFeed_local { - max-width: 25%; - max-height: 25%; - position: absolute; - right: 10px; - top: 10px; - z-index: 100; - border-radius: 4px; - - &.mx_VideoFeed_video { + .mx_VideoFeed_video { + width: 100%; background-color: transparent; + + &.mx_VideoFeed_video_mirror { + transform: scale(-1, 1); + } + } + + .mx_VideoFeed_mic { + position: absolute; + display: flex; + align-items: center; + justify-content: center; + + width: 24px; + height: 24px; + + background-color: rgba(0, 0, 0, 0.5); // Same on both themes + border-radius: 100%; + + &::before { + position: absolute; + content: ""; + width: 16px; + height: 16px; + mask-repeat: no-repeat; + mask-size: contain; + mask-position: center; + background-color: white; // Same on both themes + border-radius: 7px; + } + + &.mx_VideoFeed_mic_muted::before { + mask-image: url('$(res)/img/voip/mic-muted.svg'); + } + + &.mx_VideoFeed_mic_unmuted::before { + mask-image: url('$(res)/img/voip/mic-unmuted.svg'); + } } } - -.mx_VideoFeed_mirror { - transform: scale(-1, 1); -} diff --git a/res/img/feather-customised/globe.svg b/res/img/feather-customised/globe.svg deleted file mode 100644 index 8af7dc41dc..0000000000 --- a/res/img/feather-customised/globe.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/res/img/voip/mic-muted.svg b/res/img/voip/mic-muted.svg new file mode 100644 index 0000000000..0cb7ad1c9e --- /dev/null +++ b/res/img/voip/mic-muted.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/res/img/voip/mic-unmuted.svg b/res/img/voip/mic-unmuted.svg new file mode 100644 index 0000000000..8334cafa0a --- /dev/null +++ b/res/img/voip/mic-unmuted.svg @@ -0,0 +1,4 @@ + + + + diff --git a/res/img/voip/missed-video.svg b/res/img/voip/missed-video.svg new file mode 100644 index 0000000000..a2f3bc73ac --- /dev/null +++ b/res/img/voip/missed-video.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/voip/missed-voice.svg b/res/img/voip/missed-voice.svg new file mode 100644 index 0000000000..5e3993584e --- /dev/null +++ b/res/img/voip/missed-voice.svg @@ -0,0 +1,4 @@ + + + + diff --git a/res/img/voip/screensharing-off.svg b/res/img/voip/screensharing-off.svg new file mode 100644 index 0000000000..dc19e9892e --- /dev/null +++ b/res/img/voip/screensharing-off.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/res/img/voip/screensharing-on.svg b/res/img/voip/screensharing-on.svg new file mode 100644 index 0000000000..a8e7fe308e --- /dev/null +++ b/res/img/voip/screensharing-on.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/res/img/voip/sidebar-off.svg b/res/img/voip/sidebar-off.svg new file mode 100644 index 0000000000..7637a9ab55 --- /dev/null +++ b/res/img/voip/sidebar-off.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/res/img/voip/sidebar-on.svg b/res/img/voip/sidebar-on.svg new file mode 100644 index 0000000000..a625334be4 --- /dev/null +++ b/res/img/voip/sidebar-on.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index e7ba1aa9fb..e7c1dda54f 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -56,7 +56,6 @@ limitations under the License. import React from 'react'; import { MatrixClientPeg } from './MatrixClientPeg'; -import PlatformPeg from './PlatformPeg'; import Modal from './Modal'; import { _t } from './languageHandler'; import dis from './dispatcher/dispatcher'; @@ -80,7 +79,6 @@ import CountlyAnalytics from "./CountlyAnalytics"; import { UIFeature } from "./settings/UIFeature"; import { CallError } from "matrix-js-sdk/src/webrtc/call"; import { logger } from 'matrix-js-sdk/src/logger'; -import DesktopCapturerSourcePicker from "./components/views/elements/DesktopCapturerSourcePicker"; import { Action } from './dispatcher/actions'; import VoipUserMapper from './VoipUserMapper'; import { addManagedHybridWidget, isManagedHybridWidgetEnabled } from './widgets/ManagedHybrid'; @@ -129,14 +127,9 @@ interface ThirdpartyLookupResponse { 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 -// is concerned). export enum PlaceCallType { Voice = 'voice', Video = 'video', - ScreenSharing = 'screensharing', } export enum CallHandlerEvent { @@ -491,28 +484,18 @@ export default class CallHandler extends EventEmitter { break; case CallState.Ended: { - Analytics.trackEvent('voip', 'callEnded', 'hangupReason', call.hangupReason); + const hangupReason = call.hangupReason; + Analytics.trackEvent('voip', 'callEnded', 'hangupReason', hangupReason); this.removeCallForRoom(mappedRoomId); - if (oldState === CallState.InviteSent && ( - call.hangupParty === CallParty.Remote || - (call.hangupParty === CallParty.Local && call.hangupReason === CallErrorCode.InviteTimeout) - )) { + if (oldState === CallState.InviteSent && call.hangupParty === CallParty.Remote) { this.play(AudioID.Busy); let title; let description; - if (call.hangupReason === CallErrorCode.UserHangup) { - title = _t("Call Declined"); - description = _t("The other party declined the call."); - } else if (call.hangupReason === CallErrorCode.UserBusy) { + // TODO: We should either do away with these or figure out a copy for each code (expect user_hangup...) + if (call.hangupReason === CallErrorCode.UserBusy) { title = _t("User Busy"); description = _t("The user you called is busy."); - } else if (call.hangupReason === CallErrorCode.InviteTimeout) { - title = _t("Call Failed"); - // XXX: full stop appended as some relic here, but these - // strings need proper input from design anyway, so let's - // not change this string until we have a proper one. - description = _t('The remote side failed to pick up') + '.'; - } else { + } else if (hangupReason && ![CallErrorCode.UserHangup, "user hangup"].includes(hangupReason)) { title = _t("Call Failed"); description = _t("The call could not be established"); } @@ -521,7 +504,7 @@ export default class CallHandler extends EventEmitter { title, description, }); } else if ( - call.hangupReason === CallErrorCode.AnsweredElsewhere && oldState === CallState.Connecting + hangupReason === CallErrorCode.AnsweredElsewhere && oldState === CallState.Connecting ) { Modal.createTrackedDialog('Call Handler', 'Call Failed', ErrorDialog, { title: _t("Answered Elsewhere"), @@ -738,25 +721,6 @@ export default class CallHandler extends EventEmitter { call.placeVoiceCall(); } else if (type === 'video') { call.placeVideoCall(); - } else if (type === PlaceCallType.ScreenSharing) { - const screenCapErrorString = PlatformPeg.get().screenCaptureErrorString(); - if (screenCapErrorString) { - this.removeCallForRoom(roomId); - console.log("Can't capture screen: " + screenCapErrorString); - Modal.createTrackedDialog('Call Handler', 'Unable to capture screen', ErrorDialog, { - title: _t('Unable to capture screen'), - description: screenCapErrorString, - }); - return; - } - - call.placeScreenSharingCall( - async (): Promise => { - const { finished } = Modal.createDialog(DesktopCapturerSourcePicker); - const [source] = await finished; - return source; - }, - ); } else { console.error("Unknown conf call type: " + type); } diff --git a/src/IdentityAuthClient.js b/src/IdentityAuthClient.js index e91e1d72cf..ffece510de 100644 --- a/src/IdentityAuthClient.js +++ b/src/IdentityAuthClient.js @@ -146,23 +146,23 @@ export default class IdentityAuthClient { const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); const { finished } = Modal.createTrackedDialog('Default identity server terms warning', '', QuestionDialog, { - title: _t("Identity server has no terms of service"), - description: ( -
-

{ _t( - "This action requires accessing the default identity server " + + title: _t("Identity server has no terms of service"), + description: ( +

+

{ _t( + "This action requires accessing the default identity server " + " to validate an email address or phone number, " + "but the server does not have any terms of service.", {}, - { - server: () => { abbreviateUrl(identityServerUrl) }, - }, - ) }

-

{ _t( - "Only continue if you trust the owner of the server.", - ) }

-
- ), - button: _t("Trust"), + { + server: () => { abbreviateUrl(identityServerUrl) }, + }, + ) }

+

{ _t( + "Only continue if you trust the owner of the server.", + ) }

+
+ ), + button: _t("Trust"), }); const [confirmed] = await finished; if (confirmed) { diff --git a/src/Registration.js b/src/Registration.js index 70dcd38454..c59d244149 100644 --- a/src/Registration.js +++ b/src/Registration.js @@ -51,10 +51,15 @@ export async function startAnyRegistrationFlow(options) { description: _t("Use your account or create a new one to continue."), button: _t("Create Account"), extraButtons: [ - , + , ], onFinished: (proceed) => { if (proceed) { diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx index 9f5ac83a56..b4deb6d8c4 100644 --- a/src/SlashCommands.tsx +++ b/src/SlashCommands.tsx @@ -34,7 +34,6 @@ import { getAddressType } from './UserAddress'; import { abbreviateUrl } from './utils/UrlUtils'; import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from './utils/IdentityServerUtils'; import { isPermalinkHost, parsePermalink } from "./utils/permalinks/Permalinks"; -import { inviteUsersToRoom } from "./RoomInvite"; import { WidgetType } from "./widgets/WidgetType"; import { Jitsi } from "./widgets/Jitsi"; import { parseFragment as parseHtml, Element as ChildElement } from "parse5"; @@ -49,6 +48,7 @@ import { UIFeature } from "./settings/UIFeature"; import { CHAT_EFFECTS } from "./effects"; import CallHandler from "./CallHandler"; import { guessAndSetDMRoom } from "./Rooms"; +import { upgradeRoom } from './utils/RoomUpgrade'; import UploadConfirmDialog from './components/views/dialogs/UploadConfirmDialog'; import ErrorDialog from './components/views/dialogs/ErrorDialog'; import DevtoolsDialog from './components/views/dialogs/DevtoolsDialog'; @@ -277,50 +277,8 @@ export const Commands = [ /*isPriority=*/false, /*isStatic=*/true); return success(finished.then(async ([resp]) => { - if (!resp.continue) return; - - let checkForUpgradeFn; - try { - const upgradePromise = cli.upgradeRoom(roomId, args); - - // We have to wait for the js-sdk to give us the room back so - // we can more effectively abuse the MultiInviter behaviour - // which heavily relies on the Room object being available. - if (resp.invite) { - checkForUpgradeFn = async (newRoom) => { - // The upgradePromise should be done by the time we await it here. - const { replacement_room: newRoomId } = await upgradePromise; - if (newRoom.roomId !== newRoomId) return; - - const toInvite = [ - ...room.getMembersWithMembership("join"), - ...room.getMembersWithMembership("invite"), - ].map(m => m.userId).filter(m => m !== cli.getUserId()); - - if (toInvite.length > 0) { - // Errors are handled internally to this function - await inviteUsersToRoom(newRoomId, toInvite); - } - - cli.removeListener('Room', checkForUpgradeFn); - }; - cli.on('Room', checkForUpgradeFn); - } - - // We have to await after so that the checkForUpgradesFn has a proper reference - // to the new room's ID. - await upgradePromise; - } catch (e) { - console.error(e); - - if (checkForUpgradeFn) cli.removeListener('Room', checkForUpgradeFn); - - Modal.createTrackedDialog('Slash Commands', 'room upgrade error', ErrorDialog, { - title: _t('Error upgrading room'), - description: _t( - 'Double check that your server supports the room version chosen and try again.'), - }); - } + if (!resp?.continue) return; + await upgradeRoom(room, args, resp.invite); })); } return reject(this.getUsage()); diff --git a/src/TextForEvent.tsx b/src/TextForEvent.tsx index 7bad8eb50e..b9295be3ed 100644 --- a/src/TextForEvent.tsx +++ b/src/TextForEvent.tsx @@ -25,11 +25,44 @@ import { Action } from './dispatcher/actions'; import defaultDispatcher from './dispatcher/dispatcher'; import { SetRightPanelPhasePayload } from './dispatcher/payloads/SetRightPanelPhasePayload'; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { MatrixClientPeg } from "./MatrixClientPeg"; // These functions are frequently used just to check whether an event has // any text to display at all. For this reason they return deferred values // to avoid the expense of looking up translations when they're not needed. +function textForCallInviteEvent(event: MatrixEvent): () => string | null { + const getSenderName = () => event.sender ? event.sender.name : _t('Someone'); + // FIXME: Find a better way to determine this from the event? + let isVoice = true; + if (event.getContent().offer && event.getContent().offer.sdp && + event.getContent().offer.sdp.indexOf('m=video') !== -1) { + isVoice = false; + } + const isSupported = MatrixClientPeg.get().supportsVoip(); + + // This ladder could be reduced down to a couple string variables, however other languages + // can have a hard time translating those strings. In an effort to make translations easier + // and more accurate, we break out the string-based variables to a couple booleans. + if (isVoice && isSupported) { + return () => _t("%(senderName)s placed a voice call.", { + senderName: getSenderName(), + }); + } else if (isVoice && !isSupported) { + return () => _t("%(senderName)s placed a voice call. (not supported by this browser)", { + senderName: getSenderName(), + }); + } else if (!isVoice && isSupported) { + return () => _t("%(senderName)s placed a video call.", { + senderName: getSenderName(), + }); + } else if (!isVoice && !isSupported) { + return () => _t("%(senderName)s placed a video call. (not supported by this browser)", { + senderName: getSenderName(), + }); + } +} + function textForMemberEvent(ev: MatrixEvent, allowJSX: boolean, showHiddenEvents?: boolean): () => string | null { // XXX: SYJS-16 "sender is sometimes null for join messages" const senderName = ev.sender ? ev.sender.name : ev.getSender(); @@ -567,6 +600,7 @@ interface IHandlers { const handlers: IHandlers = { 'm.room.message': textForMessageEvent, + 'm.call.invite': textForCallInviteEvent, }; const stateHandlers: IHandlers = { diff --git a/src/accessibility/KeyboardShortcuts.tsx b/src/accessibility/KeyboardShortcuts.tsx index 9cc7b60c99..c66984191f 100644 --- a/src/accessibility/KeyboardShortcuts.tsx +++ b/src/accessibility/KeyboardShortcuts.tsx @@ -163,7 +163,7 @@ const shortcuts: Record = { modifiers: [Modifiers.SHIFT], key: Key.PAGE_UP, }], - description: _td("Jump to oldest unread message"), + description: _td("Jump to oldest unread message"), }, { keybinds: [{ modifiers: [CMD_OR_CTRL, Modifiers.SHIFT], diff --git a/src/async-components/views/dialogs/security/CreateKeyBackupDialog.js b/src/async-components/views/dialogs/security/CreateKeyBackupDialog.js index 412194ab43..2cef1c0e41 100644 --- a/src/async-components/views/dialogs/security/CreateKeyBackupDialog.js +++ b/src/async-components/views/dialogs/security/CreateKeyBackupDialog.js @@ -269,7 +269,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
{ _t("Advanced") } - + { _t("Set up with a Security Key") }
diff --git a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.js b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.js index aa78d68830..641df4f897 100644 --- a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.js +++ b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.js @@ -474,7 +474,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { outlined >
- + { _t("Generate a Security Key") }
{ _t("We’ll generate a Security Key for you to store somewhere safe, like a password manager or a safe.") }
@@ -493,7 +493,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { outlined >
- + { _t("Enter a Security Phrase") }
{ _t("Use a secret phrase only you know, and optionally save a Security Key to use for backup.") }
@@ -701,7 +701,8 @@ export default class CreateSecretStorageDialog extends React.PureComponent { { this._recoveryKey.encodedPrivateKey }
- diff --git a/src/async-components/views/dialogs/security/ExportE2eKeysDialog.js b/src/async-components/views/dialogs/security/ExportE2eKeysDialog.js index 0435d81968..dbed9f3968 100644 --- a/src/async-components/views/dialogs/security/ExportE2eKeysDialog.js +++ b/src/async-components/views/dialogs/security/ExportE2eKeysDialog.js @@ -148,8 +148,12 @@ export default class ExportE2eKeysDialog extends React.Component {
-
@@ -161,8 +165,10 @@ export default class ExportE2eKeysDialog extends React.Component {
-
diff --git a/src/async-components/views/dialogs/security/ImportE2eKeysDialog.js b/src/async-components/views/dialogs/security/ImportE2eKeysDialog.js index 6017d07047..0936ad696d 100644 --- a/src/async-components/views/dialogs/security/ImportE2eKeysDialog.js +++ b/src/async-components/views/dialogs/security/ImportE2eKeysDialog.js @@ -174,7 +174,10 @@ export default class ImportE2eKeysDialog extends React.Component {
-
; } diff --git a/src/components/structures/auth/Login.tsx b/src/components/structures/auth/Login.tsx index 6a3d339681..7a05d8c6c6 100644 --- a/src/components/structures/auth/Login.tsx +++ b/src/components/structures/auth/Login.tsx @@ -463,7 +463,9 @@ export default class LoginComponent extends React.PureComponent "Either use HTTPS or enable unsafe scripts.", {}, { 'a': (sub) => { - return { sub } diff --git a/src/components/structures/auth/Registration.tsx b/src/components/structures/auth/Registration.tsx index 549e47260f..2b97650d4b 100644 --- a/src/components/structures/auth/Registration.tsx +++ b/src/components/structures/auth/Registration.tsx @@ -557,12 +557,16 @@ export default class Registration extends React.Component { loggedInUserId: this.state.differentLoggedInUserId, }, ) }

-

{ - const sessionLoaded = await this.onLoginClickWithCheck(event); - if (sessionLoaded) { - dis.dispatch({ action: "view_welcome_page" }); - } - }}> +

{ + const sessionLoaded = await this.onLoginClickWithCheck(event); + if (sessionLoaded) { + dis.dispatch({ action: "view_welcome_page" }); + } + }} + > { _t("Continue with previous account") }

; diff --git a/src/components/views/audio_messages/AudioPlayer.tsx b/src/components/views/audio_messages/AudioPlayer.tsx index fb9270765e..b83f89fe5b 100644 --- a/src/components/views/audio_messages/AudioPlayer.tsx +++ b/src/components/views/audio_messages/AudioPlayer.tsx @@ -14,9 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { Playback, PlaybackState } from "../../../voice/Playback"; import React, { createRef, ReactNode, RefObject } from "react"; -import { UPDATE_EVENT } from "../../../stores/AsyncStore"; import PlayPauseButton from "./PlayPauseButton"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { formatBytes } from "../../../utils/FormattingUtils"; @@ -25,47 +23,13 @@ import { Key } from "../../../Keyboard"; import { _t } from "../../../languageHandler"; import SeekBar from "./SeekBar"; import PlaybackClock from "./PlaybackClock"; - -interface IProps { - // Playback instance to render. Cannot change during component lifecycle: create - // an all-new component instead. - playback: Playback; - - mediaName: string; -} - -interface IState { - playbackPhase: PlaybackState; - error?: boolean; -} +import AudioPlayerBase from "./AudioPlayerBase"; @replaceableComponent("views.audio_messages.AudioPlayer") -export default class AudioPlayer extends React.PureComponent { +export default class AudioPlayer extends AudioPlayerBase { private playPauseRef: RefObject = createRef(); private seekRef: RefObject = createRef(); - constructor(props: IProps) { - super(props); - - this.state = { - playbackPhase: PlaybackState.Decoding, // default assumption - }; - - // We don't need to de-register: the class handles this for us internally - this.props.playback.on(UPDATE_EVENT, this.onPlaybackUpdate); - - // Don't wait for the promise to complete - it will emit a progress update when it - // is done, and it's not meant to take long anyhow. - this.props.playback.prepare().catch(e => { - console.error("Error processing audio file:", e); - this.setState({ error: true }); - }); - } - - private onPlaybackUpdate = (ev: PlaybackState) => { - this.setState({ playbackPhase: ev }); - }; - private onKeyDown = (ev: React.KeyboardEvent) => { // stopPropagation() prevents the FocusComposer catch-all from triggering, // but we need to do it on key down instead of press (even though the user @@ -91,10 +55,10 @@ export default class AudioPlayer extends React.PureComponent { return `(${formatBytes(bytes)})`; } - public render(): ReactNode { + protected renderComponent(): ReactNode { // tabIndex=0 to ensure that the whole component becomes a tab stop, where we handle keyboard // events for accessibility - return <> + return (
{
- { this.state.error &&
{ _t("Error downloading audio") }
} - ; + ); } } diff --git a/src/components/views/audio_messages/AudioPlayerBase.tsx b/src/components/views/audio_messages/AudioPlayerBase.tsx new file mode 100644 index 0000000000..d8fc9d507f --- /dev/null +++ b/src/components/views/audio_messages/AudioPlayerBase.tsx @@ -0,0 +1,70 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { Playback, PlaybackState } from "../../../audio/Playback"; +import { TileShape } from "../rooms/EventTile"; +import React, { ReactNode } from "react"; +import { UPDATE_EVENT } from "../../../stores/AsyncStore"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { _t } from "../../../languageHandler"; + +interface IProps { + // Playback instance to render. Cannot change during component lifecycle: create + // an all-new component instead. + playback: Playback; + + mediaName?: string; + tileShape?: TileShape; +} + +interface IState { + playbackPhase: PlaybackState; + error?: boolean; +} + +@replaceableComponent("views.audio_messages.AudioPlayerBase") +export default abstract class AudioPlayerBase extends React.PureComponent { + constructor(props: IProps) { + super(props); + + this.state = { + playbackPhase: PlaybackState.Decoding, // default assumption + }; + + // We don't need to de-register: the class handles this for us internally + this.props.playback.on(UPDATE_EVENT, this.onPlaybackUpdate); + + // Don't wait for the promise to complete - it will emit a progress update when it + // is done, and it's not meant to take long anyhow. + this.props.playback.prepare().catch(e => { + console.error("Error processing audio file:", e); + this.setState({ error: true }); + }); + } + + private onPlaybackUpdate = (ev: PlaybackState) => { + this.setState({ playbackPhase: ev }); + }; + + protected abstract renderComponent(): ReactNode; + + public render(): ReactNode { + return <> + { this.renderComponent() } + { this.state.error &&
{ _t("Error downloading audio") }
} + ; + } +} diff --git a/src/components/views/audio_messages/DurationClock.tsx b/src/components/views/audio_messages/DurationClock.tsx index 81852b5944..15bc6c98a4 100644 --- a/src/components/views/audio_messages/DurationClock.tsx +++ b/src/components/views/audio_messages/DurationClock.tsx @@ -17,7 +17,7 @@ limitations under the License. import React from "react"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import Clock from "./Clock"; -import { Playback } from "../../../voice/Playback"; +import { Playback } from "../../../audio/Playback"; interface IProps { playback: Playback; diff --git a/src/components/views/audio_messages/LiveRecordingClock.tsx b/src/components/views/audio_messages/LiveRecordingClock.tsx index a9dbd3c52f..e7330efc1d 100644 --- a/src/components/views/audio_messages/LiveRecordingClock.tsx +++ b/src/components/views/audio_messages/LiveRecordingClock.tsx @@ -15,7 +15,7 @@ limitations under the License. */ import React from "react"; -import { IRecordingUpdate, VoiceRecording } from "../../../voice/VoiceRecording"; +import { IRecordingUpdate, VoiceRecording } from "../../../audio/VoiceRecording"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import Clock from "./Clock"; import { MarkedExecution } from "../../../utils/MarkedExecution"; diff --git a/src/components/views/audio_messages/LiveRecordingWaveform.tsx b/src/components/views/audio_messages/LiveRecordingWaveform.tsx index b9c5f80f05..9c33889884 100644 --- a/src/components/views/audio_messages/LiveRecordingWaveform.tsx +++ b/src/components/views/audio_messages/LiveRecordingWaveform.tsx @@ -15,7 +15,7 @@ limitations under the License. */ import React from "react"; -import { IRecordingUpdate, RECORDING_PLAYBACK_SAMPLES, VoiceRecording } from "../../../voice/VoiceRecording"; +import { IRecordingUpdate, RECORDING_PLAYBACK_SAMPLES, VoiceRecording } from "../../../audio/VoiceRecording"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { arrayFastResample } from "../../../utils/arrays"; import { percentageOf } from "../../../utils/numbers"; diff --git a/src/components/views/audio_messages/PlayPauseButton.tsx b/src/components/views/audio_messages/PlayPauseButton.tsx index a4f1e770f2..de2822cc39 100644 --- a/src/components/views/audio_messages/PlayPauseButton.tsx +++ b/src/components/views/audio_messages/PlayPauseButton.tsx @@ -18,7 +18,7 @@ import React, { ReactNode } from "react"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import { _t } from "../../../languageHandler"; -import { Playback, PlaybackState } from "../../../voice/Playback"; +import { Playback, PlaybackState } from "../../../audio/Playback"; import classNames from "classnames"; // omitted props are handled by render function diff --git a/src/components/views/audio_messages/PlaybackClock.tsx b/src/components/views/audio_messages/PlaybackClock.tsx index 374d47c31d..affb025d86 100644 --- a/src/components/views/audio_messages/PlaybackClock.tsx +++ b/src/components/views/audio_messages/PlaybackClock.tsx @@ -17,7 +17,7 @@ limitations under the License. import React from "react"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import Clock from "./Clock"; -import { Playback, PlaybackState } from "../../../voice/Playback"; +import { Playback, PlaybackState } from "../../../audio/Playback"; import { UPDATE_EVENT } from "../../../stores/AsyncStore"; interface IProps { diff --git a/src/components/views/audio_messages/PlaybackWaveform.tsx b/src/components/views/audio_messages/PlaybackWaveform.tsx index ea1b846c01..96fd3f5ae2 100644 --- a/src/components/views/audio_messages/PlaybackWaveform.tsx +++ b/src/components/views/audio_messages/PlaybackWaveform.tsx @@ -18,7 +18,7 @@ import React from "react"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { arraySeed, arrayTrimFill } from "../../../utils/arrays"; import Waveform from "./Waveform"; -import { Playback, PLAYBACK_WAVEFORM_SAMPLES } from "../../../voice/Playback"; +import { Playback, PLAYBACK_WAVEFORM_SAMPLES } from "../../../audio/Playback"; import { percentageOf } from "../../../utils/numbers"; interface IProps { diff --git a/src/components/views/audio_messages/RecordingPlayback.tsx b/src/components/views/audio_messages/RecordingPlayback.tsx index ca0ed83d84..e3f612c9b6 100644 --- a/src/components/views/audio_messages/RecordingPlayback.tsx +++ b/src/components/views/audio_messages/RecordingPlayback.tsx @@ -14,68 +14,30 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { Playback, PlaybackState } from "../../../voice/Playback"; import React, { ReactNode } from "react"; -import { UPDATE_EVENT } from "../../../stores/AsyncStore"; import PlayPauseButton from "./PlayPauseButton"; import PlaybackClock from "./PlaybackClock"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { TileShape } from "../rooms/EventTile"; import PlaybackWaveform from "./PlaybackWaveform"; -import { _t } from "../../../languageHandler"; - -interface IProps { - // Playback instance to render. Cannot change during component lifecycle: create - // an all-new component instead. - playback: Playback; - - tileShape?: TileShape; -} - -interface IState { - playbackPhase: PlaybackState; - error?: boolean; -} +import AudioPlayerBase from "./AudioPlayerBase"; @replaceableComponent("views.audio_messages.RecordingPlayback") -export default class RecordingPlayback extends React.PureComponent { - constructor(props: IProps) { - super(props); - - this.state = { - playbackPhase: PlaybackState.Decoding, // default assumption - }; - - // We don't need to de-register: the class handles this for us internally - this.props.playback.on(UPDATE_EVENT, this.onPlaybackUpdate); - - // Don't wait for the promise to complete - it will emit a progress update when it - // is done, and it's not meant to take long anyhow. - this.props.playback.prepare().catch(e => { - console.error("Error processing audio file:", e); - this.setState({ error: true }); - }); - } - +export default class RecordingPlayback extends AudioPlayerBase { private get isWaveformable(): boolean { return this.props.tileShape !== TileShape.Notif && this.props.tileShape !== TileShape.FileGrid && this.props.tileShape !== TileShape.Pinned; } - private onPlaybackUpdate = (ev: PlaybackState) => { - this.setState({ playbackPhase: ev }); - }; - - public render(): ReactNode { + protected renderComponent(): ReactNode { const shapeClass = !this.isWaveformable ? 'mx_VoiceMessagePrimaryContainer_noWaveform' : ''; - return <> + return (
{ this.isWaveformable && }
- { this.state.error &&
{ _t("Error downloading audio") }
} - ; + ); } } diff --git a/src/components/views/audio_messages/SeekBar.tsx b/src/components/views/audio_messages/SeekBar.tsx index 5231a2fb79..f0c03bb032 100644 --- a/src/components/views/audio_messages/SeekBar.tsx +++ b/src/components/views/audio_messages/SeekBar.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { Playback, PlaybackState } from "../../../voice/Playback"; +import { Playback, PlaybackState } from "../../../audio/Playback"; import React, { ChangeEvent, CSSProperties, ReactNode } from "react"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { MarkedExecution } from "../../../utils/MarkedExecution"; diff --git a/src/components/views/audio_messages/Waveform.tsx b/src/components/views/audio_messages/Waveform.tsx index 8a4427fd01..4e44abdf46 100644 --- a/src/components/views/audio_messages/Waveform.tsx +++ b/src/components/views/audio_messages/Waveform.tsx @@ -54,9 +54,13 @@ export default class Waveform extends React.PureComponent { 'mx_Waveform_bar': true, 'mx_Waveform_bar_100pct': isCompleteBar, }); - return ; + return ; }) } ; } diff --git a/src/components/views/auth/CaptchaForm.tsx b/src/components/views/auth/CaptchaForm.tsx index b1c09f2b22..97f45167a8 100644 --- a/src/components/views/auth/CaptchaForm.tsx +++ b/src/components/views/auth/CaptchaForm.tsx @@ -103,8 +103,8 @@ export default class CaptchaForm extends React.Component{ _t("Accept") }; + submitButton = ; } return ( @@ -616,7 +618,9 @@ export class MsisdnAuthEntry extends React.Component
- diff --git a/src/components/views/avatars/BaseAvatar.tsx b/src/components/views/avatars/BaseAvatar.tsx index 87cdbe7512..6aaef29854 100644 --- a/src/components/views/avatars/BaseAvatar.tsx +++ b/src/components/views/avatars/BaseAvatar.tsx @@ -187,7 +187,8 @@ const BaseAvatar = (props: IProps) => { width: toPx(width), height: toPx(height), }} - title={title} alt={_t("Avatar")} + title={title} + alt={_t("Avatar")} inputRef={inputRef} {...otherProps} /> ); @@ -201,7 +202,8 @@ const BaseAvatar = (props: IProps) => { width: toPx(width), height: toPx(height), }} - title={title} alt="" + title={title} + alt="" ref={inputRef} {...otherProps} /> ); diff --git a/src/components/views/avatars/MemberAvatar.tsx b/src/components/views/avatars/MemberAvatar.tsx index 61155e3880..11c24a5981 100644 --- a/src/components/views/avatars/MemberAvatar.tsx +++ b/src/components/views/avatars/MemberAvatar.tsx @@ -102,8 +102,12 @@ export default class MemberAvatar extends React.Component { } return ( - + ); } } diff --git a/src/components/views/avatars/RoomAvatar.tsx b/src/components/views/avatars/RoomAvatar.tsx index a07990c3bb..f285222f7b 100644 --- a/src/components/views/avatars/RoomAvatar.tsx +++ b/src/components/views/avatars/RoomAvatar.tsx @@ -13,9 +13,11 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ + import React, { ComponentProps } from 'react'; import { Room } from 'matrix-js-sdk/src/models/room'; import { ResizeMethod } from 'matrix-js-sdk/src/@types/partials'; +import classNames from "classnames"; import BaseAvatar from './BaseAvatar'; import ImageView from '../elements/ImageView'; @@ -32,11 +34,14 @@ interface IProps extends Omit, "name" | "idNam // oobData.avatarUrl should be set (else there // would be nowhere to get the avatar from) room?: Room; - oobData?: IOOBData; + oobData?: IOOBData & { + roomId?: string; + }; width?: number; height?: number; resizeMethod?: ResizeMethod; viewAvatarOnClick?: boolean; + className?: string; onClick?(): void; } @@ -129,15 +134,19 @@ export default class RoomAvatar extends React.Component { }; public render() { - const { room, oobData, viewAvatarOnClick, onClick, ...otherProps } = this.props; + const { room, oobData, viewAvatarOnClick, onClick, className, ...otherProps } = this.props; const roomName = room ? room.name : oobData.name; // If the room is a DM, we use the other user's ID for the color hash // in order to match the room avatar with their avatar - const idName = room ? (DMRoomMap.shared().getUserIdForRoomId(room.roomId) ?? room.roomId) : null; + const idName = room ? (DMRoomMap.shared().getUserIdForRoomId(room.roomId) ?? room.roomId) : oobData.roomId; return ( - this.props.onFinished(); }; + onKeyDown = (ev) => { + // Prevent Backspace and Delete keys from functioning in the entry field + if (ev.code === "Backspace" || ev.code === "Delete") { + ev.preventDefault(); + } + }; + onChange = (ev) => { this.setState({ value: ev.target.value }); }; @@ -60,8 +67,11 @@ export default class DialpadContextMenu extends React.Component
-
diff --git a/src/components/views/context_menus/IconizedContextMenu.tsx b/src/components/views/context_menus/IconizedContextMenu.tsx index 1d822fd246..571b0b39bf 100644 --- a/src/components/views/context_menus/IconizedContextMenu.tsx +++ b/src/components/views/context_menus/IconizedContextMenu.tsx @@ -86,14 +86,18 @@ export const IconizedContextMenuCheckbox: React.FC = ({ > { label } - { active && } + ; }; -export const IconizedContextMenuOption: React.FC = ({ label, iconClassName, ...props }) => { +export const IconizedContextMenuOption: React.FC = ({ label, iconClassName, children, ...props }) => { return { iconClassName && } { label } + { children } ; }; diff --git a/src/components/views/context_menus/SpaceContextMenu.tsx b/src/components/views/context_menus/SpaceContextMenu.tsx new file mode 100644 index 0000000000..3da00e71aa --- /dev/null +++ b/src/components/views/context_menus/SpaceContextMenu.tsx @@ -0,0 +1,216 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React, { useContext } from "react"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { EventType } from "matrix-js-sdk/src/@types/event"; + +import { + IProps as IContextMenuProps, +} from "../../structures/ContextMenu"; +import IconizedContextMenu, { IconizedContextMenuOption, IconizedContextMenuOptionList } from "./IconizedContextMenu"; +import { _t } from "../../../languageHandler"; +import { + leaveSpace, + shouldShowSpaceSettings, + showAddExistingRooms, + showCreateNewRoom, + showCreateNewSubspace, + showSpaceInvite, + showSpaceSettings, +} from "../../../utils/space"; +import MatrixClientContext from "../../../contexts/MatrixClientContext"; +import { ButtonEvent } from "../elements/AccessibleButton"; +import defaultDispatcher from "../../../dispatcher/dispatcher"; +import RoomViewStore from "../../../stores/RoomViewStore"; +import { SetRightPanelPhasePayload } from "../../../dispatcher/payloads/SetRightPanelPhasePayload"; +import { Action } from "../../../dispatcher/actions"; +import { RightPanelPhases } from "../../../stores/RightPanelStorePhases"; +import { BetaPill } from "../beta/BetaCard"; + +interface IProps extends IContextMenuProps { + space: Room; +} + +const SpaceContextMenu = ({ space, onFinished, ...props }: IProps) => { + const cli = useContext(MatrixClientContext); + const userId = cli.getUserId(); + + let inviteOption; + if (space.getJoinRule() === "public" || space.canInvite(userId)) { + const onInviteClick = (ev: ButtonEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + showSpaceInvite(space); + onFinished(); + }; + + inviteOption = ( + + ); + } + + let settingsOption; + let leaveSection; + if (shouldShowSpaceSettings(space)) { + const onSettingsClick = (ev: ButtonEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + showSpaceSettings(space); + onFinished(); + }; + + settingsOption = ( + + ); + } else { + const onLeaveClick = (ev: ButtonEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + leaveSpace(space); + onFinished(); + }; + + leaveSection = + + ; + } + + const canAddRooms = space.currentState.maySendStateEvent(EventType.SpaceChild, userId); + + let newRoomSection; + if (space.currentState.maySendStateEvent(EventType.SpaceChild, userId)) { + const onNewRoomClick = (ev: ButtonEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + showCreateNewRoom(space); + onFinished(); + }; + + const onAddExistingRoomClick = (ev: ButtonEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + showAddExistingRooms(space); + onFinished(); + }; + + const onNewSubspaceClick = (ev: ButtonEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + showCreateNewSubspace(space); + onFinished(); + }; + + newRoomSection = + + + + + + ; + } + + const onMembersClick = (ev: ButtonEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + if (!RoomViewStore.getRoomId()) { + defaultDispatcher.dispatch({ + action: "view_room", + room_id: space.roomId, + }, true); + } + + defaultDispatcher.dispatch({ + action: Action.SetRightPanelPhase, + phase: RightPanelPhases.SpaceMemberList, + refireParams: { space: space }, + }); + onFinished(); + }; + + const onExploreRoomsClick = (ev: ButtonEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + defaultDispatcher.dispatch({ + action: "view_room", + room_id: space.roomId, + }); + onFinished(); + }; + + return +
+ { space.name } +
+ + { inviteOption } + + { settingsOption } + + + { newRoomSection } + { leaveSection } +
; +}; + +export default SpaceContextMenu; + diff --git a/src/components/views/context_menus/StatusMessageContextMenu.js b/src/components/views/context_menus/StatusMessageContextMenu.js index f90d9cc005..e05b05116c 100644 --- a/src/components/views/context_menus/StatusMessageContextMenu.js +++ b/src/components/views/context_menus/StatusMessageContextMenu.js @@ -109,8 +109,10 @@ export default class StatusMessageContextMenu extends React.Component {
; } } else { - actionButton = { _t("Set status") } ; @@ -121,12 +123,19 @@ export default class StatusMessageContextMenu extends React.Component { spinner = ; } - const form =
-
diff --git a/src/components/views/context_menus/WidgetContextMenu.tsx b/src/components/views/context_menus/WidgetContextMenu.tsx index b21efdceb9..26d7b640a4 100644 --- a/src/components/views/context_menus/WidgetContextMenu.tsx +++ b/src/components/views/context_menus/WidgetContextMenu.tsx @@ -76,7 +76,8 @@ const WidgetContextMenu: React.FC = ({ onFinished(); }; streamAudioStreamButton = ; } diff --git a/src/components/views/dialogs/AddExistingSubspaceDialog.tsx b/src/components/views/dialogs/AddExistingSubspaceDialog.tsx new file mode 100644 index 0000000000..7fef2c2d9d --- /dev/null +++ b/src/components/views/dialogs/AddExistingSubspaceDialog.tsx @@ -0,0 +1,67 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React, { useState } from "react"; +import { Room } from "matrix-js-sdk/src/models/room"; + +import { _t } from '../../../languageHandler'; +import BaseDialog from "./BaseDialog"; +import AccessibleButton from "../elements/AccessibleButton"; +import MatrixClientContext from "../../../contexts/MatrixClientContext"; +import { AddExistingToSpace, defaultSpacesRenderer, SubspaceSelector } from "./AddExistingToSpaceDialog"; + +interface IProps { + space: Room; + onCreateSubspaceClick(): void; + onFinished(added?: boolean): void; +} + +const AddExistingSubspaceDialog: React.FC = ({ space, onCreateSubspaceClick, onFinished }) => { + const [selectedSpace, setSelectedSpace] = useState(space); + + return + )} + className="mx_AddExistingToSpaceDialog" + contentId="mx_AddExistingToSpace" + onFinished={onFinished} + fixedWidth={false} + > + + +
{ _t("Want to add a new space instead?") }
+ + { _t("Create a new space") } + + } + filterPlaceholder={_t("Search for spaces")} + spacesRenderer={defaultSpacesRenderer} + /> +
+
; +}; + +export default AddExistingSubspaceDialog; + diff --git a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx index 0f78b971eb..cf4f369d09 100644 --- a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx +++ b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx @@ -17,11 +17,10 @@ limitations under the License. import React, { ReactNode, useContext, useMemo, useState } from "react"; import classNames from "classnames"; import { Room } from "matrix-js-sdk/src/models/room"; -import { MatrixClient } from "matrix-js-sdk/src/client"; import { sleep } from "matrix-js-sdk/src/utils"; +import { EventType } from "matrix-js-sdk/src/@types/event"; import { _t } from '../../../languageHandler'; -import { IDialogProps } from "./IDialogProps"; import BaseDialog from "./BaseDialog"; import Dropdown from "../elements/Dropdown"; import SearchBox from "../../structures/SearchBox"; @@ -36,20 +35,20 @@ import StyledCheckbox from "../elements/StyledCheckbox"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import { sortRooms } from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm"; import ProgressBar from "../elements/ProgressBar"; -import { SpaceFeedbackPrompt } from "../../structures/SpaceRoomView"; import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar"; import QueryMatcher from "../../../autocomplete/QueryMatcher"; import TruncatedList from "../elements/TruncatedList"; import EntityTile from "../rooms/EntityTile"; import BaseAvatar from "../avatars/BaseAvatar"; -interface IProps extends IDialogProps { - matrixClient: MatrixClient; +interface IProps { space: Room; - onCreateRoomClick(cli: MatrixClient, space: Room): void; + onCreateRoomClick(): void; + onAddSubspaceClick(): void; + onFinished(added?: boolean): void; } -const Entry = ({ room, checked, onChange }) => { +export const Entry = ({ room, checked, onChange }) => { return
{ } return ( -
@@ -133,8 +135,11 @@ export default class CreateGroupDialog extends React.Component {
- { + private readonly supportsRestricted: boolean; private nameField = createRef(); private aliasField = createRef(); constructor(props) { super(props); + this.supportsRestricted = this.props.parentSpace && !!SpaceStore.instance.restrictedJoinRuleSupport?.preferred; + + let joinRule = JoinRule.Invite; + if (this.props.defaultPublic) { + joinRule = JoinRule.Public; + } else if (this.supportsRestricted) { + joinRule = JoinRule.Restricted; + } + const config = SdkConfig.get(); this.state = { - isPublic: this.props.defaultPublic || false, + joinRule, isEncrypted: privateShouldBeEncrypted(), name: this.props.defaultName || "", topic: "", @@ -81,13 +93,18 @@ export default class CreateRoomDialog extends React.Component { const opts: IOpts = {}; const createOpts: IOpts["createOpts"] = opts.createOpts = {}; createOpts.name = this.state.name; - if (this.state.isPublic) { + + if (this.state.joinRule === JoinRule.Public) { createOpts.visibility = Visibility.Public; createOpts.preset = Preset.PublicChat; opts.guestAccess = false; const { alias } = this.state; createOpts.room_alias_name = alias.substr(1, alias.indexOf(":") - 1); + } else { + // If we cannot change encryption we pass `true` for safety, the server should automatically do this for us. + opts.encryption = this.state.canChangeEncryption ? this.state.isEncrypted : true; } + if (this.state.topic) { createOpts.topic = this.state.topic; } @@ -95,22 +112,13 @@ export default class CreateRoomDialog extends React.Component { createOpts.creation_content = { 'm.federate': false }; } - if (!this.state.isPublic) { - if (this.state.canChangeEncryption) { - opts.encryption = this.state.isEncrypted; - } else { - // the server should automatically do this for us, but for safety - // we'll demand it too. - opts.encryption = true; - } - } - if (CommunityPrototypeStore.instance.getSelectedCommunityId()) { opts.associatedWithCommunity = CommunityPrototypeStore.instance.getSelectedCommunityId(); } - if (this.props.parentSpace) { + if (this.props.parentSpace && this.state.joinRule === JoinRule.Restricted) { opts.parentSpace = this.props.parentSpace; + opts.joinRule = JoinRule.Restricted; } return opts; @@ -172,8 +180,8 @@ export default class CreateRoomDialog extends React.Component { this.setState({ topic: ev.target.value }); }; - private onPublicChange = (isPublic: boolean) => { - this.setState({ isPublic }); + private onJoinRuleChange = (joinRule: JoinRule) => { + this.setState({ joinRule }); }; private onEncryptedChange = (isEncrypted: boolean) => { @@ -210,7 +218,7 @@ export default class CreateRoomDialog extends React.Component { render() { let aliasField; - if (this.state.isPublic) { + if (this.state.joinRule === JoinRule.Public) { const domain = MatrixClientPeg.get().getDomain(); aliasField = (
@@ -224,19 +232,52 @@ export default class CreateRoomDialog extends React.Component { ); } - let publicPrivateLabel =

{ _t( - "Private rooms can be found and joined by invitation only. Public rooms can be " + - "found and joined by anyone.", - ) }

; + let publicPrivateLabel: JSX.Element; if (CommunityPrototypeStore.instance.getSelectedCommunityId()) { - publicPrivateLabel =

{ _t( - "Private rooms can be found and joined by invitation only. Public rooms can be " + - "found and joined by anyone in this community.", - ) }

; + publicPrivateLabel =

+ { _t( + "Private rooms can be found and joined by invitation only. Public rooms can be " + + "found and joined by anyone in this community.", + ) } +

; + } else if (this.state.joinRule === JoinRule.Restricted) { + publicPrivateLabel =

+ { _t( + "Everyone in will be able to find and join this room.", {}, { + SpaceName: () => { this.props.parentSpace.name }, + }, + ) } +   + { _t("You can change this at any time from room settings.") } +

; + } else if (this.state.joinRule === JoinRule.Public && this.props.parentSpace) { + publicPrivateLabel =

+ { _t( + "Anyone will be able to find and join this room, not just members of .", {}, { + SpaceName: () => { this.props.parentSpace.name }, + }, + ) } +   + { _t("You can change this at any time from room settings.") } +

; + } else if (this.state.joinRule === JoinRule.Public) { + publicPrivateLabel =

+ { _t("Anyone will be able to find and join this room.") } +   + { _t("You can change this at any time from room settings.") } +

; + } else if (this.state.joinRule === JoinRule.Invite) { + publicPrivateLabel =

+ { _t( + "Only people invited will be able to find and join this room.", + ) } +   + { _t("You can change this at any time from room settings.") } +

; } let e2eeSection; - if (!this.state.isPublic) { + if (this.state.joinRule !== JoinRule.Public) { let microcopy; if (privateShouldBeEncrypted()) { if (this.state.canChangeEncryption) { @@ -273,15 +314,16 @@ export default class CreateRoomDialog extends React.Component { ); } - let title = this.state.isPublic ? _t('Create a public room') : _t('Create a private room'); + let title = _t("Create a room"); if (CommunityPrototypeStore.instance.getSelectedCommunityId()) { const name = CommunityPrototypeStore.instance.getSelectedCommunityName(); title = _t("Create a room in %(communityName)s", { communityName: name }); + } else if (!this.props.parentSpace) { + title = this.state.joinRule === JoinRule.Public ? _t('Create a public room') : _t('Create a private room'); } + return ( - +
{ value={this.state.topic} className="mx_CreateRoomDialog_topic" /> - + { publicPrivateLabel } { e2eeSection } { aliasField } diff --git a/src/components/views/dialogs/CreateSubspaceDialog.tsx b/src/components/views/dialogs/CreateSubspaceDialog.tsx new file mode 100644 index 0000000000..0d71eb2de3 --- /dev/null +++ b/src/components/views/dialogs/CreateSubspaceDialog.tsx @@ -0,0 +1,210 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React, { useRef, useState } from "react"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { JoinRule, Preset } from "matrix-js-sdk/src/@types/partials"; +import { RoomType } from "matrix-js-sdk/src/@types/event"; + +import { _t } from '../../../languageHandler'; +import BaseDialog from "./BaseDialog"; +import AccessibleButton from "../elements/AccessibleButton"; +import MatrixClientContext from "../../../contexts/MatrixClientContext"; +import { BetaPill } from "../beta/BetaCard"; +import Field from "../elements/Field"; +import RoomAliasField from "../elements/RoomAliasField"; +import SpaceStore from "../../../stores/SpaceStore"; +import { SpaceCreateForm } from "../spaces/SpaceCreateMenu"; +import createRoom from "../../../createRoom"; +import { SubspaceSelector } from "./AddExistingToSpaceDialog"; +import JoinRuleDropdown from "../elements/JoinRuleDropdown"; + +interface IProps { + space: Room; + onAddExistingSpaceClick(): void; + onFinished(added?: boolean): void; +} + +const CreateSubspaceDialog: React.FC = ({ space, onAddExistingSpaceClick, onFinished }) => { + const [parentSpace, setParentSpace] = useState(space); + + const [busy, setBusy] = useState(false); + const [name, setName] = useState(""); + const spaceNameField = useRef(); + const [alias, setAlias] = useState(""); + const spaceAliasField = useRef(); + const [avatar, setAvatar] = useState(null); + const [topic, setTopic] = useState(""); + + const supportsRestricted = !!SpaceStore.instance.restrictedJoinRuleSupport?.preferred; + + const spaceJoinRule = space.getJoinRule(); + let defaultJoinRule = JoinRule.Invite; + if (spaceJoinRule === JoinRule.Public) { + defaultJoinRule = JoinRule.Public; + } else if (supportsRestricted) { + defaultJoinRule = JoinRule.Restricted; + } + const [joinRule, setJoinRule] = useState(defaultJoinRule); + + const onCreateSubspaceClick = async (e) => { + e.preventDefault(); + if (busy) return; + + setBusy(true); + // require & validate the space name field + if (!await spaceNameField.current.validate({ allowEmpty: false })) { + spaceNameField.current.focus(); + spaceNameField.current.validate({ allowEmpty: false, focused: true }); + setBusy(false); + return; + } + // validate the space name alias field but do not require it + if (joinRule === JoinRule.Public && !await spaceAliasField.current.validate({ allowEmpty: true })) { + spaceAliasField.current.focus(); + spaceAliasField.current.validate({ allowEmpty: true, focused: true }); + setBusy(false); + return; + } + + try { + await createRoom({ + createOpts: { + preset: joinRule === JoinRule.Public ? Preset.PublicChat : Preset.PrivateChat, + name, + power_level_content_override: { + // Only allow Admins to write to the timeline to prevent hidden sync spam + events_default: 100, + ...joinRule === JoinRule.Public ? { invite: 0 } : {}, + }, + room_alias_name: joinRule === JoinRule.Public && alias + ? alias.substr(1, alias.indexOf(":") - 1) + : undefined, + topic, + }, + avatar, + roomType: RoomType.Space, + parentSpace, + spinner: false, + encryption: false, + andView: true, + inlineErrors: true, + }); + + onFinished(true); + } catch (e) { + console.error(e); + } + }; + + let joinRuleMicrocopy: JSX.Element; + if (joinRule === JoinRule.Restricted) { + joinRuleMicrocopy =

+ { _t( + "Anyone in will be able to find and join.", {}, { + SpaceName: () => { parentSpace.name }, + }, + ) } +

; + } else if (joinRule === JoinRule.Public) { + joinRuleMicrocopy =

+ { _t( + "Anyone will be able to find and join this space, not just members of .", {}, { + SpaceName: () => { parentSpace.name }, + }, + ) } +

; + } else if (joinRule === JoinRule.Invite) { + joinRuleMicrocopy =

+ { _t("Only people invited will be able to find and join this space.") } +

; + } + + return + )} + className="mx_CreateSubspaceDialog" + contentId="mx_CreateSubspaceDialog" + onFinished={onFinished} + fixedWidth={false} + > + +
+
+ + { _t("Add a space to a space you manage.") } +
+ + + + { joinRuleMicrocopy } + +
+ +
+
+
{ _t("Want to add an existing space instead?") }
+ { + onAddExistingSpaceClick(); + onFinished(); + }} + > + { _t("Add existing space") } + +
+ + onFinished(false)}> + { _t("Cancel") } + + + { busy ? _t("Adding...") : _t("Add") } + +
+
+
; +}; + +export default CreateSubspaceDialog; + diff --git a/src/components/views/dialogs/CryptoStoreTooNewDialog.tsx b/src/components/views/dialogs/CryptoStoreTooNewDialog.tsx index 134c4ab79e..d03b668cd9 100644 --- a/src/components/views/dialogs/CryptoStoreTooNewDialog.tsx +++ b/src/components/views/dialogs/CryptoStoreTooNewDialog.tsx @@ -72,7 +72,7 @@ const CryptoStoreTooNewDialog: React.FC = (props: IProps) => { hasCancel={false} onPrimaryButtonClick={props.onFinished} > - diff --git a/src/components/views/dialogs/DevtoolsDialog.tsx b/src/components/views/dialogs/DevtoolsDialog.tsx index 61cda796ee..30e6c70f0e 100644 --- a/src/components/views/dialogs/DevtoolsDialog.tsx +++ b/src/components/views/dialogs/DevtoolsDialog.tsx @@ -182,14 +182,23 @@ export class SendCustomEvent extends GenericEditor - +
{ !this.state.message && } { showTglFlip &&
- - +
{ !this.state.message && } { !this.state.message &&
- - + key={this.props.children[0] ? this.props.children[0].key : ''} + /> ; + return ; } return
@@ -594,7 +625,9 @@ class AccountDataExplorer extends React.PureComponent; + }} + forceMode={true} + />; } return
@@ -631,7 +664,9 @@ class AccountDataExplorer extends React.PureComponent
-
@@ -1040,7 +1080,9 @@ class SettingsExplorer extends React.PureComponent this.onViewClick(e, i)}> { i } - this.onEditClick(e, i)} + this.onEditClick(e, i)} className='mx_DevTools_SettingsExplorer_edit' > ✏ @@ -1104,18 +1146,26 @@ class SettingsExplorer extends React.PureComponent
diff --git a/src/components/views/dialogs/EditCommunityPrototypeDialog.tsx b/src/components/views/dialogs/EditCommunityPrototypeDialog.tsx index 1eabb68081..a0e6046d71 100644 --- a/src/components/views/dialogs/EditCommunityPrototypeDialog.tsx +++ b/src/components/views/dialogs/EditCommunityPrototypeDialog.tsx @@ -144,8 +144,10 @@ export default class EditCommunityPrototypeDialog extends React.PureComponent
= ({ room, event, matrixClient: cli, onFinish className = "mx_ForwardList_sending"; disabled = true; title = _t("Sending"); - icon =
; + icon =
; } else if (sendState === SendState.Sent) { className = "mx_ForwardList_sent"; disabled = true; title = _t("Sent"); - icon =
; + icon =
; } else { className = "mx_ForwardList_sendFailed"; disabled = true; @@ -204,10 +204,16 @@ const ForwardDialog: React.FC = ({ matrixClient: cli, event, permalinkCr function overflowTile(overflowCount, totalCount) { const text = _t("and %(count)s others...", { count: overflowCount }); return ( - - } name={text} presenceState="online" suppressOnHover={true} - onClick={() => setTruncateAt(totalCount)} /> + + } + name={text} + presenceState="online" + suppressOnHover={true} + onClick={() => setTruncateAt(totalCount)} + /> ); } diff --git a/src/components/views/dialogs/GenericFeatureFeedbackDialog.tsx b/src/components/views/dialogs/GenericFeatureFeedbackDialog.tsx new file mode 100644 index 0000000000..d68569b126 --- /dev/null +++ b/src/components/views/dialogs/GenericFeatureFeedbackDialog.tsx @@ -0,0 +1,101 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React, { useState } from "react"; + +import QuestionDialog from './QuestionDialog'; +import { _t } from '../../../languageHandler'; +import Field from "../elements/Field"; +import SdkConfig from "../../../SdkConfig"; +import { IDialogProps } from "./IDialogProps"; +import { submitFeedback } from "../../../rageshake/submit-rageshake"; +import StyledCheckbox from "../elements/StyledCheckbox"; +import Modal from "../../../Modal"; +import InfoDialog from "./InfoDialog"; + +interface IProps extends IDialogProps { + title: string; + subheading: string; + rageshakeLabel: string; + rageshakeData?: Record; +} + +const GenericFeatureFeedbackDialog: React.FC = ({ + title, + subheading, + children, + rageshakeLabel, + rageshakeData = {}, + onFinished, +}) => { + const [comment, setComment] = useState(""); + const [canContact, setCanContact] = useState(false); + + const sendFeedback = async (ok: boolean) => { + if (!ok) return onFinished(false); + + submitFeedback(SdkConfig.get().bug_report_endpoint_url, rageshakeLabel, comment, canContact, rageshakeData); + onFinished(true); + + Modal.createTrackedDialog("Feedback Sent", rageshakeLabel, InfoDialog, { + title, + description: _t("Thank you for your feedback, we really appreciate it."), + button: _t("Done"), + hasCloseButton: false, + fixedWidth: false, + }); + }; + + return ( +
+ { subheading } +   + { _t("Your platform and username will be noted to help us use your feedback as much as we can.") } + + { children } +
+ + { + setComment(ev.target.value); + }} + autoFocus={true} + /> + + setCanContact((e.target as HTMLInputElement).checked)} + > + { _t("You may contact me if you have any follow up questions") } + + } + button={_t("Send feedback")} + buttonDisabled={!comment} + onFinished={sendFeedback} + />); +}; + +export default GenericFeatureFeedbackDialog; diff --git a/src/components/views/dialogs/IncomingSasDialog.js b/src/components/views/dialogs/IncomingSasDialog.js index 963d98ef3f..a5c9f2107f 100644 --- a/src/components/views/dialogs/IncomingSasDialog.js +++ b/src/components/views/dialogs/IncomingSasDialog.js @@ -133,18 +133,23 @@ export default class IncomingSasDialog extends React.Component { ? mediaFromMxc(oppProfile.avatar_url).getSquareThumbnailHttp(48) : null; profile =
-

{ oppProfile.displayname }

; } else if (this.state.opponentProfileError) { profile =
-

{ this.props.verifier.userId }

; diff --git a/src/components/views/dialogs/InfoDialog.js b/src/components/views/dialogs/InfoDialog.tsx similarity index 74% rename from src/components/views/dialogs/InfoDialog.js rename to src/components/views/dialogs/InfoDialog.tsx index 8207d334d3..9c74e08e98 100644 --- a/src/components/views/dialogs/InfoDialog.js +++ b/src/components/views/dialogs/InfoDialog.tsx @@ -1,7 +1,6 @@ /* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2017 New Vector Ltd. Copyright 2019 Bastian Masanek, Noxware IT +Copyright 2015 - 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16,31 +15,31 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; -import PropTypes from 'prop-types'; -import * as sdk from '../../../index'; -import { _t } from '../../../languageHandler'; +import React, { ReactNode, KeyboardEvent } from 'react'; import classNames from "classnames"; -export default class InfoDialog extends React.Component { - static propTypes = { - className: PropTypes.string, - title: PropTypes.string, - description: PropTypes.node, - button: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), - onFinished: PropTypes.func, - hasCloseButton: PropTypes.bool, - onKeyDown: PropTypes.func, - fixedWidth: PropTypes.bool, - }; +import { _t } from '../../../languageHandler'; +import * as sdk from '../../../index'; +import { IDialogProps } from "./IDialogProps"; +interface IProps extends IDialogProps { + title?: string; + description?: ReactNode; + className?: string; + button?: boolean | string; + hasCloseButton?: boolean; + fixedWidth?: boolean; + onKeyDown?(event: KeyboardEvent): void; +} + +export default class InfoDialog extends React.Component { static defaultProps = { title: '', description: '', hasCloseButton: false, }; - onFinished = () => { + private onFinished = () => { this.props.onFinished(); }; @@ -63,8 +62,7 @@ export default class InfoDialog extends React.Component { { this.props.button !== false && - } + /> } ); } diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx index 46f140fd39..609829b833 100644 --- a/src/components/views/dialogs/InviteDialog.tsx +++ b/src/components/views/dialogs/InviteDialog.tsx @@ -196,7 +196,9 @@ class DMUserTile extends React.PureComponent { ? + width={avatarSize} + height={avatarSize} + /> : { className='mx_InviteDialog_userTile_remove' onClick={this.onRemove} > - {_t('Remove')} ); @@ -297,7 +302,9 @@ class DMRoomTile extends React.PureComponent { const avatar = (this.props.member as ThreepidMember).isEmail ? + width={avatarSize} + height={avatarSize} + /> : + width={14} + height={14} /> { " " + _t("Invited people will be able to read old messages.") }

; } @@ -1534,14 +1542,18 @@ export default class InviteDialog extends React.PureComponent; } else { - dialPadField = { dialPadField } -
; tabs.push(new Tab(TabId.DialPad, _td("Dial pad"), 'mx_InviteDialog_dialPadIcon', dialPadSection)); dialogContent = - { consultConnectSection } ; diff --git a/src/components/views/dialogs/KeySignatureUploadFailedDialog.js b/src/components/views/dialogs/KeySignatureUploadFailedDialog.js index 9fa9fc1373..6b36c19977 100644 --- a/src/components/views/dialogs/KeySignatureUploadFailedDialog.js +++ b/src/components/views/dialogs/KeySignatureUploadFailedDialog.js @@ -20,10 +20,10 @@ import { _t } from '../../../languageHandler'; import SdkConfig from '../../../SdkConfig'; export default function KeySignatureUploadFailedDialog({ - failures, - source, - continuation, - onFinished, + failures, + source, + continuation, + onFinished, }) { const RETRIES = 2; const BaseDialog = sdk.getComponent('dialogs.BaseDialog'); diff --git a/src/components/views/dialogs/LeaveSpaceDialog.tsx b/src/components/views/dialogs/LeaveSpaceDialog.tsx new file mode 100644 index 0000000000..6e1e798e9d --- /dev/null +++ b/src/components/views/dialogs/LeaveSpaceDialog.tsx @@ -0,0 +1,197 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React, { useEffect, useMemo, useState } from "react"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { JoinRule } from "matrix-js-sdk/src/@types/partials"; + +import { _t } from '../../../languageHandler'; +import DialogButtons from "../elements/DialogButtons"; +import BaseDialog from "../dialogs/BaseDialog"; +import SpaceStore from "../../../stores/SpaceStore"; +import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; +import { Entry } from "./AddExistingToSpaceDialog"; +import SearchBox from "../../structures/SearchBox"; +import QueryMatcher from "../../../autocomplete/QueryMatcher"; +import StyledRadioGroup from "../elements/StyledRadioGroup"; + +enum RoomsToLeave { + All = "All", + Specific = "Specific", + None = "None", +} + +const SpaceChildPicker = ({ filterPlaceholder, rooms, selected, onChange }) => { + const [query, setQuery] = useState(""); + const lcQuery = query.toLowerCase().trim(); + + const filteredRooms = useMemo(() => { + if (!lcQuery) { + return rooms; + } + + const matcher = new QueryMatcher(rooms, { + keys: ["name"], + funcs: [r => [r.getCanonicalAlias(), ...r.getAltAliases()].filter(Boolean)], + shouldMatchWordsOnly: false, + }); + + return matcher.match(lcQuery); + }, [rooms, lcQuery]); + + return
+ + + { filteredRooms.map(room => { + return { + onChange(checked, room); + }} + />; + }) } + { filteredRooms.length < 1 ? + { _t("No results") } + : undefined } + +
; +}; + +const LeaveRoomsPicker = ({ space, spaceChildren, roomsToLeave, setRoomsToLeave }) => { + const selected = useMemo(() => new Set(roomsToLeave), [roomsToLeave]); + const [state, setState] = useState(RoomsToLeave.All); + + useEffect(() => { + if (state === RoomsToLeave.All) { + setRoomsToLeave(spaceChildren); + } else { + setRoomsToLeave([]); + } + }, [setRoomsToLeave, state, spaceChildren]); + + return
+ + + { state === RoomsToLeave.Specific && ( + { + if (selected) { + setRoomsToLeave([room, ...roomsToLeave]); + } else { + setRoomsToLeave(roomsToLeave.filter(r => r !== room)); + } + }} + /> + ) } +
; +}; + +interface IProps { + space: Room; + onFinished(leave: boolean, rooms?: Room[]): void; +} + +const isOnlyAdmin = (room: Room): boolean => { + return !room.getJoinedMembers().some(member => { + return member.userId !== room.client.credentials.userId && member.powerLevelNorm === 100; + }); +}; + +const LeaveSpaceDialog: React.FC = ({ space, onFinished }) => { + const spaceChildren = useMemo(() => SpaceStore.instance.getChildren(space.roomId), [space.roomId]); + const [roomsToLeave, setRoomsToLeave] = useState([]); + + let rejoinWarning; + if (space.getJoinRule() !== JoinRule.Public) { + rejoinWarning = _t("You won't be able to rejoin unless you are re-invited."); + } + + let onlyAdminWarning; + if (isOnlyAdmin(space)) { + onlyAdminWarning = _t("You're the only admin of this space. " + + "Leaving it will mean no one has control over it."); + } else { + const numChildrenOnlyAdminIn = roomsToLeave.filter(isOnlyAdmin).length; + if (numChildrenOnlyAdminIn > 0) { + onlyAdminWarning = _t("You're the only admin of some of the rooms or spaces you wish to leave. " + + "Leaving them will leave them without any admins."); + } + } + + return onFinished(false)} + fixedWidth={false} + > +
+

+ { _t("Are you sure you want to leave ?", {}, { + spaceName: () => { space.name }, + }) } +   + { rejoinWarning } +

+ + { spaceChildren.length > 0 && } + + { onlyAdminWarning &&
+ { onlyAdminWarning } +
} +
+ onFinished(true, roomsToLeave)} + hasCancel={true} + onCancel={onFinished} + /> +
; +}; + +export default LeaveSpaceDialog; diff --git a/src/components/views/dialogs/LogoutDialog.js b/src/components/views/dialogs/LogoutDialog.tsx similarity index 82% rename from src/components/views/dialogs/LogoutDialog.js rename to src/components/views/dialogs/LogoutDialog.tsx index 469cd48093..8c035dcbba 100644 --- a/src/components/views/dialogs/LogoutDialog.js +++ b/src/components/views/dialogs/LogoutDialog.tsx @@ -16,6 +16,7 @@ limitations under the License. */ import React from 'react'; +import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup"; import Modal from '../../../Modal'; import * as sdk from '../../../index'; import dis from '../../../dispatcher/dispatcher'; @@ -24,19 +25,25 @@ import { MatrixClientPeg } from '../../../MatrixClientPeg'; import RestoreKeyBackupDialog from './security/RestoreKeyBackupDialog'; import { replaceableComponent } from "../../../utils/replaceableComponent"; +interface IProps { + onFinished: (success: boolean) => void; +} + +interface IState { + shouldLoadBackupStatus: boolean; + loading: boolean; + backupInfo: IKeyBackupInfo; + error?: string; +} + @replaceableComponent("views.dialogs.LogoutDialog") -export default class LogoutDialog extends React.Component { - defaultProps = { +export default class LogoutDialog extends React.Component { + static defaultProps = { onFinished: function() {}, }; - constructor() { - super(); - this._onSettingsLinkClick = this._onSettingsLinkClick.bind(this); - this._onExportE2eKeysClicked = this._onExportE2eKeysClicked.bind(this); - this._onFinished = this._onFinished.bind(this); - this._onSetRecoveryMethodClick = this._onSetRecoveryMethodClick.bind(this); - this._onLogoutConfirm = this._onLogoutConfirm.bind(this); + constructor(props) { + super(props); const cli = MatrixClientPeg.get(); const shouldLoadBackupStatus = cli.isCryptoEnabled() && !cli.getKeyBackupEnabled(); @@ -49,11 +56,11 @@ export default class LogoutDialog extends React.Component { }; if (shouldLoadBackupStatus) { - this._loadBackupStatus(); + this.loadBackupStatus(); } } - async _loadBackupStatus() { + private async loadBackupStatus() { try { const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion(); this.setState({ @@ -69,29 +76,29 @@ export default class LogoutDialog extends React.Component { } } - _onSettingsLinkClick() { + private onSettingsLinkClick = (): void => { // close dialog - this.props.onFinished(); - } + this.props.onFinished(true); + }; - _onExportE2eKeysClicked() { + private onExportE2eKeysClicked = (): void => { Modal.createTrackedDialogAsync('Export E2E Keys', '', import('../../../async-components/views/dialogs/security/ExportE2eKeysDialog'), { matrixClient: MatrixClientPeg.get(), }, ); - } + }; - _onFinished(confirmed) { + private onFinished = (confirmed: boolean): void => { if (confirmed) { dis.dispatch({ action: 'logout' }); } // close dialog - this.props.onFinished(); - } + this.props.onFinished(confirmed); + }; - _onSetRecoveryMethodClick() { + private onSetRecoveryMethodClick = (): void => { if (this.state.backupInfo) { // A key backup exists for this account, but the creating device is not // verified, so restore the backup which will give us the keys from it and @@ -108,15 +115,15 @@ export default class LogoutDialog extends React.Component { } // close dialog - this.props.onFinished(); - } + this.props.onFinished(true); + }; - _onLogoutConfirm() { + private onLogoutConfirm = (): void => { dis.dispatch({ action: 'logout' }); // close dialog - this.props.onFinished(); - } + this.props.onFinished(true); + }; render() { if (this.state.shouldLoadBackupStatus) { @@ -152,16 +159,16 @@ export default class LogoutDialog extends React.Component {
-
{ _t("Advanced") } -

@@ -174,7 +181,7 @@ export default class LogoutDialog extends React.Component { title={_t("You'll lose access to your encrypted messages")} contentId='mx_Dialog_content' hasCancel={true} - onFinished={this._onFinished} + onFinished={this.onFinished} > { dialogContent } ); @@ -187,7 +194,7 @@ export default class LogoutDialog extends React.Component { "Are you sure you want to sign out?", )} button={_t("Sign out")} - onFinished={this._onFinished} + onFinished={this.onFinished} />); } } diff --git a/src/components/views/dialogs/ManageRestrictedJoinRuleDialog.tsx b/src/components/views/dialogs/ManageRestrictedJoinRuleDialog.tsx new file mode 100644 index 0000000000..c63fdc4c84 --- /dev/null +++ b/src/components/views/dialogs/ManageRestrictedJoinRuleDialog.tsx @@ -0,0 +1,192 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React, { useMemo, useState } from "react"; +import { Room } from "matrix-js-sdk/src/models/room"; + +import { _t } from '../../../languageHandler'; +import { IDialogProps } from "./IDialogProps"; +import BaseDialog from "./BaseDialog"; +import SearchBox from "../../structures/SearchBox"; +import SpaceStore from "../../../stores/SpaceStore"; +import RoomAvatar from "../avatars/RoomAvatar"; +import AccessibleButton from "../elements/AccessibleButton"; +import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; +import StyledCheckbox from "../elements/StyledCheckbox"; +import MatrixClientContext from "../../../contexts/MatrixClientContext"; + +interface IProps extends IDialogProps { + room: Room; + selected?: string[]; +} + +const Entry = ({ room, checked, onChange }) => { + const localRoom = room instanceof Room; + + let description; + if (localRoom) { + description = _t("%(count)s members", { count: room.getJoinedMemberCount() }); + const numChildRooms = SpaceStore.instance.getChildRooms(room.roomId).length; + if (numChildRooms > 0) { + description += " · " + _t("%(count)s rooms", { count: numChildRooms }); + } + } + + return ; +}; + +const ManageRestrictedJoinRuleDialog: React.FC = ({ room, selected = [], onFinished }) => { + const cli = room.client; + const [newSelected, setNewSelected] = useState(new Set(selected)); + const [query, setQuery] = useState(""); + const lcQuery = query.toLowerCase().trim(); + + const [spacesContainingRoom, otherEntries] = useMemo(() => { + const spaces = cli.getVisibleRooms().filter(r => r.getMyMembership() === "join" && r.isSpaceRoom()); + return [ + spaces.filter(r => SpaceStore.instance.getSpaceFilteredRoomIds(r).has(room.roomId)), + selected.map(roomId => { + const room = cli.getRoom(roomId); + if (!room) { + return { roomId, name: roomId } as Room; + } + if (room.getMyMembership() !== "join" || !room.isSpaceRoom()) { + return room; + } + }).filter(Boolean), + ]; + }, [cli, selected, room.roomId]); + + const [filteredSpacesContainingRooms, filteredOtherEntries] = useMemo(() => [ + spacesContainingRoom.filter(r => r.name.toLowerCase().includes(lcQuery)), + otherEntries.filter(r => r.name.toLowerCase().includes(lcQuery)), + ], [spacesContainingRoom, otherEntries, lcQuery]); + + const onChange = (checked: boolean, room: Room): void => { + if (checked) { + newSelected.add(room.roomId); + } else { + newSelected.delete(room.roomId); + } + setNewSelected(new Set(newSelected)); + }; + + let inviteOnlyWarning; + if (newSelected.size < 1) { + inviteOnlyWarning =
+ { _t("You're removing all spaces. Access will default to invite only") } +
; + } + + return +

+ { _t("Decide which spaces can access this room. " + + "If a space is selected, its members can find and join .", {}, { + RoomName: () => { room.name }, + }) } +

+ + + + { filteredSpacesContainingRooms.length > 0 ? ( +
+

{ _t("Spaces you know that contain this room") }

+ { filteredSpacesContainingRooms.map(space => { + return { + onChange(checked, space); + }} + />; + }) } +
+ ) : undefined } + + { filteredOtherEntries.length > 0 ? ( +
+

{ _t("Other spaces or rooms you might not know") }

+
+
{ _t("These are likely ones other room admins are a part of.") }
+
+ { filteredOtherEntries.map(space => { + return { + onChange(checked, space); + }} + />; + }) } +
+ ) : null } + + { filteredSpacesContainingRooms.length + filteredOtherEntries.length < 1 + ? + { _t("No results") } + + : undefined + } +
+ +
+ { inviteOnlyWarning } +
+ onFinished()}> + { _t("Cancel") } + + onFinished(Array.from(newSelected))}> + { _t("Confirm") } + +
+
+
+
; +}; + +export default ManageRestrictedJoinRuleDialog; + diff --git a/src/components/views/dialogs/RoomUpgradeDialog.js b/src/components/views/dialogs/RoomUpgradeDialog.tsx similarity index 66% rename from src/components/views/dialogs/RoomUpgradeDialog.js rename to src/components/views/dialogs/RoomUpgradeDialog.tsx index e48f728383..bcca0e3829 100644 --- a/src/components/views/dialogs/RoomUpgradeDialog.js +++ b/src/components/views/dialogs/RoomUpgradeDialog.tsx @@ -1,5 +1,5 @@ /* -Copyright 2018 New Vector Ltd +Copyright 2018 - 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,19 +15,29 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; -import * as sdk from '../../../index'; -import { MatrixClientPeg } from '../../../MatrixClientPeg'; +import { Room } from "matrix-js-sdk/src/models/room"; + import Modal from '../../../Modal'; import { _t } from '../../../languageHandler'; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { upgradeRoom } from "../../../utils/RoomUpgrade"; +import { IDialogProps } from "./IDialogProps"; +import BaseDialog from "./BaseDialog"; +import ErrorDialog from './ErrorDialog'; +import DialogButtons from '../elements/DialogButtons'; +import Spinner from "../elements/Spinner"; + +interface IProps extends IDialogProps { + room: Room; +} + +interface IState { + busy: boolean; +} @replaceableComponent("views.dialogs.RoomUpgradeDialog") -export default class RoomUpgradeDialog extends React.Component { - static propTypes = { - room: PropTypes.object.isRequired, - onFinished: PropTypes.func.isRequired, - }; +export default class RoomUpgradeDialog extends React.Component { + private targetVersion: string; state = { busy: true, @@ -35,20 +45,19 @@ export default class RoomUpgradeDialog extends React.Component { async componentDidMount() { const recommended = await this.props.room.getRecommendedVersion(); - this._targetVersion = recommended.version; + this.targetVersion = recommended.version; this.setState({ busy: false }); } - _onCancelClick = () => { + private onCancelClick = (): void => { this.props.onFinished(false); }; - _onUpgradeClick = () => { + private onUpgradeClick = (): void => { this.setState({ busy: true }); - MatrixClientPeg.get().upgradeRoom(this.props.room.roomId, this._targetVersion).then(() => { + upgradeRoom(this.props.room, this.targetVersion, false, false).then(() => { this.props.onFinished(true); }).catch((err) => { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createTrackedDialog('Failed to upgrade room', '', ErrorDialog, { title: _t("Failed to upgrade room"), description: ((err && err.message) ? err.message : _t("The room upgrade could not be completed")), @@ -59,29 +68,22 @@ export default class RoomUpgradeDialog extends React.Component { }; render() { - const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); - const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); - const Spinner = sdk.getComponent('views.elements.Spinner'); - let buttons; if (this.state.busy) { buttons = ; } else { buttons = ; } return ( -
  • { _t("Create a new room with the same name, description and avatar") }
  • { _t("Update any local room aliases to point to the new room") }
  • -
  • { _t("Stop users from speaking in the old version of the room, and post a message advising users to move to the new room") }
  • -
  • { _t("Put a link back to the old room at the start of the new room so people can see old messages") }
  • +
  • { _t("Stop users from speaking in the old version of the room, " + + "and post a message advising users to move to the new room") }
  • +
  • { _t("Put a link back to the old room at the start of the new room " + + "so people can see old messages") }
  • { buttons }
    diff --git a/src/components/views/dialogs/RoomUpgradeWarningDialog.js b/src/components/views/dialogs/RoomUpgradeWarningDialog.tsx similarity index 64% rename from src/components/views/dialogs/RoomUpgradeWarningDialog.js rename to src/components/views/dialogs/RoomUpgradeWarningDialog.tsx index 29adb261be..a90f417959 100644 --- a/src/components/views/dialogs/RoomUpgradeWarningDialog.js +++ b/src/components/views/dialogs/RoomUpgradeWarningDialog.tsx @@ -1,5 +1,5 @@ /* -Copyright 2019, 2020 The Matrix.org Foundation C.I.C. +Copyright 2019 - 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,73 +14,82 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; -import PropTypes from 'prop-types'; +import React, { ReactNode } from 'react'; +import { EventType } from 'matrix-js-sdk/src/@types/event'; +import { JoinRule } from 'matrix-js-sdk/src/@types/partials'; + import { _t } from "../../../languageHandler"; import SdkConfig from "../../../SdkConfig"; -import * as sdk from "../../../index"; import LabelledToggleSwitch from "../elements/LabelledToggleSwitch"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import Modal from "../../../Modal"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { IDialogProps } from "./IDialogProps"; +import BugReportDialog from './BugReportDialog'; +import BaseDialog from "./BaseDialog"; +import DialogButtons from "../elements/DialogButtons"; + +interface IProps extends IDialogProps { + roomId: string; + targetVersion: string; + description?: ReactNode; +} + +interface IState { + inviteUsersToNewRoom: boolean; +} @replaceableComponent("views.dialogs.RoomUpgradeWarningDialog") -export default class RoomUpgradeWarningDialog extends React.Component { - static propTypes = { - onFinished: PropTypes.func.isRequired, - roomId: PropTypes.string.isRequired, - targetVersion: PropTypes.string.isRequired, - }; +export default class RoomUpgradeWarningDialog extends React.Component { + private readonly isPrivate: boolean; + private readonly currentVersion: string; constructor(props) { super(props); const room = MatrixClientPeg.get().getRoom(this.props.roomId); - const joinRules = room ? room.currentState.getStateEvents("m.room.join_rules", "") : null; - const isPrivate = joinRules ? joinRules.getContent()['join_rule'] !== 'public' : true; + const joinRules = room?.currentState.getStateEvents(EventType.RoomJoinRules, ""); + this.isPrivate = joinRules?.getContent()['join_rule'] !== JoinRule.Public ?? true; + this.currentVersion = room?.getVersion() || "1"; + this.state = { - currentVersion: room ? room.getVersion() : "1", - isPrivate, inviteUsersToNewRoom: true, }; } - _onContinue = () => { - this.props.onFinished({ continue: true, invite: this.state.isPrivate && this.state.inviteUsersToNewRoom }); + private onContinue = () => { + this.props.onFinished({ continue: true, invite: this.isPrivate && this.state.inviteUsersToNewRoom }); }; - _onCancel = () => { + private onCancel = () => { this.props.onFinished({ continue: false, invite: false }); }; - _onInviteUsersToggle = (newVal) => { - this.setState({ inviteUsersToNewRoom: newVal }); + private onInviteUsersToggle = (inviteUsersToNewRoom: boolean) => { + this.setState({ inviteUsersToNewRoom }); }; - _openBugReportDialog = (e) => { + private openBugReportDialog = (e) => { e.preventDefault(); e.stopPropagation(); - const BugReportDialog = sdk.getComponent("dialogs.BugReportDialog"); Modal.createTrackedDialog('Bug Report Dialog', '', BugReportDialog, {}); }; render() { const brand = SdkConfig.get().brand; - const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); - const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); let inviteToggle = null; - if (this.state.isPrivate) { + if (this.isPrivate) { inviteToggle = ( + onChange={this.onInviteUsersToggle} + label={_t("Automatically invite members from this room to the new one")} /> ); } - const title = this.state.isPrivate ? _t("Upgrade private room") : _t("Upgrade public room"); + const title = this.isPrivate ? _t("Upgrade private room") : _t("Upgrade public room"); let bugReports = (

    @@ -101,7 +110,7 @@ export default class RoomUpgradeWarningDialog extends React.Component { }, { "a": (sub) => { - return { sub }; + return { sub }; }, }, ) } @@ -119,18 +128,26 @@ export default class RoomUpgradeWarningDialog extends React.Component { >

    - { _t( + { this.props.description || _t( "Upgrading a room is an advanced action and is usually recommended when a room " + "is unstable due to bugs, missing features or security vulnerabilities.", ) }

    +

    + { _t( + "Please note upgrading will make a new version of the room. " + + "All current messages will stay in this archived room.", {}, { + b: sub => { sub }, + }, + ) } +

    { bugReports }

    { _t( "You'll upgrade this room from to .", {}, { - oldVersion: () => { this.state.currentVersion }, + oldVersion: () => { this.currentVersion }, newVersion: () => { this.props.targetVersion }, }, ) } @@ -139,9 +156,9 @@ export default class RoomUpgradeWarningDialog extends React.Component {

    ); diff --git a/src/components/views/dialogs/SessionRestoreErrorDialog.js b/src/components/views/dialogs/SessionRestoreErrorDialog.js index b7037d1f4f..eeeadbbfe5 100644 --- a/src/components/views/dialogs/SessionRestoreErrorDialog.js +++ b/src/components/views/dialogs/SessionRestoreErrorDialog.js @@ -85,7 +85,9 @@ export default class SessionRestoreErrorDialog extends React.Component { } return ( - { _t("Forgotten or lost all recovery methods? Reset all", null, { a: (sub) => { sub }, }) }
    diff --git a/src/components/views/dialogs/security/RestoreKeyBackupDialog.js b/src/components/views/dialogs/security/RestoreKeyBackupDialog.js index e8bd8af01c..2b272a3b88 100644 --- a/src/components/views/dialogs/security/RestoreKeyBackupDialog.js +++ b/src/components/views/dialogs/security/RestoreKeyBackupDialog.js @@ -399,7 +399,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent { let keyStatus; if (this.state.recoveryKey.length === 0) { - keyStatus =
    ; + keyStatus =
    ; } else if (this.state.recoveryKeyValid) { keyStatus =
    { "\uD83D\uDC4D " }{ _t("This looks like a valid Security Key!") } diff --git a/src/components/views/elements/AddressTile.tsx b/src/components/views/elements/AddressTile.tsx index cdeb7cacd6..52c0d84ac2 100644 --- a/src/components/views/elements/AddressTile.tsx +++ b/src/components/views/elements/AddressTile.tsx @@ -122,7 +122,7 @@ export default class AddressTile extends React.Component { let dismiss; if (this.props.canDismiss) { dismiss = ( -
    +
    ); diff --git a/src/components/views/elements/DesktopCapturerSourcePicker.tsx b/src/components/views/elements/DesktopCapturerSourcePicker.tsx index 82d0aa4976..1f00353aeb 100644 --- a/src/components/views/elements/DesktopCapturerSourcePicker.tsx +++ b/src/components/views/elements/DesktopCapturerSourcePicker.tsx @@ -17,9 +17,12 @@ limitations under the License. import React from 'react'; import { _t } from '../../../languageHandler'; import BaseDialog from "..//dialogs/BaseDialog"; +import DialogButtons from "./DialogButtons"; +import classNames from 'classnames'; import AccessibleButton from './AccessibleButton'; import { getDesktopCapturerSources } from "matrix-js-sdk/src/webrtc/call"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import TabbedView, { Tab, TabLocation } from '../../structures/TabbedView'; export interface DesktopCapturerSource { id: string; @@ -28,61 +31,70 @@ export interface DesktopCapturerSource { } export enum Tabs { - Screens = "screens", - Windows = "windows", + Screens = "screen", + Windows = "window", } -export interface DesktopCapturerSourceIProps { +export interface ExistingSourceIProps { source: DesktopCapturerSource; onSelect(source: DesktopCapturerSource): void; + selected: boolean; } -export class ExistingSource extends React.Component { - constructor(props) { +export class ExistingSource extends React.Component { + constructor(props: ExistingSourceIProps) { super(props); } - onClick = (ev) => { + private onClick = (): void => { this.props.onSelect(this.props.source); }; render() { + const thumbnailClasses = classNames({ + mx_desktopCapturerSourcePicker_source_thumbnail: true, + mx_desktopCapturerSourcePicker_source_thumbnail_selected: this.props.selected, + }); + return ( + onClick={this.onClick} + > - { this.props.source.name } + { this.props.source.name } ); } } -export interface DesktopCapturerSourcePickerIState { +export interface PickerIState { selectedTab: Tabs; sources: Array; + selectedSource: DesktopCapturerSource | null; } -export interface DesktopCapturerSourcePickerIProps { +export interface PickerIProps { onFinished(source: DesktopCapturerSource): void; } @replaceableComponent("views.elements.DesktopCapturerSourcePicker") export default class DesktopCapturerSourcePicker extends React.Component< - DesktopCapturerSourcePickerIProps, - DesktopCapturerSourcePickerIState - > { - interval; + PickerIProps, + PickerIState +> { + interval: number; - constructor(props) { + constructor(props: PickerIProps) { super(props); this.state = { selectedTab: Tabs.Screens, sources: [], + selectedSource: null, }; } @@ -106,69 +118,61 @@ export default class DesktopCapturerSourcePicker extends React.Component< clearInterval(this.interval); } - onSelect = (source) => { - this.props.onFinished(source); + private onSelect = (source: DesktopCapturerSource): void => { + this.setState({ selectedSource: source }); }; - onScreensClick = (ev) => { - this.setState({ selectedTab: Tabs.Screens }); + private onShare = (): void => { + this.props.onFinished(this.state.selectedSource); }; - onWindowsClick = (ev) => { - this.setState({ selectedTab: Tabs.Windows }); + private onTabChange = (): void => { + this.setState({ selectedSource: null }); }; - onCloseClick = (ev) => { + private onCloseClick = (): void => { this.props.onFinished(null); }; - render() { - let sources; - if (this.state.selectedTab === Tabs.Screens) { - sources = this.state.sources - .filter((source) => { - return source.id.startsWith("screen"); - }) - .map((source) => { - return ; - }); - } else { - sources = this.state.sources - .filter((source) => { - return source.id.startsWith("window"); - }) - .map((source) => { - return ; - }); - } + private getTab(type: "screen" | "window", label: string): Tab { + const sources = this.state.sources.filter((source) => source.id.startsWith(type)).map((source) => { + return ( + + ); + }); - const buttonStyle = "mx_desktopCapturerSourcePicker_tabLabel"; - const screensButtonStyle = buttonStyle + ((this.state.selectedTab === Tabs.Screens) ? "_selected" : ""); - const windowsButtonStyle = buttonStyle + ((this.state.selectedTab === Tabs.Windows) ? "_selected" : ""); + return new Tab(type, label, null, ( +
    + { sources } +
    + )); + } + + render() { + const tabs = [ + this.getTab("screen", _t("Share entire screen")), + this.getTab("window", _t("Application window")), + ]; return ( -
    - - { _t("Screens") } - - - { _t("Windows") } - -
    -
    - { sources } -
    + +
    ); } diff --git a/src/components/views/elements/Dropdown.js b/src/components/views/elements/Dropdown.tsx similarity index 68% rename from src/components/views/elements/Dropdown.js rename to src/components/views/elements/Dropdown.tsx index f95247e9ae..dddcceb97c 100644 --- a/src/components/views/elements/Dropdown.js +++ b/src/components/views/elements/Dropdown.tsx @@ -1,7 +1,6 @@ /* -Copyright 2017 Vector Creations Ltd Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2017 - 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16,34 +15,38 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { createRef } from 'react'; -import PropTypes from 'prop-types'; +import React, { ChangeEvent, createRef, CSSProperties, ReactElement, ReactNode, Ref } from 'react'; import classnames from 'classnames'; + import AccessibleButton from './AccessibleButton'; import { _t } from '../../../languageHandler'; import { Key } from "../../../Keyboard"; import { replaceableComponent } from "../../../utils/replaceableComponent"; -class MenuOption extends React.Component { - constructor(props) { - super(props); - this._onMouseEnter = this._onMouseEnter.bind(this); - this._onClick = this._onClick.bind(this); - } +interface IMenuOptionProps { + children: ReactElement; + highlighted?: boolean; + dropdownKey: string; + id?: string; + inputRef?: Ref; + onClick(dropdownKey: string): void; + onMouseEnter(dropdownKey: string): void; +} +class MenuOption extends React.Component { static defaultProps = { disabled: false, }; - _onMouseEnter() { + private onMouseEnter = () => { this.props.onMouseEnter(this.props.dropdownKey); - } + }; - _onClick(e) { + private onClick = (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); this.props.onClick(this.props.dropdownKey); - } + }; render() { const optClasses = classnames({ @@ -54,8 +57,8 @@ class MenuOption extends React.Component { return
    { + private readonly buttonRef = createRef(); + private dropdownRootElement: HTMLDivElement = null; + private ignoreEvent: MouseEvent = null; + private childrenByKey: Record = {}; + + constructor(props: IProps) { super(props); - this.dropdownRootElement = null; - this.ignoreEvent = null; + this.reindexChildren(this.props.children); - this._onInputClick = this._onInputClick.bind(this); - this._onRootClick = this._onRootClick.bind(this); - this._onDocumentClick = this._onDocumentClick.bind(this); - this._onMenuOptionClick = this._onMenuOptionClick.bind(this); - this._onInputChange = this._onInputChange.bind(this); - this._collectRoot = this._collectRoot.bind(this); - this._collectInputTextBox = this._collectInputTextBox.bind(this); - this._setHighlightedOption = this._setHighlightedOption.bind(this); - - this.inputTextBox = null; - - this._reindexChildren(this.props.children); - - const firstChild = React.Children.toArray(props.children)[0]; + const firstChild = React.Children.toArray(props.children)[0] as ReactElement; this.state = { // True if the menu is dropped-down expanded: false, // The key of the highlighted option // (the option that would become selected if you pressed enter) - highlightedOption: firstChild ? firstChild.key : null, + highlightedOption: firstChild ? firstChild.key as string : null, // the current search query searchQuery: '', }; - } - // TODO: [REACT-WARNING] Replace component with real class, use constructor for refs - UNSAFE_componentWillMount() { // eslint-disable-line camelcase - this._button = createRef(); // Listen for all clicks on the document so we can close the // menu when the user clicks somewhere else - document.addEventListener('click', this._onDocumentClick, false); + document.addEventListener('click', this.onDocumentClick, false); } componentWillUnmount() { - document.removeEventListener('click', this._onDocumentClick, false); + document.removeEventListener('click', this.onDocumentClick, false); } // TODO: [REACT-WARNING] Replace with appropriate lifecycle event - UNSAFE_componentWillReceiveProps(nextProps) { // eslint-disable-line camelcase + UNSAFE_componentWillReceiveProps(nextProps) { // eslint-disable-line if (!nextProps.children || nextProps.children.length === 0) { return; } - this._reindexChildren(nextProps.children); + this.reindexChildren(nextProps.children); const firstChild = nextProps.children[0]; this.setState({ highlightedOption: firstChild ? firstChild.key : null, }); } - _reindexChildren(children) { + private reindexChildren(children: ReactElement[]): void { this.childrenByKey = {}; React.Children.forEach(children, (child) => { this.childrenByKey[child.key] = child; }); } - _onDocumentClick(ev) { + private onDocumentClick = (ev: MouseEvent) => { // Close the dropdown if the user clicks anywhere that isn't // within our root element if (ev !== this.ignoreEvent) { @@ -157,9 +166,9 @@ export default class Dropdown extends React.Component { expanded: false, }); } - } + }; - _onRootClick(ev) { + private onRootClick = (ev: MouseEvent) => { // This captures any clicks that happen within our elements, // such that we can then ignore them when they're seen by the // click listener on the document handler, ie. not close the @@ -167,9 +176,9 @@ export default class Dropdown extends React.Component { // NB. We can't just stopPropagation() because then the event // doesn't reach the React onClick(). this.ignoreEvent = ev; - } + }; - _onInputClick(ev) { + private onInputClick = (ev: React.MouseEvent) => { if (this.props.disabled) return; if (!this.state.expanded) { @@ -178,24 +187,24 @@ export default class Dropdown extends React.Component { }); ev.preventDefault(); } - } + }; - _close() { + private close() { this.setState({ expanded: false, }); // their focus was on the input, its getting unmounted, move it to the button - if (this._button.current) { - this._button.current.focus(); + if (this.buttonRef.current) { + this.buttonRef.current.focus(); } } - _onMenuOptionClick(dropdownKey) { - this._close(); + private onMenuOptionClick = (dropdownKey: string) => { + this.close(); this.props.onOptionChange(dropdownKey); - } + }; - _onInputKeyDown = (e) => { + private onInputKeyDown = (e: React.KeyboardEvent) => { let handled = true; // These keys don't generate keypress events and so needs to be on keyup @@ -204,16 +213,16 @@ export default class Dropdown extends React.Component { this.props.onOptionChange(this.state.highlightedOption); // fallthrough case Key.ESCAPE: - this._close(); + this.close(); break; case Key.ARROW_DOWN: this.setState({ - highlightedOption: this._nextOption(this.state.highlightedOption), + highlightedOption: this.nextOption(this.state.highlightedOption), }); break; case Key.ARROW_UP: this.setState({ - highlightedOption: this._prevOption(this.state.highlightedOption), + highlightedOption: this.prevOption(this.state.highlightedOption), }); break; default: @@ -224,53 +233,46 @@ export default class Dropdown extends React.Component { e.preventDefault(); e.stopPropagation(); } - } + }; - _onInputChange(e) { + private onInputChange = (e: ChangeEvent) => { this.setState({ - searchQuery: e.target.value, + searchQuery: e.currentTarget.value, }); if (this.props.onSearchChange) { - this.props.onSearchChange(e.target.value); + this.props.onSearchChange(e.currentTarget.value); } - } + }; - _collectRoot(e) { + private collectRoot = (e: HTMLDivElement) => { if (this.dropdownRootElement) { - this.dropdownRootElement.removeEventListener( - 'click', this._onRootClick, false, - ); + this.dropdownRootElement.removeEventListener('click', this.onRootClick, false); } if (e) { - e.addEventListener('click', this._onRootClick, false); + e.addEventListener('click', this.onRootClick, false); } this.dropdownRootElement = e; - } + }; - _collectInputTextBox(e) { - this.inputTextBox = e; - if (e) e.focus(); - } - - _setHighlightedOption(optionKey) { + private setHighlightedOption = (optionKey: string) => { this.setState({ highlightedOption: optionKey, }); - } + }; - _nextOption(optionKey) { + private nextOption(optionKey: string): string { const keys = Object.keys(this.childrenByKey); const index = keys.indexOf(optionKey); return keys[(index + 1) % keys.length]; } - _prevOption(optionKey) { + private prevOption(optionKey: string): string { const keys = Object.keys(this.childrenByKey); const index = keys.indexOf(optionKey); return keys[(index - 1) % keys.length]; } - _scrollIntoView(node) { + private scrollIntoView(node: Element) { if (node) { node.scrollIntoView({ block: "nearest", @@ -279,18 +281,18 @@ export default class Dropdown extends React.Component { } } - _getMenuOptions() { + private getMenuOptions() { const options = React.Children.map(this.props.children, (child) => { const highlighted = this.state.highlightedOption === child.key; return ( { child } @@ -307,7 +309,7 @@ export default class Dropdown extends React.Component { render() { let currentValue; - const menuStyle = {}; + const menuStyle: CSSProperties = {}; if (this.props.menuWidth) menuStyle.width = this.props.menuWidth; let menu; @@ -316,10 +318,10 @@ export default class Dropdown extends React.Component { currentValue = ( - { this._getMenuOptions() } + { this.getMenuOptions() }
    ); } @@ -356,14 +358,14 @@ export default class Dropdown extends React.Component { // Note the menu sits inside the AccessibleButton div so it's anchored // to the input, but overflows below it. The root contains both. - return
    + return
    @@ -374,28 +376,3 @@ export default class Dropdown extends React.Component {
    ; } } - -Dropdown.propTypes = { - id: PropTypes.string.isRequired, - // The width that the dropdown should be. If specified, - // the dropped-down part of the menu will be set to this - // width. - menuWidth: PropTypes.number, - // Called when the selected option changes - onOptionChange: PropTypes.func.isRequired, - // Called when the value of the search field changes - onSearchChange: PropTypes.func, - searchEnabled: PropTypes.bool, - // Function that, given the key of an option, returns - // a node representing that option to be displayed in the - // box itself as the currently-selected option (ie. as - // opposed to in the actual dropped-down part). If - // unspecified, the appropriate child element is used as - // in the dropped-down menu. - getShortOption: PropTypes.func, - value: PropTypes.string, - // negative for consistency with HTML - disabled: PropTypes.bool, - // ARIA label - label: PropTypes.string.isRequired, -}; diff --git a/src/components/views/elements/EventListSummary.tsx b/src/components/views/elements/EventListSummary.tsx index b1cc9c773d..d66319ca73 100644 --- a/src/components/views/elements/EventListSummary.tsx +++ b/src/components/views/elements/EventListSummary.tsx @@ -22,6 +22,7 @@ import MemberAvatar from '../avatars/MemberAvatar'; import { _t } from '../../../languageHandler'; import { useStateToggle } from "../../../hooks/useStateToggle"; import AccessibleButton from "./AccessibleButton"; +import { Layout } from '../../../settings/Layout'; interface IProps { // An array of member events to summarise @@ -38,6 +39,8 @@ interface IProps { children: ReactNode[]; // Called when the event list expansion is toggled onToggle?(): void; + // The layout currently used + layout?: Layout; } const EventListSummary: React.FC = ({ @@ -48,6 +51,7 @@ const EventListSummary: React.FC = ({ startExpanded, summaryMembers = [], summaryText, + layout, }) => { const [expanded, toggleExpanded] = useStateToggle(startExpanded); @@ -63,7 +67,7 @@ const EventListSummary: React.FC = ({ // If we are only given few events then just pass them through if (events.length < threshold) { return ( -
  • +
  • { children }
  • ); @@ -92,7 +96,7 @@ const EventListSummary: React.FC = ({ } return ( -
  • +
  • { expanded ? _t('collapse') : _t('expand') } @@ -103,6 +107,7 @@ const EventListSummary: React.FC = ({ EventListSummary.defaultProps = { startExpanded: false, + layout: Layout.Group, }; export default EventListSummary; diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index db63dbbfc2..87f3dab718 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -419,7 +419,8 @@ export default class ImageView extends React.Component { const avatar = ( ); @@ -438,7 +439,7 @@ export default class ImageView extends React.Component { // an empty div here, since the panel uses space-between // and we want the same placement of elements info = ( -
    +
    ); } @@ -462,15 +463,15 @@ export default class ImageView extends React.Component { - + onClick={this.onZoomOutClick} + /> ); zoomInButton = ( - + onClick={this.onZoomInClick} + /> ); } @@ -492,24 +493,24 @@ export default class ImageView extends React.Component { - + onClick={this.onRotateCounterClockwiseClick} + /> - + onClick={this.onRotateClockwiseClick} + /> - + onClick={this.onDownloadClick} + /> { contextMenuButton } - + onClick={this.props.onFinished} + /> { this.renderContextMenu() }
  • diff --git a/src/components/views/elements/JoinRuleDropdown.tsx b/src/components/views/elements/JoinRuleDropdown.tsx new file mode 100644 index 0000000000..e2d9b6d872 --- /dev/null +++ b/src/components/views/elements/JoinRuleDropdown.tsx @@ -0,0 +1,68 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import { JoinRule } from 'matrix-js-sdk/src/@types/partials'; + +import Dropdown from "./Dropdown"; + +interface IProps { + value: JoinRule; + label: string; + width?: number; + labelInvite: string; + labelPublic: string; + labelRestricted?: string; // if omitted then this option will be hidden, e.g if unsupported + onChange(value: JoinRule): void; +} + +const JoinRuleDropdown = ({ + label, + labelInvite, + labelPublic, + labelRestricted, + value, + width = 448, + onChange, +}: IProps) => { + const options = [ +
    + { labelInvite } +
    , +
    + { labelPublic } +
    , + ]; + + if (labelRestricted) { + options.unshift(
    + { labelRestricted } +
    ); + } + + return + { options } + ; +}; + +export default JoinRuleDropdown; diff --git a/src/components/views/elements/MemberEventListSummary.tsx b/src/components/views/elements/MemberEventListSummary.tsx index d52462f629..604e6c63b0 100644 --- a/src/components/views/elements/MemberEventListSummary.tsx +++ b/src/components/views/elements/MemberEventListSummary.tsx @@ -25,12 +25,15 @@ import { formatCommaSeparatedList } from '../../../utils/FormattingUtils'; import { isValid3pidInvite } from "../../../RoomInvite"; import EventListSummary from "./EventListSummary"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { Layout } from '../../../settings/Layout'; interface IProps extends Omit, "summaryText" | "summaryMembers"> { // The maximum number of names to show in either each summary e.g. 2 would result "A, B and 234 others left" summaryLength?: number; // The maximum number of avatars to display in the summary avatarsMaxLength?: number; + // The currently selected layout + layout: Layout; } interface IUserEvents { @@ -67,6 +70,7 @@ export default class MemberEventListSummary extends React.Component { summaryLength: 1, threshold: 3, avatarsMaxLength: 5, + layout: Layout.Group, }; shouldComponentUpdate(nextProps) { @@ -453,6 +457,7 @@ export default class MemberEventListSummary extends React.Component { startExpanded={this.props.startExpanded} children={this.props.children} summaryMembers={[...latestUserAvatarMember.values()]} + layout={this.props.layout} summaryText={this.generateSummary(aggregate.names, orderedTransitionSequences)} />; } } diff --git a/src/components/views/elements/MiniAvatarUploader.tsx b/src/components/views/elements/MiniAvatarUploader.tsx index 47bcd845ba..22ff4bf4b3 100644 --- a/src/components/views/elements/MiniAvatarUploader.tsx +++ b/src/components/views/elements/MiniAvatarUploader.tsx @@ -92,7 +92,7 @@ const MiniAvatarUploader: React.FC = ({ hasAvatar, hasAvatarLabel, noAva
    { busy ? : -
    } +
    }
    { + onUserPillClicked = (e) => { + e.preventDefault(); dis.dispatch({ action: Action.ViewUser, member: this.state.member, @@ -258,7 +259,10 @@ class Pill extends React.Component { linkText = groupId; if (this.props.shouldShowPillAvatar) { avatar =
    -
    +
    { callType }
    diff --git a/src/components/views/messages/DownloadActionButton.tsx b/src/components/views/messages/DownloadActionButton.tsx index 2bdae04eda..6dc48b0936 100644 --- a/src/components/views/messages/DownloadActionButton.tsx +++ b/src/components/views/messages/DownloadActionButton.tsx @@ -16,12 +16,13 @@ limitations under the License. import { MatrixEvent } from "matrix-js-sdk/src"; import { MediaEventHelper } from "../../../utils/MediaEventHelper"; -import React, { createRef } from "react"; +import React from "react"; import { RovingAccessibleTooltipButton } from "../../../accessibility/RovingTabIndex"; import Spinner from "../elements/Spinner"; import classNames from "classnames"; import { _t } from "../../../languageHandler"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { FileDownloader } from "../../../utils/FileDownloader"; interface IProps { mxEvent: MatrixEvent; @@ -39,7 +40,7 @@ interface IState { @replaceableComponent("views.messages.DownloadActionButton") export default class DownloadActionButton extends React.PureComponent { - private iframe: React.RefObject = createRef(); + private downloader = new FileDownloader(); public constructor(props: IProps) { super(props); @@ -56,27 +57,21 @@ export default class DownloadActionButton extends React.PureComponent { - this.setState({ loading: false }); - - // we aren't showing the iframe, so we can send over the bare minimum styles and such. - this.iframe.current.contentWindow.postMessage({ - imgSrc: "", // no image - imgStyle: null, - style: "", + private async doDownload() { + await this.downloader.download({ blob: this.state.blob, - download: this.props.mediaEventHelperGet().fileName, - textContent: "", - auto: true, // autodownload - }, '*'); - }; + name: this.props.mediaEventHelperGet().fileName, + }); + this.setState({ loading: false }); + } public render() { let spinner: JSX.Element; @@ -92,18 +87,11 @@ export default class DownloadActionButton extends React.PureComponent { spinner } - { this.state.blob &&