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 4e2e933a52..b73462d188 100644 --- a/package.json +++ b/package.json @@ -65,8 +65,8 @@ "counterpart": "^0.18.6", "diff-dom": "^4.2.2", "diff-match-patch": "^1.0.5", - "emojibase-data": "^5.1.1", - "emojibase-regex": "^4.1.1", + "emojibase-data": "^6.2.0", + "emojibase-regex": "^5.1.3", "escape-html": "^1.0.3", "file-saver": "^2.0.5", "filesize": "6.1.0", 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 7df45b857e..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"; @@ -159,6 +163,7 @@ @import "./views/groups/_GroupPublicityToggle.scss"; @import "./views/groups/_GroupRoomList.scss"; @import "./views/groups/_GroupUserSettings.scss"; +@import "./views/messages/_CallEvent.scss"; @import "./views/messages/_CreateEvent.scss"; @import "./views/messages/_DateSeparator.scss"; @import "./views/messages/_EventTileBubble.scss"; @@ -171,7 +176,6 @@ @import "./views/messages/_MStickerBody.scss"; @import "./views/messages/_MTextBody.scss"; @import "./views/messages/_MVideoBody.scss"; -@import "./views/messages/_MVoiceMessageBody.scss"; @import "./views/messages/_MediaBody.scss"; @import "./views/messages/_MessageActionBar.scss"; @import "./views/messages/_MessageTimestamp.scss"; @@ -200,8 +204,8 @@ @import "./views/rooms/_E2EIcon.scss"; @import "./views/rooms/_EditMessageComposer.scss"; @import "./views/rooms/_EntityTile.scss"; -@import "./views/rooms/_EventTile.scss"; @import "./views/rooms/_EventBubbleTile.scss"; +@import "./views/rooms/_EventTile.scss"; @import "./views/rooms/_GroupLayout.scss"; @import "./views/rooms/_IRCLayout.scss"; @import "./views/rooms/_JumpToBottomButton.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/_FilePanel.scss b/res/css/structures/_FilePanel.scss index 7b975110e1..c180a8a02d 100644 --- a/res/css/structures/_FilePanel.scss +++ b/res/css/structures/_FilePanel.scss @@ -45,9 +45,14 @@ limitations under the License. /* Overrides for the attachment body tiles */ -.mx_FilePanel .mx_EventTile { +.mx_FilePanel .mx_EventTile:not([data-layout=bubble]) { word-break: break-word; - margin-top: 32px; + margin-top: 10px; + padding-top: 0; + + .mx_EventTile_line { + padding-left: 0; + } } .mx_FilePanel .mx_EventTile .mx_MImageBody { @@ -118,10 +123,6 @@ limitations under the License. padding-left: 0px; } -.mx_FilePanel .mx_EventTile:hover .mx_EventTile_line { - background-color: $primary-bg-color; -} - .mx_FilePanel_empty::before { mask-image: url('$(res)/img/element-icons/room/files.svg'); } diff --git a/res/css/structures/_NotificationPanel.scss b/res/css/structures/_NotificationPanel.scss index e54feca175..d271cd2bcc 100644 --- a/res/css/structures/_NotificationPanel.scss +++ b/res/css/structures/_NotificationPanel.scss @@ -84,7 +84,7 @@ limitations under the License. display: inline; } -.mx_NotificationPanel .mx_EventTile_senderDetails { +.mx_NotificationPanel .mx_EventTile:not([data-layout=bubble]) .mx_EventTile_senderDetails { padding-left: 36px; // align with the room name position: relative; @@ -105,7 +105,7 @@ limitations under the License. padding-left: 5px; } -.mx_NotificationPanel .mx_EventTile_line { +.mx_NotificationPanel .mx_EventTile:not([data-layout=bubble]) .mx_EventTile_line { margin-right: 0px; padding-left: 36px; // align with the room name padding-top: 0px; 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/audio_messages/_AudioPlayer.scss b/res/css/views/audio_messages/_AudioPlayer.scss index 9a65ad008f..77dcebbb9a 100644 --- a/res/css/views/audio_messages/_AudioPlayer.scss +++ b/res/css/views/audio_messages/_AudioPlayer.scss @@ -14,9 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_AudioPlayer_container { +.mx_MediaBody.mx_AudioPlayer_container { padding: 16px 12px 12px 12px; - max-width: 267px; // use max to make the control fit in the files/pinned panels .mx_AudioPlayer_primaryContainer { display: flex; diff --git a/res/css/views/audio_messages/_PlaybackContainer.scss b/res/css/views/audio_messages/_PlaybackContainer.scss index 5548f6198e..773fc50fb9 100644 --- a/res/css/views/audio_messages/_PlaybackContainer.scss +++ b/res/css/views/audio_messages/_PlaybackContainer.scss @@ -18,10 +18,10 @@ limitations under the License. // are shared amongst multiple voice message components. // Container for live recording and playback controls -.mx_VoiceMessagePrimaryContainer { - // 7px top and bottom for visual design. 12px left & right, but the waveform (right) - // has a 1px padding on it that we want to account for. - padding: 7px 12px 7px 11px; +.mx_MediaBody.mx_VoiceMessagePrimaryContainer { + // The waveform (right) has a 1px padding on it that we want to account for, otherwise + // inherit from mx_MediaBody + padding-right: 11px; // Cheat at alignment a bit display: flex; 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/_ForwardDialog.scss b/res/css/views/dialogs/_ForwardDialog.scss index 95d7ce74c4..e018f60172 100644 --- a/res/css/views/dialogs/_ForwardDialog.scss +++ b/res/css/views/dialogs/_ForwardDialog.scss @@ -36,6 +36,10 @@ limitations under the License. flex-shrink: 0; overflow-y: auto; + .mx_EventTile[data-layout=bubble] { + margin-top: 20px; + } + div { pointer-events: none; } 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/elements/_InfoTooltip.scss b/res/css/views/elements/_InfoTooltip.scss index 5858a60629..5329e7f1f8 100644 --- a/res/css/views/elements/_InfoTooltip.scss +++ b/res/css/views/elements/_InfoTooltip.scss @@ -30,5 +30,12 @@ limitations under the License. mask-position: center; content: ''; vertical-align: middle; +} + +.mx_InfoTooltip_icon_info::before { mask-image: url('$(res)/img/element-icons/info.svg'); } + +.mx_InfoTooltip_icon_warning::before { + mask-image: url('$(res)/img/element-icons/warning.svg'); +} diff --git a/res/css/views/elements/_ReplyThread.scss b/res/css/views/elements/_ReplyThread.scss index 44532ea6a7..032cb49359 100644 --- a/res/css/views/elements/_ReplyThread.scss +++ b/res/css/views/elements/_ReplyThread.scss @@ -19,8 +19,9 @@ limitations under the License. margin-left: 0; margin-right: 0; margin-bottom: 8px; - padding-left: 10px; - border-left: 4px solid $button-bg-color; + padding: 0 10px; + border-left: 2px solid $button-bg-color; + border-radius: 2px; .mx_ReplyThread_show { cursor: pointer; diff --git a/res/css/views/messages/_CallEvent.scss b/res/css/views/messages/_CallEvent.scss new file mode 100644 index 0000000000..0c1b41ca38 --- /dev/null +++ b/res/css/views/messages/_CallEvent.scss @@ -0,0 +1,162 @@ +/* +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_CallEvent { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + + background-color: $dark-panel-bg-color; + border-radius: 8px; + margin: 10px auto; + width: 75%; + box-sizing: border-box; + height: 60px; + + &.mx_CallEvent_voice { + .mx_CallEvent_type_icon::before, + .mx_CallEvent_content_button_callBack span::before, + .mx_CallEvent_content_button_answer span::before { + mask-image: url('$(res)/img/element-icons/call/voice-call.svg'); + } + } + + &.mx_CallEvent_video { + .mx_CallEvent_type_icon::before, + .mx_CallEvent_content_button_callBack span::before, + .mx_CallEvent_content_button_answer span::before { + mask-image: url('$(res)/img/element-icons/call/video-call.svg'); + } + } + + &.mx_CallEvent_voice.mx_CallEvent_missed .mx_CallEvent_type_icon::before { + mask-image: url('$(res)/img/voip/missed-voice.svg'); + } + + &.mx_CallEvent_video.mx_CallEvent_missed .mx_CallEvent_type_icon::before { + mask-image: url('$(res)/img/voip/missed-video.svg'); + } + + .mx_CallEvent_info { + display: flex; + flex-direction: row; + align-items: center; + margin-left: 12px; + + .mx_CallEvent_info_basic { + display: flex; + flex-direction: column; + margin-left: 10px; // To match mx_CallEvent + + .mx_CallEvent_sender { + font-weight: 600; + font-size: 1.5rem; + line-height: 1.8rem; + margin-bottom: 3px; + } + + .mx_CallEvent_type { + font-weight: 400; + color: $secondary-fg-color; + font-size: 1.2rem; + line-height: $font-13px; + display: flex; + align-items: center; + + .mx_CallEvent_type_icon { + height: 13px; + width: 13px; + margin-right: 5px; + + &::before { + content: ''; + position: absolute; + height: 13px; + width: 13px; + background-color: $tertiary-fg-color; + mask-repeat: no-repeat; + mask-size: contain; + } + } + } + } + } + + .mx_CallEvent_content { + display: flex; + flex-direction: row; + align-items: center; + color: $secondary-fg-color; + margin-right: 16px; + + .mx_CallEvent_content_button { + height: 24px; + padding: 0px 12px; + margin-left: 8px; + + span { + padding: 8px 0; + display: flex; + align-items: center; + + &::before { + content: ''; + display: inline-block; + background-color: $button-fg-color; + mask-position: center; + mask-repeat: no-repeat; + mask-size: 16px; + width: 16px; + height: 16px; + margin-right: 8px; + } + } + } + + .mx_CallEvent_content_button_reject span::before { + mask-image: url('$(res)/img/element-icons/call/hangup.svg'); + } + + .mx_CallEvent_content_tooltip { + margin-right: 5px; + } + + .mx_CallEvent_iconButton { + display: inline-flex; + margin-right: 8px; + + &::before { + content: ''; + + height: 16px; + width: 16px; + background-color: $tertiary-fg-color; + mask-repeat: no-repeat; + mask-size: contain; + mask-position: center; + } + } + + .mx_CallEvent_silence::before { + mask-image: url('$(res)/img/voip/silence.svg'); + } + + .mx_CallEvent_unSilence::before { + mask-image: url('$(res)/img/voip/un-silence.svg'); + } + } +} diff --git a/res/css/views/messages/_MFileBody.scss b/res/css/views/messages/_MFileBody.scss index b91c461ce5..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,11 +60,7 @@ limitations under the License. } .mx_MFileBody_info { - background-color: $message-body-panel-bg-color; - border-radius: 12px; - width: 243px; // same width as a playable voice message, accounting for padding - padding: 6px 12px; - color: $message-body-panel-fg-color; + cursor: pointer; .mx_MFileBody_info_icon { background-color: $message-body-panel-icon-bg-color; diff --git a/res/css/views/messages/_MImageBody.scss b/res/css/views/messages/_MImageBody.scss index 0a199c1f45..a748435cd8 100644 --- a/res/css/views/messages/_MImageBody.scss +++ b/res/css/views/messages/_MImageBody.scss @@ -16,23 +16,15 @@ limitations under the License. $timelineImageBorderRadius: 4px; -.mx_MImageBody { - display: block; -} - .mx_MImageBody_thumbnail { - position: absolute; - width: 100%; - height: 100%; - left: 0; - top: 0; + object-fit: contain; border-radius: $timelineImageBorderRadius; display: flex; justify-content: center; align-items: center; - > canvas { + > div > canvas { border-radius: $timelineImageBorderRadius; } } diff --git a/res/css/views/messages/_MediaBody.scss b/res/css/views/messages/_MediaBody.scss index 12e441750c..7f4bfd3fdc 100644 --- a/res/css/views/messages/_MediaBody.scss +++ b/res/css/views/messages/_MediaBody.scss @@ -20,9 +20,11 @@ limitations under the License. .mx_MediaBody { background-color: $message-body-panel-bg-color; border-radius: 12px; + max-width: 243px; // use max-width instead of width so it fits within right panels color: $message-body-panel-fg-color; font-size: $font-14px; line-height: $font-24px; -} + padding: 6px 12px; +} diff --git a/res/css/views/messages/_MessageActionBar.scss b/res/css/views/messages/_MessageActionBar.scss index e2fafe6c62..69f3c672b7 100644 --- a/res/css/views/messages/_MessageActionBar.scss +++ b/res/css/views/messages/_MessageActionBar.scss @@ -107,3 +107,12 @@ limitations under the License. .mx_MessageActionBar_cancelButton::after { mask-image: url('$(res)/img/element-icons/trashcan.svg'); } + +.mx_MessageActionBar_downloadButton::after { + mask-size: 14px; + mask-image: url('$(res)/img/download.svg'); +} + +.mx_MessageActionBar_downloadButton.mx_MessageActionBar_downloadSpinnerButton::after { + background-color: transparent; // hide the download icon mask +} 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 c66f635ffe..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 { @@ -80,7 +85,7 @@ limitations under the License. .mx_MessageActionBar { right: 0; - transform: translate3d(50%, 50%, 0); + transform: translate3d(90%, 50%, 0); } --backgroundColor: $eventbubble-others-bg; @@ -91,12 +96,17 @@ limitations under the License. float: right; > a { left: auto; - right: -48px; + right: -68px; } } .mx_SenderProfile { display: none; } + + .mx_ReplyTile .mx_SenderProfile { + display: block; + } + .mx_ReactionsRow { float: right; clear: right; @@ -126,7 +136,9 @@ limitations under the License. margin: 0 -12px 0 -9px; > a { position: absolute; - left: -48px; + padding: 10px 20px; + top: 0; + left: -68px; } } @@ -148,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; @@ -204,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(50%, 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; @@ -321,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 d6ad37f6bb..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,10 +131,6 @@ $hover-select-border: 4px; } } - &.mx_EventTile_info .mx_EventTile_line { - padding-left: calc($left-gutter + 18px); - } - &.mx_EventTile_selected.mx_EventTile_info .mx_EventTile_line { padding-left: calc($left-gutter + 18px - $hover-select-border); } @@ -208,43 +203,11 @@ $hover-select-border: 4px; text-decoration: none; } - /* 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 - horizontal scrollbars occasionally appearing, like in - https://github.com/vector-im/vector-web/issues/1154 - */ - .mx_EventTile_content { - display: block; - overflow-y: hidden; - overflow-x: hidden; - margin-right: 34px; - } - /* De-zalgoing */ .mx_EventTile_body { overflow-y: hidden; } - /* Spoiler stuff */ - .mx_EventTile_spoiler { - cursor: pointer; - } - - .mx_EventTile_spoiler_reason { - color: $event-timestamp-color; - font-size: $font-11px; - } - - .mx_EventTile_spoiler_content { - filter: blur(5px) saturate(0.1) sepia(1); - transition-duration: 0.5s; - } - - .mx_EventTile_spoiler.visible > .mx_EventTile_spoiler_content { - filter: none; - } - &:hover.mx_EventTile_verified .mx_EventTile_line, &:hover.mx_EventTile_unverified .mx_EventTile_line, &:hover.mx_EventTile_unknown .mx_EventTile_line { @@ -303,10 +266,49 @@ $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 + horizontal scrollbars occasionally appearing, like in + https://github.com/vector-im/vector-web/issues/1154 */ +.mx_EventTile_content { + overflow-y: hidden; + overflow-x: hidden; + margin-right: 34px; +} + +/* Spoiler stuff */ +.mx_EventTile_spoiler { + cursor: pointer; +} + +.mx_EventTile_spoiler_reason { + color: $event-timestamp-color; + font-size: $font-11px; +} + +.mx_EventTile_spoiler_content { + filter: blur(5px) saturate(0.1) sepia(1); + transition-duration: 0.5s; +} + +.mx_EventTile_spoiler.visible > .mx_EventTile_spoiler_content { + filter: none; +} + .mx_RoomView_timeline_rr_enabled { .mx_EventTile:not([data-layout=bubble]) { @@ -319,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; @@ -333,6 +339,13 @@ $hover-select-border: 4px; .mx_EventTile_msgOption { grid-column: 2; } + + &:hover { + .mx_EventTile_line { + // To avoid bubble events being highlighted + background-color: inherit !important; + } + } } .mx_EventTile_readAvatars { @@ -446,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 { @@ -462,6 +481,10 @@ $hover-select-border: 4px; background-color: $header-panel-bg-color; } + pre code > * { + display: inline-block; + } + pre { // have to use overlay rather than auto otherwise Linux and Windows // Chrome gets very confused about vertical spacing: @@ -559,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/_GroupLayout.scss b/res/css/views/rooms/_GroupLayout.scss index ddee81a914..ebb7f99e45 100644 --- a/res/css/views/rooms/_GroupLayout.scss +++ b/res/css/views/rooms/_GroupLayout.scss @@ -26,6 +26,7 @@ $left-gutter: 64px; > .mx_EventTile_avatar { position: absolute; + z-index: 9; } .mx_MessageTimestamp { 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 0832337ecd..24900ee14b 100644 --- a/res/css/views/rooms/_LinkPreviewWidget.scss +++ b/res/css/views/rooms/_LinkPreviewWidget.scss @@ -19,7 +19,8 @@ limitations under the License. margin-right: 15px; margin-bottom: 15px; display: flex; - border-left: 4px solid $preview-widget-bar-color; + border-left: 2px solid $preview-widget-bar-color; + border-radius: 2px; color: $preview-widget-fg-color; } @@ -33,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/rooms/_SendMessageComposer.scss b/res/css/views/rooms/_SendMessageComposer.scss index 9f6a8d52ce..4b7eb54188 100644 --- a/res/css/views/rooms/_SendMessageComposer.scss +++ b/res/css/views/rooms/_SendMessageComposer.scss @@ -29,8 +29,10 @@ limitations under the License. display: flex; flex-direction: column; // min-height at this level so the mx_BasicMessageComposer_input - // still stays vertically centered when less than 50px - min-height: 50px; + // still stays vertically centered when less than 55px. + // We also set this to ensure the voice message recording widget + // doesn't cause a jump. + min-height: 55px; .mx_BasicMessageComposer_input { padding: 3px 0; 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/settings/tabs/user/_AppearanceUserSettingsTab.scss b/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss index 94983a60bf..ca5a6f0a66 100644 --- a/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss @@ -15,8 +15,7 @@ limitations under the License. */ .mx_AppearanceUserSettingsTab_fontSlider, -.mx_AppearanceUserSettingsTab_fontSlider_preview, -.mx_AppearanceUserSettingsTab_Layout { +.mx_AppearanceUserSettingsTab_fontSlider_preview { @mixin mx_Settings_fullWidthField; } @@ -45,6 +44,11 @@ limitations under the License. border-radius: 10px; padding: 0 16px 9px 16px; pointer-events: none; + display: flow-root; + + .mx_EventTile[data-layout=bubble] { + margin-top: 30px; + } .mx_EventTile_msgOption { display: none; @@ -154,13 +158,10 @@ limitations under the License. .mx_AppearanceUserSettingsTab_Layout_RadioButtons { display: flex; flex-direction: row; + gap: 24px; color: $primary-fg-color; - .mx_AppearanceUserSettingsTab_spacer { - width: 24px; - } - > .mx_AppearanceUserSettingsTab_Layout_RadioButton { flex-grow: 0; flex-shrink: 1; @@ -210,6 +211,21 @@ limitations under the License. .mx_RadioButton_checked { background-color: rgba($accent-color, 0.08); } + + .mx_EventTile { + margin: 0; + &[data-layout=bubble] { + margin-right: 40px; + } + &[data-layout=irc] { + > a { + display: none; + } + } + .mx_EventTile_line { + max-width: 90%; + } + } } .mx_AppearanceUserSettingsTab_Advanced { 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..104e2993d8 100644 --- a/res/css/views/voip/_CallView.scss +++ b/res/css/views/voip/_CallView.scss @@ -67,7 +67,26 @@ 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 + padding-bottom: 52px; + background-color: $inverted-bg-color; + display: flex; + justify-content: center; + align-items: center; + } + + &.mx_VideoFeed_video { + background-color: #000; + } + } } .mx_CallView_voice { @@ -260,7 +279,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 +293,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 +313,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 +323,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 +362,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 +391,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 +422,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..79bf3cbf09 --- /dev/null +++ b/res/css/views/voip/_CallViewSidebar.scss @@ -0,0 +1,52 @@ +/* +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 { + display: flex; + align-items: center; + justify-content: center; + + aspect-ratio: 16 / 9; + } + } + + &.mx_CallViewSidebar_pipMode { + top: 16px; + bottom: unset; + justify-content: flex-end; + gap: 4px; + } +} diff --git a/res/css/views/voip/_VideoFeed.scss b/res/css/views/voip/_VideoFeed.scss index 4a3fbdf597..07a4a0e530 100644 --- a/res/css/views/voip/_VideoFeed.scss +++ b/res/css/views/voip/_VideoFeed.scss @@ -14,32 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_VideoFeed_voice { - background-color: $inverted-bg-color; -} - - -.mx_VideoFeed_remote { - width: 100%; - height: 100%; - display: flex; - justify-content: center; - align-items: center; - - &.mx_VideoFeed_video { - background-color: #000; - } -} - -.mx_VideoFeed_local { - max-width: 25%; - max-height: 25%; - position: absolute; - right: 10px; - top: 10px; - z-index: 100; +.mx_VideoFeed { border-radius: 4px; + + &.mx_VideoFeed_voice { + background-color: $inverted-bg-color; + } + &.mx_VideoFeed_video { background-color: transparent; } diff --git a/res/fonts/Twemoji_Mozilla/TwemojiMozilla-colr.woff2 b/res/fonts/Twemoji_Mozilla/TwemojiMozilla-colr.woff2 index a52e5a3800..128aac8139 100644 Binary files a/res/fonts/Twemoji_Mozilla/TwemojiMozilla-colr.woff2 and b/res/fonts/Twemoji_Mozilla/TwemojiMozilla-colr.woff2 differ diff --git a/res/fonts/Twemoji_Mozilla/TwemojiMozilla-sbix.woff2 b/res/fonts/Twemoji_Mozilla/TwemojiMozilla-sbix.woff2 index 660a93193d..a95e89c094 100644 Binary files a/res/fonts/Twemoji_Mozilla/TwemojiMozilla-sbix.woff2 and b/res/fonts/Twemoji_Mozilla/TwemojiMozilla-sbix.woff2 differ diff --git a/res/img/element-icons/warning.svg b/res/img/element-icons/warning.svg new file mode 100644 index 0000000000..eef5193140 --- /dev/null +++ b/res/img/element-icons/warning.svg @@ -0,0 +1,3 @@ + + + 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/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/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index 2a4ebff034..655492661c 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -209,8 +209,8 @@ $user-tile-hover-bg-color: $header-panel-bg-color; $message-body-panel-fg-color: $secondary-fg-color; $message-body-panel-bg-color: #394049; // "Dark Tile" -$message-body-panel-icon-fg-color: #21262C; // "Separator" -$message-body-panel-icon-bg-color: $tertiary-fg-color; +$message-body-panel-icon-fg-color: $secondary-fg-color; +$message-body-panel-icon-bg-color: #21262C; // "System Dark" $voice-record-stop-border-color: $quaternary-fg-color; $voice-record-waveform-incomplete-fg-color: $quaternary-fg-color; @@ -295,3 +295,11 @@ $eventbubble-reply-color: #C1C6CD; .hljs-tag { color: inherit; // Without this they'd be weirdly blue which doesn't match the theme } + +.hljs-addition { + background: #1a4b59; +} + +.hljs-deletion { + background: #53232a; +} diff --git a/res/themes/legacy-dark/css/_legacy-dark.scss b/res/themes/legacy-dark/css/_legacy-dark.scss index 555ef4f66c..0c0197cfb0 100644 --- a/res/themes/legacy-dark/css/_legacy-dark.scss +++ b/res/themes/legacy-dark/css/_legacy-dark.scss @@ -207,8 +207,8 @@ $user-tile-hover-bg-color: $header-panel-bg-color; $message-body-panel-fg-color: $secondary-fg-color; $message-body-panel-bg-color: #394049; -$message-body-panel-icon-fg-color: $primary-bg-color; -$message-body-panel-icon-bg-color: $secondary-fg-color; +$message-body-panel-icon-fg-color: $secondary-fg-color; +$message-body-panel-icon-bg-color: #21262C; // See non-legacy dark for variable information $voice-record-stop-border-color: #6F7882; diff --git a/res/themes/legacy-light/css/_legacy-light.scss b/res/themes/legacy-light/css/_legacy-light.scss index f349a804a8..b7d45452ff 100644 --- a/res/themes/legacy-light/css/_legacy-light.scss +++ b/res/themes/legacy-light/css/_legacy-light.scss @@ -331,7 +331,7 @@ $user-tile-hover-bg-color: $header-panel-bg-color; $message-body-panel-fg-color: $secondary-fg-color; $message-body-panel-bg-color: #E3E8F0; $message-body-panel-icon-fg-color: $secondary-fg-color; -$message-body-panel-icon-bg-color: $primary-bg-color; +$message-body-panel-icon-bg-color: #F4F6FA; // See non-legacy _light for variable information $voice-record-stop-symbol-color: #ff4b55; diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index ef5f4d8c86..32722515d8 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -327,7 +327,7 @@ $user-tile-hover-bg-color: $header-panel-bg-color; $message-body-panel-fg-color: $secondary-fg-color; $message-body-panel-bg-color: #E3E8F0; // "Separator" $message-body-panel-icon-fg-color: $secondary-fg-color; -$message-body-panel-icon-bg-color: $primary-bg-color; +$message-body-panel-icon-bg-color: #F4F6FA; // These two don't change between themes. They are the $warning-color, but we don't // want custom themes to affect them by accident. diff --git a/src/@types/common.ts b/src/@types/common.ts index 1fb9ba4303..36ef7a9ace 100644 --- a/src/@types/common.ts +++ b/src/@types/common.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { JSXElementConstructor } from "react"; +import React, { JSXElementConstructor } from "react"; // Based on https://stackoverflow.com/a/53229857/3532235 export type Without = {[P in Exclude]?: never}; @@ -22,3 +22,4 @@ export type XOR = (T | U) extends object ? (Without & U) | (Without< export type Writeable = { -readonly [P in keyof T]: T[P] }; export type ComponentClass = keyof JSX.IntrinsicElements | JSXElementConstructor; +export type ReactAnyComponent = React.Component | React.ExoticComponent; diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index 051e865464..9d6bc2c6fb 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -92,6 +92,7 @@ declare global { mxUIStore: UIStore; mxSetupEncryptionStore?: SetupEncryptionStore; mxRoomScrollStateStore?: RoomScrollStateStore; + mxOnRecaptchaLoaded?: () => void; } interface Document { @@ -116,7 +117,7 @@ declare global { } interface StorageEstimate { - usageDetails?: {[key: string]: number}; + usageDetails?: { [key: string]: number }; } interface HTMLAudioElement { @@ -187,6 +188,21 @@ declare global { parameterDescriptors?: AudioParamDescriptor[]; } ); + + // eslint-disable-next-line no-var + var grecaptcha: + | undefined + | { + reset: (id: string) => void; + render: ( + divId: string, + options: { + sitekey: string; + callback: (response: string) => void; + }, + ) => string; + isReady: () => boolean; + }; } /* eslint-enable @typescript-eslint/naming-convention */ diff --git a/src/@types/svg.d.ts b/src/@types/svg.d.ts new file mode 100644 index 0000000000..96f671c52f --- /dev/null +++ b/src/@types/svg.d.ts @@ -0,0 +1,20 @@ +/* +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. +*/ + +declare module "*.svg" { + const path: string; + export default path; +} diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index 489d28e26b..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'; @@ -99,7 +97,7 @@ const CHECK_PROTOCOLS_ATTEMPTS = 3; // (and store the ID of their native room) export const VIRTUAL_ROOM_EVENT_TYPE = 'im.vector.is_virtual_room'; -export enum AudioID { +enum AudioID { Ring = 'ringAudio', Ringback = 'ringbackAudio', CallEnd = 'callendAudio', @@ -129,19 +127,15 @@ 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 { CallsChanged = "calls_changed", CallChangeRoom = "call_change_room", + SilencedCallsChanged = "silenced_calls_changed", } export default class CallHandler extends EventEmitter { @@ -164,6 +158,8 @@ export default class CallHandler extends EventEmitter { // do the async lookup when we get new information and then store these mappings here private assertedIdentityNativeUsers = new Map(); + private silencedCalls = new Set(); // callIds + static sharedInstance() { if (!window.mxCallHandler) { window.mxCallHandler = new CallHandler(); @@ -224,6 +220,33 @@ export default class CallHandler extends EventEmitter { } } + public silenceCall(callId: string) { + this.silencedCalls.add(callId); + this.emit(CallHandlerEvent.SilencedCallsChanged, this.silencedCalls); + + // Don't pause audio if we have calls which are still ringing + if (this.areAnyCallsUnsilenced()) return; + this.pause(AudioID.Ring); + } + + public unSilenceCall(callId: string) { + this.silencedCalls.delete(callId); + this.emit(CallHandlerEvent.SilencedCallsChanged, this.silencedCalls); + this.play(AudioID.Ring); + } + + public isCallSilenced(callId: string): boolean { + return this.silencedCalls.has(callId); + } + + /** + * Returns true if there is at least one unsilenced call + * @returns {boolean} + */ + private areAnyCallsUnsilenced(): boolean { + return this.calls.size > this.silencedCalls.size; + } + private async checkProtocols(maxTries) { try { const protocols = await MatrixClientPeg.get().getThirdpartyProtocols(); @@ -301,6 +324,13 @@ export default class CallHandler extends EventEmitter { }, true); }; + public getCallById(callId: string): MatrixCall { + for (const call of this.calls.values()) { + if (call.callId === callId) return call; + } + return null; + } + getCallForRoom(roomId: string): MatrixCall { return this.calls.get(roomId) || null; } @@ -441,6 +471,10 @@ export default class CallHandler extends EventEmitter { break; } + if (newState !== CallState.Ringing) { + this.silencedCalls.delete(call.callId); + } + switch (newState) { case CallState.Ringing: this.play(AudioID.Ring); @@ -450,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"); } @@ -480,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"), @@ -697,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/HtmlUtils.tsx b/src/HtmlUtils.tsx index a37b7f0ac9..af5d2b3019 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -33,7 +33,7 @@ import { IExtendedSanitizeOptions } from './@types/sanitize-html'; import linkifyMatrix from './linkify-matrix'; import SettingsStore from './settings/SettingsStore'; import { tryTransformPermalinkToLocalHref } from "./utils/permalinks/Permalinks"; -import { SHORTCODE_TO_EMOJI, getEmojiFromUnicode } from "./emoji"; +import { getEmojiFromUnicode } from "./emoji"; import ReplyThread from "./components/views/elements/ReplyThread"; import { mediaFromMxc } from "./customisations/Media"; @@ -57,7 +57,7 @@ const BIGEMOJI_REGEX = new RegExp(`^(${EMOJIBASE_REGEX.source})+$`, 'i'); const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/; -export const PERMITTED_URL_SCHEMES = ['http', 'https', 'ftp', 'mailto', 'magnet']; +export const PERMITTED_URL_SCHEMES = ['http', 'https', 'ftp', 'mailto', 'magnet', 'matrix']; const MEDIA_API_MXC_REGEX = /\/_matrix\/media\/r0\/(?:download|thumbnail)\/(.+?)\/(.+?)(?:[?/]|$)/; @@ -79,20 +79,8 @@ function mightContainEmoji(str: string): boolean { * @return {String} The shortcode (such as :thumbup:) */ export function unicodeToShortcode(char: string): string { - const data = getEmojiFromUnicode(char); - return (data && data.shortcodes ? `:${data.shortcodes[0]}:` : ''); -} - -/** - * Returns the unicode character for an emoji shortcode - * - * @param {String} shortcode The shortcode (such as :thumbup:) - * @return {String} The emoji character; null if none exists - */ -export function shortcodeToUnicode(shortcode: string): string { - shortcode = shortcode.slice(1, shortcode.length - 1); - const data = SHORTCODE_TO_EMOJI.get(shortcode); - return data ? data.unicode : null; + const shortcodes = getEmojiFromUnicode(char)?.shortcodes; + return shortcodes?.length ? `:${shortcodes[0]}:` : ''; } export function processHtmlForSending(html: string): string { 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 0056a37c85..7bad8eb50e 100644 --- a/src/TextForEvent.tsx +++ b/src/TextForEvent.tsx @@ -14,7 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ import React from 'react'; -import { MatrixClientPeg } from './MatrixClientPeg'; import { _t } from './languageHandler'; import * as Roles from './Roles'; import { isValid3pidInvite } from "./RoomInvite"; @@ -318,90 +317,6 @@ function textForCanonicalAliasEvent(ev: MatrixEvent): () => string | null { }); } -function textForCallAnswerEvent(event: MatrixEvent): () => string | null { - return () => { - const senderName = event.sender ? event.sender.name : _t('Someone'); - const supported = MatrixClientPeg.get().supportsVoip() ? '' : _t('(not supported by this browser)'); - return _t('%(senderName)s answered the call.', { senderName }) + ' ' + supported; - }; -} - -function textForCallHangupEvent(event: MatrixEvent): () => string | null { - const getSenderName = () => event.sender ? event.sender.name : _t('Someone'); - const eventContent = event.getContent(); - let getReason = () => ""; - if (!MatrixClientPeg.get().supportsVoip()) { - getReason = () => _t('(not supported by this browser)'); - } else if (eventContent.reason) { - if (eventContent.reason === "ice_failed") { - // We couldn't establish a connection at all - getReason = () => _t('(could not connect media)'); - } else if (eventContent.reason === "ice_timeout") { - // We established a connection but it died - getReason = () => _t('(connection failed)'); - } else if (eventContent.reason === "user_media_failed") { - // The other side couldn't open capture devices - getReason = () => _t("(their device couldn't start the camera / microphone)"); - } else if (eventContent.reason === "unknown_error") { - // An error code the other side doesn't have a way to express - // (as opposed to an error code they gave but we don't know about, - // in which case we show the error code) - getReason = () => _t("(an error occurred)"); - } else if (eventContent.reason === "invite_timeout") { - getReason = () => _t('(no answer)'); - } else if (eventContent.reason === "user hangup" || eventContent.reason === "user_hangup") { - // workaround for https://github.com/vector-im/element-web/issues/5178 - // it seems Android randomly sets a reason of "user hangup" which is - // interpreted as an error code :( - // https://github.com/vector-im/riot-android/issues/2623 - // Also the correct hangup code as of VoIP v1 (with underscore) - getReason = () => ''; - } else { - getReason = () => _t('(unknown failure: %(reason)s)', { reason: eventContent.reason }); - } - } - return () => _t('%(senderName)s ended the call.', { senderName: getSenderName() }) + ' ' + getReason(); -} - -function textForCallRejectEvent(event: MatrixEvent): () => string | null { - return () => { - const senderName = event.sender ? event.sender.name : _t('Someone'); - return _t('%(senderName)s declined the call.', { senderName }); - }; -} - -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 textForThreePidInviteEvent(event: MatrixEvent): () => string | null { const senderName = event.sender ? event.sender.name : event.getSender(); @@ -652,10 +567,6 @@ interface IHandlers { const handlers: IHandlers = { 'm.room.message': textForMessageEvent, - 'm.call.invite': textForCallInviteEvent, - 'm.call.answer': textForCallAnswerEvent, - 'm.call.hangup': textForCallHangupEvent, - 'm.call.reject': textForCallRejectEvent, }; const stateHandlers: IHandlers = { diff --git a/src/UserAddress.ts b/src/UserAddress.ts index a2c546deb7..248814aa01 100644 --- a/src/UserAddress.ts +++ b/src/UserAddress.ts @@ -14,35 +14,33 @@ See the License for the specific language governing permissions and limitations under the License. */ -import PropTypes from "prop-types"; - const emailRegex = /^\S+@\S+\.\S+$/; const mxUserIdRegex = /^@\S+:\S+$/; const mxRoomIdRegex = /^!\S+:\S+$/; -export const addressTypes = ['mx-user-id', 'mx-room-id', 'email']; - export enum AddressType { Email = "email", MatrixUserId = "mx-user-id", MatrixRoomId = "mx-room-id", } +export const addressTypes = [AddressType.Email, AddressType.MatrixRoomId, AddressType.MatrixUserId]; + // PropType definition for an object describing // an address that can be invited to a room (which // could be a third party identifier or a matrix ID) // along with some additional information about the // address / target. -export const UserAddressType = PropTypes.shape({ - addressType: PropTypes.oneOf(addressTypes).isRequired, - address: PropTypes.string.isRequired, - displayName: PropTypes.string, - avatarMxc: PropTypes.string, +export interface IUserAddress { + addressType: AddressType; + address: string; + displayName?: string; + avatarMxc?: string; // true if the address is known to be a valid address (eg. is a real // user we've seen) or false otherwise (eg. is just an address the // user has entered) - isKnown: PropTypes.bool, -}); + isKnown?: boolean; +} export function getAddressType(inputText: string): AddressType | null { if (emailRegex.test(inputText)) { diff --git a/src/async-components/views/dialogs/eventindex/DisableEventIndexDialog.js b/src/async-components/views/dialogs/eventindex/DisableEventIndexDialog.tsx similarity index 77% rename from src/async-components/views/dialogs/eventindex/DisableEventIndexDialog.js rename to src/async-components/views/dialogs/eventindex/DisableEventIndexDialog.tsx index e1c2b7b202..4d8f5e5663 100644 --- a/src/async-components/views/dialogs/eventindex/DisableEventIndexDialog.js +++ b/src/async-components/views/dialogs/eventindex/DisableEventIndexDialog.tsx @@ -15,8 +15,10 @@ limitations under the License. */ import React from 'react'; -import * as sdk from '../../../../index'; -import PropTypes from 'prop-types'; + +import BaseDialog from "../../../../components/views/dialogs/BaseDialog"; +import Spinner from "../../../../components/views/elements/Spinner"; +import DialogButtons from "../../../../components/views/elements/DialogButtons"; import dis from "../../../../dispatcher/dispatcher"; import { _t } from '../../../../languageHandler'; @@ -24,46 +26,44 @@ import SettingsStore from "../../../../settings/SettingsStore"; import EventIndexPeg from "../../../../indexing/EventIndexPeg"; import { Action } from "../../../../dispatcher/actions"; import { SettingLevel } from "../../../../settings/SettingLevel"; +interface IProps { + onFinished: (success: boolean) => void; +} + +interface IState { + disabling: boolean; +} /* * Allows the user to disable the Event Index. */ -export default class DisableEventIndexDialog extends React.Component { - static propTypes = { - onFinished: PropTypes.func.isRequired, - } - - constructor(props) { +export default class DisableEventIndexDialog extends React.Component { + constructor(props: IProps) { super(props); - this.state = { disabling: false, }; } - _onDisable = async () => { + private onDisable = async (): Promise => { this.setState({ disabling: true, }); await SettingsStore.setValue('enableEventIndexing', null, SettingLevel.DEVICE, false); await EventIndexPeg.deleteEventIndex(); - this.props.onFinished(); + this.props.onFinished(true); dis.fire(Action.ViewUserSettings); - } - - render() { - const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); - const Spinner = sdk.getComponent('elements.Spinner'); - const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + }; + public render(): React.ReactNode { return ( { _t("If disabled, messages from encrypted rooms won't appear in search results.") } { this.state.disabling ? :
} { - Modal.createTrackedDialogAsync("Disable message search", "Disable message search", - import("./DisableEventIndexDialog"), + const DisableEventIndexDialog = (await import("./DisableEventIndexDialog")).default; + Modal.createTrackedDialog("Disable message search", "Disable message search", + DisableEventIndexDialog, null, null, /* priority = */ false, /* static = */ true, ); }; 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 748b1c9ffc..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,44 +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; -} +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. - // noinspection JSIgnoredPromiseFromCall - this.props.playback.prepare(); - } - - 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 @@ -88,37 +55,39 @@ 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
-
- -
- - { this.props.mediaName || _t("Unnamed audio") } - -
- -   { /* easiest way to introduce a gap between the components */ } - { this.renderFileSize() } + return ( +
+
+ +
+ + { this.props.mediaName || _t("Unnamed audio") } + +
+ +   { /* easiest way to introduce a gap between the components */ } + { this.renderFileSize() } +
+
+ + +
-
- - -
-
; + ); } } 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 7d9312f369..e3f612c9b6 100644 --- a/src/components/views/audio_messages/RecordingPlayback.tsx +++ b/src/components/views/audio_messages/RecordingPlayback.tsx @@ -14,61 +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"; - -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; -} +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. - // noinspection JSIgnoredPromiseFromCall - this.props.playback.prepare(); - } - +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
- - - { this.isWaveformable && } -
; + return ( +
+ + + { this.isWaveformable && } +
+ ); } } 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/AuthBody.js b/src/components/views/auth/AuthBody.tsx similarity index 95% rename from src/components/views/auth/AuthBody.js rename to src/components/views/auth/AuthBody.tsx index abe7fd2fd3..3543a573d7 100644 --- a/src/components/views/auth/AuthBody.js +++ b/src/components/views/auth/AuthBody.tsx @@ -19,7 +19,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent"; @replaceableComponent("views.auth.AuthBody") export default class AuthBody extends React.PureComponent { - render() { + public render(): React.ReactNode { return
{ this.props.children }
; diff --git a/src/components/views/auth/AuthFooter.js b/src/components/views/auth/AuthFooter.tsx similarity index 96% rename from src/components/views/auth/AuthFooter.js rename to src/components/views/auth/AuthFooter.tsx index e81d2cd969..00bced8c39 100644 --- a/src/components/views/auth/AuthFooter.js +++ b/src/components/views/auth/AuthFooter.tsx @@ -22,7 +22,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent"; @replaceableComponent("views.auth.AuthFooter") export default class AuthFooter extends React.Component { - render() { + public render(): React.ReactNode { return (
{ _t("powered by Matrix") } diff --git a/src/components/views/auth/AuthHeader.js b/src/components/views/auth/AuthHeader.tsx similarity index 71% rename from src/components/views/auth/AuthHeader.js rename to src/components/views/auth/AuthHeader.tsx index d9bd81adcb..cab7da1468 100644 --- a/src/components/views/auth/AuthHeader.js +++ b/src/components/views/auth/AuthHeader.tsx @@ -16,20 +16,17 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; -import * as sdk from '../../../index'; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import AuthHeaderLogo from "./AuthHeaderLogo"; +import LanguageSelector from "./LanguageSelector"; + +interface IProps { + disableLanguageSelector?: boolean; +} @replaceableComponent("views.auth.AuthHeader") -export default class AuthHeader extends React.Component { - static propTypes = { - disableLanguageSelector: PropTypes.bool, - }; - - render() { - const AuthHeaderLogo = sdk.getComponent('auth.AuthHeaderLogo'); - const LanguageSelector = sdk.getComponent('views.auth.LanguageSelector'); - +export default class AuthHeader extends React.Component { + public render(): React.ReactNode { return (
diff --git a/src/components/views/auth/AuthHeaderLogo.js b/src/components/views/auth/AuthHeaderLogo.tsx similarity index 95% rename from src/components/views/auth/AuthHeaderLogo.js rename to src/components/views/auth/AuthHeaderLogo.tsx index 0adf18dc1c..b6724793a5 100644 --- a/src/components/views/auth/AuthHeaderLogo.js +++ b/src/components/views/auth/AuthHeaderLogo.tsx @@ -19,7 +19,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent"; @replaceableComponent("views.auth.AuthHeaderLogo") export default class AuthHeaderLogo extends React.PureComponent { - render() { + public render(): React.ReactNode { return
Matrix
; diff --git a/src/components/views/auth/AuthPage.js b/src/components/views/auth/AuthPage.tsx similarity index 90% rename from src/components/views/auth/AuthPage.js rename to src/components/views/auth/AuthPage.tsx index 6a73b06c16..c402d5b699 100644 --- a/src/components/views/auth/AuthPage.js +++ b/src/components/views/auth/AuthPage.tsx @@ -17,14 +17,12 @@ limitations under the License. */ import React from 'react'; -import * as sdk from '../../../index'; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import AuthFooter from "./AuthFooter"; @replaceableComponent("views.auth.AuthPage") export default class AuthPage extends React.PureComponent { - render() { - const AuthFooter = sdk.getComponent('auth.AuthFooter'); - + public render(): React.ReactNode { return (
diff --git a/src/components/views/auth/CaptchaForm.js b/src/components/views/auth/CaptchaForm.tsx similarity index 67% rename from src/components/views/auth/CaptchaForm.js rename to src/components/views/auth/CaptchaForm.tsx index e4a2fa247b..97f45167a8 100644 --- a/src/components/views/auth/CaptchaForm.js +++ b/src/components/views/auth/CaptchaForm.tsx @@ -15,66 +15,74 @@ limitations under the License. */ import React, { createRef } from 'react'; -import PropTypes from 'prop-types'; import { _t } from '../../../languageHandler'; import CountlyAnalytics from "../../../CountlyAnalytics"; import { replaceableComponent } from "../../../utils/replaceableComponent"; const DIV_ID = 'mx_recaptcha'; +interface ICaptchaFormProps { + sitePublicKey: string; + onCaptchaResponse: (response: string) => void; +} + +interface ICaptchaFormState { + errorText?: string; + +} + /** * A pure UI component which displays a captcha form. */ @replaceableComponent("views.auth.CaptchaForm") -export default class CaptchaForm extends React.Component { - static propTypes = { - sitePublicKey: PropTypes.string, - - // called with the captcha response - onCaptchaResponse: PropTypes.func, - }; - +export default class CaptchaForm extends React.Component { static defaultProps = { onCaptchaResponse: () => {}, }; - constructor(props) { + private captchaWidgetId?: string; + private recaptchaContainer = createRef(); + + constructor(props: ICaptchaFormProps) { super(props); this.state = { - errorText: null, + errorText: undefined, }; - this._captchaWidgetId = null; - - this._recaptchaContainer = createRef(); - CountlyAnalytics.instance.track("onboarding_grecaptcha_begin"); } componentDidMount() { // Just putting a script tag into the returned jsx doesn't work, annoyingly, // so we do this instead. - if (global.grecaptcha) { + if (this.isRecaptchaReady()) { // already loaded - this._onCaptchaLoaded(); + this.onCaptchaLoaded(); } else { console.log("Loading recaptcha script..."); - window.mx_on_recaptcha_loaded = () => {this._onCaptchaLoaded();}; + window.mxOnRecaptchaLoaded = () => { this.onCaptchaLoaded(); }; const scriptTag = document.createElement('script'); scriptTag.setAttribute( - 'src', `https://www.recaptcha.net/recaptcha/api.js?onload=mx_on_recaptcha_loaded&render=explicit`, + 'src', `https://www.recaptcha.net/recaptcha/api.js?onload=mxOnRecaptchaLoaded&render=explicit`, ); - this._recaptchaContainer.current.appendChild(scriptTag); + this.recaptchaContainer.current.appendChild(scriptTag); } } componentWillUnmount() { - this._resetRecaptcha(); + this.resetRecaptcha(); } - _renderRecaptcha(divId) { - if (!global.grecaptcha) { + // Borrowed directly from: https://github.com/codeep/react-recaptcha-google/commit/e118fa5670fa268426969323b2e7fe77698376ba + private isRecaptchaReady(): boolean { + return typeof window !== "undefined" && + typeof global.grecaptcha !== "undefined" && + typeof global.grecaptcha.render === 'function'; + } + + private renderRecaptcha(divId: string) { + if (!this.isRecaptchaReady()) { console.error("grecaptcha not loaded!"); throw new Error("Recaptcha did not load successfully"); } @@ -84,26 +92,26 @@ export default class CaptchaForm extends React.Component { console.error("No public key for recaptcha!"); throw new Error( "This server has not supplied enough information for Recaptcha " - + "authentication"); + + "authentication"); } console.info("Rendering to %s", divId); - this._captchaWidgetId = global.grecaptcha.render(divId, { + this.captchaWidgetId = global.grecaptcha.render(divId, { sitekey: publicKey, callback: this.props.onCaptchaResponse, }); } - _resetRecaptcha() { - if (this._captchaWidgetId !== null) { - global.grecaptcha.reset(this._captchaWidgetId); + private resetRecaptcha() { + if (this.captchaWidgetId) { + global?.grecaptcha?.reset(this.captchaWidgetId); } } - _onCaptchaLoaded() { + private onCaptchaLoaded() { console.log("Loaded recaptcha script."); try { - this._renderRecaptcha(DIV_ID); + this.renderRecaptcha(DIV_ID); // clear error if re-rendered this.setState({ errorText: null, @@ -128,7 +136,7 @@ export default class CaptchaForm extends React.Component { } return ( -
+

{ _t( "This homeserver would like to make sure you are not a robot.", ) }

diff --git a/src/components/views/auth/CompleteSecurityBody.js b/src/components/views/auth/CompleteSecurityBody.tsx similarity index 95% rename from src/components/views/auth/CompleteSecurityBody.js rename to src/components/views/auth/CompleteSecurityBody.tsx index 745d7abbf2..8f6affb64e 100644 --- a/src/components/views/auth/CompleteSecurityBody.js +++ b/src/components/views/auth/CompleteSecurityBody.tsx @@ -19,7 +19,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent"; @replaceableComponent("views.auth.CompleteSecurityBody") export default class CompleteSecurityBody extends React.PureComponent { - render() { + public render(): React.ReactNode { return
{ this.props.children }
; diff --git a/src/components/views/auth/CountryDropdown.js b/src/components/views/auth/CountryDropdown.tsx similarity index 75% rename from src/components/views/auth/CountryDropdown.js rename to src/components/views/auth/CountryDropdown.tsx index cbc19e0f8d..eb5b27be9d 100644 --- a/src/components/views/auth/CountryDropdown.js +++ b/src/components/views/auth/CountryDropdown.tsx @@ -15,21 +15,19 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; -import * as sdk from '../../../index'; - -import { COUNTRIES, getEmojiFlag } from '../../../phonenumber'; +import { COUNTRIES, getEmojiFlag, PhoneNumberCountryDefinition } from '../../../phonenumber'; import SdkConfig from "../../../SdkConfig"; import { _t } from "../../../languageHandler"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import Dropdown from "../elements/Dropdown"; const COUNTRIES_BY_ISO2 = {}; for (const c of COUNTRIES) { COUNTRIES_BY_ISO2[c.iso2] = c; } -function countryMatchesSearchQuery(query, country) { +function countryMatchesSearchQuery(query: string, country: PhoneNumberCountryDefinition): boolean { // Remove '+' if present (when searching for a prefix) if (query[0] === '+') { query = query.slice(1); @@ -41,15 +39,26 @@ function countryMatchesSearchQuery(query, country) { return false; } -@replaceableComponent("views.auth.CountryDropdown") -export default class CountryDropdown extends React.Component { - constructor(props) { - super(props); - this._onSearchChange = this._onSearchChange.bind(this); - this._onOptionChange = this._onOptionChange.bind(this); - this._getShortOption = this._getShortOption.bind(this); +interface IProps { + value?: string; + onOptionChange: (country: PhoneNumberCountryDefinition) => void; + isSmall: boolean; // if isSmall, show +44 in the selected value + showPrefix: boolean; + className?: string; + disabled?: boolean; +} - let defaultCountry = COUNTRIES[0]; +interface IState { + searchQuery: string; + defaultCountry: PhoneNumberCountryDefinition; +} + +@replaceableComponent("views.auth.CountryDropdown") +export default class CountryDropdown extends React.Component { + constructor(props: IProps) { + super(props); + + let defaultCountry: PhoneNumberCountryDefinition = COUNTRIES[0]; const defaultCountryCode = SdkConfig.get()["defaultCountryCode"]; if (defaultCountryCode) { const country = COUNTRIES.find(c => c.iso2 === defaultCountryCode.toUpperCase()); @@ -62,7 +71,7 @@ export default class CountryDropdown extends React.Component { }; } - componentDidMount() { + public componentDidMount(): void { if (!this.props.value) { // If no value is given, we start with the default // country selected, but our parent component @@ -71,21 +80,21 @@ export default class CountryDropdown extends React.Component { } } - _onSearchChange(search) { + private onSearchChange = (search: string): void => { this.setState({ searchQuery: search, }); - } + }; - _onOptionChange(iso2) { + private onOptionChange = (iso2: string): void => { this.props.onOptionChange(COUNTRIES_BY_ISO2[iso2]); - } + }; - _flagImgForIso2(iso2) { + private flagImgForIso2(iso2: string): React.ReactNode { return
{ getEmojiFlag(iso2) }
; } - _getShortOption(iso2) { + private getShortOption = (iso2: string): React.ReactNode => { if (!this.props.isSmall) { return undefined; } @@ -94,14 +103,12 @@ export default class CountryDropdown extends React.Component { countryPrefix = '+' + COUNTRIES_BY_ISO2[iso2].prefix; } return - { this._flagImgForIso2(iso2) } + { this.flagImgForIso2(iso2) } { countryPrefix } ; - } - - render() { - const Dropdown = sdk.getComponent('elements.Dropdown'); + }; + public render(): React.ReactNode { let displayedCountries; if (this.state.searchQuery) { displayedCountries = COUNTRIES.filter( @@ -124,7 +131,7 @@ export default class CountryDropdown extends React.Component { const options = displayedCountries.map((country) => { return
- { this._flagImgForIso2(country.iso2) } + { this.flagImgForIso2(country.iso2) } { _t(country.name) } (+{ country.prefix })
; }); @@ -136,10 +143,10 @@ export default class CountryDropdown extends React.Component { return ; } } - -CountryDropdown.propTypes = { - className: PropTypes.string, - isSmall: PropTypes.bool, - // if isSmall, show +44 in the selected value - showPrefix: PropTypes.bool, - onOptionChange: PropTypes.func.isRequired, - value: PropTypes.string, - disabled: PropTypes.bool, -}; diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.tsx b/src/components/views/auth/InteractiveAuthEntryComponents.tsx index 763ce10cd9..d9db140645 100644 --- a/src/components/views/auth/InteractiveAuthEntryComponents.tsx +++ b/src/components/views/auth/InteractiveAuthEntryComponents.tsx @@ -416,8 +416,10 @@ export class TermsAuthEntry extends React.Component{ _t("Accept") }; + submitButton = ; } return ( @@ -616,7 +618,9 @@ export class MsisdnAuthEntry extends React.Component
- diff --git a/src/components/views/auth/LanguageSelector.js b/src/components/views/auth/LanguageSelector.tsx similarity index 85% rename from src/components/views/auth/LanguageSelector.js rename to src/components/views/auth/LanguageSelector.tsx index 88293310e7..c26b4797f3 100644 --- a/src/components/views/auth/LanguageSelector.js +++ b/src/components/views/auth/LanguageSelector.tsx @@ -18,21 +18,23 @@ import SdkConfig from "../../../SdkConfig"; import { getCurrentLanguage } from "../../../languageHandler"; import SettingsStore from "../../../settings/SettingsStore"; import PlatformPeg from "../../../PlatformPeg"; -import * as sdk from '../../../index'; import React from 'react'; import { SettingLevel } from "../../../settings/SettingLevel"; +import LanguageDropdown from "../elements/LanguageDropdown"; -function onChange(newLang) { +function onChange(newLang: string): void { if (getCurrentLanguage() !== newLang) { SettingsStore.setValue("language", null, SettingLevel.DEVICE, newLang); PlatformPeg.get().reload(); } } -export default function LanguageSelector({ disabled }) { - if (SdkConfig.get()['disable_login_language_selector']) return
; +interface IProps { + disabled?: boolean; +} - const LanguageDropdown = sdk.getComponent('views.elements.LanguageDropdown'); +export default function LanguageSelector({ disabled }: IProps): JSX.Element { + if (SdkConfig.get()['disable_login_language_selector']) return
; return { + constructor(props: IProps) { super(props); CountlyAnalytics.instance.track("onboarding_welcome"); } - render() { - const EmbeddedPage = sdk.getComponent('structures.EmbeddedPage'); - const LanguageSelector = sdk.getComponent('auth.LanguageSelector'); + public render(): React.ReactNode { + // FIXME: Using an import will result in wrench-element-tests failures + const EmbeddedPage = sdk.getComponent("structures.EmbeddedPage"); const pagesConfig = SdkConfig.get().embeddedPages; let pageUrl = null; 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 ( -
-
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/MessageContextMenu.tsx b/src/components/views/context_menus/MessageContextMenu.tsx index bf171353e8..8f5d3baa17 100644 --- a/src/components/views/context_menus/MessageContextMenu.tsx +++ b/src/components/views/context_menus/MessageContextMenu.tsx @@ -43,11 +43,15 @@ export function canCancel(eventStatus: EventStatus): boolean { return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT; } -interface IEventTileOps { +export interface IEventTileOps { isWidgetHidden(): boolean; unhideWidget(): void; } +export interface IOperableEventTile { + getEventTileOps(): IEventTileOps; +} + interface IProps { /* the MatrixEvent associated with the context menu */ mxEvent: MatrixEvent; 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