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 95040ce855..7eb1208d50 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -269,6 +269,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/_SpaceRoomDirectory.scss b/res/css/structures/_SpaceRoomDirectory.scss index 20a19ea2bb..cb91aa3c7d 100644 --- a/res/css/structures/_SpaceRoomDirectory.scss +++ b/res/css/structures/_SpaceRoomDirectory.scss @@ -191,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/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/_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/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/_CallEvent.scss b/res/css/views/messages/_CallEvent.scss index 54c7df3e0b..f2ceb086c4 100644 --- a/res/css/views/messages/_CallEvent.scss +++ b/res/css/views/messages/_CallEvent.scss @@ -43,6 +43,14 @@ limitations under the License. } } + &.mx_CallEvent_voice.mx_CallEvent_missed .mx_CallEvent_type_icon::before { + mask-image: url('$(res)/img/voip/missed-voice.svg'); + } + + &.mx_CallEvent_video.mx_CallEvent_missed .mx_CallEvent_type_icon::before { + mask-image: url('$(res)/img/voip/missed-video.svg'); + } + .mx_CallEvent_info { display: flex; flex-direction: row; diff --git a/res/css/views/rooms/_EventBubbleTile.scss b/res/css/views/rooms/_EventBubbleTile.scss index 8629682693..1d43c3030d 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; @@ -155,12 +155,20 @@ limitations under the License. .mx_EventTile_avatar { 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; @@ -216,90 +224,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; - padding: 5px 0; - - .mx_EventTile_avatar { - position: static; - order: -1; - margin-right: 5px; - } - } - - & ~ .mx_EventListSummary { - --maxWidth: 80%; - margin-left: calc(var(--avatarSize) + var(--gutterSize)); - margin-right: calc(var(--gutterSize) + var(--avatarSize)); - .mx_EventListSummary_toggle { - float: none; - margin: 0; - order: 9; - margin-left: 5px; - } - .mx_EventListSummary_avatars { - padding-top: 0; - } - - &::after { - content: ""; - clear: both; - } - - .mx_EventTile { - margin: 0 6px; - } - - .mx_EventTile_line { - margin: 0 5px; - > a { - left: auto; - right: 0; - transform: translateX(calc(100% + 5px)); - } - } - - .mx_MessageActionBar { - transform: translate3d(90%, 0, 0); - } - } - - & ~ .mx_EventListSummary[data-expanded=false] { - padding: 0 34px; - } - - /* events that do not require bubble layout */ - & ~ .mx_EventListSummary, - &.mx_EventTile_bad { - .mx_EventTile_line { - background: transparent; - } - - &:hover { - &::before { - background: transparent; - } - } - } - - & + .mx_EventListSummary { - .mx_EventTile { - margin-top: 0; - padding: 2px 0; - } - } - - .mx_EventListSummary_toggle { - margin-right: 55px; - } - /* Special layout scenario for "Unable To Decrypt (UTD)" events */ &.mx_EventTile_bad > .mx_EventTile_line { display: grid; @@ -334,3 +258,78 @@ 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: center; + padding: 5px 0; + + .mx_EventTile_avatar { + position: static; + order: -1; + margin-right: 5px; + } +} + +.mx_EventListSummary[data-layout=bubble] { + --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; + margin-right: 55px; + } + .mx_EventListSummary_avatars { + padding-top: 0; + } + + &::after { + content: ""; + clear: both; + } + + .mx_EventTile { + margin: 0 6px; + padding: 2px 0; + } + + .mx_EventTile_line { + margin: 0 5px; + > a { + left: auto; + right: 0; + transform: translateX(calc(100% + 5px)); + } + } + + .mx_MessageActionBar { + transform: translate3d(90%, 0, 0); + } +} + +.mx_EventListSummary[data-expanded=false][data-layout=bubble] { + padding: 0 34px; +} + +/* events that do not require bubble layout */ +.mx_EventListSummary[data-layout=bubble], +.mx_EventTile.mx_EventTile_bad[data-layout=bubble] { + .mx_EventTile_line { + background: transparent; + } + + &:hover { + &::before { + background: transparent; + } + } +} diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index bda4a15f45..206fe843de 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -132,15 +132,6 @@ $hover-select-border: 4px; } } - &.mx_EventTile_info .mx_EventTile_line, - & ~ .mx_EventListSummary .mx_EventTile_avatar ~ .mx_EventTile_line { - padding-left: calc($left-gutter + 18px); - } - - & ~ .mx_EventListSummary .mx_EventTile_line { - padding-left: calc($left-gutter); - } - &.mx_EventTile_selected.mx_EventTile_info .mx_EventTile_line { padding-left: calc($left-gutter + 18px - $hover-select-border); } @@ -276,10 +267,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 @@ -573,6 +573,12 @@ $hover-select-border: 4px; color: $accent-color-alt; } +.mx_EventTile_content .markdown-body blockquote { + border-left: 2px solid $blockquote-bar-color; + border-radius: 2px; + padding: 0 10px; +} + .mx_EventTile_content .markdown-body .hljs { display: inline !important; } diff --git a/res/css/views/rooms/_IRCLayout.scss b/res/css/views/rooms/_IRCLayout.scss index 97190807ca..578c0325d2 100644 --- a/res/css/views/rooms/_IRCLayout.scss +++ b/res/css/views/rooms/_IRCLayout.scss @@ -116,6 +116,11 @@ $irc-line-height: $font-18px; .mx_EditMessageComposer_buttons { position: relative; } + + .mx_ReactionsRow { + padding-left: 0; + padding-right: 0; + } } .mx_EventTile_emote { diff --git a/res/css/views/rooms/_MessageComposer.scss b/res/css/views/rooms/_MessageComposer.scss index e6c0cc3f46..5e2eff4047 100644 --- a/res/css/views/rooms/_MessageComposer.scss +++ b/res/css/views/rooms/_MessageComposer.scss @@ -165,8 +165,6 @@ limitations under the License. font-size: $font-14px; max-height: 120px; overflow: auto; - /* needed for FF */ - font-family: $font-family; } /* hack for FF as vertical alignment of custom placeholder text is broken */ diff --git a/res/css/views/rooms/_ReplyTile.scss b/res/css/views/rooms/_ReplyTile.scss index f3e204e415..fd21e5f348 100644 --- a/res/css/views/rooms/_ReplyTile.scss +++ b/res/css/views/rooms/_ReplyTile.scss @@ -60,8 +60,6 @@ limitations under the License. $reply-lines: 2; $line-height: $font-22px; - pointer-events: none; - text-overflow: ellipsis; display: -webkit-box; -webkit-box-orient: vertical; diff --git a/res/css/views/settings/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/missed-video.svg b/res/img/voip/missed-video.svg new file mode 100644 index 0000000000..a2f3bc73ac --- /dev/null +++ b/res/img/voip/missed-video.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/voip/missed-voice.svg b/res/img/voip/missed-voice.svg new file mode 100644 index 0000000000..5e3993584e --- /dev/null +++ b/res/img/voip/missed-voice.svg @@ -0,0 +1,4 @@ + + + + diff --git a/res/img/voip/screensharing-off.svg b/res/img/voip/screensharing-off.svg new file mode 100644 index 0000000000..dc19e9892e --- /dev/null +++ b/res/img/voip/screensharing-off.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/res/img/voip/screensharing-on.svg b/res/img/voip/screensharing-on.svg new file mode 100644 index 0000000000..a8e7fe308e --- /dev/null +++ b/res/img/voip/screensharing-on.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/res/img/voip/sidebar-off.svg b/res/img/voip/sidebar-off.svg new file mode 100644 index 0000000000..7637a9ab55 --- /dev/null +++ b/res/img/voip/sidebar-off.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/res/img/voip/sidebar-on.svg b/res/img/voip/sidebar-on.svg new file mode 100644 index 0000000000..a625334be4 --- /dev/null +++ b/res/img/voip/sidebar-on.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index e7ba1aa9fb..e7c1dda54f 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -56,7 +56,6 @@ limitations under the License. import React from 'react'; import { MatrixClientPeg } from './MatrixClientPeg'; -import PlatformPeg from './PlatformPeg'; import Modal from './Modal'; import { _t } from './languageHandler'; import dis from './dispatcher/dispatcher'; @@ -80,7 +79,6 @@ import CountlyAnalytics from "./CountlyAnalytics"; import { UIFeature } from "./settings/UIFeature"; import { CallError } from "matrix-js-sdk/src/webrtc/call"; import { logger } from 'matrix-js-sdk/src/logger'; -import DesktopCapturerSourcePicker from "./components/views/elements/DesktopCapturerSourcePicker"; import { Action } from './dispatcher/actions'; import VoipUserMapper from './VoipUserMapper'; import { addManagedHybridWidget, isManagedHybridWidgetEnabled } from './widgets/ManagedHybrid'; @@ -129,14 +127,9 @@ interface ThirdpartyLookupResponse { fields: ThirdpartyLookupResponseFields; } -// Unlike 'CallType' in js-sdk, this one includes screen sharing -// (because a screen sharing call is only a screen sharing call to the caller, -// to the callee it's just a video call, at least as far as the current impl -// is concerned). export enum PlaceCallType { Voice = 'voice', Video = 'video', - ScreenSharing = 'screensharing', } export enum CallHandlerEvent { @@ -491,28 +484,18 @@ export default class CallHandler extends EventEmitter { break; case CallState.Ended: { - Analytics.trackEvent('voip', 'callEnded', 'hangupReason', call.hangupReason); + const hangupReason = call.hangupReason; + Analytics.trackEvent('voip', 'callEnded', 'hangupReason', hangupReason); this.removeCallForRoom(mappedRoomId); - if (oldState === CallState.InviteSent && ( - call.hangupParty === CallParty.Remote || - (call.hangupParty === CallParty.Local && call.hangupReason === CallErrorCode.InviteTimeout) - )) { + if (oldState === CallState.InviteSent && call.hangupParty === CallParty.Remote) { this.play(AudioID.Busy); let title; let description; - if (call.hangupReason === CallErrorCode.UserHangup) { - title = _t("Call Declined"); - description = _t("The other party declined the call."); - } else if (call.hangupReason === CallErrorCode.UserBusy) { + // TODO: We should either do away with these or figure out a copy for each code (expect user_hangup...) + if (call.hangupReason === CallErrorCode.UserBusy) { title = _t("User Busy"); description = _t("The user you called is busy."); - } else if (call.hangupReason === CallErrorCode.InviteTimeout) { - title = _t("Call Failed"); - // XXX: full stop appended as some relic here, but these - // strings need proper input from design anyway, so let's - // not change this string until we have a proper one. - description = _t('The remote side failed to pick up') + '.'; - } else { + } else if (hangupReason && ![CallErrorCode.UserHangup, "user hangup"].includes(hangupReason)) { title = _t("Call Failed"); description = _t("The call could not be established"); } @@ -521,7 +504,7 @@ export default class CallHandler extends EventEmitter { title, description, }); } else if ( - call.hangupReason === CallErrorCode.AnsweredElsewhere && oldState === CallState.Connecting + hangupReason === CallErrorCode.AnsweredElsewhere && oldState === CallState.Connecting ) { Modal.createTrackedDialog('Call Handler', 'Call Failed', ErrorDialog, { title: _t("Answered Elsewhere"), @@ -738,25 +721,6 @@ export default class CallHandler extends EventEmitter { call.placeVoiceCall(); } else if (type === 'video') { call.placeVideoCall(); - } else if (type === PlaceCallType.ScreenSharing) { - const screenCapErrorString = PlatformPeg.get().screenCaptureErrorString(); - if (screenCapErrorString) { - this.removeCallForRoom(roomId); - console.log("Can't capture screen: " + screenCapErrorString); - Modal.createTrackedDialog('Call Handler', 'Unable to capture screen', ErrorDialog, { - title: _t('Unable to capture screen'), - description: screenCapErrorString, - }); - return; - } - - call.placeScreenSharingCall( - async (): Promise => { - const { finished } = Modal.createDialog(DesktopCapturerSourcePicker); - const [source] = await finished; - return source; - }, - ); } else { console.error("Unknown conf call type: " + type); } diff --git a/src/components/structures/CallEventGrouper.ts b/src/components/structures/CallEventGrouper.ts index 384f20cd4e..ce3b530858 100644 --- a/src/components/structures/CallEventGrouper.ts +++ b/src/components/structures/CallEventGrouper.ts @@ -74,6 +74,14 @@ export default class CallEventGrouper extends EventEmitter { return this.hangup?.getContent()?.reason; } + public get rejectParty(): string { + return this.reject?.getSender(); + } + + public get gotRejected(): boolean { + return Boolean(this.reject); + } + /** * Returns true if there are only events from the other side - we missed the call */ diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx index 39ede68a75..698a36127b 100644 --- a/src/components/structures/MessagePanel.tsx +++ b/src/components/structures/MessagePanel.tsx @@ -618,7 +618,15 @@ export default class MessagePanel extends React.Component { for (const Grouper of groupers) { if (Grouper.canStartGroup(this, mxEv)) { - grouper = new Grouper(this, mxEv, prevEvent, lastShownEvent, nextEvent, nextTile); + grouper = new Grouper( + this, + mxEv, + prevEvent, + lastShownEvent, + this.props.layout, + nextEvent, + nextTile, + ); } } if (!grouper) { @@ -981,6 +989,7 @@ abstract class BaseGrouper { public readonly event: MatrixEvent, public readonly prevEvent: MatrixEvent, public readonly lastShownEvent: MatrixEvent, + protected readonly layout: Layout, public readonly nextEvent?: MatrixEvent, public readonly nextEventTile?: MatrixEvent, ) { @@ -1107,6 +1116,7 @@ class CreationGrouper extends BaseGrouper { onToggle={panel.onHeightChanged} // Update scroll state summaryMembers={[ev.sender]} summaryText={summaryText} + layout={this.layout} > { eventTiles } , @@ -1134,10 +1144,11 @@ class RedactionGrouper extends BaseGrouper { ev: MatrixEvent, prevEvent: MatrixEvent, lastShownEvent: MatrixEvent, + layout: Layout, nextEvent: MatrixEvent, nextEventTile: MatrixEvent, ) { - super(panel, ev, prevEvent, lastShownEvent, nextEvent, nextEventTile); + super(panel, ev, prevEvent, lastShownEvent, layout, nextEvent, nextEventTile); this.events = [ev]; } @@ -1202,6 +1213,7 @@ class RedactionGrouper extends BaseGrouper { onToggle={panel.onHeightChanged} // Update scroll state summaryMembers={Array.from(senders)} summaryText={_t("%(count)s messages deleted.", { count: eventTiles.length })} + layout={this.layout} > { eventTiles } , @@ -1230,8 +1242,9 @@ class MemberGrouper extends BaseGrouper { public readonly event: MatrixEvent, public readonly prevEvent: MatrixEvent, public readonly lastShownEvent: MatrixEvent, + protected readonly layout: Layout, ) { - super(panel, event, prevEvent, lastShownEvent); + super(panel, event, prevEvent, lastShownEvent, layout); this.events = [event]; } @@ -1306,6 +1319,7 @@ class MemberGrouper extends BaseGrouper { events={this.events} onToggle={panel.onHeightChanged} // Update scroll state startExpanded={highlightInMels} + layout={this.layout} > { eventTiles } , diff --git a/src/components/structures/ScrollPanel.tsx b/src/components/structures/ScrollPanel.tsx index 1d16755106..112f8d2c21 100644 --- a/src/components/structures/ScrollPanel.tsx +++ b/src/components/structures/ScrollPanel.tsx @@ -183,8 +183,14 @@ export default class ScrollPanel extends React.Component { private readonly itemlist = createRef(); private unmounted = false; private scrollTimeout: Timer; + // Are we currently trying to backfill? private isFilling: boolean; + // Is the current fill request caused by a props update? + private isFillingDueToPropsUpdate = false; + // Did another request to check the fill state arrive while we were trying to backfill? private fillRequestWhileRunning: boolean; + // Is that next fill request scheduled because of a props update? + private pendingFillDueToPropsUpdate: boolean; private scrollState: IScrollState; private preventShrinkingState: IPreventShrinkingState; private unfillDebouncer: number; @@ -213,7 +219,7 @@ export default class ScrollPanel extends React.Component { // adding events to the top). // // This will also re-check the fill state, in case the paginate was inadequate - this.checkScroll(); + this.checkScroll(true); this.updatePreventShrinking(); } @@ -251,12 +257,12 @@ export default class ScrollPanel extends React.Component { // after an update to the contents of the panel, check that the scroll is // where it ought to be, and set off pagination requests if necessary. - public checkScroll = () => { + public checkScroll = (isFromPropsUpdate = false) => { if (this.unmounted) { return; } this.restoreSavedScrollState(); - this.checkFillState(); + this.checkFillState(0, isFromPropsUpdate); }; // return true if the content is fully scrolled down right now; else false. @@ -319,7 +325,7 @@ export default class ScrollPanel extends React.Component { } // check the scroll state and send out backfill requests if necessary. - public checkFillState = async (depth = 0): Promise => { + public checkFillState = async (depth = 0, isFromPropsUpdate = false): Promise => { if (this.unmounted) { return; } @@ -355,14 +361,20 @@ export default class ScrollPanel extends React.Component { // don't allow more than 1 chain of calls concurrently // do make a note when a new request comes in while already running one, // so we can trigger a new chain of calls once done. + // However, we make an exception for when we're already filling due to a + // props (or children) update, because very often the children include + // spinners to say whether we're paginating or not, so this would cause + // infinite paginating. if (isFirstCall) { - if (this.isFilling) { + if (this.isFilling && !this.isFillingDueToPropsUpdate) { debuglog("isFilling: not entering while request is ongoing, marking for a subsequent request"); this.fillRequestWhileRunning = true; + this.pendingFillDueToPropsUpdate = isFromPropsUpdate; return; } debuglog("isFilling: setting"); this.isFilling = true; + this.isFillingDueToPropsUpdate = isFromPropsUpdate; } const itemlist = this.itemlist.current; @@ -393,11 +405,14 @@ export default class ScrollPanel extends React.Component { if (isFirstCall) { debuglog("isFilling: clearing"); this.isFilling = false; + this.isFillingDueToPropsUpdate = false; } if (this.fillRequestWhileRunning) { + const refillDueToPropsUpdate = this.pendingFillDueToPropsUpdate; this.fillRequestWhileRunning = false; - this.checkFillState(); + this.pendingFillDueToPropsUpdate = false; + this.checkFillState(0, refillDueToPropsUpdate); } }; diff --git a/src/components/views/dialogs/CreateRoomDialog.tsx b/src/components/views/dialogs/CreateRoomDialog.tsx index a06f508908..572212a96c 100644 --- a/src/components/views/dialogs/CreateRoomDialog.tsx +++ b/src/components/views/dialogs/CreateRoomDialog.tsx @@ -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( 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/EventListSummary.tsx b/src/components/views/elements/EventListSummary.tsx index b1cc9c773d..d66319ca73 100644 --- a/src/components/views/elements/EventListSummary.tsx +++ b/src/components/views/elements/EventListSummary.tsx @@ -22,6 +22,7 @@ import MemberAvatar from '../avatars/MemberAvatar'; import { _t } from '../../../languageHandler'; import { useStateToggle } from "../../../hooks/useStateToggle"; import AccessibleButton from "./AccessibleButton"; +import { Layout } from '../../../settings/Layout'; interface IProps { // An array of member events to summarise @@ -38,6 +39,8 @@ interface IProps { children: ReactNode[]; // Called when the event list expansion is toggled onToggle?(): void; + // The layout currently used + layout?: Layout; } const EventListSummary: React.FC = ({ @@ -48,6 +51,7 @@ const EventListSummary: React.FC = ({ startExpanded, summaryMembers = [], summaryText, + layout, }) => { const [expanded, toggleExpanded] = useStateToggle(startExpanded); @@ -63,7 +67,7 @@ const EventListSummary: React.FC = ({ // If we are only given few events then just pass them through if (events.length < threshold) { return ( -
  • +
  • { children }
  • ); @@ -92,7 +96,7 @@ const EventListSummary: React.FC = ({ } return ( -
  • +
  • { expanded ? _t('collapse') : _t('expand') } @@ -103,6 +107,7 @@ const EventListSummary: React.FC = ({ EventListSummary.defaultProps = { startExpanded: false, + layout: Layout.Group, }; export default EventListSummary; diff --git a/src/components/views/elements/MemberEventListSummary.tsx b/src/components/views/elements/MemberEventListSummary.tsx index d52462f629..604e6c63b0 100644 --- a/src/components/views/elements/MemberEventListSummary.tsx +++ b/src/components/views/elements/MemberEventListSummary.tsx @@ -25,12 +25,15 @@ import { formatCommaSeparatedList } from '../../../utils/FormattingUtils'; import { isValid3pidInvite } from "../../../RoomInvite"; import EventListSummary from "./EventListSummary"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { Layout } from '../../../settings/Layout'; interface IProps extends Omit, "summaryText" | "summaryMembers"> { // The maximum number of names to show in either each summary e.g. 2 would result "A, B and 234 others left" summaryLength?: number; // The maximum number of avatars to display in the summary avatarsMaxLength?: number; + // The currently selected layout + layout: Layout; } interface IUserEvents { @@ -67,6 +70,7 @@ export default class MemberEventListSummary extends React.Component { summaryLength: 1, threshold: 3, avatarsMaxLength: 5, + layout: Layout.Group, }; shouldComponentUpdate(nextProps) { @@ -453,6 +457,7 @@ export default class MemberEventListSummary extends React.Component { startExpanded={this.props.startExpanded} children={this.props.children} summaryMembers={[...latestUserAvatarMember.values()]} + layout={this.props.layout} summaryText={this.generateSummary(aggregate.names, orderedTransitionSequences)} />; } } diff --git a/src/components/views/messages/CallEvent.tsx b/src/components/views/messages/CallEvent.tsx index c81055bfb7..2de66f897a 100644 --- a/src/components/views/messages/CallEvent.tsx +++ b/src/components/views/messages/CallEvent.tsx @@ -25,6 +25,7 @@ import { CallErrorCode, CallState } from 'matrix-js-sdk/src/webrtc/call'; import InfoTooltip, { InfoTooltipKind } from '../elements/InfoTooltip'; import classNames from 'classnames'; import AccessibleTooltipButton from '../elements/AccessibleTooltipButton'; +import { MatrixClientPeg } from '../../../MatrixClientPeg'; interface IProps { mxEvent: MatrixEvent; @@ -69,6 +70,18 @@ export default class CallEvent extends React.Component { this.setState({ callState: newState }); }; + private renderCallBackButton(text: string): JSX.Element { + return ( + + { text } + + ); + } + private renderContent(state: CallState | CustomCallState): JSX.Element { if (state === CallState.Ringing) { const silenceClass = classNames({ @@ -103,8 +116,18 @@ export default class CallEvent extends React.Component { } if (state === CallState.Ended) { const hangupReason = this.props.callEventGrouper.hangupReason; + const gotRejected = this.props.callEventGrouper.gotRejected; + const rejectParty = this.props.callEventGrouper.rejectParty; - if ([CallErrorCode.UserHangup, "user hangup"].includes(hangupReason) || !hangupReason) { + if (gotRejected) { + const weDeclinedCall = MatrixClientPeg.get().getUserId() === rejectParty; + return ( +
    + { weDeclinedCall ? _t("You declined this call") : _t("They declined this call") } + { this.renderCallBackButton(weDeclinedCall ? _t("Call back") : _t("Call again")) } +
    + ); + } else if (([CallErrorCode.UserHangup, "user hangup"].includes(hangupReason) || !hangupReason)) { // 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 :( @@ -116,6 +139,13 @@ export default class CallEvent extends React.Component { { _t("This call has ended") } ); + } else if (hangupReason === CallErrorCode.InviteTimeout) { + return ( +
    + { _t("They didn't pick up") } + { this.renderCallBackButton(_t("Call again")) } +
    + ); } let reason; @@ -133,8 +163,6 @@ export default class CallEvent extends React.Component { // (as opposed to an error code they gave but we don't know about, // in which case we show the error code) reason = _t("An unknown error occurred"); - } else if (hangupReason === CallErrorCode.InviteTimeout) { - reason = _t("No answer"); } else if (hangupReason === CallErrorCode.UserBusy) { reason = _t("The user you called is busy."); } else { @@ -163,13 +191,7 @@ export default class CallEvent extends React.Component { return (
    { _t("You missed this call") } - - { _t("Call back") } - + { this.renderCallBackButton(_t("Call back")) }
    ); } @@ -186,11 +208,17 @@ export default class CallEvent extends React.Component { const sender = event.sender ? event.sender.name : event.getSender(); const isVoice = this.props.callEventGrouper.isVoice; const callType = isVoice ? _t("Voice call") : _t("Video call"); - const content = this.renderContent(this.state.callState); + const callState = this.state.callState; + const hangupReason = this.props.callEventGrouper.hangupReason; + const content = this.renderContent(callState); const className = classNames({ mx_CallEvent: true, mx_CallEvent_voice: isVoice, mx_CallEvent_video: !isVoice, + mx_CallEvent_missed: ( + callState === CustomCallState.Missed || + (callState === CallState.Ended && hangupReason === CallErrorCode.InviteTimeout) + ), }); return ( diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 87876e685f..884d004551 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -896,6 +896,7 @@ export default class EventTile extends React.Component { mx_EventTile_unknown: !isBubbleMessage && this.state.verified === E2E_STATE.UNKNOWN, mx_EventTile_bad: isEncryptionFailure, mx_EventTile_emote: msgtype === 'm.emote', + mx_EventTile_noSender: this.props.hideSender, }); // If the tile is in the Sending state, don't speak the message. @@ -1165,8 +1166,9 @@ export default class EventTile extends React.Component { /> { keyRequestInfo } { actionBar } + { this.props.layout === Layout.IRC && (reactionsRow) } - { reactionsRow } + { this.props.layout !== Layout.IRC && (reactionsRow) } { msgOption } ) ); 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/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 (