From a5b795c93422b531c0b9a51ac68f29ad725b8e4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 4 May 2022 17:16:38 +0200 Subject: [PATCH] Improve UI/UX in calls (#7791) --- .../views/voip/CallView/_CallViewButtons.scss | 4 +- res/css/views/voip/_CallView.scss | 357 +++++++-------- res/css/views/voip/_CallViewHeader.scss | 4 +- res/css/views/voip/_CallViewSidebar.scss | 24 +- res/css/views/voip/_VideoFeed.scss | 26 +- res/themes/dark/css/_dark.scss | 1 + res/themes/legacy-dark/css/_legacy-dark.scss | 1 + .../legacy-light/css/_legacy-light.scss | 1 + res/themes/light/css/_light.scss | 1 + src/components/views/voip/CallView.tsx | 431 +++++++++--------- src/components/views/voip/VideoFeed.tsx | 10 +- src/i18n/strings/en_EN.json | 8 +- 12 files changed, 433 insertions(+), 435 deletions(-) diff --git a/res/css/views/voip/CallView/_CallViewButtons.scss b/res/css/views/voip/CallView/_CallViewButtons.scss index 380b972764..4c375ee222 100644 --- a/res/css/views/voip/CallView/_CallViewButtons.scss +++ b/res/css/views/voip/CallView/_CallViewButtons.scss @@ -1,7 +1,7 @@ /* Copyright 2015, 2016 OpenMarket Ltd Copyright 2020 - 2021 The Matrix.org Foundation C.I.C. -Copyright 2021 Šimon Brandner +Copyright 2021 - 2022 Šimon Brandner Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -27,7 +27,7 @@ limitations under the License. position: absolute; display: flex; justify-content: center; - bottom: 24px; + bottom: 32px; opacity: 1; transition: opacity 0.5s; z-index: 200; // To be above _all_ feeds diff --git a/res/css/views/voip/_CallView.scss b/res/css/views/voip/_CallView.scss index 9c9548444e..9e34134a7d 100644 --- a/res/css/views/voip/_CallView.scss +++ b/res/css/views/voip/_CallView.scss @@ -1,6 +1,7 @@ /* Copyright 2015, 2016 OpenMarket Ltd -Copyright 2020 The Matrix.org Foundation C.I.C. +Copyright 2020 - 2021 The Matrix.org Foundation C.I.C. +Copyright 2021 - 2022 Šimon Brandner Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -22,202 +23,176 @@ limitations under the License. padding-right: 8px; // XXX: PiPContainer sets pointer-events: none - should probably be set back in a better place pointer-events: initial; -} -.mx_CallView_large { - padding-bottom: 10px; - margin: $container-gap-width; - // The left side gap is fully handled by this margin. To prohibit bleeding on webkit browser. - margin-right: calc($container-gap-width / 2); - margin-bottom: 10px; - display: flex; - flex-direction: column; - flex: 1; + .mx_CallView_toast { + position: absolute; + top: 74px; + + padding: 4px 8px; + + border-radius: 4px; + z-index: 50; + + // Same on both themes + color: white; + background-color: #17191c; + } + + .mx_CallView_content_wrapper { + display: flex; + justify-content: center; + + width: 100%; + height: 100%; + + overflow: hidden; + + .mx_CallView_content { + position: relative; + + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + flex: 1; + overflow: hidden; + + border-radius: 10px; + + padding: 10px; + padding-right: calc(20% + 20px); // Space for the sidebar + + background-color: $call-view-content-background; + + .mx_CallView_status { + z-index: 50; + color: $accent-fg-color; + } + + .mx_CallView_avatarsContainer { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + + div { + margin-left: 12px; + margin-right: 12px; + } + } + + .mx_CallView_holdBackground { + position: absolute; + left: 0; + right: 0; + + width: 100%; + height: 100%; + + background-repeat: no-repeat; + background-size: cover; + background-position: center; + filter: blur(20px); + + &::after { + content: ""; + display: block; + position: absolute; + width: 100%; + height: 100%; + left: 0; + right: 0; + background-color: rgba(0, 0, 0, 0.6); + } + } + + &.mx_CallView_content_hold .mx_CallView_status { + font-weight: bold; + text-align: center; + + &::before { + display: block; + margin-left: auto; + margin-right: auto; + content: ""; + width: 40px; + height: 40px; + background-image: url("$(res)/img/voip/paused.svg"); + background-position: center; + background-size: cover; + } + + .mx_CallView_pip &::before { + width: 30px; + height: 30px; + } + + .mx_AccessibleButton_hasKind { + padding: 0px; + } + } + } + } + + &:not(.mx_CallView_sidebar) .mx_CallView_content { + padding: 0; + width: 100%; + height: 100%; + + .mx_VideoFeed_primary { + aspect-ratio: unset; + border: 0; + + width: 100%; + height: 100%; + } + } + + &.mx_CallView_pip { + width: 320px; + padding-bottom: 8px; + + border-radius: 8px; + + background-color: $system; + box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.2); + + .mx_CallViewButtons { + bottom: 13px; + + .mx_CallViewButtons_button { + width: 34px; + height: 34px; + + &::before { + width: 22px; + height: 22px; + } + } + } + + .mx_CallView_content { + min-height: 180px; + } + } + + &.mx_CallView_large { + display: flex; + flex-direction: column; + align-items: center; - .mx_CallView_voice { flex: 1; + + padding-bottom: 10px; + + margin: $container-gap-width; + // The left side gap is fully handled by this margin. To prohibit bleeding on webkit browser. + margin-right: calc($container-gap-width / 2); + margin-bottom: 10px; } &.mx_CallView_belowWidget { margin-top: 0; } } - -.mx_CallView_pip { - width: 320px; - padding-bottom: 8px; - background-color: $system; - box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.2); - border-radius: 8px; - - .mx_CallView_video_hold, - .mx_CallView_voice { - height: 180px; - } - - .mx_CallViewButtons { - bottom: 13px; - } - - .mx_CallViewButtons_button { - width: 34px; - height: 34px; - - &::before { - width: 22px; - height: 22px; - } - } - - .mx_CallView_holdTransferContent { - padding-top: 10px; - padding-bottom: 25px; - } -} - -.mx_CallView_content { - position: relative; - display: flex; - justify-content: center; - border-radius: 8px; - - > .mx_VideoFeed { - width: 100%; - height: 100%; - - &.mx_VideoFeed_voice { - display: flex; - justify-content: center; - align-items: center; - } - - .mx_VideoFeed_video { - height: 100%; - background-color: #000; - } - - .mx_VideoFeed_mic { - left: 10px; - bottom: 10px; - } - } -} - -.mx_CallView_voice { - align-items: center; - justify-content: center; - flex-direction: column; - background-color: $inverted-bg-color; -} - -.mx_CallView_voice_avatarsContainer { - display: flex; - flex-direction: row; - align-items: center; - justify-content: center; - div { - margin-left: 12px; - margin-right: 12px; - } -} - -.mx_CallView_voice .mx_CallView_holdTransferContent { - // This masks the avatar image so when it's blurred, the edge is still crisp - .mx_CallView_voice_avatarContainer { - border-radius: 2000px; - overflow: hidden; - position: relative; - } -} - -.mx_CallView_holdTransferContent { - height: 20px; - padding-top: 20px; - padding-bottom: 15px; - color: $accent-fg-color; - user-select: none; - - .mx_AccessibleButton_hasKind { - padding: 0px; - font-weight: bold; - } -} - -.mx_CallView_video { - width: 100%; - height: 100%; - z-index: 30; - overflow: hidden; -} - -.mx_CallView_video_hold { - overflow: hidden; - - // we keep these around in the DOM: it saved wiring them up again when the call - // is resumed and keeps the container the right size - .mx_VideoFeed { - visibility: hidden; - } -} - -.mx_CallView_video_holdBackground { - position: absolute; - width: 100%; - height: 100%; - left: 0; - right: 0; - background-repeat: no-repeat; - background-size: cover; - background-position: center; - filter: blur(20px); - &::after { - content: ""; - display: block; - position: absolute; - width: 100%; - height: 100%; - left: 0; - right: 0; - background-color: rgba(0, 0, 0, 0.6); - } -} - -.mx_CallView_video .mx_CallView_holdTransferContent { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - font-weight: bold; - color: $accent-fg-color; - text-align: center; - - &::before { - display: block; - margin-left: auto; - margin-right: auto; - content: ""; - width: 40px; - height: 40px; - background-image: url("$(res)/img/voip/paused.svg"); - background-position: center; - background-size: cover; - } - .mx_CallView_pip &::before { - width: 30px; - height: 30px; - } - .mx_AccessibleButton_hasKind { - padding: 0px; - } -} - -.mx_CallView_presenting { - position: absolute; - margin-top: 18px; - padding: 4px 8px; - border-radius: 4px; - - // Same on both themes - color: white; - background-color: #17191c; -} diff --git a/res/css/views/voip/_CallViewHeader.scss b/res/css/views/voip/_CallViewHeader.scss index 358357f134..9340dfb040 100644 --- a/res/css/views/voip/_CallViewHeader.scss +++ b/res/css/views/voip/_CallViewHeader.scss @@ -1,5 +1,6 @@ /* Copyright 2021 The Matrix.org Foundation C.I.C. +Copyright 2021 - 2022 Šimon Brandner Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -19,8 +20,9 @@ limitations under the License. display: flex; flex-direction: row; align-items: center; - justify-content: left; + justify-content: space-between; flex-shrink: 0; + width: 100%; &.mx_CallViewHeader_pip { cursor: pointer; diff --git a/res/css/views/voip/_CallViewSidebar.scss b/res/css/views/voip/_CallViewSidebar.scss index 4871ccfe65..351f4061f4 100644 --- a/res/css/views/voip/_CallViewSidebar.scss +++ b/res/css/views/voip/_CallViewSidebar.scss @@ -1,5 +1,5 @@ /* -Copyright 2021 Šimon Brandner +Copyright 2021 - 2022 Šimon Brandner Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16,18 +16,15 @@ limitations under the License. .mx_CallViewSidebar { position: absolute; - right: 16px; - bottom: 16px; - z-index: 100; // To be above the primary feed + right: 10px; + width: 20%; + height: 100%; overflow: auto; - height: calc(100% - 32px); // Subtract the top and bottom padding - width: 20%; - display: flex; - flex-direction: column-reverse; - justify-content: flex-start; + flex-direction: column; + justify-content: center; align-items: flex-end; gap: 12px; @@ -42,15 +39,6 @@ limitations under the License. background-color: $video-feed-secondary-background; } - - .mx_VideoFeed_video { - border-radius: 4px; - } - - .mx_VideoFeed_mic { - left: 6px; - bottom: 6px; - } } &.mx_CallViewSidebar_pipMode { diff --git a/res/css/views/voip/_VideoFeed.scss b/res/css/views/voip/_VideoFeed.scss index 29dcb5cba3..a0ab8269c0 100644 --- a/res/css/views/voip/_VideoFeed.scss +++ b/res/css/views/voip/_VideoFeed.scss @@ -1,5 +1,6 @@ /* -Copyright 2015, 2016, 2020 The Matrix.org Foundation C.I.C. +Copyright 2015, 2016, 2020, 2021 The Matrix.org Foundation C.I.C. +Copyright 2021 - 2022 Šimon Brandner Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -20,15 +21,32 @@ limitations under the License. box-sizing: border-box; border: transparent 2px solid; display: flex; + border-radius: 4px; + + &.mx_VideoFeed_secondary { + position: absolute; + right: 24px; + bottom: 72px; + width: 20%; + } &.mx_VideoFeed_voice { background-color: $inverted-bg-color; - aspect-ratio: 16 / 9; + + display: flex; + justify-content: center; + align-items: center; + + &:not(.mx_VideoFeed_primary) { + aspect-ratio: 16 / 9; + } } .mx_VideoFeed_video { + height: 100%; width: 100%; - background-color: transparent; + border-radius: 4px; + background-color: #000000; &.mx_VideoFeed_video_mirror { transform: scale(-1, 1); @@ -37,6 +55,8 @@ limitations under the License. .mx_VideoFeed_mic { position: absolute; + left: 6px; + bottom: 6px; display: flex; align-items: center; justify-content: center; diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index 38fd3a58da..ce18734594 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -185,6 +185,7 @@ $call-view-button-on-foreground: $primary-content; $call-view-button-on-background: $system; $call-view-button-off-foreground: $system; $call-view-button-off-background: $primary-content; +$call-view-content-background: $quinary-content; $video-feed-secondary-background: $system; diff --git a/res/themes/legacy-dark/css/_legacy-dark.scss b/res/themes/legacy-dark/css/_legacy-dark.scss index 6f958c08fd..9ee07c0968 100644 --- a/res/themes/legacy-dark/css/_legacy-dark.scss +++ b/res/themes/legacy-dark/css/_legacy-dark.scss @@ -117,6 +117,7 @@ $call-view-button-on-foreground: $primary-content; $call-view-button-on-background: $system; $call-view-button-off-foreground: $system; $call-view-button-off-background: $primary-content; +$call-view-content-background: $quinary-content; $video-feed-secondary-background: $system; diff --git a/res/themes/legacy-light/css/_legacy-light.scss b/res/themes/legacy-light/css/_legacy-light.scss index e1da4d277d..e1c475fc63 100644 --- a/res/themes/legacy-light/css/_legacy-light.scss +++ b/res/themes/legacy-light/css/_legacy-light.scss @@ -175,6 +175,7 @@ $call-view-button-on-foreground: $secondary-content; $call-view-button-on-background: $background; $call-view-button-off-foreground: $background; $call-view-button-off-background: $secondary-content; +$call-view-content-background: #21262C; $video-feed-secondary-background: #394049; // XXX: Color from dark theme diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index 14ed62f726..3d09e40081 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -277,6 +277,7 @@ $call-view-button-on-foreground: $secondary-content; $call-view-button-on-background: $background; $call-view-button-off-foreground: $background; $call-view-button-off-background: $secondary-content; +$call-view-content-background: #21262C; $video-feed-secondary-background: #394049; // XXX: Color from dark theme $voipcall-plinth-color: $system; diff --git a/src/components/views/voip/CallView.tsx b/src/components/views/voip/CallView.tsx index af7180c5a2..bdcf7b38ad 100644 --- a/src/components/views/voip/CallView.tsx +++ b/src/components/views/voip/CallView.tsx @@ -1,7 +1,7 @@ /* Copyright 2015, 2016 OpenMarket Ltd Copyright 2019 - 2021 The Matrix.org Foundation C.I.C. -Copyright 2021 Šimon Brandner +Copyright 2021 - 2022 Šimon Brandner Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16,7 +16,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { createRef, CSSProperties } from 'react'; +import React, { createRef } from 'react'; import { CallEvent, CallState, MatrixCall } from 'matrix-js-sdk/src/webrtc/call'; import classNames from 'classnames'; import { CallFeed } from 'matrix-js-sdk/src/webrtc/callFeed'; @@ -36,6 +36,7 @@ import CallViewSidebar from './CallViewSidebar'; import CallViewHeader from './CallView/CallViewHeader'; import CallViewButtons from "./CallView/CallViewButtons"; import PlatformPeg from "../../../PlatformPeg"; +import { ActionPayload } from "../../../dispatcher/payloads"; import { getKeyBindingsManager } from "../../../KeyBindingsManager"; import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts"; @@ -69,8 +70,9 @@ interface IState { vidMuted: boolean; screensharing: boolean; callState: CallState; - primaryFeed: CallFeed; - secondaryFeeds: Array; + primaryFeed?: CallFeed; + secondaryFeed?: CallFeed; + sidebarFeeds: Array; sidebarShown: boolean; } @@ -104,13 +106,13 @@ function exitFullscreen() { export default class CallView extends React.Component { private dispatcherRef: string; - private contentRef = createRef(); + private contentWrapperRef = createRef(); private buttonsRef = createRef(); constructor(props: IProps) { super(props); - const { primary, secondary } = CallView.getOrderedFeeds(this.props.call.getFeeds()); + const { primary, secondary, sidebar } = CallView.getOrderedFeeds(this.props.call.getFeeds()); this.state = { isLocalOnHold: this.props.call.isLocalOnHold(), @@ -120,19 +122,20 @@ export default class CallView extends React.Component { screensharing: this.props.call.isScreensharing(), callState: this.props.call.state, primaryFeed: primary, - secondaryFeeds: secondary, + secondaryFeed: secondary, + sidebarFeeds: sidebar, sidebarShown: true, }; this.updateCallListeners(null, this.props.call); } - public componentDidMount() { + public componentDidMount(): void { this.dispatcherRef = dis.register(this.onAction); document.addEventListener('keydown', this.onNativeKeyDown); } - public componentWillUnmount() { + public componentWillUnmount(): void { if (getFullScreenElement()) { exitFullscreen(); } @@ -143,11 +146,12 @@ export default class CallView extends React.Component { } static getDerivedStateFromProps(props: IProps): Partial { - const { primary, secondary } = CallView.getOrderedFeeds(props.call.getFeeds()); + const { primary, secondary, sidebar } = CallView.getOrderedFeeds(props.call.getFeeds()); return { primaryFeed: primary, - secondaryFeeds: secondary, + secondaryFeed: secondary, + sidebarFeeds: sidebar, }; } @@ -165,14 +169,14 @@ export default class CallView extends React.Component { this.updateCallListeners(null, this.props.call); } - private onAction = (payload) => { + private onAction = (payload: ActionPayload): void => { switch (payload.action) { case 'video_fullscreen': { - if (!this.contentRef.current) { + if (!this.contentWrapperRef.current) { return; } if (payload.fullscreen) { - requestFullscreen(this.contentRef.current); + requestFullscreen(this.contentWrapperRef.current); } else if (getFullScreenElement()) { exitFullscreen(); } @@ -181,7 +185,7 @@ export default class CallView extends React.Component { } }; - private updateCallListeners(oldCall: MatrixCall, newCall: MatrixCall) { + private updateCallListeners(oldCall: MatrixCall, newCall: MatrixCall): void { if (oldCall === newCall) return; if (oldCall) { @@ -198,29 +202,30 @@ export default class CallView extends React.Component { } } - private onCallState = (state) => { + private onCallState = (state: CallState): void => { this.setState({ callState: state, }); }; - private onFeedsChanged = (newFeeds: Array) => { - const { primary, secondary } = CallView.getOrderedFeeds(newFeeds); + private onFeedsChanged = (newFeeds: Array): void => { + const { primary, secondary, sidebar } = CallView.getOrderedFeeds(newFeeds); this.setState({ primaryFeed: primary, - secondaryFeeds: secondary, + secondaryFeed: secondary, + sidebarFeeds: sidebar, micMuted: this.props.call.isMicrophoneMuted(), vidMuted: this.props.call.isLocalVideoMuted(), }); }; - private onCallLocalHoldUnhold = () => { + private onCallLocalHoldUnhold = (): void => { this.setState({ isLocalOnHold: this.props.call.isLocalOnHold(), }); }; - private onCallRemoteHoldUnhold = () => { + private onCallRemoteHoldUnhold = (): void => { this.setState({ isRemoteOnHold: this.props.call.isRemoteOnHold(), // update both here because isLocalOnHold changes when we hold the call too @@ -228,12 +233,22 @@ export default class CallView extends React.Component { }); }; - private onMouseMove = () => { + private onMouseMove = (): void => { this.buttonsRef.current?.showControls(); }; - static getOrderedFeeds(feeds: Array): { primary: CallFeed, secondary: Array } { - let primary; + static getOrderedFeeds( + feeds: Array, + ): { primary?: CallFeed, secondary?: CallFeed, sidebar: Array } { + if (feeds.length <= 2) { + return { + primary: feeds.find((feed) => !feed.isLocal()), + secondary: feeds.find((feed) => feed.isLocal()), + sidebar: [], + }; + } + + let primary: CallFeed; // Try to use a screensharing as primary, a remote one if possible const screensharingFeeds = feeds.filter((feed) => feed.purpose === SDPStreamMetadataPurpose.Screenshare); @@ -243,16 +258,16 @@ export default class CallView extends React.Component { primary = feeds.find((feed) => !feed.isLocal()); } - const secondary = [...feeds]; + const sidebar = [...feeds]; // Remove the primary feed from the array - if (primary) secondary.splice(secondary.indexOf(primary), 1); - secondary.sort((a, b) => { + if (primary) sidebar.splice(sidebar.indexOf(primary), 1); + sidebar.sort((a, b) => { if (a.isLocal() && !b.isLocal()) return -1; if (!a.isLocal() && b.isLocal()) return 1; return 0; }); - return { primary, secondary }; + return { primary, sidebar }; } private onMicMuteClick = async (): Promise => { @@ -336,7 +351,7 @@ export default class CallView extends React.Component { private renderCallControls(): JSX.Element { const { call, pipMode } = this.props; - const { primaryFeed, callState, micMuted, vidMuted, screensharing, sidebarShown } = this.state; + const { callState, micMuted, vidMuted, screensharing, sidebarShown, secondaryFeed, sidebarFeeds } = this.state; // If SDPStreamMetadata isn't supported don't show video mute button in voice calls const vidMuteButtonShown = call.opponentSupportsSDPStreamMetadata() || call.hasLocalUserMediaVideoTrack; @@ -348,13 +363,8 @@ export default class CallView extends React.Component { (call.opponentSupportsSDPStreamMetadata() || call.hasLocalUserMediaVideoTrack) && call.state === CallState.Connected ); - // 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 - const sidebarButtonShown = ( - primaryFeed?.purpose === SDPStreamMetadataPurpose.Screenshare || - call.isScreensharing() - ); + // Show the sidebar button only if there is something to hide/show + const sidebarButtonShown = (secondaryFeed && !secondaryFeed.isVideoMuted()) || sidebarFeeds.length > 0; // The dial pad & 'more' button actions are only relevant in a connected call const contextMenuButtonShown = callState === CallState.Connected; const dialpadButtonShown = ( @@ -391,158 +401,126 @@ export default class CallView extends React.Component { ); } - public render() { - const client = MatrixClientPeg.get(); - const callRoomId = CallHandler.instance.roomIdForCall(this.props.call); - const secondaryCallRoomId = CallHandler.instance.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; - const transfereeCall = CallHandler.instance.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) => { + private renderToast(): JSX.Element { + const { call } = this.props; + const someoneIsScreensharing = call.getFeeds().some((feed) => { return feed.purpose === SDPStreamMetadataPurpose.Screenshare; }); - const call = this.props.call; - let contentView: React.ReactNode; - let holdTransferContent; + if (!someoneIsScreensharing) return null; - if (transfereeCall) { - const transferTargetRoom = MatrixClientPeg.get().getRoom( - CallHandler.instance.roomIdForCall(this.props.call), - ); - const transferTargetName = transferTargetRoom ? transferTargetRoom.name : _t("unknown person"); + const isScreensharing = call.isScreensharing(); + const { primaryFeed, sidebarShown } = this.state; + const sharerName = primaryFeed.getMember().name; - const transfereeRoom = MatrixClientPeg.get().getRoom( - CallHandler.instance.roomIdForCall(transfereeCall), - ); - const transfereeName = transfereeRoom ? transfereeRoom.name : _t("unknown person"); - - holdTransferContent =
- { _t( - "Consulting with %(transferTarget)s. Transfer to %(transferee)s", - { - transferTarget: transferTargetName, - transferee: transfereeName, - }, - { - a: sub => - { sub } - , - }, - ) } -
; - } else if (isOnHold) { - let onHoldText = null; - if (this.state.isRemoteOnHold) { - const holdString = CallHandler.instance.hasAnyUnheldCall() ? - _td("You held the call Switch") : _td("You held the call Resume"); - onHoldText = _t(holdString, {}, { - a: sub => - { sub } - , - }); - } else if (this.state.isLocalOnHold) { - onHoldText = _t("%(peerName)s held the call", { - peerName: this.props.call.getOpponentMember().name, - }); - } - holdTransferContent =
- { onHoldText } -
; + let text = isScreensharing + ? _t("You are presenting") + : _t('%(sharerName)s is presenting', { sharerName }); + if (!sidebarShown) { + text += " • " + (call.isLocalVideoMuted() + ? _t("Your camera is turned off") + : _t("Your camera is still enabled")); } - let sidebar; - if ( - !isOnHold && - !transfereeCall && - sidebarShown && - (call.hasLocalUserMediaVideoTrack || someoneIsScreensharing) - ) { - sidebar = ( - + { text } + + ); + } + + private renderContent(): JSX.Element { + const { pipMode, call, onResize } = this.props; + const { isLocalOnHold, isRemoteOnHold, sidebarShown, primaryFeed, secondaryFeed, sidebarFeeds } = this.state; + + const callRoom = MatrixClientPeg.get().getRoom(call.roomId); + const avatarSize = pipMode ? 76 : 160; + const transfereeCall = CallHandler.instance.getTransfereeForCallId(call.callId); + const isOnHold = isLocalOnHold || isRemoteOnHold; + + let secondaryFeedElement: React.ReactNode; + if (sidebarShown && secondaryFeed && !secondaryFeed.isVideoMuted()) { + secondaryFeedElement = ( + ); } - // This is a bit messy. I can't see a reason to have two onHold/transfer screens - if (isOnHold || transfereeCall) { - if (call.hasLocalUserMediaVideoTrack || call.hasRemoteUserMediaVideoTrack) { - const containerClasses = classNames({ - mx_CallView_content: true, - mx_CallView_video: true, - mx_CallView_video_hold: isOnHold, - }); - let onHoldBackground = null; - const backgroundStyle: CSSProperties = {}; - const backgroundAvatarUrl = avatarUrlForMember( - // is it worth getting the size of the div to pass here? - this.props.call.getOpponentMember(), 1024, 1024, 'crop', - ); - backgroundStyle.backgroundImage = 'url(' + backgroundAvatarUrl + ')'; - onHoldBackground =
; + if (transfereeCall || isOnHold) { + const containerClasses = classNames("mx_CallView_content", { + mx_CallView_content_hold: isOnHold, + }); + const backgroundAvatarUrl = avatarUrlForMember(call.getOpponentMember(), 1024, 1024, 'crop'); - contentView = ( -
- { onHoldBackground } - { holdTransferContent } - { this.renderCallControls() } -
+ let holdTransferContent: React.ReactNode; + if (transfereeCall) { + const transferTargetRoom = MatrixClientPeg.get().getRoom( + CallHandler.instance.roomIdForCall(call), ); + const transferTargetName = transferTargetRoom ? transferTargetRoom.name : _t("unknown person"); + const transfereeRoom = MatrixClientPeg.get().getRoom( + CallHandler.instance.roomIdForCall(transfereeCall), + ); + const transfereeName = transfereeRoom ? transfereeRoom.name : _t("unknown person"); + + holdTransferContent =
+ { _t( + "Consulting with %(transferTarget)s. Transfer to %(transferee)s", + { + transferTarget: transferTargetName, + transferee: transfereeName, + }, + { + a: sub => + { sub } + , + }, + ) } +
; } else { - const classes = classNames({ - mx_CallView_content: true, - mx_CallView_voice: true, - mx_CallView_voice_hold: isOnHold, - }); + let onHoldText: React.ReactNode; + if (isRemoteOnHold) { + onHoldText = _t( + CallHandler.instance.hasAnyUnheldCall() + ? _td("You held the call Switch") + : _td("You held the call Resume"), + {}, + { + a: sub => + { sub } + , + }, + ); + } else if (isLocalOnHold) { + onHoldText = _t("%(peerName)s held the call", { + peerName: call.getOpponentMember().name, + }); + } - contentView = ( -
-
-
- -
-
- { holdTransferContent } - { this.renderCallControls() } + holdTransferContent = ( +
+ { onHoldText }
); } - } else if (this.props.call.noIncomingFeeds()) { - // Here we're reusing the css classes from voice on hold, because - // I am lazy. If this gets merged, the CallView might be subject - // to change anyway - I might take an axe to this file in order to - // try to get other things working - const classes = classNames({ - mx_CallView_content: true, - mx_CallView_voice: true, - }); - // 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 = ( -
- { sidebar } -
+ return ( +
+
+ { holdTransferContent } +
+ ); + } else if (call.noIncomingFeeds()) { + return ( +
+
{ />
-
{ _t("Connecting") }
- { this.renderCallControls() } +
{ _t("Connecting") }
+ { secondaryFeedElement } +
+ ); + } else if (pipMode) { + return ( +
+ +
+ ); + } else if (secondaryFeed) { + return ( +
+ + { secondaryFeedElement }
); } else { - const containerClasses = classNames({ - mx_CallView_content: true, - mx_CallView_video: true, - }); - - let toast; - if (someoneIsScreensharing) { - const sharerName = this.state.primaryFeed.getMember().name; - let text = isScreensharing - ? _t("You are presenting") - : _t('%(sharerName)s is presenting', { sharerName }); - if (!this.state.sidebarShown) { - text += " • " + (this.props.call.isLocalVideoMuted() - ? _t("Your camera is turned off") - : _t("Your camera is still enabled")); - } - - toast = ( -
- { text } -
- ); - } - - contentView = ( -
- { toast } - { sidebar } + return ( +
- { this.renderCallControls() } + { sidebarShown && }
); } + } + + public render(): JSX.Element { + const { + call, + secondaryCall, + pipMode, + showApps, + onMouseDownOnHeader, + } = this.props; + const { + sidebarShown, + sidebarFeeds, + } = this.state; + + const client = MatrixClientPeg.get(); + const callRoomId = CallHandler.instance.roomIdForCall(call); + const secondaryCallRoomId = CallHandler.instance.roomIdForCall(secondaryCall); + const callRoom = client.getRoom(callRoomId); + const secCallRoom = secondaryCall ? client.getRoom(secondaryCallRoomId) : null; const callViewClasses = classNames({ mx_CallView: true, - mx_CallView_pip: this.props.pipMode, - mx_CallView_large: !this.props.pipMode, - mx_CallView_belowWidget: this.props.showApps, // css to correct the margins if the call is below the AppsDrawer. + mx_CallView_pip: pipMode, + mx_CallView_large: !pipMode, + mx_CallView_sidebar: sidebarShown && sidebarFeeds.length !== 0 && !pipMode, + mx_CallView_belowWidget: showApps, // css to correct the margins if the call is below the AppsDrawer. }); return
- { contentView } +
+ { this.renderToast() } + { this.renderContent() } + { this.renderCallControls() } +
; } } diff --git a/src/components/views/voip/VideoFeed.tsx b/src/components/views/voip/VideoFeed.tsx index 12f4f31eea..b9155fe51d 100644 --- a/src/components/views/voip/VideoFeed.tsx +++ b/src/components/views/voip/VideoFeed.tsx @@ -1,5 +1,6 @@ /* -Copyright 2015, 2016, 2019 The Matrix.org Foundation C.I.C. +Copyright 2015, 2016, 2019, 2020, 2021 The Matrix.org Foundation C.I.C. +Copyright 2021 - 2022 Šimon Brandner Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -39,7 +40,8 @@ interface IProps { // due to a change in video metadata onResize?: (e: Event) => void; - primary: boolean; + primary?: boolean; + secondary?: boolean; } interface IState { @@ -178,9 +180,11 @@ export default class VideoFeed extends React.PureComponent { }; render() { - const { pipMode, primary, feed } = this.props; + const { pipMode, primary, secondary, feed } = this.props; const wrapperClasses = classnames("mx_VideoFeed", { + mx_VideoFeed_primary: primary, + mx_VideoFeed_secondary: secondary, mx_VideoFeed_voice: this.state.videoMuted, }); const micIconClasses = classnames("mx_VideoFeed_mic", { diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 47a349f565..7f885a25b4 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1014,16 +1014,16 @@ "You can use /help to list available commands. Did you mean to send this as a message?": "You can use /help to list available commands. Did you mean to send this as a message?", "Hint: Begin your message with // to start it with a slash.": "Hint: Begin your message with // to start it with a slash.", "Send as message": "Send as message", + "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", "unknown person": "unknown person", "Consulting with %(transferTarget)s. Transfer to %(transferee)s": "Consulting with %(transferTarget)s. Transfer to %(transferee)s", "You held the call Switch": "You held the call Switch", "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", "Dial": "Dial", "%(count)s people connected|other": "%(count)s people connected", "%(count)s people connected|one": "%(count)s person connected",