mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2026-09-12 11:37:39 +01:00
Calling PiP enhancements
This commit is contained in:
@@ -3894,6 +3894,13 @@ button.module-image__border-overlay:focus {
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
|
||||
&--darken {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-color: rgba(variables.$color-gray-60, 0.6);
|
||||
}
|
||||
|
||||
&--blur {
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
@@ -4636,13 +4643,16 @@ button.module-image__border-overlay:focus {
|
||||
0px 0px 8px rgba(0, 0, 0, 0.05),
|
||||
0px 8px 20px rgba(0, 0, 0, 0.3);
|
||||
cursor: grab;
|
||||
// This is just a starting height; the component will figure out what height it should
|
||||
// be, given the aspect ratio of the provided video, pinning the width.
|
||||
// These both should be kept in sync with the height/width in CallingPip.tsx
|
||||
// This size is just a starting place; the component will figure out what it should
|
||||
// be. When resizing for incoming portrait video, the width will be kept constant. The
|
||||
// width only changes with window size.
|
||||
// These both should be kept in sync with the _normal_ height/width in CallingPip.tsx
|
||||
height: 286px;
|
||||
width: 160px;
|
||||
|
||||
position: fixed;
|
||||
z-index: variables.$z-index-calling-pip;
|
||||
overflow: hidden;
|
||||
|
||||
& .module-ongoing-call__group-call-remote-participant {
|
||||
border-radius: 0;
|
||||
@@ -4676,6 +4686,8 @@ button.module-image__border-overlay:focus {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
inset-inline-start: 8px;
|
||||
// These are the normal widths; the size will change based on whether the window
|
||||
// is large, like the overall pip.
|
||||
height: 54px;
|
||||
width: 80px;
|
||||
|
||||
@@ -4717,7 +4729,7 @@ button.module-image__border-overlay:focus {
|
||||
position: absolute;
|
||||
bottom: 66px;
|
||||
inset-inline-start: 8px;
|
||||
transition: bottom 0.3s variables.$ease-out-local-preview 0.3s;
|
||||
transition: bottom 0.5s variables.$ease-out-local-preview;
|
||||
|
||||
&--no-controls {
|
||||
bottom: 8px;
|
||||
@@ -4760,16 +4772,18 @@ button.module-image__border-overlay:focus {
|
||||
|
||||
&__actions {
|
||||
position: absolute;
|
||||
bottom: 4px;
|
||||
// It starts offscreen then animates in/out as it fades in/outs
|
||||
bottom: -60px;
|
||||
inset-inline-start: 4px;
|
||||
inset-inline-end: 4px;
|
||||
padding: 12px;
|
||||
height: 56px;
|
||||
opacity: 0;
|
||||
transition: opacity 1s ease-in-out;
|
||||
transition: all 0.5s variables.$ease-out-local-preview;
|
||||
|
||||
&--visible {
|
||||
opacity: 1;
|
||||
bottom: 4px;
|
||||
}
|
||||
|
||||
display: flex;
|
||||
@@ -4780,13 +4794,20 @@ button.module-image__border-overlay:focus {
|
||||
justify-content: space-around;
|
||||
background-color: variables.$color-gray-78;
|
||||
|
||||
&__spacer {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
&__button {
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
}
|
||||
&__middle-button {
|
||||
flex-grow: 1;
|
||||
// icon width, and 15px of border on each side
|
||||
width: 62px;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
.CallingButton__icon {
|
||||
@@ -4801,7 +4822,7 @@ button.module-image__border-overlay:focus {
|
||||
inset-inline-end: 16px;
|
||||
|
||||
opacity: 0;
|
||||
transition: opacity 1s ease-in-out;
|
||||
transition: opacity 0.5s variables.$ease-out-local-preview;
|
||||
|
||||
&--visible {
|
||||
opacity: 1;
|
||||
|
||||
@@ -8,12 +8,14 @@ export type PropsType = {
|
||||
avatarUrl?: string;
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
darken?: boolean;
|
||||
};
|
||||
|
||||
export function CallBackgroundBlur({
|
||||
avatarUrl,
|
||||
children,
|
||||
className,
|
||||
darken,
|
||||
}: PropsType): JSX.Element {
|
||||
return (
|
||||
<div
|
||||
@@ -31,6 +33,7 @@ export function CallBackgroundBlur({
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{darken && <div className="module-calling__background--darken" />}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -90,15 +90,19 @@ export type PropsType = {
|
||||
toggleVideo: () => void;
|
||||
};
|
||||
|
||||
const PIP_STARTING_HEIGHT = 286;
|
||||
const PIP_WIDTH = 160;
|
||||
const PIP_STARTING_HEIGHT_NORMAL = 286;
|
||||
const PIP_STARTING_HEIGHT_LARGE = 400;
|
||||
const LARGE_THRESHOLD = 1200;
|
||||
|
||||
export const PIP_WIDTH_NORMAL = 160;
|
||||
const PIP_WIDTH_LARGE = 224;
|
||||
const PIP_TOP_MARGIN = 78;
|
||||
const PIP_PADDING = 8;
|
||||
|
||||
// Receiving portrait video will cause the PIP to update to match that video size, but
|
||||
// we need limits
|
||||
export const PIP_MINIMUM_HEIGHT = 180;
|
||||
export const PIP_MAXIMUM_HEIGHT = 360;
|
||||
export const PIP_MINIMUM_HEIGHT_MULTIPLIER = 1.2;
|
||||
export const PIP_MAXIMUM_HEIGHT_MULTIPLIER = 2;
|
||||
|
||||
export function CallingPip({
|
||||
activeCall,
|
||||
@@ -120,7 +124,6 @@ export function CallingPip({
|
||||
|
||||
const videoContainerRef = React.useRef<null | HTMLDivElement>(null);
|
||||
|
||||
const [height, setHeight] = React.useState(PIP_STARTING_HEIGHT);
|
||||
const [windowWidth, setWindowWidth] = React.useState(window.innerWidth);
|
||||
const [windowHeight, setWindowHeight] = React.useState(window.innerHeight);
|
||||
const [positionState, setPositionState] = React.useState<PositionState>({
|
||||
@@ -128,6 +131,14 @@ export function CallingPip({
|
||||
offsetY: PIP_TOP_MARGIN,
|
||||
});
|
||||
|
||||
const isWindowLarge = windowWidth >= LARGE_THRESHOLD;
|
||||
const [height, setHeight] = React.useState(
|
||||
isWindowLarge ? PIP_STARTING_HEIGHT_LARGE : PIP_STARTING_HEIGHT_NORMAL
|
||||
);
|
||||
const [width, setWidth] = React.useState(
|
||||
isWindowLarge ? PIP_WIDTH_LARGE : PIP_WIDTH_NORMAL
|
||||
);
|
||||
|
||||
useActivateSpeakerViewOnPresenting({
|
||||
remoteParticipants: activeCall.remoteParticipants,
|
||||
switchToPresentationView,
|
||||
@@ -164,11 +175,11 @@ export function CallingPip({
|
||||
let distanceToLeftEdge: number;
|
||||
let distanceToRightEdge: number;
|
||||
if (isRTL) {
|
||||
distanceToLeftEdge = innerWidth - (offsetX + PIP_WIDTH);
|
||||
distanceToLeftEdge = innerWidth - (offsetX + width);
|
||||
distanceToRightEdge = offsetX;
|
||||
} else {
|
||||
distanceToLeftEdge = offsetX;
|
||||
distanceToRightEdge = innerWidth - (offsetX + PIP_WIDTH);
|
||||
distanceToRightEdge = innerWidth - (offsetX + width);
|
||||
}
|
||||
|
||||
const snapCandidates: Array<SnapCandidate> = [
|
||||
@@ -208,14 +219,14 @@ export function CallingPip({
|
||||
case PositionMode.SnapToBottom:
|
||||
setPositionState({
|
||||
mode: snapTo.mode,
|
||||
offsetX: isRTL ? innerWidth - (offsetX + PIP_WIDTH) : offsetX,
|
||||
offsetX: isRTL ? innerWidth - (offsetX + width) : offsetX,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
throw missingCaseError(snapTo.mode);
|
||||
}
|
||||
}
|
||||
}, [height, isRTL, positionState, setPositionState]);
|
||||
}, [height, isRTL, positionState, setPositionState, width]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (positionState.mode === PositionMode.BeingDragged) {
|
||||
@@ -248,6 +259,17 @@ export function CallingPip({
|
||||
};
|
||||
}, []);
|
||||
|
||||
// This only runs when isWindowLarge changes, so we aggressively change height + width
|
||||
React.useEffect(() => {
|
||||
if (isWindowLarge) {
|
||||
setHeight(PIP_STARTING_HEIGHT_LARGE);
|
||||
setWidth(PIP_WIDTH_LARGE);
|
||||
} else {
|
||||
setHeight(PIP_STARTING_HEIGHT_NORMAL);
|
||||
setWidth(PIP_WIDTH_NORMAL);
|
||||
}
|
||||
}, [isWindowLarge, setHeight, setWidth]);
|
||||
|
||||
const [translateX, translateY] = React.useMemo<[number, number]>(() => {
|
||||
const topMin = PIP_TOP_MARGIN;
|
||||
const bottomMax = windowHeight - PIP_PADDING - height;
|
||||
@@ -256,7 +278,7 @@ export function CallingPip({
|
||||
const leftMin = PIP_PADDING + leftScrollPadding;
|
||||
|
||||
const rightScrollPadding = isRTL ? 0 : 1;
|
||||
const rightMax = windowWidth - PIP_PADDING - PIP_WIDTH - rightScrollPadding;
|
||||
const rightMax = windowWidth - PIP_PADDING - width - rightScrollPadding;
|
||||
|
||||
switch (positionState.mode) {
|
||||
case PositionMode.BeingDragged:
|
||||
@@ -264,7 +286,7 @@ export function CallingPip({
|
||||
isRTL
|
||||
? windowWidth -
|
||||
positionState.mouseX -
|
||||
(PIP_WIDTH - positionState.dragOffsetX)
|
||||
(width - positionState.dragOffsetX)
|
||||
: positionState.mouseX - positionState.dragOffsetX,
|
||||
positionState.mouseY - positionState.dragOffsetY,
|
||||
];
|
||||
@@ -291,7 +313,7 @@ export function CallingPip({
|
||||
default:
|
||||
throw missingCaseError(positionState);
|
||||
}
|
||||
}, [height, isRTL, windowWidth, windowHeight, positionState]);
|
||||
}, [height, isRTL, width, windowWidth, windowHeight, positionState]);
|
||||
const localizedTranslateX = isRTL ? -translateX : translateX;
|
||||
|
||||
const [showControls, setShowControls] = React.useState(false);
|
||||
@@ -356,13 +378,17 @@ export function CallingPip({
|
||||
const isLonelyInCall = !activeCall.remoteParticipants.length;
|
||||
const isSendingVideo =
|
||||
activeCall.hasLocalVideo || activeCall.presentingSource;
|
||||
const avatarSize = isWindowLarge
|
||||
? AvatarSize.NINETY_SIX
|
||||
: AvatarSize.SIXTY_FOUR;
|
||||
|
||||
if (isLonelyInCall) {
|
||||
remoteVideoNode = (
|
||||
<div className="module-calling-pip__video--remote">
|
||||
{isSendingVideo ? (
|
||||
// TODO: DESKTOP-8537 - when black bars go away, need to make some CSS changes
|
||||
<>
|
||||
<CallBackgroundBlur avatarUrl={me.avatarUrl} />
|
||||
<CallBackgroundBlur avatarUrl={me.avatarUrl} darken />
|
||||
<div
|
||||
className={classNames(
|
||||
'module-calling-pip__full-size-local-preview',
|
||||
@@ -387,7 +413,7 @@ export function CallingPip({
|
||||
phoneNumber={me.phoneNumber}
|
||||
profileName={me.profileName}
|
||||
title={me.title}
|
||||
size={AvatarSize.FORTY_EIGHT}
|
||||
size={avatarSize}
|
||||
sharedGroupNames={[]}
|
||||
/>
|
||||
</div>
|
||||
@@ -405,13 +431,15 @@ export function CallingPip({
|
||||
setRendererCanvas={setRendererCanvas}
|
||||
setGroupCallVideoRequest={setGroupCallVideoRequest}
|
||||
height={height}
|
||||
width={PIP_WIDTH}
|
||||
width={width}
|
||||
updateHeight={(newHeight: number) => {
|
||||
setHeight(newHeight);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const localVideoWidth = isWindowLarge ? 120 : 80;
|
||||
const localVideoHeight = isWindowLarge ? 80 : 54;
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
|
||||
@@ -449,6 +477,7 @@ export function CallingPip({
|
||||
ref={videoContainerRef}
|
||||
style={{
|
||||
height: `${height}px`,
|
||||
width: `${width}px`,
|
||||
cursor:
|
||||
positionState.mode === PositionMode.BeingDragged
|
||||
? '-webkit-grabbing'
|
||||
@@ -463,7 +492,14 @@ export function CallingPip({
|
||||
{remoteVideoNode}
|
||||
|
||||
{!isLonelyInCall && activeCall.hasLocalVideo ? (
|
||||
<div className={localVideoClassName} ref={setLocalPreviewContainer} />
|
||||
<div
|
||||
style={{
|
||||
height: `${localVideoHeight}px`,
|
||||
width: `${localVideoWidth}px`,
|
||||
}}
|
||||
className={localVideoClassName}
|
||||
ref={setLocalPreviewContainer}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<div
|
||||
@@ -520,6 +556,7 @@ export function CallingPip({
|
||||
showControls ? 'module-calling-pip__actions--visible' : undefined
|
||||
)}
|
||||
>
|
||||
<div className="module-calling-pip__actions__spacer" />
|
||||
<div className="module-calling-pip__actions__button">
|
||||
<CallingButton
|
||||
buttonType={videoButtonType}
|
||||
@@ -550,6 +587,7 @@ export function CallingPip({
|
||||
tooltipDirection={TooltipPlacement.Top}
|
||||
/>
|
||||
</div>
|
||||
<div className="module-calling-pip__actions__spacer" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -27,15 +27,23 @@ import { isReconnecting } from '../util/callingIsReconnecting';
|
||||
import { isGroupOrAdhocActiveCall } from '../util/isGroupOrAdhocCall';
|
||||
import { assertDev } from '../util/assert';
|
||||
import type { CallingImageDataCache } from './CallManager';
|
||||
import { PIP_MAXIMUM_HEIGHT, PIP_MINIMUM_HEIGHT } from './CallingPip';
|
||||
import {
|
||||
PIP_MAXIMUM_HEIGHT_MULTIPLIER,
|
||||
PIP_MINIMUM_HEIGHT_MULTIPLIER,
|
||||
PIP_WIDTH_NORMAL,
|
||||
} from './CallingPip';
|
||||
|
||||
function BlurredBackground({
|
||||
activeCall,
|
||||
activeGroupCallSpeaker,
|
||||
avatarSize,
|
||||
darken,
|
||||
i18n,
|
||||
}: {
|
||||
activeCall: ActiveCallType;
|
||||
activeGroupCallSpeaker?: undefined | GroupCallRemoteParticipantType;
|
||||
avatarSize: AvatarSize;
|
||||
darken?: boolean;
|
||||
i18n: LocalizerType;
|
||||
}): JSX.Element {
|
||||
const {
|
||||
@@ -51,7 +59,7 @@ function BlurredBackground({
|
||||
activeGroupCallSpeaker?.avatarUrl ?? activeCall.conversation.avatarUrl;
|
||||
|
||||
return (
|
||||
<CallBackgroundBlur avatarUrl={avatarUrl}>
|
||||
<CallBackgroundBlur avatarUrl={avatarUrl} darken={darken}>
|
||||
<div className="module-calling-pip__video--avatar">
|
||||
<Avatar
|
||||
avatarPlaceholderGradient={avatarPlaceholderGradient}
|
||||
@@ -64,7 +72,7 @@ function BlurredBackground({
|
||||
phoneNumber={phoneNumber}
|
||||
profileName={profileName}
|
||||
title={title}
|
||||
size={AvatarSize.FORTY_EIGHT}
|
||||
size={avatarSize}
|
||||
sharedGroupNames={sharedGroupNames}
|
||||
/>
|
||||
</div>
|
||||
@@ -129,16 +137,14 @@ export function CallingPipRemoteVideo({
|
||||
return;
|
||||
}
|
||||
|
||||
const newHeight = clamp(
|
||||
Math.floor(width * (1 / videoAspectRatio)),
|
||||
1,
|
||||
MAX_FRAME_HEIGHT
|
||||
);
|
||||
const ratio = 1 / videoAspectRatio;
|
||||
const newHeight = clamp(Math.floor(width * ratio), 1, MAX_FRAME_HEIGHT);
|
||||
|
||||
// Update only for portrait video that fits, otherwise leave things as they are
|
||||
if (
|
||||
newHeight !== height &&
|
||||
newHeight >= PIP_MINIMUM_HEIGHT &&
|
||||
newHeight <= PIP_MAXIMUM_HEIGHT
|
||||
ratio >= PIP_MINIMUM_HEIGHT_MULTIPLIER &&
|
||||
ratio <= PIP_MAXIMUM_HEIGHT_MULTIPLIER
|
||||
) {
|
||||
updateHeight(newHeight);
|
||||
}
|
||||
@@ -179,13 +185,20 @@ export function CallingPipRemoteVideo({
|
||||
width,
|
||||
]);
|
||||
|
||||
const avatarSize =
|
||||
width > PIP_WIDTH_NORMAL ? AvatarSize.NINETY_SIX : AvatarSize.SIXTY_FOUR;
|
||||
|
||||
switch (activeCall.callMode) {
|
||||
case CallMode.Direct: {
|
||||
const { hasRemoteVideo } = activeCall.remoteParticipants[0];
|
||||
if (!hasRemoteVideo) {
|
||||
return (
|
||||
<div className="module-calling-pip__video--remote">
|
||||
<BlurredBackground activeCall={activeCall} i18n={i18n} />
|
||||
<BlurredBackground
|
||||
activeCall={activeCall}
|
||||
avatarSize={avatarSize}
|
||||
i18n={i18n}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -196,7 +209,12 @@ export function CallingPipRemoteVideo({
|
||||
// TODO: DESKTOP-8537 - when black bars go away, we need to make some CSS changes
|
||||
return (
|
||||
<div className="module-calling-pip__video--remote">
|
||||
<BlurredBackground activeCall={activeCall} i18n={i18n} />
|
||||
<BlurredBackground
|
||||
activeCall={activeCall}
|
||||
avatarSize={avatarSize}
|
||||
darken
|
||||
i18n={i18n}
|
||||
/>
|
||||
<DirectCallRemoteParticipant
|
||||
conversation={conversation}
|
||||
hasRemoteVideo={hasRemoteVideo}
|
||||
@@ -212,7 +230,11 @@ export function CallingPipRemoteVideo({
|
||||
if (!activeGroupCallSpeaker) {
|
||||
return (
|
||||
<div className="module-calling-pip__video--remote">
|
||||
<BlurredBackground activeCall={activeCall} i18n={i18n} />
|
||||
<BlurredBackground
|
||||
activeCall={activeCall}
|
||||
avatarSize={avatarSize}
|
||||
i18n={i18n}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -221,6 +243,8 @@ export function CallingPipRemoteVideo({
|
||||
<BlurredBackground
|
||||
activeCall={activeCall}
|
||||
activeGroupCallSpeaker={activeGroupCallSpeaker}
|
||||
avatarSize={avatarSize}
|
||||
darken={activeGroupCallSpeaker.hasRemoteVideo}
|
||||
i18n={i18n}
|
||||
/>
|
||||
<GroupCallRemoteParticipant
|
||||
|
||||
Reference in New Issue
Block a user