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 01ce87c5c6..673b1f5a08 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -76,6 +76,7 @@ @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"; @@ -85,6 +86,7 @@ @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"; @@ -270,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/views/context_menus/_IconizedContextMenu.scss b/res/css/views/context_menus/_IconizedContextMenu.scss index 204435995f..2aa43283b5 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; + } } } diff --git a/res/css/views/dialogs/_AddExistingToSpaceDialog.scss b/res/css/views/dialogs/_AddExistingToSpaceDialog.scss index e71c34b800..5d9ea06e52 100644 --- a/res/css/views/dialogs/_AddExistingToSpaceDialog.scss +++ b/res/css/views/dialogs/_AddExistingToSpaceDialog.scss @@ -49,41 +49,41 @@ limitations under the License. font-weight: $font-semi-bold; line-height: $font-15px; } - } - .mx_AddExistingToSpace_section_spaces { - .mx_BaseAvatar { - margin-right: 12px; + .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; + } } - .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; } } @@ -181,78 +181,78 @@ limitations under the License. min-height: 0; height: 80vh; - .mx_Dialog_title { - display: flex; + .mx_AddExistingToSpace { + display: contents; + } +} - .mx_BaseAvatar_image { - border-radius: 8px; +.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; - vertical-align: unset; } + } - .mx_BaseAvatar { - display: inline-flex; - margin: auto 16px auto 5px; - vertical-align: middle; - } + .mx_Dropdown_input { + border: none; - > div { - > h1 { - font-weight: $font-semi-bold; - font-size: $font-18px; - line-height: $font-22px; - margin: 0; - } + > .mx_Dropdown_option { + padding-left: 0; + flex: unset; + height: unset; + color: $secondary-fg-color; + font-size: $font-15px; + line-height: $font-24px; - .mx_AddExistingToSpaceDialog_onlySpace { - color: $secondary-fg-color; - font-size: $font-15px; - line-height: $font-24px; + .mx_BaseAvatar { + display: none; } } - .mx_Dropdown_input { - border: none; + .mx_Dropdown_menu { + .mx_SubspaceSelector_dropdownOptionActive { + color: $accent-color; + padding-right: 32px; + position: relative; - > .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'); - } + &::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_onlySpace { + color: $secondary-fg-color; + font-size: $font-15px; + line-height: $font-24px; } } 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 5321d8ff69..e7cfbf6050 100644 --- a/res/css/views/dialogs/_CreateRoomDialog.scss +++ b/res/css/views/dialogs/_CreateRoomDialog.scss @@ -109,56 +109,4 @@ limitations under the License. margin: 0 85px 0 0; font-size: $font-12px; } - - .mx_Dropdown { - 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_CreateRoomDialog_dropdown_invite::before { - mask-image: url('$(res)/img/element-icons/lock.svg'); - mask-size: contain; - } - - .mx_CreateRoomDialog_dropdown_public::before { - mask-image: url('$(res)/img/globe.svg'); - mask-size: 12px; - } - - .mx_CreateRoomDialog_dropdown_restricted::before { - mask-image: url('$(res)/img/element-icons/community-members.svg'); - mask-size: contain; - } - } } - 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/_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/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/_Field.scss b/res/css/views/elements/_Field.scss index f67da6477b..cae81dcc97 100644 --- a/res/css/views/elements/_Field.scss +++ b/res/css/views/elements/_Field.scss @@ -39,7 +39,6 @@ limitations under the License. .mx_Field select, .mx_Field textarea { font-weight: normal; - font-family: $font-family; font-size: $font-14px; border: none; // Even without a border here, we still need this avoid overlapping the rounded diff --git a/res/css/views/messages/_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 1d43c3030d..e495c2c596 100644 --- a/res/css/views/rooms/_EventBubbleTile.scss +++ b/res/css/views/rooms/_EventBubbleTile.scss @@ -38,18 +38,22 @@ limitations under the License. padding-top: 0; } + &::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 { @@ -267,7 +271,7 @@ limitations under the License. display: flex; align-items: center; - justify-content: center; + justify-content: start; padding: 5px 0; .mx_EventTile_avatar { @@ -275,12 +279,22 @@ limitations under the License. order: -1; margin-right: 5px; } + + .mx_EventTile_e2eIcon { + margin-left: 9px; + } + + .mx_EventTile_line > a { + right: auto; + top: -15px; + left: -68px; + } } .mx_EventListSummary[data-layout=bubble] { - --maxWidth: 80%; + --maxWidth: 70%; margin-left: calc(var(--avatarSize) + var(--gutterSize)); - margin-right: calc(var(--gutterSize) + var(--avatarSize)); + margin-right: 94px; .mx_EventListSummary_toggle { float: none; margin: 0; diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 4a419244ff..5de9a9f9d1 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -59,7 +59,6 @@ $hover-select-border: 4px; font-size: $font-14px; display: inline-block; /* anti-zalgo, with overflow hidden */ overflow: hidden; - cursor: pointer; padding-bottom: 0px; padding-top: 0px; margin: 0px; @@ -132,15 +131,6 @@ $hover-select-border: 4px; } } - &.mx_EventTile_info .mx_EventTile_line, - & ~ .mx_EventListSummary > :not(.mx_EventTile) .mx_EventTile_avatar ~ .mx_EventTile_line { - padding-left: calc($left-gutter + 18px); - } - - & ~ .mx_EventListSummary .mx_EventTile_line { - padding-left: calc($left-gutter); - } - &.mx_EventTile_selected.mx_EventTile_info .mx_EventTile_line { padding-left: calc($left-gutter + 18px - $hover-select-border); } @@ -276,10 +266,19 @@ $hover-select-border: 4px; .mx_ReactionsRow { margin: 0; - padding: 6px 60px; + padding: 4px 64px; } } +.mx_EventTile:not([data-layout=bubble]).mx_EventTile_info .mx_EventTile_line, +.mx_EventListSummary:not([data-layout=bubble]) > :not(.mx_EventTile) .mx_EventTile_avatar ~ .mx_EventTile_line { + padding-left: calc($left-gutter + 18px); +} + +.mx_EventListSummary:not([data-layout=bubble]) .mx_EventTile_line { + padding-left: calc($left-gutter); +} + /* all the overflow-y: hidden; are to trap Zalgos - but they introduce an implicit overflow-x: auto. so make that explicitly hidden too to avoid random @@ -322,6 +321,10 @@ $hover-select-border: 4px; // on ELS we need the margin to allow interaction with the expand/collapse button which is normally in the RR gutter } +.mx_SenderProfile { + cursor: pointer; +} + .mx_EventTile_bubbleContainer { display: grid; grid-template-columns: 1fr 100px; 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/settings/tabs/_SettingsTab.scss b/res/css/views/settings/tabs/_SettingsTab.scss index 0d679af4e5..8e6b99871c 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; 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/img/voip/screensharing-off.svg b/res/img/voip/screensharing-off.svg new file mode 100644 index 0000000000..dc19e9892e --- /dev/null +++ b/res/img/voip/screensharing-off.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/res/img/voip/screensharing-on.svg b/res/img/voip/screensharing-on.svg new file mode 100644 index 0000000000..a8e7fe308e --- /dev/null +++ b/res/img/voip/screensharing-on.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/res/img/voip/sidebar-off.svg b/res/img/voip/sidebar-off.svg new file mode 100644 index 0000000000..7637a9ab55 --- /dev/null +++ b/res/img/voip/sidebar-off.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/res/img/voip/sidebar-on.svg b/res/img/voip/sidebar-on.svg new file mode 100644 index 0000000000..a625334be4 --- /dev/null +++ b/res/img/voip/sidebar-on.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index 72be3f8e67..e7c1dda54f 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -56,7 +56,6 @@ limitations under the License. import React from 'react'; import { MatrixClientPeg } from './MatrixClientPeg'; -import PlatformPeg from './PlatformPeg'; import Modal from './Modal'; import { _t } from './languageHandler'; import dis from './dispatcher/dispatcher'; @@ -80,7 +79,6 @@ import CountlyAnalytics from "./CountlyAnalytics"; import { UIFeature } from "./settings/UIFeature"; import { CallError } from "matrix-js-sdk/src/webrtc/call"; import { logger } from 'matrix-js-sdk/src/logger'; -import DesktopCapturerSourcePicker from "./components/views/elements/DesktopCapturerSourcePicker"; import { Action } from './dispatcher/actions'; import VoipUserMapper from './VoipUserMapper'; import { addManagedHybridWidget, isManagedHybridWidgetEnabled } from './widgets/ManagedHybrid'; @@ -129,14 +127,9 @@ interface ThirdpartyLookupResponse { fields: ThirdpartyLookupResponseFields; } -// Unlike 'CallType' in js-sdk, this one includes screen sharing -// (because a screen sharing call is only a screen sharing call to the caller, -// to the callee it's just a video call, at least as far as the current impl -// is concerned). export enum PlaceCallType { Voice = 'voice', Video = 'video', - ScreenSharing = 'screensharing', } export enum CallHandlerEvent { @@ -728,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/components/structures/SpaceRoomDirectory.tsx b/src/components/structures/SpaceRoomDirectory.tsx index 038c1df514..d8cc9593f0 100644 --- a/src/components/structures/SpaceRoomDirectory.tsx +++ b/src/components/structures/SpaceRoomDirectory.tsx @@ -16,7 +16,6 @@ limitations under the License. import React, { ReactNode, useMemo, useState } from "react"; import { Room } from "matrix-js-sdk/src/models/room"; -import { MatrixClient } from "matrix-js-sdk/src/client"; import { EventType, RoomType } from "matrix-js-sdk/src/@types/event"; import { ISpaceSummaryRoom, ISpaceSummaryEvent } from "matrix-js-sdk/src/@types/spaces"; import classNames from "classnames"; @@ -44,11 +43,13 @@ import { getChildOrder } from "../../stores/SpaceStore"; import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton"; import { linkifyElement } from "../../HtmlUtils"; import { getDisplayAliasForAliasSet } from "../../Rooms"; +import { useDispatcher } from "../../hooks/useDispatcher"; +import defaultDispatcher from "../../dispatcher/dispatcher"; +import { Action } from "../../dispatcher/actions"; interface IHierarchyProps { space: Room; initialText?: string; - refreshToken?: any; additionalButtons?: ReactNode; showRoom(room: ISpaceSummaryRoom, viaServers?: string[], autoJoin?: boolean): void; } @@ -315,18 +316,25 @@ export const HierarchyLevel = ({ ; }; -// mutate argument refreshToken to force a reload -export const useSpaceSummary = (cli: MatrixClient, space: Room, refreshToken?: any): [ +export const useSpaceSummary = (space: Room): [ null, ISpaceSummaryRoom[], Map>?, Map>?, Map>?, ] | [Error] => { + // crude temporary refresh token approach until we have pagination and rework the data flow here + const [refreshToken, setRefreshToken] = useState(0); + useDispatcher(defaultDispatcher, (payload => { + if (payload.action === Action.UpdateSpaceHierarchy) { + setRefreshToken(t => t + 1); + } + })); + // TODO pagination return useAsyncMemo(async () => { try { - const data = await cli.getSpaceSummary(space.roomId); + const data = await space.client.getSpaceSummary(space.roomId); const parentChildRelations = new EnhancedMap>(); const childParentRelations = new EnhancedMap>(); @@ -354,7 +362,6 @@ export const SpaceHierarchy: React.FC = ({ space, initialText = "", showRoom, - refreshToken, additionalButtons, children, }) => { @@ -364,7 +371,7 @@ export const SpaceHierarchy: React.FC = ({ const [selected, setSelected] = useState(new Map>()); // Map> - const [summaryError, rooms, parentChildMap, viaMap, childParentMap] = useSpaceSummary(cli, space, refreshToken); + const [summaryError, rooms, parentChildMap, viaMap, childParentMap] = useSpaceSummary(space); const roomsMap = useMemo(() => { if (!rooms) return null; diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx index a077fddadf..682a0c68db 100644 --- a/src/components/structures/SpaceRoomView.tsx +++ b/src/components/structures/SpaceRoomView.tsx @@ -47,13 +47,23 @@ import { RightPanelPhases } from "../../stores/RightPanelStorePhases"; import { SetRightPanelPhasePayload } from "../../dispatcher/payloads/SetRightPanelPhasePayload"; import { useStateArray } from "../../hooks/useStateArray"; import SpacePublicShare from "../views/spaces/SpacePublicShare"; -import { shouldShowSpaceSettings, showAddExistingRooms, showCreateNewRoom, showSpaceSettings } from "../../utils/space"; +import { + shouldShowSpaceSettings, + showAddExistingRooms, + showCreateNewRoom, + showCreateNewSubspace, + showSpaceSettings, +} from "../../utils/space"; import { showRoom, SpaceHierarchy } from "./SpaceRoomDirectory"; import MemberAvatar from "../views/avatars/MemberAvatar"; -import { useStateToggle } from "../../hooks/useStateToggle"; import SpaceStore from "../../stores/SpaceStore"; import FacePile from "../views/elements/FacePile"; -import { AddExistingToSpace } from "../views/dialogs/AddExistingToSpaceDialog"; +import { + AddExistingToSpace, + defaultDmsRenderer, + defaultRoomsRenderer, + defaultSpacesRenderer, +} from "../views/dialogs/AddExistingToSpaceDialog"; import { ChevronFace, ContextMenuButton, useContextMenu } from "./ContextMenu"; import IconizedContextMenu, { IconizedContextMenuOption, @@ -307,7 +317,7 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) => ; }; -const SpaceLandingAddButton = ({ space, onNewRoomAdded }) => { +const SpaceLandingAddButton = ({ space }) => { const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu(); let contextMenu; @@ -331,24 +341,32 @@ const SpaceLandingAddButton = ({ space, onNewRoomAdded }) => { closeMenu(); if (await showCreateNewRoom(space)) { - onNewRoomAdded(); + defaultDispatcher.fire(Action.UpdateSpaceHierarchy); } }} /> { + onClick={(e) => { e.preventDefault(); e.stopPropagation(); closeMenu(); - - const [added] = await showAddExistingRooms(space); - if (added) { - onNewRoomAdded(); - } + showAddExistingRooms(space); }} /> + { + e.preventDefault(); + e.stopPropagation(); + closeMenu(); + showCreateNewSubspace(space); + }} + > + + ; } @@ -389,11 +407,9 @@ const SpaceLanding = ({ space }) => { const canAddRooms = myMembership === "join" && space.currentState.maySendStateEvent(EventType.SpaceChild, userId); - const [refreshToken, forceUpdate] = useStateToggle(false); - let addRoomButton; if (canAddRooms) { - addRoomButton = ; + addRoomButton = ; } let settingsButton; @@ -443,12 +459,7 @@ const SpaceLanding = ({ space }) => {
- + ; }; @@ -550,10 +561,12 @@ const SpaceAddExistingRooms = ({ space, onFinished }) => { { _t("Skip for now") } } + filterPlaceholder={_t("Search for rooms or spaces")} onFinished={onFinished} + roomsRenderer={defaultRoomsRenderer} + spacesRenderer={defaultSpacesRenderer} + dmsRenderer={defaultDmsRenderer} /> - -
; }; diff --git a/src/components/views/auth/CaptchaForm.tsx b/src/components/views/auth/CaptchaForm.tsx index b1c09f2b22..97f45167a8 100644 --- a/src/components/views/auth/CaptchaForm.tsx +++ b/src/components/views/auth/CaptchaForm.tsx @@ -103,8 +103,8 @@ export default class CaptchaForm extends React.Component = ({ ; }; -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/dialogs/AddExistingSubspaceDialog.tsx b/src/components/views/dialogs/AddExistingSubspaceDialog.tsx new file mode 100644 index 0000000000..a6dbf9dd42 --- /dev/null +++ b/src/components/views/dialogs/AddExistingSubspaceDialog.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 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 { SpaceFeedbackPrompt } from "../../structures/SpaceRoomView"; +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} + /> +
+ + onFinished(false)} /> +
; +}; + +export default AddExistingSubspaceDialog; + diff --git a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx index 7f4b365a8b..9b64af0c80 100644 --- a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx +++ b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx @@ -18,9 +18,9 @@ import React, { ReactNode, useContext, useMemo, useState } from "react"; import classNames from "classnames"; import { Room } from "matrix-js-sdk/src/models/room"; 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"; @@ -42,9 +42,11 @@ import TruncatedList from "../elements/TruncatedList"; import EntityTile from "../rooms/EntityTile"; import BaseAvatar from "../avatars/BaseAvatar"; -interface IProps extends IDialogProps { +interface IProps { space: Room; - onCreateRoomClick(space: Room): void; + onCreateRoomClick(): void; + onAddSubspaceClick(): void; + onFinished(added?: boolean): void; } export const Entry = ({ room, checked, onChange }) => { @@ -65,14 +67,36 @@ export const Entry = ({ room, checked, onChange }) => { interface IAddExistingToSpaceProps { space: Room; footerPrompt?: ReactNode; + filterPlaceholder: string; emptySelectionButton?: ReactNode; onFinished(added: boolean): void; + roomsRenderer?( + rooms: Room[], + selectedToAdd: Set, + onChange: undefined | ((checked: boolean, room: Room) => void), + truncateAt: number, + overflowTile: (overflowCount: number, totalCount: number) => JSX.Element, + ): ReactNode; + spacesRenderer?( + spaces: Room[], + selectedToAdd: Set, + onChange?: (checked: boolean, room: Room) => void, + ): ReactNode; + dmsRenderer?( + dms: Room[], + selectedToAdd: Set, + onChange?: (checked: boolean, room: Room) => void, + ): ReactNode; } export const AddExistingToSpace: React.FC = ({ space, footerPrompt, emptySelectionButton, + filterPlaceholder, + roomsRenderer, + dmsRenderer, + spacesRenderer, onFinished, }) => { const cli = useContext(MatrixClientContext); @@ -196,7 +220,7 @@ export const AddExistingToSpace: React.FC = ({ ; } - const onChange = !busy && !error ? (checked, room) => { + const onChange = !busy && !error ? (checked: boolean, room: Room) => { if (checked) { selectedToAdd.add(room); } else { @@ -206,7 +230,7 @@ export const AddExistingToSpace: React.FC = ({ } : null; const [truncateAt, setTruncateAt] = useState(20); - function overflowTile(overflowCount, totalCount) { + function overflowTile(overflowCount: number, totalCount: number): JSX.Element { const text = _t("and %(count)s others...", { count: overflowCount }); return ( = ({ ); } + let noResults = true; + if ((roomsRenderer && rooms.length > 0) || + (dmsRenderer && dms.length > 0) || + (!roomsRenderer && !dmsRenderer && spacesRenderer && spaces.length > 0) // only count spaces when alone + ) { + noResults = false; + } + return
- { rooms.length > 0 ? ( -
-

{ _t("Rooms") }

- rooms.slice(start, end).map(room => - { - onChange(checked, room); - } : null} - />, - )} - getChildCount={() => rooms.length} - /> -
+ { rooms.length > 0 && roomsRenderer ? ( + roomsRenderer(rooms, selectedToAdd, onChange, truncateAt, overflowTile) ) : undefined } - { spaces.length > 0 ? ( -
-

{ _t("Spaces") }

-
-
{ _t("Feeling experimental?") }
-
{ _t("You can add existing spaces to a space.") }
-
- { spaces.map(space => { - return { - onChange(checked, space); - } : null} - />; - }) } -
+ { spaces.length > 0 && spacesRenderer ? ( + spacesRenderer(spaces, selectedToAdd, onChange) ) : null } - { dms.length > 0 ? ( -
-

{ _t("Direct Messages") }

- { dms.map(room => { - return { - onChange(checked, room); - } : null} - />; - }) } -
+ { dms.length > 0 && dmsRenderer ? ( + dmsRenderer(dms, selectedToAdd, onChange) ) : null } - { spaces.length + rooms.length + dms.length < 1 ? + { noResults ? { _t("No results") } : undefined }
@@ -299,50 +286,126 @@ export const AddExistingToSpace: React.FC = ({
; }; -const AddExistingToSpaceDialog: React.FC = ({ space, onCreateRoomClick, onFinished }) => { - const [selectedSpace, setSelectedSpace] = useState(space); - const existingSubspaces = SpaceStore.instance.getChildSpaces(space.roomId); +export const defaultRoomsRenderer: IAddExistingToSpaceProps["roomsRenderer"] = ( + rooms, selectedToAdd, onChange, truncateAt, overflowTile, +) => ( +
+

{ _t("Rooms") }

+ rooms.slice(start, end).map(room => + { + onChange(checked, room); + } : null} + />, + )} + getChildCount={() => rooms.length} + /> +
+); - let spaceOptionSection; - if (existingSubspaces.length > 0) { - const options = [space, ...existingSubspaces].map((space) => { - const classes = classNames("mx_AddExistingToSpaceDialog_dropdownOption", { - mx_AddExistingToSpaceDialog_dropdownOptionActive: space === selectedSpace, - }); - return
- - { space.name || getDisplayAliasForRoom(space) || space.roomId } -
; - }); +export const defaultSpacesRenderer: IAddExistingToSpaceProps["spacesRenderer"] = (spaces, selectedToAdd, onChange) => ( +
+ { spaces.map(space => { + return { + onChange(checked, space); + } : null} + />; + }) } +
+); - spaceOptionSection = ( +export const defaultDmsRenderer: IAddExistingToSpaceProps["dmsRenderer"] = (dms, selectedToAdd, onChange) => ( +
+

{ _t("Direct Messages") }

+ { dms.map(room => { + return { + onChange(checked, room); + } : null} + />; + }) } +
+); + +interface ISubspaceSelectorProps { + title: string; + space: Room; + value: Room; + onChange(space: Room): void; +} + +export const SubspaceSelector = ({ title, space, value, onChange }: ISubspaceSelectorProps) => { + const options = useMemo(() => { + return [space, ...SpaceStore.instance.getChildSpaces(space.roomId).filter(space => { + return space.currentState.maySendStateEvent(EventType.SpaceChild, space.client.credentials.userId); + })]; + }, [space]); + + let body; + if (options.length > 1) { + body = ( { - setSelectedSpace(existingSubspaces.find(space => space.roomId === key) || space); + onChange(options.find(space => space.roomId === key) || space); }} - value={selectedSpace.roomId} + value={value.roomId} label={_t("Space selection")} > - { options } + { options.map((space) => { + const classes = classNames({ + mx_SubspaceSelector_dropdownOptionActive: space === value, + }); + return
+ + { space.name || getDisplayAliasForRoom(space) || space.roomId } +
; + }) }
); } else { - spaceOptionSection =
- { space.name || getDisplayAliasForRoom(space) || space.roomId } -
; + body = ( +
+ { space.name || getDisplayAliasForRoom(space) || space.roomId } +
+ ); } - const title = - + return
+
-

{ _t("Add existing rooms") }

- { spaceOptionSection } +

{ title }

+ { body }
- ; +
; +}; + +const AddExistingToSpaceDialog: React.FC = ({ space, onCreateRoomClick, onAddSubspaceClick, onFinished }) => { + const [selectedSpace, setSelectedSpace] = useState(space); return + )} className="mx_AddExistingToSpaceDialog" contentId="mx_AddExistingToSpace" onFinished={onFinished} @@ -354,10 +417,33 @@ const AddExistingToSpaceDialog: React.FC = ({ space, onCreateRoomClick, onFinished={onFinished} footerPrompt={<>
{ _t("Want to add a new room instead?") }
- onCreateRoomClick(space)} kind="link"> + { + onCreateRoomClick(); + onFinished(); + }} + > { _t("Create a new room") } } + filterPlaceholder={_t("Search for rooms")} + roomsRenderer={defaultRoomsRenderer} + spacesRenderer={() => ( +
+

{ _t("Spaces") }

+ { + onAddSubspaceClick(); + onFinished(); + }} + > + { _t("Adding spaces has moved.") } + +
+ )} + dmsRenderer={defaultDmsRenderer} /> diff --git a/src/components/views/dialogs/CreateRoomDialog.tsx b/src/components/views/dialogs/CreateRoomDialog.tsx index a06f508908..597fe33777 100644 --- a/src/components/views/dialogs/CreateRoomDialog.tsx +++ b/src/components/views/dialogs/CreateRoomDialog.tsx @@ -32,8 +32,8 @@ import RoomAliasField from "../elements/RoomAliasField"; import LabelledToggleSwitch from "../elements/LabelledToggleSwitch"; import DialogButtons from "../elements/DialogButtons"; import BaseDialog from "../dialogs/BaseDialog"; -import Dropdown from "../elements/Dropdown"; import SpaceStore from "../../../stores/SpaceStore"; +import JoinRuleDropdown from "../elements/JoinRuleDropdown"; interface IProps { defaultPublic?: boolean; @@ -250,7 +250,7 @@ export default class CreateRoomDialog extends React.Component {   { _t("You can change this at any time from room settings.") }

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

{ _t( "Anyone will be able to find and join this room, not just members of .", {}, { @@ -260,6 +260,12 @@ export default class CreateRoomDialog extends React.Component {   { _t("You can change this at any time from room settings.") }

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

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

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

{ _t( @@ -316,21 +322,6 @@ export default class CreateRoomDialog extends React.Component { title = this.state.joinRule === JoinRule.Public ? _t('Create a public room') : _t('Create a private room'); } - const options = [ -

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

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

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

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

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

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

; + } + + return + )} + className="mx_CreateSubspaceDialog" + contentId="mx_CreateSubspaceDialog" + onFinished={onFinished} + fixedWidth={false} + > + +
+
+ + { _t("Add a space to a space you manage.") } +
+ + + + { joinRuleMicrocopy } + +
+ +
+
+
{ _t("Want to add an existing space instead?") }
+ { + onAddExistingSpaceClick(); + onFinished(); + }} + > + { _t("Add existing space") } + +
+ + onFinished(false)}> + { _t("Cancel") } + + + { busy ? _t("Adding...") : _t("Add") } + +
+
+
; +}; + +export default CreateSubspaceDialog; + diff --git a/src/components/views/elements/DesktopCapturerSourcePicker.tsx b/src/components/views/elements/DesktopCapturerSourcePicker.tsx index 20ecf08a5f..1f00353aeb 100644 --- a/src/components/views/elements/DesktopCapturerSourcePicker.tsx +++ b/src/components/views/elements/DesktopCapturerSourcePicker.tsx @@ -17,9 +17,12 @@ limitations under the License. import React from 'react'; import { _t } from '../../../languageHandler'; import BaseDialog from "..//dialogs/BaseDialog"; +import DialogButtons from "./DialogButtons"; +import classNames from 'classnames'; import AccessibleButton from './AccessibleButton'; import { getDesktopCapturerSources } from "matrix-js-sdk/src/webrtc/call"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import TabbedView, { Tab, TabLocation } from '../../structures/TabbedView'; export interface DesktopCapturerSource { id: string; @@ -28,62 +31,70 @@ export interface DesktopCapturerSource { } export enum Tabs { - Screens = "screens", - Windows = "windows", + Screens = "screen", + Windows = "window", } -export interface DesktopCapturerSourceIProps { +export interface ExistingSourceIProps { source: DesktopCapturerSource; onSelect(source: DesktopCapturerSource): void; + selected: boolean; } -export class ExistingSource extends React.Component { - constructor(props) { +export class ExistingSource extends React.Component { + constructor(props: ExistingSourceIProps) { super(props); } - onClick = (ev) => { + private onClick = (): void => { this.props.onSelect(this.props.source); }; render() { + const thumbnailClasses = classNames({ + mx_desktopCapturerSourcePicker_source_thumbnail: true, + mx_desktopCapturerSourcePicker_source_thumbnail_selected: this.props.selected, + }); + return ( - { this.props.source.name } + { this.props.source.name } ); } } -export interface DesktopCapturerSourcePickerIState { +export interface PickerIState { selectedTab: Tabs; sources: Array; + selectedSource: DesktopCapturerSource | null; } -export interface DesktopCapturerSourcePickerIProps { +export interface PickerIProps { onFinished(source: DesktopCapturerSource): void; } @replaceableComponent("views.elements.DesktopCapturerSourcePicker") export default class DesktopCapturerSourcePicker extends React.Component< - DesktopCapturerSourcePickerIProps, - DesktopCapturerSourcePickerIState - > { - interval; + PickerIProps, + PickerIState +> { + interval: number; - constructor(props) { + constructor(props: PickerIProps) { super(props); this.state = { selectedTab: Tabs.Screens, sources: [], + selectedSource: null, }; } @@ -107,69 +118,61 @@ export default class DesktopCapturerSourcePicker extends React.Component< clearInterval(this.interval); } - onSelect = (source) => { - this.props.onFinished(source); + private onSelect = (source: DesktopCapturerSource): void => { + this.setState({ selectedSource: source }); }; - onScreensClick = (ev) => { - this.setState({ selectedTab: Tabs.Screens }); + private onShare = (): void => { + this.props.onFinished(this.state.selectedSource); }; - onWindowsClick = (ev) => { - this.setState({ selectedTab: Tabs.Windows }); + private onTabChange = (): void => { + this.setState({ selectedSource: null }); }; - onCloseClick = (ev) => { + private onCloseClick = (): void => { this.props.onFinished(null); }; - render() { - let sources; - if (this.state.selectedTab === Tabs.Screens) { - sources = this.state.sources - .filter((source) => { - return source.id.startsWith("screen"); - }) - .map((source) => { - return ; - }); - } else { - sources = this.state.sources - .filter((source) => { - return source.id.startsWith("window"); - }) - .map((source) => { - return ; - }); - } + private getTab(type: "screen" | "window", label: string): Tab { + const sources = this.state.sources.filter((source) => source.id.startsWith(type)).map((source) => { + return ( + + ); + }); - const buttonStyle = "mx_desktopCapturerSourcePicker_tabLabel"; - const screensButtonStyle = buttonStyle + ((this.state.selectedTab === Tabs.Screens) ? "_selected" : ""); - const windowsButtonStyle = buttonStyle + ((this.state.selectedTab === Tabs.Windows) ? "_selected" : ""); + return new Tab(type, label, null, ( +
+ { sources } +
+ )); + } + + render() { + const tabs = [ + this.getTab("screen", _t("Share entire screen")), + this.getTab("window", _t("Application window")), + ]; return ( -
- - { _t("Screens") } - - - { _t("Windows") } - -
-
- { sources } -
+ +
); } diff --git a/src/components/views/elements/JoinRuleDropdown.tsx b/src/components/views/elements/JoinRuleDropdown.tsx new file mode 100644 index 0000000000..e2d9b6d872 --- /dev/null +++ b/src/components/views/elements/JoinRuleDropdown.tsx @@ -0,0 +1,68 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import { JoinRule } from 'matrix-js-sdk/src/@types/partials'; + +import Dropdown from "./Dropdown"; + +interface IProps { + value: JoinRule; + label: string; + width?: number; + labelInvite: string; + labelPublic: string; + labelRestricted?: string; // if omitted then this option will be hidden, e.g if unsupported + onChange(value: JoinRule): void; +} + +const JoinRuleDropdown = ({ + label, + labelInvite, + labelPublic, + labelRestricted, + value, + width = 448, + onChange, +}: IProps) => { + const options = [ +
+ { labelInvite } +
, +
+ { labelPublic } +
, + ]; + + if (labelRestricted) { + options.unshift(
+ { labelRestricted } +
); + } + + return + { options } + ; +}; + +export default JoinRuleDropdown; diff --git a/src/components/views/messages/ViewSourceEvent.js b/src/components/views/messages/ViewSourceEvent.tsx similarity index 87% rename from src/components/views/messages/ViewSourceEvent.js rename to src/components/views/messages/ViewSourceEvent.tsx index 5a5c015dcf..488f8de5df 100644 --- a/src/components/views/messages/ViewSourceEvent.js +++ b/src/components/views/messages/ViewSourceEvent.tsx @@ -15,18 +15,21 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; +import { MatrixEvent } from 'matrix-js-sdk/src'; import classNames from 'classnames'; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; -@replaceableComponent("views.messages.ViewSourceEvent") -export default class ViewSourceEvent extends React.PureComponent { - static propTypes = { - /* the MatrixEvent to show */ - mxEvent: PropTypes.object.isRequired, - }; +interface IProps { + mxEvent: MatrixEvent; +} +interface IState { + expanded: boolean; +} + +@replaceableComponent("views.messages.ViewSourceEvent") +export default class ViewSourceEvent extends React.PureComponent { constructor(props) { super(props); @@ -35,7 +38,7 @@ export default class ViewSourceEvent extends React.PureComponent { }; } - componentDidMount() { + public componentDidMount(): void { const { mxEvent } = this.props; const client = MatrixClientPeg.get(); @@ -46,15 +49,15 @@ export default class ViewSourceEvent extends React.PureComponent { } } - onToggle = (ev) => { + private onToggle = (ev: React.MouseEvent) => { ev.preventDefault(); const { expanded } = this.state; this.setState({ expanded: !expanded, }); - } + }; - render() { + public render(): React.ReactNode { const { mxEvent } = this.props; const { expanded } = this.state; diff --git a/src/components/views/rooms/MessageComposer.tsx b/src/components/views/rooms/MessageComposer.tsx index 327626908d..add35f38f4 100644 --- a/src/components/views/rooms/MessageComposer.tsx +++ b/src/components/views/rooms/MessageComposer.tsx @@ -342,8 +342,11 @@ export default class MessageComposer extends React.Component { private onVoiceStoreUpdate = () => { const recording = VoiceRecordingStore.instance.activeRecording; - this.setState({ haveRecording: !!recording }); if (recording) { + // Delay saying we have a recording until it is started, as we might not yet have A/V permissions + recording.on(RecordingState.Started, () => { + this.setState({ haveRecording: !!VoiceRecordingStore.instance.activeRecording }); + }); // We show a little heads up that the recording is about to automatically end soon. The 3s // display time is completely arbitrary. Note that we don't need to deregister the listener // because the recording instance will clean that up for us. @@ -351,6 +354,8 @@ export default class MessageComposer extends React.Component { this.setState({ recordingTimeLeftSeconds: secondsLeft }); setTimeout(() => this.setState({ recordingTimeLeftSeconds: null }), 3000); }); + } else { + this.setState({ haveRecording: false }); } }; diff --git a/src/components/views/rooms/ReplyTile.tsx b/src/components/views/rooms/ReplyTile.tsx index 18b30d33d5..16a7141bd7 100644 --- a/src/components/views/rooms/ReplyTile.tsx +++ b/src/components/views/rooms/ReplyTile.tsx @@ -67,15 +67,21 @@ export default class ReplyTile extends React.PureComponent { }; private onClick = (e: React.MouseEvent): void => { - // This allows the permalink to be opened in a new tab/window or copied as - // matrix.to, but also for it to enable routing within Riot when clicked. - e.preventDefault(); - dis.dispatch({ - action: 'view_room', - event_id: this.props.mxEvent.getId(), - highlighted: true, - room_id: this.props.mxEvent.getRoomId(), - }); + const clickTarget = e.target as HTMLElement; + // Following a link within a reply should not dispatch the `view_room` action + // so that the browser can direct the user to the correct location + // The exception being the link wrapping the reply + if (clickTarget.tagName.toLowerCase() !== "a" || clickTarget.closest("a") === null) { + // This allows the permalink to be opened in a new tab/window or copied as + // matrix.to, but also for it to enable routing within Riot when clicked. + e.preventDefault(); + dis.dispatch({ + action: 'view_room', + event_id: this.props.mxEvent.getId(), + highlighted: true, + room_id: this.props.mxEvent.getRoomId(), + }); + } }; render() { diff --git a/src/components/views/rooms/RoomHeader.tsx b/src/components/views/rooms/RoomHeader.tsx index c79b5bddd5..15b25ed64b 100644 --- a/src/components/views/rooms/RoomHeader.tsx +++ b/src/components/views/rooms/RoomHeader.tsx @@ -29,6 +29,8 @@ import RoomTopic from "../elements/RoomTopic"; import RoomName from "../elements/RoomName"; import { PlaceCallType } from "../../../CallHandler"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import Modal from '../../../Modal'; +import InfoDialog from "../dialogs/InfoDialog"; import { throttle } from 'lodash'; import { MatrixEvent, Room, RoomState } from 'matrix-js-sdk/src'; import { E2EStatus } from '../../../utils/ShieldUtils'; @@ -87,6 +89,14 @@ export default class RoomHeader extends React.Component { this.forceUpdate(); }, 500, { leading: true, trailing: true }); + private displayInfoDialogAboutScreensharing() { + Modal.createDialog(InfoDialog, { + title: _t("Screen sharing is here!"), + description: _t("You can now share your screen by pressing the \"screen share\" " + + "button during a call. You can even do this in audio calls if both sides support it!"), + }); + } + public render() { let searchStatus = null; @@ -185,8 +195,8 @@ export default class RoomHeader extends React.Component { videoCallButton = this.props.onCallPlaced( - ev.shiftKey ? PlaceCallType.ScreenSharing : PlaceCallType.Video)} + onClick={(ev) => ev.shiftKey ? + this.displayInfoDialogAboutScreensharing() : this.props.onCallPlaced(PlaceCallType.Video)} title={_t("Video call")} />; } diff --git a/src/components/views/spaces/SpaceCreateMenu.tsx b/src/components/views/spaces/SpaceCreateMenu.tsx index 5f16684fb8..16c8ec6fb5 100644 --- a/src/components/views/spaces/SpaceCreateMenu.tsx +++ b/src/components/views/spaces/SpaceCreateMenu.tsx @@ -14,9 +14,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { useContext, useRef, useState } from "react"; +import React, { ComponentProps, RefObject, SyntheticEvent, useContext, useRef, useState } from "react"; import classNames from "classnames"; -import { EventType, RoomType, RoomCreateTypeField } from "matrix-js-sdk/src/@types/event"; +import { RoomType } from "matrix-js-sdk/src/@types/event"; import FocusLock from "react-focus-lock"; import { _t } from "../../../languageHandler"; @@ -24,7 +24,7 @@ import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import { ChevronFace, ContextMenu } from "../../structures/ContextMenu"; import createRoom from "../../../createRoom"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; -import { SpaceAvatar } from "./SpaceBasicSettings"; +import SpaceBasicSettings, { SpaceAvatar } from "./SpaceBasicSettings"; import AccessibleButton from "../elements/AccessibleButton"; import { BetaPill } from "../beta/BetaCard"; import defaultDispatcher from "../../../dispatcher/dispatcher"; @@ -33,8 +33,7 @@ import { UserTab } from "../dialogs/UserSettingsDialog"; import Field from "../elements/Field"; import withValidation from "../elements/Validation"; import { SpaceFeedbackPrompt } from "../../structures/SpaceRoomView"; -import { Preset } from "matrix-js-sdk/src/@types/partials"; -import { ICreateRoomStateEvent } from "matrix-js-sdk/src/@types/requests"; +import { HistoryVisibility, Preset } from "matrix-js-sdk/src/@types/partials"; import RoomAliasField from "../elements/RoomAliasField"; const SpaceCreateMenuType = ({ title, description, className, onClick }) => { @@ -66,8 +65,83 @@ const nameToAlias = (name: string, domain: string): string => { return `#${localpart}:${domain}`; }; -const SpaceCreateMenu = ({ onFinished }) => { +type BProps = Pick, "setAvatar" | "name" | "setName" | "topic" | "setTopic">; +interface ISpaceCreateFormProps extends BProps { + busy: boolean; + alias: string; + nameFieldRef: RefObject; + aliasFieldRef: RefObject; + showAliasField?: boolean; + onSubmit(e: SyntheticEvent): void; + setAlias(alias: string): void; +} + +export const SpaceCreateForm: React.FC = ({ + busy, + onSubmit, + setAvatar, + name, + setName, + nameFieldRef, + alias, + aliasFieldRef, + setAlias, + showAliasField, + topic, + setTopic, + children, +}) => { const cli = useContext(MatrixClientContext); + const domain = cli.getDomain(); + + return + + + { + const newName = ev.target.value; + if (!alias || alias === nameToAlias(name, domain)) { + setAlias(nameToAlias(newName, domain)); + } + setName(newName); + }} + ref={nameFieldRef} + onValidate={spaceNameValidator} + disabled={busy} + /> + + { showAliasField + ? + : null + } + + setTopic(ev.target.value)} + rows={3} + disabled={busy} + /> + + { children } + ; +}; + +const SpaceCreateMenu = ({ onFinished }) => { const [visibility, setVisibility] = useState(null); const [busy, setBusy] = useState(false); @@ -98,42 +172,26 @@ const SpaceCreateMenu = ({ onFinished }) => { return; } - const initialState: ICreateRoomStateEvent[] = [ - { - type: EventType.RoomHistoryVisibility, - content: { - "history_visibility": visibility === Visibility.Public ? "world_readable" : "invited", - }, - }, - ]; - if (avatar) { - const url = await cli.uploadContent(avatar); - - initialState.push({ - type: EventType.RoomAvatar, - content: { url }, - }); - } - try { await createRoom({ createOpts: { preset: visibility === Visibility.Public ? Preset.PublicChat : Preset.PrivateChat, name, - creation_content: { - [RoomCreateTypeField]: RoomType.Space, - }, - initial_state: initialState, power_level_content_override: { // Only allow Admins to write to the timeline to prevent hidden sync spam events_default: 100, - ...Visibility.Public ? { invite: 0 } : {}, + ...visibility === Visibility.Public ? { invite: 0 } : {}, }, room_alias_name: visibility === Visibility.Public && alias ? alias.substr(1, alias.indexOf(":") - 1) : undefined, topic, }, + avatar, + roomType: RoomType.Space, + historyVisibility: visibility === Visibility.Public + ? HistoryVisibility.WorldReadable + : HistoryVisibility.Invited, spinner: false, encryption: false, andView: true, @@ -171,7 +229,6 @@ const SpaceCreateMenu = ({ onFinished }) => {
; } else { - const domain = cli.getDomain(); body = { }

-
- - - { - const newName = ev.target.value; - if (!alias || alias === nameToAlias(name, domain)) { - setAlias(nameToAlias(newName, domain)); - } - setName(newName); - }} - ref={spaceNameField} - onValidate={spaceNameValidator} - disabled={busy} - /> - - { visibility === Visibility.Public - ? - : null - } - - setTopic(ev.target.value)} - rows={3} - disabled={busy} - /> - + { busy ? _t("Creating...") : _t("Create") } diff --git a/src/components/views/spaces/SpaceTreeLevel.tsx b/src/components/views/spaces/SpaceTreeLevel.tsx index f33f3a3987..827fc6bde1 100644 --- a/src/components/views/spaces/SpaceTreeLevel.tsx +++ b/src/components/views/spaces/SpaceTreeLevel.tsx @@ -35,6 +35,7 @@ import { shouldShowSpaceSettings, showAddExistingRooms, showCreateNewRoom, + showCreateNewSubspace, showSpaceInvite, showSpaceSettings, } from "../../../utils/space"; @@ -49,6 +50,7 @@ import { EventType } from "matrix-js-sdk/src/@types/event"; import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState"; import { NotificationColor } from "../../../stores/notifications/NotificationColor"; import { getKeyBindingsManager, RoomListAction } from "../../../KeyBindingsManager"; +import { BetaPill } from "../beta/BetaCard"; interface IItemProps extends InputHTMLAttributes { space?: Room; @@ -232,6 +234,14 @@ export class SpaceItem extends React.PureComponent { this.setState({ contextMenuPosition: null }); // also close the menu }; + private onNewSubspaceClick = (ev: ButtonEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + showCreateNewSubspace(this.props.space); + this.setState({ contextMenuPosition: null }); // also close the menu + }; + private onMembersClick = (ev: ButtonEvent) => { ev.preventDefault(); ev.stopPropagation(); @@ -316,6 +326,13 @@ export class SpaceItem extends React.PureComponent { label={_t("Add existing room")} onClick={this.onAddExistingRoomClick} /> + + + ; } diff --git a/src/components/views/voip/AudioFeed.tsx b/src/components/views/voip/AudioFeed.tsx index a2ab760c86..3049d80c72 100644 --- a/src/components/views/voip/AudioFeed.tsx +++ b/src/components/views/voip/AudioFeed.tsx @@ -23,9 +23,21 @@ interface IProps { feed: CallFeed; } -export default class AudioFeed extends React.Component { +interface IState { + audioMuted: boolean; +} + +export default class AudioFeed extends React.Component { private element = createRef(); + constructor(props: IProps) { + super(props); + + this.state = { + audioMuted: this.props.feed.isAudioMuted(), + }; + } + componentDidMount() { MediaDeviceHandler.instance.addListener( MediaDeviceHandlerEvent.AudioOutputChanged, @@ -62,6 +74,7 @@ export default class AudioFeed extends React.Component { private playMedia() { const element = this.element.current; + if (!element) return; this.onAudioOutputChanged(MediaDeviceHandler.getAudioOutput()); element.muted = false; element.srcObject = this.props.feed.stream; @@ -85,6 +98,7 @@ export default class AudioFeed extends React.Component { private stopMedia() { const element = this.element.current; + if (!element) return; element.pause(); element.src = null; @@ -96,10 +110,16 @@ export default class AudioFeed extends React.Component { } private onNewStream = () => { + this.setState({ + audioMuted: this.props.feed.isAudioMuted(), + }); this.playMedia(); }; render() { + // Do not render the audio element if there is no audio track + if (this.state.audioMuted) return null; + return (