client/packages/call/src/views/main/focused.tsx
Alois a0e3e85467
All checks were successful
/ build-web (push) Successful in 1m13s
/ build-desktop (push) Successful in 11m50s
/ build-mobile (push) Successful in 16m27s
/ release (push) Successful in 24s
(fix): call being getting initialised even on the login screen
2026-05-23 21:43:11 +02:00

214 lines
6.8 KiB
TypeScript

import { RoomEvent } from "livekit-client";
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
import { useCall, getRoom } from "../../store";
import Base from "../../components/modals/base";
const SECONDARY_ROW_HEIGHT_PX = 180;
const STACK_GAP_PX = 12;
export default function View() {
const room = getRoom();
const layoutVersion = useCall((state) => state.layoutVersion);
const usersInFocusedViewHidden = useCall(
(state) => state.usersInFocusedViewHidden,
);
const callIsFullscreen = useCall((state) => state.callIsFullscreen);
const focusedParticipantId = useCall((state) => state.focusedParticipantId);
const focusedParticipantType = useCall(
(state) => state.focusedParticipantType,
);
const activeScreenShareParticipantIds = useCall(
(state) => state.activeScreenShareParticipantIds,
);
const containerRef = useRef<HTMLDivElement | null>(null);
const focusedTileRef = useRef<HTMLDivElement | null>(null);
const [isFocusedTileFlush, setIsFocusedTileFlush] = useState(false);
const [participantVersion, setParticipantVersion] = useState(0);
useEffect(() => {
const syncParticipants = () => {
setParticipantVersion((version) => version + 1);
};
syncParticipants();
room.on(RoomEvent.ParticipantConnected, syncParticipants);
room.on(RoomEvent.ParticipantDisconnected, syncParticipants);
room.on(RoomEvent.TrackPublished, syncParticipants);
room.on(RoomEvent.TrackUnpublished, syncParticipants);
room.on(RoomEvent.TrackSubscribed, syncParticipants);
room.on(RoomEvent.TrackUnsubscribed, syncParticipants);
room.on(RoomEvent.ParticipantAttributesChanged, syncParticipants);
return () => {
room.off(RoomEvent.ParticipantConnected, syncParticipants);
room.off(RoomEvent.ParticipantDisconnected, syncParticipants);
room.off(RoomEvent.TrackPublished, syncParticipants);
room.off(RoomEvent.TrackUnpublished, syncParticipants);
room.off(RoomEvent.TrackSubscribed, syncParticipants);
room.off(RoomEvent.TrackUnsubscribed, syncParticipants);
room.off(RoomEvent.ParticipantAttributesChanged, syncParticipants);
};
}, [room]);
const users = useMemo(() => {
const participants = [
...room.remoteParticipants.values(),
room.localParticipant,
];
return participants.filter((participant) => {
const participantId = Number(participant.identity);
return Number.isInteger(participantId) && participantId > 0;
});
// eslint-disable-next-line
}, [participantVersion, room]);
const userIds = useMemo(
() => users.map((participant) => Number(participant.identity)),
[users],
);
const activeScreenShareParticipantIdSet = useMemo(
() => new Set(activeScreenShareParticipantIds),
[activeScreenShareParticipantIds],
);
const tiles = useMemo(
() => [
...activeScreenShareParticipantIds
.filter((id) => id !== focusedParticipantId)
.map((participantId) => ({
key: `stream:${participantId}`,
kind: "stream" as const,
participantId,
})),
...userIds
.filter((id) => id !== focusedParticipantId)
.filter((id) => !activeScreenShareParticipantIdSet.has(id))
.map((participantId) => ({
key: `user:${participantId}`,
kind: "user" as const,
participantId,
})),
],
[
activeScreenShareParticipantIds,
activeScreenShareParticipantIdSet,
userIds,
focusedParticipantId,
],
);
useLayoutEffect(() => {
const container = containerRef.current;
const focusedTile = focusedTileRef.current;
if (!container || !focusedTile) {
return;
}
let frameId = 0;
const syncTileLayout = () => {
const containerWidth = Math.floor(
container.getBoundingClientRect().width,
);
const tileWidth = Math.floor(focusedTile.getBoundingClientRect().width);
setIsFocusedTileFlush(Math.abs(containerWidth - tileWidth) <= 1);
};
const scheduleSyncTileLayout = () => {
cancelAnimationFrame(frameId);
frameId = requestAnimationFrame(syncTileLayout);
};
syncTileLayout();
const observer = new ResizeObserver(() => {
scheduleSyncTileLayout();
});
observer.observe(container);
observer.observe(focusedTile);
window.addEventListener("resize", scheduleSyncTileLayout);
return () => {
observer.disconnect();
cancelAnimationFrame(frameId);
window.removeEventListener("resize", scheduleSyncTileLayout);
};
}, [
layoutVersion,
tiles.length,
focusedParticipantId,
usersInFocusedViewHidden,
]);
if (focusedParticipantId == null) {
return null;
}
function getParticipantById(participantId: number) {
if (Number(room.localParticipant.identity) === participantId) {
return room.localParticipant;
}
return room.getParticipantByIdentity(String(participantId));
}
const focusedParticipant = getParticipantById(focusedParticipantId);
const focusedParticipantHasActiveScreenShare =
activeScreenShareParticipantIdSet.has(focusedParticipantId);
const focusedTileType: "user" | "stream" =
focusedParticipantType === "stream" &&
focusedParticipantHasActiveScreenShare
? "stream"
: "user";
const isImmersiveFocusedView = callIsFullscreen && usersInFocusedViewHidden;
return (
<div ref={containerRef} className="flex h-full w-full overflow-hidden">
<div
className="flex h-full w-full flex-col items-center overflow-hidden"
style={{
gap: tiles.length > 0 && !isImmersiveFocusedView ? STACK_GAP_PX : 0,
}}
>
<div className="flex min-h-0 w-full flex-1 items-center justify-center overflow-hidden">
<div
ref={focusedTileRef}
className={
isImmersiveFocusedView
? "h-full w-full overflow-hidden"
: "h-full max-w-full aspect-video overflow-hidden flex items-center"
}
>
<Base
fill={isImmersiveFocusedView}
flush={isImmersiveFocusedView || isFocusedTileFlush}
type={focusedTileType}
participant={focusedParticipant}
/>
</div>
</div>
{tiles.length > 0 && (
<div
hidden={usersInFocusedViewHidden}
className="w-full shrink-0 flex justify-center gap-2 overflow-x-auto"
style={{ height: SECONDARY_ROW_HEIGHT_PX }}
>
{tiles.map((tile) => (
<div key={tile.key} className="h-full shrink-0 aspect-video">
<Base
type={tile.kind}
participant={getParticipantById(tile.participantId)}
/>
</div>
))}
</div>
)}
</div>
</div>
);
}