diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss index 108c087a66..74686820d9 100644 --- a/stylesheets/_modules.scss +++ b/stylesheets/_modules.scss @@ -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; diff --git a/ts/components/CallBackgroundBlur.tsx b/ts/components/CallBackgroundBlur.tsx index 0640111960..ce046620ba 100644 --- a/ts/components/CallBackgroundBlur.tsx +++ b/ts/components/CallBackgroundBlur.tsx @@ -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 (
)} + {darken &&
} {children}
); diff --git a/ts/components/CallingPip.tsx b/ts/components/CallingPip.tsx index 7a95c61bc6..5384a94866 100644 --- a/ts/components/CallingPip.tsx +++ b/ts/components/CallingPip.tsx @@ -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); - 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({ @@ -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 = [ @@ -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 = (
{isSendingVideo ? ( // TODO: DESKTOP-8537 - when black bars go away, need to make some CSS changes <> - +
@@ -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 ? ( -
+
) : null}
+
+
); diff --git a/ts/components/CallingPipRemoteVideo.tsx b/ts/components/CallingPipRemoteVideo.tsx index 4a5b061e3b..e649fd4966 100644 --- a/ts/components/CallingPipRemoteVideo.tsx +++ b/ts/components/CallingPipRemoteVideo.tsx @@ -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 ( - +
@@ -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 (
- +
); } @@ -196,7 +209,12 @@ export function CallingPipRemoteVideo({ // TODO: DESKTOP-8537 - when black bars go away, we need to make some CSS changes return (
- + - +
); } @@ -221,6 +243,8 @@ export function CallingPipRemoteVideo({