721 lines
21 KiB
TypeScript
721 lines
21 KiB
TypeScript
import { Participant, Track } from "livekit-client";
|
|
import VideoViewer from "./videoViewer";
|
|
import { useLocation } from "@tanstack/react-router";
|
|
import { getRoom, openCallPage, stopWatchingStream, useCall } from "../store";
|
|
import { useState, useRef, useEffect, useCallback } from "react";
|
|
import {
|
|
Avatar,
|
|
AvatarFallback,
|
|
AvatarImage,
|
|
Button,
|
|
Card,
|
|
cn,
|
|
useIsMobile,
|
|
useSidebar,
|
|
} from "@methanium/ui";
|
|
import { ScreenShareOff } from "lucide-react";
|
|
import { useUserFields } from "@tensamin/user/context";
|
|
import { useIsSpeaking, useLastSpeakingParticipantId } from "../speakingState";
|
|
import { getAverageImageColor } from "./modals/base";
|
|
|
|
const USER_FIELDS = ["Avatar", "Display"] as const;
|
|
|
|
function getTrackPublicationBySource(
|
|
participant: Participant | undefined,
|
|
source: Track.Source,
|
|
) {
|
|
if (!participant) {
|
|
return undefined;
|
|
}
|
|
|
|
return [...participant.trackPublications.values()].find(
|
|
(publication) => publication.source === source,
|
|
);
|
|
}
|
|
|
|
type Positions = "top-left" | "top-right" | "bottom-left" | "bottom-right";
|
|
type ResizeEdge =
|
|
| "top"
|
|
| "right"
|
|
| "bottom"
|
|
| "left"
|
|
| "top-left"
|
|
| "top-right"
|
|
| "bottom-right"
|
|
| "bottom-left";
|
|
type Point = {
|
|
x: number;
|
|
y: number;
|
|
};
|
|
|
|
const MARGIN = 40;
|
|
const MIN_SIZE = 240;
|
|
const MAX_SIZE = 1000;
|
|
const ASPECT_RATIO = 9 / 16;
|
|
const MOBILE_MARGIN = 16;
|
|
const MOBILE_PILL_WIDTH = 128;
|
|
const MOBILE_PILL_HEIGHT = 48;
|
|
|
|
function MobileCallPill({
|
|
active,
|
|
callId,
|
|
}: {
|
|
active: boolean;
|
|
callId: string;
|
|
}) {
|
|
const { setOpenMobile } = useSidebar();
|
|
const lastSpeakingParticipantId = useLastSpeakingParticipantId();
|
|
const isSpeaking = useIsSpeaking(lastSpeakingParticipantId ?? -1);
|
|
const { data: lastSpeakingUser } = useUserFields(
|
|
lastSpeakingParticipantId,
|
|
USER_FIELDS,
|
|
);
|
|
const [avatarBackgroundColor, setAvatarBackgroundColor] = useState<
|
|
string | undefined
|
|
>(undefined);
|
|
const safeAreaRef = useRef<HTMLDivElement>(null);
|
|
const pillRef = useRef<HTMLDivElement>(null);
|
|
const initialCoords = {
|
|
x: window.innerWidth - MOBILE_PILL_WIDTH - MOBILE_MARGIN,
|
|
y: MOBILE_MARGIN,
|
|
};
|
|
const coordsRef = useRef<Point>(initialCoords);
|
|
const dragOffsetRef = useRef<Point>({ x: 0, y: 0 });
|
|
const dragStartRef = useRef<Point>({ x: 0, y: 0 });
|
|
const movedRef = useRef(false);
|
|
const [position, setPosition] = useState<Positions>("top-right");
|
|
const [coords, setCoords] = useState(initialCoords);
|
|
const [isDragging, setIsDragging] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (!lastSpeakingUser?.Avatar) {
|
|
setAvatarBackgroundColor(undefined);
|
|
return;
|
|
}
|
|
|
|
let mounted = true;
|
|
|
|
void getAverageImageColor(lastSpeakingUser.Avatar).then((color) => {
|
|
if (mounted) {
|
|
setAvatarBackgroundColor(color);
|
|
}
|
|
});
|
|
|
|
return () => {
|
|
mounted = false;
|
|
};
|
|
}, [lastSpeakingUser?.Avatar]);
|
|
|
|
const getSafeArea = useCallback(() => {
|
|
const element = safeAreaRef.current;
|
|
if (!element) return { top: 0, right: 0, bottom: 0, left: 0 };
|
|
const style = getComputedStyle(element);
|
|
return {
|
|
top: parseFloat(style.paddingTop) || 0,
|
|
right: parseFloat(style.paddingRight) || 0,
|
|
bottom: parseFloat(style.paddingBottom) || 0,
|
|
left: parseFloat(style.paddingLeft) || 0,
|
|
};
|
|
}, []);
|
|
|
|
const getCoordsForPosition = useCallback(
|
|
(nextPosition: Positions): Point => {
|
|
const bounds = pillRef.current?.getBoundingClientRect();
|
|
const width = bounds?.width ?? MOBILE_PILL_WIDTH;
|
|
const height = bounds?.height ?? MOBILE_PILL_HEIGHT;
|
|
const safeArea = getSafeArea();
|
|
|
|
return {
|
|
x: nextPosition.endsWith("right")
|
|
? window.innerWidth - safeArea.right - width - MOBILE_MARGIN
|
|
: safeArea.left + MOBILE_MARGIN,
|
|
y: nextPosition.startsWith("bottom")
|
|
? window.innerHeight - safeArea.bottom - height - MOBILE_MARGIN
|
|
: safeArea.top + MOBILE_MARGIN,
|
|
};
|
|
},
|
|
[getSafeArea],
|
|
);
|
|
|
|
const setCoordsSafe = useCallback((next: Point) => {
|
|
coordsRef.current = next;
|
|
setCoords(next);
|
|
}, []);
|
|
|
|
const snapToPosition = useCallback(
|
|
(nextPosition: Positions) => {
|
|
setPosition(nextPosition);
|
|
setCoordsSafe(getCoordsForPosition(nextPosition));
|
|
},
|
|
[getCoordsForPosition, setCoordsSafe],
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (active && !isDragging) {
|
|
snapToPosition(position);
|
|
}
|
|
}, [active, isDragging, position, snapToPosition]);
|
|
|
|
useEffect(() => {
|
|
const handleResize = () => snapToPosition(position);
|
|
|
|
window.addEventListener("resize", handleResize);
|
|
return () => window.removeEventListener("resize", handleResize);
|
|
}, [position, snapToPosition]);
|
|
|
|
if (!active) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<div
|
|
ref={safeAreaRef}
|
|
className="pointer-events-none fixed inset-0 z-200 pt-[env(safe-area-inset-top)] pr-[env(safe-area-inset-right)] pb-[env(safe-area-inset-bottom)] pl-[env(safe-area-inset-left)]"
|
|
>
|
|
<Card
|
|
ref={pillRef}
|
|
onClick={() => {
|
|
if (movedRef.current) {
|
|
movedRef.current = false;
|
|
return;
|
|
}
|
|
|
|
setOpenMobile(false);
|
|
void openCallPage(callId);
|
|
}}
|
|
onPointerDown={(event) => {
|
|
if (event.button !== 0) return;
|
|
|
|
event.currentTarget.setPointerCapture(event.pointerId);
|
|
dragOffsetRef.current = {
|
|
x: event.clientX - coordsRef.current.x,
|
|
y: event.clientY - coordsRef.current.y,
|
|
};
|
|
dragStartRef.current = { x: event.clientX, y: event.clientY };
|
|
movedRef.current = false;
|
|
setIsDragging(true);
|
|
}}
|
|
onPointerMove={(event) => {
|
|
if (!isDragging) return;
|
|
|
|
const bounds = pillRef.current?.getBoundingClientRect();
|
|
const width = bounds?.width ?? MOBILE_PILL_WIDTH;
|
|
const height = bounds?.height ?? MOBILE_PILL_HEIGHT;
|
|
const safeArea = getSafeArea();
|
|
const next = {
|
|
x: Math.min(
|
|
window.innerWidth - safeArea.right - width - MOBILE_MARGIN,
|
|
Math.max(
|
|
safeArea.left + MOBILE_MARGIN,
|
|
event.clientX - dragOffsetRef.current.x,
|
|
),
|
|
),
|
|
y: Math.min(
|
|
window.innerHeight - safeArea.bottom - height - MOBILE_MARGIN,
|
|
Math.max(
|
|
safeArea.top + MOBILE_MARGIN,
|
|
event.clientY - dragOffsetRef.current.y,
|
|
),
|
|
),
|
|
};
|
|
|
|
if (
|
|
Math.abs(event.clientX - dragStartRef.current.x) > 3 ||
|
|
Math.abs(event.clientY - dragStartRef.current.y) > 3
|
|
) {
|
|
movedRef.current = true;
|
|
}
|
|
|
|
setCoordsSafe(next);
|
|
}}
|
|
onPointerUp={(event) => {
|
|
if (!isDragging) return;
|
|
|
|
const nextPosition = `${
|
|
event.clientY < window.innerHeight / 2 ? "top" : "bottom"
|
|
}-${event.clientX < window.innerWidth / 2 ? "left" : "right"}` as Positions;
|
|
|
|
setIsDragging(false);
|
|
snapToPosition(nextPosition);
|
|
}}
|
|
onPointerCancel={() => {
|
|
setIsDragging(false);
|
|
snapToPosition(position);
|
|
}}
|
|
className={cn(
|
|
"pointer-events-auto fixed left-0 top-0 z-200 flex w-23 h-23! touch-none select-none shadow-xl rounded-2xl flex items-center justify-center",
|
|
isSpeaking && "border-3! border-(--primary-foreground-alt)/75!",
|
|
isDragging ? "cursor-grabbing" : "cursor-grab",
|
|
)}
|
|
style={{
|
|
backgroundColor: avatarBackgroundColor,
|
|
transform: `translate3d(${coords.x}px, ${coords.y}px, 0)`,
|
|
transition: isDragging
|
|
? "none"
|
|
: "transform 420ms cubic-bezier(0.34, 1.56, 0.64, 1)",
|
|
willChange: "transform",
|
|
}}
|
|
>
|
|
<Avatar className="size-14">
|
|
<AvatarImage src={lastSpeakingUser?.Avatar} />
|
|
<AvatarFallback className="text-lg">
|
|
{lastSpeakingUser?.Display.slice(0, 2).toUpperCase() ?? "..."}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function Popout({ participant }: { participant: Participant }) {
|
|
const screenSharePublication = getTrackPublicationBySource(
|
|
participant,
|
|
Track.Source.ScreenShare,
|
|
);
|
|
|
|
const popoutRef = useRef<HTMLDivElement>(null);
|
|
const coordsRef = useRef<Point>({ x: MARGIN, y: MARGIN });
|
|
const dragOffsetRef = useRef<Point>({ x: 0, y: 0 });
|
|
const sizeRef = useRef(400);
|
|
const hideOverlayTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(
|
|
null,
|
|
);
|
|
const resizeRef = useRef<{
|
|
size: number;
|
|
clientX: number;
|
|
clientY: number;
|
|
edge: ResizeEdge;
|
|
}>({ size: 400, clientX: 0, clientY: 0, edge: "right" });
|
|
|
|
const [size, setSize] = useState(400);
|
|
const [isDragging, setIsDragging] = useState(false);
|
|
const [isResizing, setIsResizing] = useState(false);
|
|
const [isOverlayVisible, setIsOverlayVisible] = useState(false);
|
|
const [position, setPosition] = useState<Positions>("top-left");
|
|
const [coords, setCoords] = useState<Point>({ x: MARGIN, y: MARGIN });
|
|
|
|
const setCoordsSafe = (next: Point) => {
|
|
coordsRef.current = next;
|
|
setCoords(next);
|
|
};
|
|
|
|
const setSizeSafe = (next: number) => {
|
|
sizeRef.current = next;
|
|
setSize(next);
|
|
};
|
|
|
|
const getMaxSize = useCallback(() => {
|
|
const maxWidth = window.innerWidth - MARGIN * 2;
|
|
const maxHeightWidth = (window.innerHeight - MARGIN * 2) / ASPECT_RATIO;
|
|
|
|
return Math.max(MIN_SIZE, Math.min(MAX_SIZE, maxWidth, maxHeightWidth));
|
|
}, []);
|
|
|
|
const clampSize = useCallback(
|
|
(nextSize: number) => {
|
|
return Math.min(getMaxSize(), Math.max(MIN_SIZE, nextSize));
|
|
},
|
|
[getMaxSize],
|
|
);
|
|
|
|
const getPositionFromPoint = (
|
|
clientX: number,
|
|
clientY: number,
|
|
): Positions => {
|
|
const height = window.innerHeight / 2 > clientY ? "top" : "bottom";
|
|
const width = window.innerWidth / 2 > clientX ? "left" : "right";
|
|
|
|
return `${height}-${width}` as Positions;
|
|
};
|
|
|
|
const getCoordsForPosition = useCallback(
|
|
(nextPosition: Positions, nextSize = size): Point => {
|
|
const width = nextSize;
|
|
const height = nextSize * ASPECT_RATIO;
|
|
|
|
return {
|
|
x: nextPosition.endsWith("right")
|
|
? window.innerWidth - width - MARGIN
|
|
: MARGIN,
|
|
y: nextPosition.startsWith("bottom")
|
|
? window.innerHeight - height - MARGIN
|
|
: MARGIN,
|
|
};
|
|
},
|
|
[size],
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (isDragging) return;
|
|
|
|
setCoordsSafe(getCoordsForPosition(position));
|
|
}, [position, size, isDragging, getCoordsForPosition]);
|
|
|
|
useEffect(() => {
|
|
const handleResize = () => {
|
|
if (isDragging) return;
|
|
|
|
const nextSize = clampSize(size);
|
|
|
|
setSizeSafe(nextSize);
|
|
setCoordsSafe(getCoordsForPosition(position, nextSize));
|
|
};
|
|
|
|
window.addEventListener("resize", handleResize);
|
|
return () => window.removeEventListener("resize", handleResize);
|
|
}, [position, size, isDragging, getCoordsForPosition, clampSize]);
|
|
|
|
useEffect(() => {
|
|
return () => {
|
|
if (hideOverlayTimeoutRef.current) {
|
|
clearTimeout(hideOverlayTimeoutRef.current);
|
|
}
|
|
};
|
|
}, []);
|
|
|
|
const scheduleOverlayHide = () => {
|
|
if (hideOverlayTimeoutRef.current) {
|
|
clearTimeout(hideOverlayTimeoutRef.current);
|
|
}
|
|
|
|
hideOverlayTimeoutRef.current = setTimeout(() => {
|
|
setIsOverlayVisible(false);
|
|
hideOverlayTimeoutRef.current = null;
|
|
}, 3000);
|
|
};
|
|
|
|
const showOverlay = () => {
|
|
setIsOverlayVisible(true);
|
|
scheduleOverlayHide();
|
|
};
|
|
|
|
const hideOverlay = () => {
|
|
if (hideOverlayTimeoutRef.current) {
|
|
clearTimeout(hideOverlayTimeoutRef.current);
|
|
hideOverlayTimeoutRef.current = null;
|
|
}
|
|
|
|
setIsOverlayVisible(false);
|
|
};
|
|
|
|
const getCornerResizeDelta = (
|
|
horizontalDelta: number,
|
|
verticalDelta: number,
|
|
) => {
|
|
return Math.abs(horizontalDelta) > Math.abs(verticalDelta)
|
|
? horizontalDelta
|
|
: verticalDelta;
|
|
};
|
|
|
|
const getResizeDelta = (e: React.PointerEvent, edge: ResizeEdge) => {
|
|
switch (edge) {
|
|
case "left":
|
|
return resizeRef.current.clientX - e.clientX;
|
|
case "right":
|
|
return e.clientX - resizeRef.current.clientX;
|
|
case "top":
|
|
return (resizeRef.current.clientY - e.clientY) / ASPECT_RATIO;
|
|
case "bottom":
|
|
return (e.clientY - resizeRef.current.clientY) / ASPECT_RATIO;
|
|
case "top-left":
|
|
return getCornerResizeDelta(
|
|
resizeRef.current.clientX - e.clientX,
|
|
(resizeRef.current.clientY - e.clientY) / ASPECT_RATIO,
|
|
);
|
|
case "top-right":
|
|
return getCornerResizeDelta(
|
|
e.clientX - resizeRef.current.clientX,
|
|
(resizeRef.current.clientY - e.clientY) / ASPECT_RATIO,
|
|
);
|
|
case "bottom-right":
|
|
return getCornerResizeDelta(
|
|
e.clientX - resizeRef.current.clientX,
|
|
(e.clientY - resizeRef.current.clientY) / ASPECT_RATIO,
|
|
);
|
|
case "bottom-left":
|
|
return getCornerResizeDelta(
|
|
resizeRef.current.clientX - e.clientX,
|
|
(e.clientY - resizeRef.current.clientY) / ASPECT_RATIO,
|
|
);
|
|
}
|
|
};
|
|
|
|
const startResize = (
|
|
e: React.PointerEvent<HTMLDivElement>,
|
|
edge: ResizeEdge,
|
|
) => {
|
|
if (e.button !== 0) return;
|
|
|
|
e.stopPropagation();
|
|
e.currentTarget.setPointerCapture(e.pointerId);
|
|
|
|
resizeRef.current = {
|
|
size,
|
|
clientX: e.clientX,
|
|
clientY: e.clientY,
|
|
edge,
|
|
};
|
|
|
|
setIsResizing(true);
|
|
};
|
|
|
|
const resizePopout = (e: React.PointerEvent) => {
|
|
if (!isResizing) return;
|
|
|
|
const nextSize = clampSize(
|
|
resizeRef.current.size + getResizeDelta(e, resizeRef.current.edge),
|
|
);
|
|
|
|
setSizeSafe(nextSize);
|
|
setCoordsSafe(getCoordsForPosition(position, nextSize));
|
|
};
|
|
|
|
const stopResize = (e: React.PointerEvent) => {
|
|
if (!isResizing) return;
|
|
|
|
e.stopPropagation();
|
|
setIsResizing(false);
|
|
setCoordsSafe(getCoordsForPosition(position, sizeRef.current));
|
|
};
|
|
|
|
const resizeEdgeClassName = "absolute z-10 bg-transparent";
|
|
const resizeCornerClassName = "absolute z-20 h-4 w-4 bg-transparent";
|
|
|
|
if (!screenSharePublication) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<div
|
|
ref={popoutRef}
|
|
onPointerDown={(e) => {
|
|
if (e.button !== 0) return;
|
|
if (isResizing) return;
|
|
|
|
e.currentTarget.setPointerCapture(e.pointerId);
|
|
|
|
const current = coordsRef.current;
|
|
|
|
dragOffsetRef.current = {
|
|
x: e.clientX - current.x,
|
|
y: e.clientY - current.y,
|
|
};
|
|
|
|
setIsDragging(true);
|
|
}}
|
|
onPointerMove={(e) => {
|
|
if (!isDragging) return;
|
|
|
|
setCoordsSafe({
|
|
x: e.clientX - dragOffsetRef.current.x,
|
|
y: e.clientY - dragOffsetRef.current.y,
|
|
});
|
|
}}
|
|
onPointerUp={(e) => {
|
|
if (!isDragging) return;
|
|
|
|
const nextPosition = getPositionFromPoint(e.clientX, e.clientY);
|
|
|
|
setPosition(nextPosition);
|
|
setIsDragging(false);
|
|
|
|
requestAnimationFrame(() => {
|
|
setCoordsSafe(getCoordsForPosition(nextPosition));
|
|
});
|
|
}}
|
|
onPointerCancel={() => {
|
|
setIsDragging(false);
|
|
|
|
requestAnimationFrame(() => {
|
|
setCoordsSafe(getCoordsForPosition(position));
|
|
});
|
|
}}
|
|
onMouseEnter={showOverlay}
|
|
onMouseMove={showOverlay}
|
|
onMouseLeave={hideOverlay}
|
|
className={cn(
|
|
"fixed left-0 top-0 aspect-video z-200 rounded-lg border-2 border-muted-foreground bg-black",
|
|
"select-none touch-none",
|
|
isDragging ? "cursor-grabbing" : "cursor-grab",
|
|
)}
|
|
style={{
|
|
width: size,
|
|
transform: `translate3d(${coords.x}px, ${coords.y}px, 0)`,
|
|
transition:
|
|
isDragging || isResizing
|
|
? "none"
|
|
: "transform 420ms cubic-bezier(0.34, 1.56, 0.64, 1)",
|
|
willChange: "transform",
|
|
}}
|
|
>
|
|
<VideoViewer
|
|
participantId={participant.identity}
|
|
publication={screenSharePublication}
|
|
/>
|
|
<div
|
|
className={cn(
|
|
"pointer-events-none absolute inset-0 z-40 transition-opacity duration-200",
|
|
isOverlayVisible ? "opacity-100" : "opacity-0",
|
|
)}
|
|
>
|
|
<div className="absolute inset-x-0 top-0 h-16 bg-gradient-to-b from-black/55 to-transparent" />
|
|
<div className="absolute inset-x-0 bottom-0 h-20 bg-gradient-to-t from-black/65 to-transparent" />
|
|
</div>
|
|
<div
|
|
className={cn(
|
|
"absolute bottom-3 right-3 z-50 transition-all duration-200",
|
|
isOverlayVisible
|
|
? "opacity-100 translate-y-0 pointer-events-auto"
|
|
: "opacity-0 translate-y-2 pointer-events-none",
|
|
)}
|
|
>
|
|
<div className="bg-[#070707] h-10 w-14 rounded-lg">
|
|
<Button
|
|
className="w-full h-full border-0!"
|
|
variant="destructive"
|
|
onPointerDown={(e) => e.stopPropagation()}
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
stopWatchingStream(Number(participant.identity ?? 0));
|
|
}}
|
|
>
|
|
<ScreenShareOff style={{ scale: "115%" }} />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
<div
|
|
aria-label="Resize popout from top edge"
|
|
role="separator"
|
|
className={cn(
|
|
resizeEdgeClassName,
|
|
"inset-x-4 -top-1 h-2 cursor-n-resize",
|
|
)}
|
|
onPointerDown={(e) => startResize(e, "top")}
|
|
onPointerMove={resizePopout}
|
|
onPointerUp={stopResize}
|
|
onPointerCancel={stopResize}
|
|
/>
|
|
<div
|
|
aria-label="Resize popout from right edge"
|
|
role="separator"
|
|
className={cn(
|
|
resizeEdgeClassName,
|
|
"-right-1 inset-y-4 w-2 cursor-e-resize",
|
|
)}
|
|
onPointerDown={(e) => startResize(e, "right")}
|
|
onPointerMove={resizePopout}
|
|
onPointerUp={stopResize}
|
|
onPointerCancel={stopResize}
|
|
/>
|
|
<div
|
|
aria-label="Resize popout from bottom edge"
|
|
role="separator"
|
|
className={cn(
|
|
resizeEdgeClassName,
|
|
"inset-x-4 -bottom-1 h-2 cursor-s-resize",
|
|
)}
|
|
onPointerDown={(e) => startResize(e, "bottom")}
|
|
onPointerMove={resizePopout}
|
|
onPointerUp={stopResize}
|
|
onPointerCancel={stopResize}
|
|
/>
|
|
<div
|
|
aria-label="Resize popout from left edge"
|
|
role="separator"
|
|
className={cn(
|
|
resizeEdgeClassName,
|
|
"-left-1 inset-y-4 w-2 cursor-w-resize",
|
|
)}
|
|
onPointerDown={(e) => startResize(e, "left")}
|
|
onPointerMove={resizePopout}
|
|
onPointerUp={stopResize}
|
|
onPointerCancel={stopResize}
|
|
/>
|
|
<div
|
|
aria-label="Resize popout from top left corner"
|
|
role="separator"
|
|
className={cn(resizeCornerClassName, "-left-1 -top-1 cursor-nw-resize")}
|
|
onPointerDown={(e) => startResize(e, "top-left")}
|
|
onPointerMove={resizePopout}
|
|
onPointerUp={stopResize}
|
|
onPointerCancel={stopResize}
|
|
/>
|
|
<div
|
|
aria-label="Resize popout from top right corner"
|
|
role="separator"
|
|
className={cn(
|
|
resizeCornerClassName,
|
|
"-right-1 -top-1 cursor-ne-resize",
|
|
)}
|
|
onPointerDown={(e) => startResize(e, "top-right")}
|
|
onPointerMove={resizePopout}
|
|
onPointerUp={stopResize}
|
|
onPointerCancel={stopResize}
|
|
/>
|
|
<div
|
|
aria-label="Resize popout from bottom right corner"
|
|
role="separator"
|
|
className={cn(
|
|
resizeCornerClassName,
|
|
"-bottom-1 -right-1 cursor-se-resize",
|
|
)}
|
|
onPointerDown={(e) => startResize(e, "bottom-right")}
|
|
onPointerMove={resizePopout}
|
|
onPointerUp={stopResize}
|
|
onPointerCancel={stopResize}
|
|
/>
|
|
<div
|
|
aria-label="Resize popout from bottom left corner"
|
|
role="separator"
|
|
className={cn(
|
|
resizeCornerClassName,
|
|
"-bottom-1 -left-1 cursor-sw-resize",
|
|
)}
|
|
onPointerDown={(e) => startResize(e, "bottom-left")}
|
|
onPointerMove={resizePopout}
|
|
onPointerUp={stopResize}
|
|
onPointerCancel={stopResize}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default function Wrapper() {
|
|
const room = getRoom();
|
|
const { pathname } = useLocation();
|
|
const { openMobile } = useSidebar();
|
|
const isMobile = useIsMobile();
|
|
const state = useCall((state) => state.state);
|
|
const callId = useCall((state) => state.callId);
|
|
const watchedStreamParticipantIds = useCall(
|
|
(state) => state.watchedStreamParticipantIds,
|
|
);
|
|
const lastFocusedParticipantId = useCall(
|
|
(state) => state.lastFocusedParticipantId,
|
|
);
|
|
const participant = room.getParticipantByIdentity(
|
|
String(lastFocusedParticipantId),
|
|
);
|
|
|
|
if (isMobile) {
|
|
return (
|
|
<MobileCallPill
|
|
active={
|
|
(!pathname.startsWith("/call") || openMobile) && state === "open"
|
|
}
|
|
callId={callId ?? ""}
|
|
/>
|
|
);
|
|
}
|
|
|
|
if (!participant || !lastFocusedParticipantId) {
|
|
return null;
|
|
}
|
|
|
|
const active =
|
|
!pathname.startsWith("/call") &&
|
|
state === "open" &&
|
|
watchedStreamParticipantIds.includes(Number(participant.identity ?? 0));
|
|
|
|
return active && <Popout participant={participant} />;
|
|
}
|