Calling: New option to expand your local preview
This commit is contained in:
@@ -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"
|
||||
|
1
images/icons/v3/maximize/maximize.svg
Normal file
1
images/icons/v3/maximize/maximize.svg
Normal 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 |
1
images/icons/v3/minimize/minimize.svg
Normal file
1
images/icons/v3/minimize/minimize.svg
Normal 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 |
@@ -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 {
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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 {
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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,
|
||||
};
|
||||
};
|
||||
|
@@ -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>
|
||||
|
@@ -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({
|
||||
|
@@ -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',
|
||||
|
@@ -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"
|
||||
|
@@ -51,6 +51,7 @@ const getCommonActiveCallData = (overrides: Overrides) => ({
|
||||
joinedAt: Date.now() - MINUTE,
|
||||
outgoingRing: true,
|
||||
pip: true,
|
||||
selfViewExpanded: false,
|
||||
settingsDialogOpen: false,
|
||||
showParticipantsList: false,
|
||||
});
|
||||
|
@@ -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']],
|
||||
},
|
||||
|
@@ -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;
|
||||
|
@@ -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}
|
||||
/>
|
||||
);
|
||||
|
@@ -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,
|
||||
},
|
||||
|
@@ -78,6 +78,7 @@ describe('state/selectors/calling', () => {
|
||||
showParticipantsList: false,
|
||||
outgoingRing: true,
|
||||
pip: false,
|
||||
selfViewExpanded: false,
|
||||
settingsDialogOpen: false,
|
||||
joinedAt: null,
|
||||
},
|
||||
|
@@ -57,6 +57,7 @@ export type ActiveCallBaseType = {
|
||||
presentingSource?: PresentedSource;
|
||||
presentingSourcesAvailable?: ReadonlyArray<PresentableSource>;
|
||||
settingsDialogOpen: boolean;
|
||||
selfViewExpanded: boolean;
|
||||
showNeedsScreenRecordingPermissionsWarning?: boolean;
|
||||
showParticipantsList: boolean;
|
||||
reactions?: ActiveCallReactionsType;
|
||||
|
Reference in New Issue
Block a user