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 f9e3ab1160..20b2461960 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/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/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss
index 808af30329..206fe843de 100644
--- a/res/css/views/rooms/_EventTile.scss
+++ b/res/css/views/rooms/_EventTile.scss
@@ -267,7 +267,7 @@ $hover-select-border: 4px;
.mx_ReactionsRow {
margin: 0;
- padding: 6px 60px;
+ padding: 4px 64px;
}
}
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..59298ef8e6 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 {
@@ -290,6 +309,7 @@ limitations under the License.
width: 100%;
opacity: 1;
transition: opacity 0.5s;
+ z-index: 200; // To be above _all_ feeds
}
.mx_CallView_callControls_hidden {
@@ -297,10 +317,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 +356,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 +385,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 +416,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/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/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 (
);
diff --git a/src/components/views/voip/CallView.tsx b/src/components/views/voip/CallView.tsx
index e53c2f4823..a98c42526e 100644
--- a/src/components/views/voip/CallView.tsx
+++ b/src/components/views/voip/CallView.tsx
@@ -1,6 +1,7 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
+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.
@@ -32,6 +33,10 @@ import { avatarUrlForMember } from '../../../Avatar';
import DialpadContextMenu from '../context_menus/DialpadContextMenu';
import { CallFeed } from 'matrix-js-sdk/src/webrtc/callFeed';
import { replaceableComponent } from "../../../utils/replaceableComponent";
+import DesktopCapturerSourcePicker from "../elements/DesktopCapturerSourcePicker";
+import Modal from '../../../Modal';
+import { SDPStreamMetadataPurpose } from 'matrix-js-sdk/src/webrtc/callEventTypes';
+import CallViewSidebar from './CallViewSidebar';
interface IProps {
// The call for us to display
@@ -59,11 +64,14 @@ interface IState {
isRemoteOnHold: boolean;
micMuted: boolean;
vidMuted: boolean;
+ screensharing: boolean;
callState: CallState;
controlsVisible: boolean;
showMoreMenu: boolean;
showDialpad: boolean;
- feeds: CallFeed[];
+ primaryFeed: CallFeed;
+ secondaryFeeds: Array;
+ sidebarShown: boolean;
}
function getFullScreenElement() {
@@ -110,16 +118,21 @@ export default class CallView extends React.Component {
constructor(props: IProps) {
super(props);
+ const { primary, secondary } = this.getOrderedFeeds(this.props.call.getFeeds());
+
this.state = {
isLocalOnHold: this.props.call.isLocalOnHold(),
isRemoteOnHold: this.props.call.isRemoteOnHold(),
micMuted: this.props.call.isMicrophoneMuted(),
vidMuted: this.props.call.isLocalVideoMuted(),
+ screensharing: this.props.call.isScreensharing(),
callState: this.props.call.state,
controlsVisible: true,
showMoreMenu: false,
showDialpad: false,
- feeds: this.props.call.getFeeds(),
+ primaryFeed: primary,
+ secondaryFeeds: secondary,
+ sidebarShown: true,
};
this.updateCallListeners(null, this.props.call);
@@ -194,7 +207,11 @@ export default class CallView extends React.Component {
};
private onFeedsChanged = (newFeeds: Array) => {
- this.setState({ feeds: newFeeds });
+ const { primary, secondary } = this.getOrderedFeeds(newFeeds);
+ this.setState({
+ primaryFeed: primary,
+ secondaryFeeds: secondary,
+ });
};
private onCallLocalHoldUnhold = () => {
@@ -237,7 +254,30 @@ export default class CallView extends React.Component {
this.showControls();
};
- private showControls() {
+ private getOrderedFeeds(feeds: Array): { primary: CallFeed, secondary: Array } {
+ let primary;
+
+ // Try to use a screensharing as primary, a remote one if possible
+ const screensharingFeeds = feeds.filter((feed) => feed.purpose === SDPStreamMetadataPurpose.Screenshare);
+ primary = screensharingFeeds.find((feed) => !feed.isLocal()) || screensharingFeeds[0];
+ // If we didn't find remote screen-sharing stream, try to find any remote stream
+ if (!primary) {
+ primary = feeds.find((feed) => !feed.isLocal());
+ }
+
+ const secondary = [...feeds];
+ // Remove the primary feed from the array
+ if (primary) secondary.splice(secondary.indexOf(primary), 1);
+ secondary.sort((a, b) => {
+ if (a.isLocal() && !b.isLocal()) return -1;
+ if (!a.isLocal() && b.isLocal()) return 1;
+ return 0;
+ });
+
+ return { primary, secondary };
+ }
+
+ private showControls(): void {
if (this.state.showMoreMenu || this.state.showDialpad) return;
if (!this.state.controlsVisible) {
@@ -251,7 +291,7 @@ export default class CallView extends React.Component {
this.controlsHideTimer = window.setTimeout(this.onControlsHideTimer, CONTROLS_HIDE_DELAY);
}
- private onDialpadClick = () => {
+ private onDialpadClick = (): void => {
if (!this.state.showDialpad) {
if (this.controlsHideTimer) {
clearTimeout(this.controlsHideTimer);
@@ -274,21 +314,37 @@ export default class CallView extends React.Component {
}
};
- private onMicMuteClick = () => {
+ private onMicMuteClick = (): void => {
const newVal = !this.state.micMuted;
this.props.call.setMicrophoneMuted(newVal);
this.setState({ micMuted: newVal });
};
- private onVidMuteClick = () => {
+ private onVidMuteClick = (): void => {
const newVal = !this.state.vidMuted;
this.props.call.setLocalVideoMuted(newVal);
this.setState({ vidMuted: newVal });
};
- private onMoreClick = () => {
+ private onScreenshareClick = async (): Promise => {
+ const isScreensharing = await this.props.call.setScreensharingEnabled(
+ !this.state.screensharing,
+ async (): Promise => {
+ const { finished } = Modal.createDialog(DesktopCapturerSourcePicker);
+ const [source] = await finished;
+ return source;
+ },
+ );
+
+ this.setState({
+ sidebarShown: true,
+ screensharing: isScreensharing,
+ });
+ };
+
+ private onMoreClick = (): void => {
if (this.controlsHideTimer) {
clearTimeout(this.controlsHideTimer);
this.controlsHideTimer = null;
@@ -300,14 +356,14 @@ export default class CallView extends React.Component {
});
};
- private closeDialpad = () => {
+ private closeDialpad = (): void => {
this.setState({
showDialpad: false,
});
this.controlsHideTimer = window.setTimeout(this.onControlsHideTimer, CONTROLS_HIDE_DELAY);
};
- private closeContextMenu = () => {
+ private closeContextMenu = (): void => {
this.setState({
showMoreMenu: false,
});
@@ -317,7 +373,7 @@ export default class CallView extends React.Component {
// we register global shortcuts here, they *must not conflict* with local shortcuts elsewhere or both will fire
// Note that this assumes we always have a CallView on screen at any given time
// CallHandler would probably be a better place for this
- private onNativeKeyDown = ev => {
+ private onNativeKeyDown = (ev): void => {
let handled = false;
const ctrlCmdOnly = isOnlyCtrlOrCmdKeyEvent(ev);
@@ -347,7 +403,7 @@ export default class CallView extends React.Component {
}
};
- private onRoomAvatarClick = () => {
+ private onRoomAvatarClick = (): void => {
const userFacingRoomId = CallHandler.sharedInstance().roomIdForCall(this.props.call);
dis.dispatch({
action: 'view_room',
@@ -355,7 +411,7 @@ export default class CallView extends React.Component {
});
};
- private onSecondaryRoomAvatarClick = () => {
+ private onSecondaryRoomAvatarClick = (): void => {
const userFacingRoomId = CallHandler.sharedInstance().roomIdForCall(this.props.secondaryCall);
dis.dispatch({
@@ -364,50 +420,30 @@ export default class CallView extends React.Component {
});
};
- private onCallResumeClick = () => {
+ private onCallResumeClick = (): void => {
const userFacingRoomId = CallHandler.sharedInstance().roomIdForCall(this.props.call);
CallHandler.sharedInstance().setActiveCallRoomId(userFacingRoomId);
};
- private onTransferClick = () => {
+ private onTransferClick = (): void => {
const transfereeCall = CallHandler.sharedInstance().getTransfereeForCallId(this.props.call.callId);
this.props.call.transferToCall(transfereeCall);
};
- public render() {
- const client = MatrixClientPeg.get();
- const callRoomId = CallHandler.sharedInstance().roomIdForCall(this.props.call);
- const secondaryCallRoomId = CallHandler.sharedInstance().roomIdForCall(this.props.secondaryCall);
- const callRoom = client.getRoom(callRoomId);
- const secCallRoom = this.props.secondaryCall ? client.getRoom(secondaryCallRoomId) : null;
+ private onHangupClick = (): void => {
+ dis.dispatch({
+ action: 'hangup',
+ room_id: CallHandler.sharedInstance().roomIdForCall(this.props.call),
+ });
+ };
- let dialPad;
- let contextMenu;
-
- if (this.state.showDialpad) {
- dialPad = ;
- }
-
- if (this.state.showMoreMenu) {
- contextMenu = ;
- }
+ private onToggleSidebar = (): void => {
+ this.setState({
+ sidebarShown: !this.state.sidebarShown,
+ });
+ };
+ private renderCallControls(): JSX.Element {
const micClasses = classNames({
mx_CallView_callControls_button: true,
mx_CallView_callControls_button_micOn: !this.state.micMuted,
@@ -420,6 +456,18 @@ export default class CallView extends React.Component {
mx_CallView_callControls_button_vidOff: this.state.vidMuted,
});
+ const screensharingClasses = classNames({
+ mx_CallView_callControls_button: true,
+ mx_CallView_callControls_button_screensharingOn: this.state.screensharing,
+ mx_CallView_callControls_button_screensharingOff: !this.state.screensharing,
+ });
+
+ const sidebarButtonClasses = classNames({
+ mx_CallView_callControls_button: true,
+ mx_CallView_callControls_button_sidebarOn: this.state.sidebarShown,
+ mx_CallView_callControls_button_sidebarOff: !this.state.sidebarShown,
+ });
+
// Put the other states of the mic/video icons in the document to make sure they're cached
// (otherwise the icon disappears briefly when toggled)
const micCacheClasses = classNames({
@@ -441,59 +489,116 @@ export default class CallView extends React.Component {
mx_CallView_callControls_hidden: !this.state.controlsVisible,
});
- const vidMuteButton = this.props.call.type === CallType.Video ? : null;
+ // We don't support call upgrades (yet) so hide the video mute button in voice calls
+ let vidMuteButton;
+ if (this.props.call.type === CallType.Video) {
+ vidMuteButton = (
+
+ );
+ }
+
+ // Screensharing is possible, if we can send a second stream and
+ // identify it using SDPStreamMetadata or if we can replace the already
+ // existing usermedia track by a screensharing track. We also need to be
+ // connected to know the state of the other side
+ let screensharingButton;
+ if (
+ (this.props.call.opponentSupportsSDPStreamMetadata() || this.props.call.type === CallType.Video) &&
+ this.props.call.state === CallState.Connected
+ ) {
+ screensharingButton = (
+
+ );
+ }
+
+ // To show the sidebar we need secondary feeds, if we don't have them,
+ // we can hide this button. If we are in PiP, sidebar is also hidden, so
+ // we can hide the button too
+ let sidebarButton;
+ if (
+ !this.props.pipMode &&
+ (
+ this.state.primaryFeed?.purpose === SDPStreamMetadataPurpose.Screenshare ||
+ this.props.call.isScreensharing()
+ )
+ ) {
+ sidebarButton = (
+
+ );
+ }
// The dial pad & 'more' button actions are only relevant in a connected call
// When not connected, we have to put something there to make the flexbox alignment correct
- const dialpadButton = this.state.callState === CallState.Connected ? :
;
+ let dialpadButton;
+ let contextMenuButton;
+ if (this.state.callState === CallState.Connected) {
+ contextMenuButton = (
+
+ );
+ dialpadButton = (
+
+ );
+ }
- const contextMenuButton = this.state.callState === CallState.Connected ? :
;
-
- // in the near future, the dial pad button will go on the left. For now, it's the nothing button
- // because something needs to have margin-right: auto to make the alignment correct.
- const callControls =
- { dialpadButton }
-
-
{
- dis.dispatch({
- action: 'hangup',
- room_id: callRoomId,
- });
- }}
- />
- { vidMuteButton }
-
-
- { contextMenuButton }
- ;
+ return (
+
+ { dialpadButton }
+
+ { vidMuteButton }
+
+
+ { screensharingButton }
+ { sidebarButton }
+ { contextMenuButton }
+
+
+ );
+ }
+ public render() {
+ const client = MatrixClientPeg.get();
+ const callRoomId = CallHandler.sharedInstance().roomIdForCall(this.props.call);
+ const secondaryCallRoomId = CallHandler.sharedInstance().roomIdForCall(this.props.secondaryCall);
+ const callRoom = client.getRoom(callRoomId);
+ const secCallRoom = this.props.secondaryCall ? client.getRoom(secondaryCallRoomId) : null;
const avatarSize = this.props.pipMode ? 76 : 160;
-
- // The 'content' for the call, ie. the videos for a video call and profile picture
- // for voice calls (fills the bg)
- let contentView: React.ReactNode;
-
const transfereeCall = CallHandler.sharedInstance().getTransfereeForCallId(this.props.call.callId);
const isOnHold = this.state.isLocalOnHold || this.state.isRemoteOnHold;
+ const isScreensharing = this.props.call.isScreensharing();
+ const sidebarShown = this.state.sidebarShown;
+ const someoneIsScreensharing = this.props.call.getFeeds().some((feed) => {
+ return feed.purpose === SDPStreamMetadataPurpose.Screenshare;
+ });
+ const isVideoCall = this.props.call.type === CallType.Video;
+
+ let contentView: React.ReactNode;
let holdTransferContent;
+
if (transfereeCall) {
const transferTargetRoom = MatrixClientPeg.get().getRoom(
CallHandler.sharedInstance().roomIdForCall(this.props.call),
@@ -539,9 +644,25 @@ export default class CallView extends React.Component {
;
}
+ let sidebar;
+ if (
+ !isOnHold &&
+ !transfereeCall &&
+ sidebarShown &&
+ (isVideoCall || someoneIsScreensharing)
+ ) {
+ sidebar = (
+
+ );
+ }
+
// This is a bit messy. I can't see a reason to have two onHold/transfer screens
if (isOnHold || transfereeCall) {
- if (this.props.call.type === CallType.Video) {
+ if (isVideoCall) {
const containerClasses = classNames({
mx_CallView_content: true,
mx_CallView_video: true,
@@ -560,7 +681,7 @@ export default class CallView extends React.Component {
{ onHoldBackground }
{ holdTransferContent }
- { callControls }
+ { this.renderCallControls() }
);
} else {
@@ -585,7 +706,7 @@ export default class CallView extends React.Component {
{ holdTransferContent }
- { callControls }
+ { this.renderCallControls() }
);
}
@@ -599,77 +720,91 @@ export default class CallView extends React.Component {
mx_CallView_voice: true,
});
- const feeds = this.props.call.getLocalFeeds().map((feed, i) => {
- // Here we check to hide local audio feeds to achieve the same UI/UX
- // as before. But once again this might be subject to change
- if (feed.isVideoMuted()) return;
- return (
-
- );
- });
-
// Saying "Connecting" here isn't really true, but the best thing
// I can come up with, but this might be subject to change as well
- contentView =
- { feeds }
-
-
-
+ contentView = (
+
+ { sidebar }
+
+
{ _t("Connecting") }
+ { this.renderCallControls() }
-
{ _t("Connecting") }
- { callControls }
-
;
+ );
} else {
const containerClasses = classNames({
mx_CallView_content: true,
mx_CallView_video: true,
});
- // TODO: Later the CallView should probably be reworked to support
- // any number of feeds but now we can always expect there to be two
- // feeds. This is because the js-sdk ignores any new incoming streams
- const feeds = this.state.feeds.map((feed, i) => {
- // Here we check to hide local audio feeds to achieve the same UI/UX
- // as before. But once again this might be subject to change
- if (feed.isVideoMuted() && feed.isLocal()) return;
- return (
+ let toast;
+ if (someoneIsScreensharing) {
+ const presentingClasses = classNames({
+ mx_CallView_presenting: true,
+ mx_CallView_presenting_hidden: !this.state.controlsVisible,
+ });
+ const sharerName = this.state.primaryFeed.getMember().name;
+ let text = isScreensharing
+ ? _t("You are presenting")
+ : _t('%(sharerName)s is presenting', { sharerName });
+ if (!this.state.sidebarShown && isVideoCall) {
+ text += " • " + (this.props.call.isLocalVideoMuted()
+ ? _t("Your camera is turned off")
+ : _t("Your camera is still enabled"));
+ }
+
+ toast = (
+
+ { text }
+
+ );
+ }
+
+ contentView = (
+
+ { toast }
+ { sidebar }
- );
- });
-
- contentView =
- { feeds }
- { callControls }
-
;
+ { this.renderCallControls() }
+
+ );
}
- const callTypeText = this.props.call.type === CallType.Video ? _t("Video Call") : _t("Voice Call");
+ const callTypeText = isVideoCall ? _t("Video Call") : _t("Voice Call");
let myClassName;
let fullScreenButton;
- if (this.props.call.type === CallType.Video && !this.props.pipMode) {
- fullScreenButton =
;
+ if (!this.props.pipMode) {
+ fullScreenButton = (
+
+ );
}
let expandButton;
@@ -728,6 +863,32 @@ export default class CallView extends React.Component
{
myClassName = 'mx_CallView_pip';
}
+ let dialPad;
+ if (this.state.showDialpad) {
+ dialPad = ;
+ }
+
+ let contextMenu;
+ if (this.state.showMoreMenu) {
+ contextMenu = ;
+ }
+
return
{ header }
{ contentView }
diff --git a/src/components/views/voip/CallViewSidebar.tsx b/src/components/views/voip/CallViewSidebar.tsx
new file mode 100644
index 0000000000..a0cb25b3df
--- /dev/null
+++ b/src/components/views/voip/CallViewSidebar.tsx
@@ -0,0 +1,53 @@
+/*
+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.
+*/
+
+import React from "react";
+import { MatrixCall } from "matrix-js-sdk/src/webrtc/call";
+import { CallFeed } from "matrix-js-sdk/src/webrtc/callFeed";
+import VideoFeed from "./VideoFeed";
+import classNames from "classnames";
+
+interface IProps {
+ feeds: Array;
+ call: MatrixCall;
+ pipMode: boolean;
+}
+
+export default class CallViewSidebar extends React.Component {
+ render() {
+ const feeds = this.props.feeds.map((feed) => {
+ return (
+
+ );
+ });
+
+ const className = classNames("mx_CallViewSidebar", {
+ mx_CallViewSidebar_pipMode: this.props.pipMode,
+ });
+
+ return (
+
+ { feeds }
+
+ );
+ }
+}
diff --git a/src/components/views/voip/VideoFeed.tsx b/src/components/views/voip/VideoFeed.tsx
index 2d98314ae5..51d2adb845 100644
--- a/src/components/views/voip/VideoFeed.tsx
+++ b/src/components/views/voip/VideoFeed.tsx
@@ -16,7 +16,7 @@ limitations under the License.
import classnames from 'classnames';
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
-import React, { createRef } from 'react';
+import React from 'react';
import SettingsStore from "../../../settings/SettingsStore";
import { CallFeed, CallFeedEvent } from 'matrix-js-sdk/src/webrtc/callFeed';
import { logger } from 'matrix-js-sdk/src/logger';
@@ -37,6 +37,8 @@ interface IProps {
// a callback which is called when the video element is resized
// due to a change in video metadata
onResize?: (e: Event) => void;
+
+ primary: boolean;
}
interface IState {
@@ -46,7 +48,7 @@ interface IState {
@replaceableComponent("views.voip.VideoFeed")
export default class VideoFeed extends React.Component {
- private element = createRef();
+ private element: HTMLVideoElement;
constructor(props: IProps) {
super(props);
@@ -58,19 +60,50 @@ export default class VideoFeed extends React.Component {
}
componentDidMount() {
- this.props.feed.addListener(CallFeedEvent.NewStream, this.onNewStream);
- this.element.current?.addEventListener('resize', this.onResize);
+ this.updateFeed(null, this.props.feed);
this.playMedia();
}
componentWillUnmount() {
- this.props.feed.removeListener(CallFeedEvent.NewStream, this.onNewStream);
- this.element.current?.removeEventListener('resize', this.onResize);
- this.stopMedia();
+ this.updateFeed(this.props.feed, null);
+ }
+
+ componentDidUpdate(prevProps: IProps) {
+ this.updateFeed(prevProps.feed, this.props.feed);
+ }
+
+ static getDerivedStateFromProps(props: IProps) {
+ return {
+ audioMuted: props.feed.isAudioMuted(),
+ videoMuted: props.feed.isVideoMuted(),
+ };
+ }
+
+ private setElementRef = (element: HTMLVideoElement): void => {
+ if (!element) {
+ this.element?.removeEventListener('resize', this.onResize);
+ return;
+ }
+
+ this.element = element;
+ element.addEventListener('resize', this.onResize);
+ };
+
+ private updateFeed(oldFeed: CallFeed, newFeed: CallFeed) {
+ if (oldFeed === newFeed) return;
+
+ if (oldFeed) {
+ this.props.feed.removeListener(CallFeedEvent.NewStream, this.onNewStream);
+ this.stopMedia();
+ }
+ if (newFeed) {
+ this.props.feed.addListener(CallFeedEvent.NewStream, this.onNewStream);
+ this.playMedia();
+ }
}
private playMedia() {
- const element = this.element.current;
+ const element = this.element;
if (!element) return;
// We play audio in AudioFeed, not here
element.muted = true;
@@ -93,7 +126,7 @@ export default class VideoFeed extends React.Component {
}
private stopMedia() {
- const element = this.element.current;
+ const element = this.element;
if (!element) return;
element.pause();
@@ -122,8 +155,6 @@ export default class VideoFeed extends React.Component {
render() {
const videoClasses = {
mx_VideoFeed: true,
- mx_VideoFeed_local: this.props.feed.isLocal(),
- mx_VideoFeed_remote: !this.props.feed.isLocal(),
mx_VideoFeed_voice: this.state.videoMuted,
mx_VideoFeed_video: !this.state.videoMuted,
mx_VideoFeed_mirror: (
@@ -132,9 +163,15 @@ export default class VideoFeed extends React.Component {
),
};
+ const { pipMode, primary } = this.props;
+
if (this.state.videoMuted) {
const member = this.props.feed.getMember();
- const avatarSize = this.props.pipMode ? 76 : 160;
+ let avatarSize;
+ if (pipMode && primary) avatarSize = 76;
+ else if (pipMode && !primary) avatarSize = 16;
+ else if (!pipMode && primary) avatarSize = 160;
+ else; // TBD
return (
@@ -147,7 +184,7 @@ export default class VideoFeed extends React.Component
{
);
} else {
return (
-
+
);
}
}
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 102a481f52..53b832d1b2 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -52,7 +52,6 @@
"A microphone and webcam are plugged in and set up correctly": "A microphone and webcam are plugged in and set up correctly",
"Permission is granted to use the webcam": "Permission is granted to use the webcam",
"No other application is using the webcam": "No other application is using the webcam",
- "Unable to capture screen": "Unable to capture screen",
"VoIP is unsupported": "VoIP is unsupported",
"You cannot place VoIP calls in this browser.": "You cannot place VoIP calls in this browser.",
"Too Many Calls": "Too Many Calls",
@@ -903,6 +902,10 @@
"You held the call Resume ": "You held the call Resume ",
"%(peerName)s held the call": "%(peerName)s held the call",
"Connecting": "Connecting",
+ "You are presenting": "You are presenting",
+ "%(sharerName)s is presenting": "%(sharerName)s is presenting",
+ "Your camera is turned off": "Your camera is turned off",
+ "Your camera is still enabled": "Your camera is still enabled",
"Video Call": "Video Call",
"Voice Call": "Voice Call",
"Fill Screen": "Fill Screen",
@@ -1570,6 +1573,8 @@
"Unnamed room": "Unnamed room",
"World readable": "World readable",
"Guests can join": "Guests can join",
+ "Screen sharing is here!": "Screen sharing is here!",
+ "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!": "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!",
"(~%(count)s results)|other": "(~%(count)s results)",
"(~%(count)s results)|one": "(~%(count)s result)",
"Join Room": "Join Room",
@@ -1998,9 +2003,9 @@
"Use the Desktop app to search encrypted messages": "Use the Desktop app to search encrypted messages",
"This version of %(brand)s does not support viewing some encrypted files": "This version of %(brand)s does not support viewing some encrypted files",
"This version of %(brand)s does not support searching encrypted messages": "This version of %(brand)s does not support searching encrypted messages",
- "Share your screen": "Share your screen",
- "Screens": "Screens",
- "Windows": "Windows",
+ "Share entire screen": "Share entire screen",
+ "Application window": "Application window",
+ "Share content": "Share content",
"Join": "Join",
"No results": "No results",
"Please create a new issue on GitHub so that we can investigate this bug.": "Please create a new issue on GitHub so that we can investigate this bug.",
@@ -2195,6 +2200,7 @@
"Everyone in will be able to find and join this room.": "Everyone in will be able to find and join this room.",
"You can change this at any time from room settings.": "You can change this at any time from room settings.",
"Anyone will be able to find and join this room, not just members of .": "Anyone will be able to find and join this room, not just members of .",
+ "Anyone will be able to find and join this room.": "Anyone will be able to find and join this room.",
"Only people invited will be able to find and join this room.": "Only people invited will be able to find and join this room.",
"You can’t disable this later. Bridges & most bots won’t work yet.": "You can’t disable this later. Bridges & most bots won’t work yet.",
"Your server requires encryption to be enabled in private rooms.": "Your server requires encryption to be enabled in private rooms.",