Calling: New option to expand your local preview

This commit is contained in:
Scott Nonnenberg
2025-03-11 08:30:55 +10:00
committed by GitHub
parent aab4021d37
commit 0eb697fa82
21 changed files with 400 additions and 97 deletions

View File

@@ -3555,6 +3555,10 @@
"messageformat": "Toggle video on and off",
"description": "Shown in the shortcuts guide"
},
"icu:Keyboard--toggle-preview": {
"messageformat": "Toggle expanded preview on and off",
"description": "Shown in the shortcuts guide"
},
"icu:Keyboard--accept-video-call": {
"messageformat": "Answer call with video (video calls only)",
"description": "Shown in the calling keyboard shortcuts guide"
@@ -4115,6 +4119,14 @@
"messageformat": "Fullscreen call",
"description": "Title for picture-in-picture toggle"
},
"icu:calling__preview--maximize": {
"messageformat": "Maximize preview",
"description": "Title for button to make in-call video preview bigger"
},
"icu:calling__preview--minimize": {
"messageformat": "Minimize preview",
"description": "Title for button to make in-call video preview smaller"
},
"icu:calling__change-view": {
"messageformat": "Change view",
"description": "Tooltip for changing the in-call layout of remote participants in a group call"

View File

@@ -0,0 +1 @@
<svg width="20" height="20" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M16.98 3.75a.729.729 0 0 0-.73-.73h-4.583a.73.73 0 1 0 0 1.46h3.032l-1.256 1.046-2.709 2.708a.73.73 0 0 0 1.031 1.032l2.709-2.709 1.047-1.256v3.032a.73.73 0 1 0 1.458 0V3.75ZM3.234 16.766a.73.73 0 0 1-.213-.516v-4.583a.73.73 0 0 1 1.458 0v3.032l1.047-1.256 2.708-2.709a.73.73 0 0 1 1.032 1.032l-2.709 2.708-1.256 1.047h3.032a.73.73 0 1 1 0 1.458H3.75a.729.729 0 0 1-.516-.213Z" fill="#000"/></svg>

After

Width:  |  Height:  |  Size: 481 B

View File

@@ -0,0 +1 @@
<svg width="20" height="20" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M9.27 11.458a.73.73 0 0 0-.728-.729H3.958a.73.73 0 1 0 0 1.459h3.033l-1.257 1.046-2.708 2.709a.73.73 0 1 0 1.031 1.03l2.709-2.707 1.046-1.257v3.033a.73.73 0 0 0 1.459 0v-4.584Zm1.673-2.401a.73.73 0 0 1-.214-.515V3.958a.73.73 0 1 1 1.459 0v3.033l1.046-1.257 2.709-2.708a.73.73 0 1 1 1.03 1.031l-2.707 2.709-1.257 1.046h3.033a.73.73 0 0 1 0 1.459h-4.584a.73.73 0 0 1-.515-.214Z" fill="#000"/></svg>

After

Width:  |  Height:  |  Size: 480 B

View File

@@ -3996,8 +3996,6 @@ button.module-image__border-overlay:focus {
}
}
.module-ongoing-call {
$local-preview-height: 80px;
&__remote-video-enabled {
background-color: variables.$color-gray-95;
height: 100%;
@@ -4075,6 +4073,13 @@ button.module-image__border-overlay:focus {
position: absolute;
inset-inline-end: 16px;
bottom: 112px;
transition: bottom 0.3s variables.$ease-out-local-preview;
}
&__direct-call-speaking-indicator--self-view-expanded {
bottom: 330px;
}
&__direct-call-speaking-indicator--expanded-no-controls {
bottom: 232px;
}
&__participants {
@@ -4523,6 +4528,56 @@ button.module-image__border-overlay:focus {
}
}
&__local-preview {
z-index: variables.$z-index-calling-pip;
border-radius: 12px;
position: absolute;
display: flex;
height: 80px;
width: variables.$calling-local-preview-normal-width;
inset-inline-end: 16px;
bottom: 16px;
transition: all 0.3s variables.$ease-out-local-preview;
overflow: hidden;
cursor: pointer;
&--active {
box-shadow: 0px 4px 14px 0px variables.$color-black-alpha-40;
}
&--expanded {
bottom: 112px;
height: 200px;
width: 312px;
}
&--controls-hidden {
bottom: 16px;
}
&__video {
height: 100%;
width: 100%;
video {
// The background-color is seen while the video loads.
background-color: variables.$color-gray-75;
height: 100%;
width: 100%;
transform: rotateY(180deg);
}
&--presenting video {
transform: inherit;
}
}
}
&__footer {
bottom: 0;
display: flex;
@@ -4537,40 +4592,6 @@ button.module-image__border-overlay:focus {
flex-grow: 1;
justify-content: center;
}
&__local-preview {
border-radius: 10px;
display: flex;
flex-shrink: 0;
height: $local-preview-height;
margin-block-end: 16px;
margin-inline: 0 16px;
overflow: hidden;
position: relative;
width: variables.$calling-local-preview-width;
&--active {
box-shadow: 0px 4px 14px 0px variables.$color-black-alpha-40;
}
&__video {
height: 100%;
width: 100%;
video {
// The background-color is seen while the video loads.
background-color: variables.$color-gray-75;
height: 100%;
width: 100%;
transform: rotateY(180deg);
}
&--presenting video {
transform: inherit;
}
}
}
}
&__controls {

View File

@@ -272,11 +272,12 @@ $color-selected-message-background-dark: $color-gray-65;
$header-height: 52px;
$ease-out-expo: cubic-bezier(0.19, 1, 0.22, 1);
$ease-out-local-preview: cubic-bezier(0.17, 0.17, 0, 1);
$calling-background-color: $color-gray-90;
// Maintain aspect ratio 960x720 with $local-preview-height
$calling-local-preview-width: 106.67px;
$calling-local-preview-normal-width: 106.67px;
// General

View File

@@ -118,7 +118,7 @@
}
.CallControls__OuterSpacer {
flex-basis: calc(variables.$calling-local-preview-width + 16px);
flex-basis: calc(variables.$calling-local-preview-normal-width + 16px);
}
.CallControls__ReactionPickerContainer {

View File

@@ -38,10 +38,10 @@
}
}
.module-ongoing-call__footer__local-preview .CallingAudioIndicator {
.module-ongoing-call__local-preview .CallingAudioIndicator {
position: absolute;
top: 6px;
inset-inline-end: 6px;
top: 8px;
inset-inline-end: 8px;
z-index: variables.$z-index-base;
}

View File

@@ -160,6 +160,22 @@
&--more-options {
@include calling-button-icon-regular('../images/icons/v3/more/more.svg');
}
&--maximize {
@include calling-button-icon(
'../images/icons/v3/maximize/maximize.svg',
rgba(variables.$color-gray-80, 0.7),
variables.$color-white
);
}
&--minimize {
@include calling-button-icon(
'../images/icons/v3/minimize/minimize.svg',
rgba(variables.$color-gray-80, 0.7),
variables.$color-white
);
}
}
&__button-container {
@@ -228,3 +244,23 @@
border-top-color: variables.$color-gray-80 !important;
}
}
.CallingButton__Button--self-view {
position: absolute;
inset-inline-start: 8px;
top: 8px;
.CallingButton__button-container {
margin: 0;
}
}
.CallingButton__Button--self-view-normal .CallingButton__icon {
height: 28px;
width: 28px;
div {
height: 16px;
width: 16px;
}
}

View File

@@ -36,27 +36,26 @@
);
}
.CallingStatusIndicator--Video::after {
.CallingStatusIndicator--NoVideo::after {
@include mixins.color-svg(
'../images/icons/v3/video/video-slash-fill-light.svg',
variables.$color-white
);
}
.module-ongoing-call__footer__local-preview .CallingStatusIndicator {
.module-ongoing-call__local-preview .CallingStatusIndicator {
position: absolute;
z-index: variables.$z-index-base;
}
.module-ongoing-call__footer__local-preview .CallingStatusIndicator--Video {
top: 6px;
inset-inline-start: 6px;
.module-ongoing-call__local-preview .CallingStatusIndicator--NoVideo {
top: 8px;
inset-inline-start: 8px;
}
.module-ongoing-call__footer__local-preview
.CallingStatusIndicator--HandRaised {
bottom: 6px;
inset-inline-start: 6px;
.module-ongoing-call__local-preview .CallingStatusIndicator--HandRaised {
bottom: 8px;
inset-inline-start: 8px;
}
.module-ongoing-call__participants__grid

View File

@@ -81,6 +81,7 @@ const getCommonActiveCallData = () => ({
viewMode: CallViewMode.Paginated,
outgoingRing: true,
pip: false,
selfViewExpanded: false,
settingsDialogOpen: false,
showParticipantsList: false,
});
@@ -147,6 +148,7 @@ const createProps = (storyProps: Partial<PropsType> = {}): PropsType => ({
toggleScreenRecordingPermissionsDialog: action(
'toggle-screen-recording-permissions-dialog'
),
toggleSelfViewExpanded: action('toggle-self-view-expanded'),
toggleSettings: action('toggle-settings'),
pauseVoiceNotePlayer: action('pause-audio-player'),
});
@@ -181,6 +183,7 @@ const getActiveCallForCallLink = (
pendingParticipants: overrideProps.pendingParticipants ?? [],
raisedHands: new Set<number>(),
remoteAudioLevels: new Map<number, number>(),
selfViewExpanded: false,
suggestLowerHand: false,
};
};

View File

@@ -138,6 +138,7 @@ export type PropsType = {
togglePip: () => void;
toggleCallLinkPendingParticipantModal: (contactId: string) => void;
toggleScreenRecordingPermissionsDialog: () => unknown;
toggleSelfViewExpanded: () => unknown;
toggleSettings: () => void;
pauseVoiceNotePlayer: () => void;
} & Pick<ReactionPickerProps, 'renderEmojiPicker'>;
@@ -200,6 +201,7 @@ function ActiveCallManager({
toggleParticipants,
togglePip,
toggleScreenRecordingPermissionsDialog,
toggleSelfViewExpanded,
toggleSettings,
pauseVoiceNotePlayer,
}: ActiveCallManagerPropsType): JSX.Element {
@@ -480,6 +482,7 @@ function ActiveCallManager({
}
toggleParticipants={toggleParticipants}
togglePip={togglePip}
toggleSelfViewExpanded={toggleSelfViewExpanded}
toggleSettings={toggleSettings}
/>
{presentingSourcesAvailable && presentingSourcesAvailable.length ? (
@@ -573,6 +576,7 @@ export function CallManager({
togglePip,
toggleCallLinkPendingParticipantModal,
toggleScreenRecordingPermissionsDialog,
toggleSelfViewExpanded,
toggleSettings,
}: PropsType): JSX.Element | null {
const isCallActive = Boolean(activeCall);
@@ -667,6 +671,7 @@ export function CallManager({
toggleScreenRecordingPermissionsDialog={
toggleScreenRecordingPermissionsDialog
}
toggleSelfViewExpanded={toggleSelfViewExpanded}
toggleSettings={toggleSettings}
/>
</CallingToastProvider>

View File

@@ -56,6 +56,7 @@ type OverridePropsBase = {
hasLocalVideo?: boolean;
localAudioLevel?: number;
viewMode?: CallViewMode;
outgoingRing?: boolean;
reactions?: ActiveCallReactionsType;
};
@@ -63,6 +64,9 @@ type DirectCallOverrideProps = OverridePropsBase & {
callMode: CallMode.Direct;
callState?: CallState;
hasRemoteVideo?: boolean;
outgoingRing?: boolean;
selfViewExpanded?: boolean;
remoteAudioLevel?: number;
};
type GroupCallOverrideProps = OverridePropsBase & {
@@ -72,9 +76,11 @@ type GroupCallOverrideProps = OverridePropsBase & {
peekedParticipants?: Array<ConversationType>;
pendingParticipants?: Array<ConversationType>;
raisedHands?: Set<number>;
remoteParticipants?: Array<GroupCallRemoteParticipantType>;
remoteAudioLevel?: number;
remoteParticipants?: Array<GroupCallRemoteParticipantType>;
selfViewExpanded?: boolean;
suggestLowerHand?: boolean;
outgoingRing?: boolean;
};
const createActiveDirectCallProp = (
@@ -84,7 +90,7 @@ const createActiveDirectCallProp = (
conversation,
callState: overrideProps.callState ?? CallState.Accepted,
peekedParticipants: [] as [],
remoteAudioLevel: 0,
remoteAudioLevel: overrideProps.remoteAudioLevel ?? 0,
remoteParticipants: [
{
hasRemoteVideo: overrideProps.hasRemoteVideo ?? false,
@@ -168,9 +174,10 @@ const createActiveCallProp = (
hasLocalVideo: overrideProps.hasLocalVideo ?? false,
localAudioLevel: overrideProps.localAudioLevel ?? 0,
viewMode: overrideProps.viewMode ?? CallViewMode.Sidebar,
outgoingRing: true,
outgoingRing: overrideProps.outgoingRing ?? true,
pip: false,
settingsDialogOpen: false,
selfViewExpanded: overrideProps.selfViewExpanded ?? false,
showParticipantsList: false,
};
@@ -236,6 +243,7 @@ const createProps = (
toggleScreenRecordingPermissionsDialog: action(
'toggle-screen-recording-permissions-dialog'
),
toggleSelfViewExpanded: action('toggle-self-view-expanded'),
toggleSettings: action('toggle-settings'),
});
@@ -269,7 +277,7 @@ export function PreRing(): JSX.Element {
);
}
export function Ringing(): JSX.Element {
export function DirectRinging(): JSX.Element {
return (
<CallScreen
{...createProps({
@@ -335,6 +343,46 @@ export function HasRemoteVideo(): JSX.Element {
);
}
export function BothSpeaking(): JSX.Element {
return (
<CallScreen
{...createProps({
callMode: CallMode.Direct,
hasRemoteVideo: true,
hasLocalAudio: true,
localAudioLevel: 0.75,
remoteAudioLevel: 0.75,
})}
/>
);
}
export function SelfViewExpanded(): JSX.Element {
return (
<CallScreen
{...createProps({
callMode: CallMode.Direct,
selfViewExpanded: true,
})}
/>
);
}
export function SelfViewExpandedBothSpeaking(): JSX.Element {
return (
<CallScreen
{...createProps({
callMode: CallMode.Direct,
selfViewExpanded: true,
hasRemoteVideo: true,
hasLocalAudio: true,
localAudioLevel: 0.75,
remoteAudioLevel: 0.75,
})}
/>
);
}
export function GroupCall1(): JSX.Element {
return (
<CallScreen
@@ -363,6 +411,16 @@ export function GroupCall1(): JSX.Element {
);
}
export function GroupCall0(): JSX.Element {
const props = createProps({
callMode: CallMode.Group,
remoteParticipants: [],
groupMembers: [],
outgoingRing: false,
});
return <CallScreen {...props} />;
}
export function GroupCallYourHandRaised(): JSX.Element {
return (
<CallScreen
@@ -516,7 +574,7 @@ export function GroupCallReconnecting(): JSX.Element {
);
}
export function GroupCall0(): JSX.Element {
export function GroupCallOutgoingRinging(): JSX.Element {
return (
<CallScreen
{...createProps({

View File

@@ -126,6 +126,7 @@ export type PropsType = {
toggleParticipants: () => void;
togglePip: () => void;
toggleScreenRecordingPermissionsDialog: () => unknown;
toggleSelfViewExpanded: () => void;
toggleSettings: () => void;
changeCallView: (mode: CallViewMode) => void;
} & Pick<ReactionPickerProps, 'renderEmojiPicker'>;
@@ -215,6 +216,7 @@ export function CallScreen({
toggleParticipants,
togglePip,
toggleScreenRecordingPermissionsDialog,
toggleSelfViewExpanded,
toggleSettings,
}: PropsType): JSX.Element {
const {
@@ -280,7 +282,6 @@ export function CallScreen({
}, []);
const [controlsHover, setControlsHover] = useState(false);
const onControlsMouseEnter = useCallback(() => {
setControlsHover(true);
}, [setControlsHover]);
@@ -290,7 +291,6 @@ export function CallScreen({
}, [setControlsHover]);
const [showControls, setShowControls] = useState(true);
useEffect(() => {
if (
!showControls ||
@@ -306,6 +306,28 @@ export function CallScreen({
return clearTimeout.bind(null, timer);
}, [showControls, showReactionPicker, stickyControls, controlsHover]);
const [selfViewHover, setSelfViewHover] = useState(false);
const onSelfViewMouseEnter = useCallback(() => {
setSelfViewHover(true);
}, [setSelfViewHover]);
const onSelfViewMouseLeave = useCallback(() => {
setSelfViewHover(false);
}, [setSelfViewHover]);
const [showSelfViewControls, setShowSelfViewControls] = useState(false);
useEffect(() => {
if (selfViewHover) {
setShowSelfViewControls(true);
return;
}
const timer = setTimeout(() => {
setShowSelfViewControls(false);
}, 2000);
return clearTimeout.bind(null, timer);
}, [showSelfViewControls, setShowSelfViewControls, selfViewHover]);
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent): void => {
let eventHandled = false;
@@ -314,16 +336,20 @@ export function CallScreen({
if (event.shiftKey && (key === 'V' || key === 'v')) {
toggleVideo();
setShowControls(true);
eventHandled = true;
} else if (event.shiftKey && (key === 'M' || key === 'm')) {
toggleAudio();
setShowControls(true);
eventHandled = true;
} else if (event.shiftKey && (key === 'P' || key === 'p')) {
toggleSelfViewExpanded();
eventHandled = true;
}
if (eventHandled) {
event.preventDefault();
event.stopPropagation();
setShowControls(true);
}
};
@@ -331,7 +357,7 @@ export function CallScreen({
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [toggleAudio, toggleVideo]);
}, [setShowControls, toggleAudio, toggleSelfViewExpanded, toggleVideo]);
useEffect(() => {
if (!showReactionPicker) {
@@ -411,7 +437,24 @@ export function CallScreen({
let lonelyInCallNode: ReactNode;
let localPreviewNode: ReactNode;
const raisedHands = isGroupOrAdhocActiveCall(activeCall)
? activeCall.raisedHands
: undefined;
// This is the value of our hand raised as seen by remote clients. We should prefer
// to use it in UI so the user understands what remote clients see.
const syncedLocalHandRaised = isHandRaised(raisedHands, localDemuxId);
const isLonelyInCall = !activeCall.remoteParticipants.length;
const handlePreviewClick = useCallback(
(event?: React.MouseEvent) => {
event?.preventDefault();
event?.stopPropagation();
toggleSelfViewExpanded();
},
[toggleSelfViewExpanded]
);
if (isLonelyInCall) {
lonelyInCallNode = (
@@ -438,12 +481,12 @@ export function CallScreen({
</div>
);
} else {
localPreviewNode = isSendingVideo ? (
const innerPreviewNode = isSendingVideo ? (
<div
className={classNames(
'module-ongoing-call__footer__local-preview__video',
'module-ongoing-call__local-preview__video',
presentingSource &&
'module-ongoing-call__footer__local-preview__video--presenting'
'module-ongoing-call__local-preview__video--presenting'
)}
ref={setLocalPreviewContainer}
/>
@@ -467,6 +510,74 @@ export function CallScreen({
/>
</CallBackgroundBlur>
);
localPreviewNode = (
// Keyboard shortcuts are available for this gesture, no need for keyboard support
/* eslint-disable-next-line max-len */
/* eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */
<div
className={classNames(
'module-ongoing-call__local-preview',
'module-ongoing-call__local-preview--active',
activeCall.selfViewExpanded
? 'module-ongoing-call__local-preview--expanded'
: undefined,
!showControls
? 'module-ongoing-call__local-preview--controls-hidden'
: undefined
)}
onMouseEnter={onSelfViewMouseEnter}
onMouseLeave={onSelfViewMouseLeave}
onClick={handlePreviewClick}
>
{innerPreviewNode}
{!isSendingVideo && (
<div
className={classNames(
'CallingStatusIndicator',
'CallingStatusIndicator--NoVideo',
!showSelfViewControls
? 'module-ongoing-call__controls--fadeIn'
: undefined,
showSelfViewControls
? 'module-ongoing-call__controls--fadeOut'
: undefined
)}
/>
)}
<CallingAudioIndicator
hasAudio={hasLocalAudio}
audioLevel={localAudioLevel}
shouldShowSpeaking={isSpeaking}
/>
<div
className={classNames(
'CallingButton__Button--self-view',
showSelfViewControls
? 'module-ongoing-call__controls--fadeIn'
: undefined,
!showSelfViewControls
? 'module-ongoing-call__controls--fadeOut'
: undefined,
!activeCall.selfViewExpanded
? 'CallingButton__Button--self-view-normal'
: undefined
)}
>
<CallingButton
buttonType={
activeCall.selfViewExpanded
? CallingButtonType.MINIMIZE
: CallingButtonType.MAXIMIZE
}
i18n={i18n}
onClick={handlePreviewClick}
/>
</div>
{syncedLocalHandRaised && (
<div className="CallingStatusIndicator CallingStatusIndicator--HandRaised" />
)}
</div>
);
}
let videoButtonType: CallingButtonType;
@@ -503,14 +614,6 @@ export function CallScreen({
presentingButtonType = CallingButtonType.PRESENTING_OFF;
}
const raisedHands = isGroupOrAdhocActiveCall(activeCall)
? activeCall.raisedHands
: undefined;
// This is the value of our hand raised as seen by remote clients. We should prefer
// to use it in UI so the user understands what remote clients see.
const syncedLocalHandRaised = isHandRaised(raisedHands, localDemuxId);
// Don't call setLocalHandRaised because it only sets local state. Instead call
// toggleRaiseHand() which will set ringrtc state and call setLocalHandRaised.
const [localHandRaised, setLocalHandRaised] = useState<boolean>(
@@ -877,7 +980,17 @@ export function CallScreen({
/>
) : null}
{activeCall.callMode === CallMode.Direct && (
<div className="module-ongoing-call__direct-call-speaking-indicator">
<div
className={classNames(
'module-ongoing-call__direct-call-speaking-indicator',
activeCall.selfViewExpanded
? 'module-ongoing-call__direct-call-speaking-indicator--self-view-expanded'
: undefined,
activeCall.selfViewExpanded && !showControls
? 'module-ongoing-call__direct-call-speaking-indicator--expanded-no-controls'
: undefined
)}
>
<CallingAudioIndicator
hasAudio
audioLevel={activeCall.remoteAudioLevel}
@@ -885,27 +998,10 @@ export function CallScreen({
/>
</div>
)}
{/* We render the local preview first and set the footer flex direction to row-reverse
to ensure the preview is visible at low viewport widths. */}
{localPreviewNode}
{/* We set flex direction to row-reverse to render outward from local preview */}
<div className="module-ongoing-call__footer">
{localPreviewNode ? (
<div className="module-ongoing-call__footer__local-preview module-ongoing-call__footer__local-preview--active">
{localPreviewNode}
{!isSendingVideo && (
<div className="CallingStatusIndicator CallingStatusIndicator--Video" />
)}
<CallingAudioIndicator
hasAudio={hasLocalAudio}
audioLevel={localAudioLevel}
shouldShowSpeaking={isSpeaking}
/>
{syncedLocalHandRaised && (
<div className="CallingStatusIndicator CallingStatusIndicator--HandRaised" />
)}
</div>
) : (
<div className="module-ongoing-call__footer__local-preview" />
)}
<div className="module-calling__spacer CallControls__OuterSpacer" />
<div
className={classNames(
'CallControls',

View File

@@ -13,6 +13,9 @@ export enum CallingButtonType {
AUDIO_DISABLED = 'AUDIO_DISABLED',
AUDIO_OFF = 'AUDIO_OFF',
AUDIO_ON = 'AUDIO_ON',
MAXIMIZE = 'MAXIMIZE',
MINIMIZE = 'MINIMIZE',
MORE_OPTIONS = 'MORE_OPTIONS',
PRESENTING_DISABLED = 'PRESENTING_DISABLED',
PRESENTING_OFF = 'PRESENTING_OFF',
PRESENTING_ON = 'PRESENTING_ON',
@@ -26,7 +29,6 @@ export enum CallingButtonType {
VIDEO_DISABLED = 'VIDEO_DISABLED',
VIDEO_OFF = 'VIDEO_OFF',
VIDEO_ON = 'VIDEO_ON',
MORE_OPTIONS = 'MORE_OPTIONS',
}
export type PropsType = {
@@ -109,8 +111,23 @@ export function CallingButton({
} else if (buttonType === CallingButtonType.MORE_OPTIONS) {
classNameSuffix = 'more-options';
tooltipContent = i18n('icu:CallingButton--more-options');
} else if (buttonType === CallingButtonType.MAXIMIZE) {
classNameSuffix = 'maximize';
tooltipContent = i18n('icu:calling__preview--maximize');
} else if (buttonType === CallingButtonType.MINIMIZE) {
classNameSuffix = 'minimize';
tooltipContent = i18n('icu:calling__preview--minimize');
}
const handleClick = React.useCallback(
(event: React.MouseEvent) => {
event.preventDefault();
event.stopPropagation();
onClick();
},
[onClick]
);
const buttonContent = (
<button
aria-label={tooltipContent}
@@ -120,7 +137,7 @@ export function CallingButton({
)}
disabled={disabled}
id={uniqueButtonId}
onClick={onClick}
onClick={handleClick}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
type="button"

View File

@@ -51,6 +51,7 @@ const getCommonActiveCallData = (overrides: Overrides) => ({
joinedAt: Date.now() - MINUTE,
outgoingRing: true,
pip: true,
selfViewExpanded: false,
settingsDialogOpen: false,
showParticipantsList: false,
});

View File

@@ -317,12 +317,17 @@ function getCallingShortcuts(i18n: LocalizerType): Array<ShortcutType> {
keys: [['shift', 'V']],
},
{
id: 'icu:Keyboard--accept-video-call',
id: 'Keyboard--toggle-preview',
description: i18n('icu:Keyboard--toggle-preview'),
keys: [['shift', 'P']],
},
{
id: 'Keyboard--accept-video-call',
description: i18n('icu:Keyboard--accept-video-call'),
keys: [['ctrlOrAlt', 'shift', 'V']],
},
{
id: 'icu:Keyboard--accept-call-without-video',
id: 'Keyboard--accept-call-without-video',
description: i18n('icu:Keyboard--accept-call-without-video'),
keys: [['ctrlOrAlt', 'shift', 'A']],
},

View File

@@ -190,6 +190,7 @@ export type ActiveCallStateType = {
pip: boolean;
presentingSource?: PresentedSource;
presentingSourcesAvailable?: ReadonlyArray<PresentableSource>;
selfViewExpanded: boolean;
settingsDialogOpen: boolean;
showNeedsScreenRecordingPermissionsWarning?: boolean;
showParticipantsList: boolean;
@@ -659,6 +660,7 @@ const TOGGLE_NEEDS_SCREEN_RECORDING_PERMISSIONS =
const START_DIRECT_CALL = 'calling/START_DIRECT_CALL';
const TOGGLE_PARTICIPANTS = 'calling/TOGGLE_PARTICIPANTS';
const TOGGLE_PIP = 'calling/TOGGLE_PIP';
const TOGGLE_SELF_VIEW_EXPANDED = 'calling/TOGGLE_SELF_VIEW_EXPANDED';
const TOGGLE_SETTINGS = 'calling/TOGGLE_SETTINGS';
const SWITCH_TO_PRESENTATION_VIEW = 'calling/SWITCH_TO_PRESENTATION_VIEW';
const SWITCH_FROM_PRESENTATION_VIEW = 'calling/SWITCH_FROM_PRESENTATION_VIEW';
@@ -941,9 +943,11 @@ type ToggleParticipantsActionType = ReadonlyDeep<{
}>;
type TogglePipActionType = ReadonlyDeep<{
type: 'calling/TOGGLE_PIP';
type: typeof TOGGLE_PIP;
}>;
type ToggleSelfViewExpandedActionType = ReadonlyDeep<{
type: typeof TOGGLE_SELF_VIEW_EXPANDED;
}>;
type ToggleSettingsActionType = ReadonlyDeep<{
type: 'calling/TOGGLE_SETTINGS';
}>;
@@ -1008,6 +1012,7 @@ export type CallingActionType =
| ToggleNeedsScreenRecordingPermissionsActionType
| ToggleParticipantsActionType
| TogglePipActionType
| ToggleSelfViewExpandedActionType
| SetPresentingFulfilledActionType
| ToggleSettingsActionType
| SuggestLowerHandActionType
@@ -2665,6 +2670,12 @@ function togglePip(): TogglePipActionType {
};
}
function toggleSelfViewExpanded(): ToggleSelfViewExpandedActionType {
return {
type: TOGGLE_SELF_VIEW_EXPANDED,
};
}
function toggleScreenRecordingPermissionsDialog(): ToggleNeedsScreenRecordingPermissionsActionType {
return {
type: TOGGLE_NEEDS_SCREEN_RECORDING_PERMISSIONS,
@@ -2757,6 +2768,7 @@ export const actions = {
toggleParticipants,
togglePip,
toggleScreenRecordingPermissionsDialog,
toggleSelfViewExpanded,
toggleSettings,
updateCallLinkName,
updateCallLinkRestrictions,
@@ -3052,6 +3064,7 @@ export function reducer(
localAudioLevel: 0,
viewMode: CallViewMode.Paginated,
pip: false,
selfViewExpanded: false,
settingsDialogOpen: false,
showParticipantsList: false,
outgoingRing,
@@ -3098,6 +3111,7 @@ export function reducer(
localAudioLevel: 0,
viewMode: CallViewMode.Paginated,
pip: false,
selfViewExpanded: false,
settingsDialogOpen: false,
showParticipantsList: false,
outgoingRing: true,
@@ -3130,6 +3144,7 @@ export function reducer(
localAudioLevel: 0,
viewMode: CallViewMode.Paginated,
pip: false,
selfViewExpanded: false,
settingsDialogOpen: false,
showParticipantsList: false,
outgoingRing: false,
@@ -3342,6 +3357,7 @@ export function reducer(
viewMode: CallViewMode.Paginated,
pip: false,
settingsDialogOpen: false,
selfViewExpanded: false,
showParticipantsList: false,
outgoingRing: true,
joinedAt: null,
@@ -4038,6 +4054,21 @@ export function reducer(
},
};
}
if (action.type === TOGGLE_SELF_VIEW_EXPANDED) {
const { activeCallState } = state;
if (activeCallState?.state !== 'Active') {
log.warn('Cannot toggle PiP when there is no active call');
return state;
}
return {
...state,
activeCallState: {
...activeCallState,
selfViewExpanded: !activeCallState.selfViewExpanded,
},
};
}
if (action.type === SET_PRESENTING) {
const { activeCallState } = state;

View File

@@ -181,6 +181,7 @@ const mapStateToActiveCallProp = (
presentingSource: activeCallState.presentingSource,
presentingSourcesAvailable: activeCallState.presentingSourcesAvailable,
settingsDialogOpen: activeCallState.settingsDialogOpen,
selfViewExpanded: activeCallState.selfViewExpanded,
showNeedsScreenRecordingPermissionsWarning: Boolean(
activeCallState.showNeedsScreenRecordingPermissionsWarning
),
@@ -465,6 +466,7 @@ export const SmartCallManager = memo(function SmartCallManager() {
hangUpActiveCall,
togglePip,
toggleScreenRecordingPermissionsDialog,
toggleSelfViewExpanded,
toggleSettings,
} = useCallingActions();
const { pauseVoiceNotePlayer } = useAudioPlayerActions();
@@ -533,6 +535,7 @@ export const SmartCallManager = memo(function SmartCallManager() {
toggleScreenRecordingPermissionsDialog={
toggleScreenRecordingPermissionsDialog
}
toggleSelfViewExpanded={toggleSelfViewExpanded}
toggleSettings={toggleSettings}
/>
);

View File

@@ -88,6 +88,7 @@ describe('calling duck', () => {
showParticipantsList: false,
outgoingRing: true,
pip: false,
selfViewExpanded: false,
settingsDialogOpen: false,
joinedAt: null,
},
@@ -193,6 +194,7 @@ describe('calling duck', () => {
showParticipantsList: false,
outgoingRing: false,
pip: false,
selfViewExpanded: false,
settingsDialogOpen: false,
joinedAt: null,
};
@@ -495,6 +497,7 @@ describe('calling duck', () => {
showParticipantsList: false,
outgoingRing: false,
pip: false,
selfViewExpanded: false,
settingsDialogOpen: false,
joinedAt: null,
} satisfies ActiveCallStateType);
@@ -590,6 +593,7 @@ describe('calling duck', () => {
showParticipantsList: false,
outgoingRing: false,
pip: false,
selfViewExpanded: false,
settingsDialogOpen: false,
joinedAt: null,
} satisfies ActiveCallStateType);
@@ -1223,6 +1227,7 @@ describe('calling duck', () => {
showParticipantsList: false,
outgoingRing: false,
pip: false,
selfViewExpanded: false,
settingsDialogOpen: false,
joinedAt: null,
} satisfies ActiveCallStateType);
@@ -2310,6 +2315,7 @@ describe('calling duck', () => {
viewMode: CallViewMode.Paginated,
showParticipantsList: false,
pip: false,
selfViewExpanded: false,
settingsDialogOpen: false,
outgoingRing: true,
joinedAt: null,
@@ -2588,6 +2594,7 @@ describe('calling duck', () => {
joinedAt: null,
outgoingRing: true,
pip: false,
selfViewExpanded: false,
settingsDialogOpen: false,
showParticipantsList: false,
},
@@ -2631,6 +2638,7 @@ describe('calling duck', () => {
joinedAt: null,
outgoingRing: true,
pip: false,
selfViewExpanded: false,
settingsDialogOpen: false,
showParticipantsList: false,
},
@@ -2675,6 +2683,7 @@ describe('calling duck', () => {
joinedAt: null,
outgoingRing: true,
pip: false,
selfViewExpanded: false,
settingsDialogOpen: false,
showParticipantsList: false,
},
@@ -2709,6 +2718,7 @@ describe('calling duck', () => {
viewMode: CallViewMode.Paginated,
showParticipantsList: false,
pip: false,
selfViewExpanded: false,
settingsDialogOpen: false,
outgoingRing: true,
joinedAt: null,
@@ -2735,6 +2745,7 @@ describe('calling duck', () => {
joinedAt: null,
outgoingRing: true,
pip: false,
selfViewExpanded: false,
settingsDialogOpen: false,
showParticipantsList: false,
},

View File

@@ -78,6 +78,7 @@ describe('state/selectors/calling', () => {
showParticipantsList: false,
outgoingRing: true,
pip: false,
selfViewExpanded: false,
settingsDialogOpen: false,
joinedAt: null,
},

View File

@@ -57,6 +57,7 @@ export type ActiveCallBaseType = {
presentingSource?: PresentedSource;
presentingSourcesAvailable?: ReadonlyArray<PresentableSource>;
settingsDialogOpen: boolean;
selfViewExpanded: boolean;
showNeedsScreenRecordingPermissionsWarning?: boolean;
showParticipantsList: boolean;
reactions?: ActiveCallReactionsType;