(feat): add basic call ui
(feat): add screensharing (incl. broken desktop picker) (qol): add todo
This commit is contained in:
parent
7b5cdca0ff
commit
fbd8ef4fa0
21 changed files with 1167 additions and 380 deletions
|
|
@ -1,3 +1,147 @@
|
|||
import { useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||
import { useCall } from "../../store";
|
||||
import Base from "../../components/modals/base";
|
||||
|
||||
const TILE_ASPECT_RATIO = 16 / 9;
|
||||
const SECONDARY_ROW_HEIGHT_PX = 180;
|
||||
const STACK_GAP_PX = 12;
|
||||
|
||||
export default function View() {
|
||||
return <div>Focused</div>;
|
||||
const room = useCall((state) => state.room);
|
||||
|
||||
const focusedParticipantId = useCall((state) => state.focusedParticipantId);
|
||||
const activeScreenShareParticipantIds = useCall(
|
||||
(state) => state.activeScreenShareParticipantIds,
|
||||
);
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const [focusedTileSize, setFocusedTileSize] = useState({ width: 0, height: 0 });
|
||||
const [isFocusedTileFlush, setIsFocusedTileFlush] = useState(false);
|
||||
|
||||
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;
|
||||
});
|
||||
}, [room]);
|
||||
|
||||
const userIds = useMemo(
|
||||
() => users.map((participant) => Number(participant.identity)),
|
||||
[users],
|
||||
);
|
||||
|
||||
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) => !activeScreenShareParticipantIds.includes(id))
|
||||
.map((participantId) => ({
|
||||
key: `user:${participantId}`,
|
||||
kind: "user" as const,
|
||||
participantId,
|
||||
})),
|
||||
],
|
||||
[activeScreenShareParticipantIds, userIds, focusedParticipantId],
|
||||
);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const container = containerRef.current;
|
||||
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
|
||||
const syncTileLayout = () => {
|
||||
const rect = container.getBoundingClientRect();
|
||||
const width = Math.max(0, Math.floor(window.innerWidth + 1 - rect.left));
|
||||
const height = Math.max(0, Math.floor(container.clientHeight));
|
||||
const reservedHeight =
|
||||
tiles.length > 0 ? SECONDARY_ROW_HEIGHT_PX + STACK_GAP_PX : 0;
|
||||
const availableHeight = Math.max(0, height - reservedHeight);
|
||||
const nextWidth = Math.max(
|
||||
0,
|
||||
Math.min(width, availableHeight * TILE_ASPECT_RATIO),
|
||||
);
|
||||
const nextHeight = Math.max(0, nextWidth / TILE_ASPECT_RATIO);
|
||||
const widthDelta = Math.abs(width - nextWidth);
|
||||
|
||||
setFocusedTileSize((current) =>
|
||||
current.width === nextWidth && current.height === nextHeight
|
||||
? current
|
||||
: { width: nextWidth, height: nextHeight },
|
||||
);
|
||||
setIsFocusedTileFlush(widthDelta <= 1);
|
||||
};
|
||||
|
||||
syncTileLayout();
|
||||
|
||||
const observer = new ResizeObserver(() => {
|
||||
requestAnimationFrame(syncTileLayout);
|
||||
});
|
||||
observer.observe(container);
|
||||
|
||||
window.addEventListener("resize", syncTileLayout);
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
window.removeEventListener("resize", syncTileLayout);
|
||||
};
|
||||
}, [tiles.length, focusedParticipantId]);
|
||||
|
||||
if (focusedParticipantId == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const focusedParticipant = room.getParticipantByIdentity(
|
||||
String(focusedParticipantId),
|
||||
);
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="flex h-full w-full items-center justify-center overflow-hidden">
|
||||
<div
|
||||
className="flex max-h-full w-full flex-col items-center overflow-hidden"
|
||||
style={{ gap: tiles.length > 0 ? STACK_GAP_PX : 0 }}
|
||||
>
|
||||
<div className="w-full flex justify-center overflow-hidden">
|
||||
<div className="overflow-hidden" style={focusedTileSize}>
|
||||
<Base
|
||||
flush={isFocusedTileFlush}
|
||||
type={focusedParticipant?.isScreenShareEnabled ? "stream" : "user"}
|
||||
participant={room.getParticipantByIdentity(
|
||||
String(focusedParticipantId),
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{tiles.length > 0 && (
|
||||
<div
|
||||
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={room.getParticipantByIdentity(
|
||||
String(tile.participantId),
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { RoomEvent } from "livekit-client";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useCall } from "../../store";
|
||||
import Base from "../../components/modals/base";
|
||||
|
||||
const TILE_ASPECT_RATIO = 16 / 9;
|
||||
const GRID_GAP = 12;
|
||||
|
|
@ -82,12 +83,13 @@ function calculateOptimalGridLayout(
|
|||
|
||||
export default function View() {
|
||||
const room = useCall((state) => state.room);
|
||||
const activeScreenShareParticipantIds = useCall(
|
||||
(state) => state.activeScreenShareParticipantIds,
|
||||
);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [containerSize, setContainerSize] = useState({ width: 0, height: 0 });
|
||||
const [participantCount, setParticipantCount] = useState(
|
||||
() => room.remoteParticipants.size + 1,
|
||||
);
|
||||
const [participantVersion, setParticipantVersion] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const element = containerRef.current;
|
||||
|
|
@ -107,33 +109,53 @@ export default function View() {
|
|||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const syncParticipantCount = () => {
|
||||
setParticipantCount(room.remoteParticipants.size + 1);
|
||||
const syncParticipants = () => {
|
||||
setParticipantVersion((version) => version + 1);
|
||||
};
|
||||
|
||||
syncParticipantCount();
|
||||
syncParticipants();
|
||||
|
||||
room.on(RoomEvent.Connected, syncParticipantCount);
|
||||
room.on(RoomEvent.Disconnected, syncParticipantCount);
|
||||
room.on(RoomEvent.ParticipantConnected, syncParticipantCount);
|
||||
room.on(RoomEvent.ParticipantDisconnected, syncParticipantCount);
|
||||
room.on(RoomEvent.Connected, syncParticipants);
|
||||
room.on(RoomEvent.Disconnected, syncParticipants);
|
||||
room.on(RoomEvent.ParticipantConnected, syncParticipants);
|
||||
room.on(RoomEvent.ParticipantDisconnected, syncParticipants);
|
||||
|
||||
return () => {
|
||||
room.off(RoomEvent.Connected, syncParticipantCount);
|
||||
room.off(RoomEvent.Disconnected, syncParticipantCount);
|
||||
room.off(RoomEvent.ParticipantConnected, syncParticipantCount);
|
||||
room.off(RoomEvent.ParticipantDisconnected, syncParticipantCount);
|
||||
room.off(RoomEvent.Connected, syncParticipants);
|
||||
room.off(RoomEvent.Disconnected, syncParticipants);
|
||||
room.off(RoomEvent.ParticipantConnected, syncParticipants);
|
||||
room.off(RoomEvent.ParticipantDisconnected, 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 layout = useMemo(
|
||||
() =>
|
||||
calculateOptimalGridLayout(
|
||||
containerSize.width,
|
||||
containerSize.height,
|
||||
participantCount,
|
||||
activeScreenShareParticipantIds.length + userIds.length,
|
||||
),
|
||||
[containerSize.height, containerSize.width, participantCount],
|
||||
[
|
||||
activeScreenShareParticipantIds.length,
|
||||
containerSize.height,
|
||||
containerSize.width,
|
||||
userIds.length,
|
||||
],
|
||||
);
|
||||
|
||||
const rows = useMemo(() => {
|
||||
|
|
@ -148,31 +170,57 @@ export default function View() {
|
|||
});
|
||||
}, [layout.rowCounts]);
|
||||
|
||||
const users = useMemo(() => {
|
||||
const participants = [
|
||||
...room.remoteParticipants.values(),
|
||||
room.localParticipant,
|
||||
];
|
||||
return participants;
|
||||
}, [room.localParticipant, room.remoteParticipants]);
|
||||
const tiles = useMemo(
|
||||
() => [
|
||||
...activeScreenShareParticipantIds.map((participantId) => ({
|
||||
key: `stream:${participantId}`,
|
||||
kind: "stream" as const,
|
||||
participantId,
|
||||
})),
|
||||
...userIds.map((participantId) => ({
|
||||
key: `user:${participantId}`,
|
||||
kind: "user" as const,
|
||||
participantId,
|
||||
})),
|
||||
],
|
||||
[activeScreenShareParticipantIds, userIds],
|
||||
);
|
||||
|
||||
function getParticipantById(participantId: number) {
|
||||
if (Number(room.localParticipant.identity) === participantId) {
|
||||
return room.localParticipant;
|
||||
}
|
||||
|
||||
return room.getParticipantByIdentity(String(participantId));
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="h-[75vh] w-full overflow-hidden p-3">
|
||||
<div ref={containerRef} style={{
|
||||
height: "calc(100% - 2rem)"
|
||||
}} className="w-full overflow-hidden p-3">
|
||||
<div className="flex h-full w-full flex-col items-center justify-center gap-3">
|
||||
{rows.map((row) => (
|
||||
<div key={row.rowIndex} className="flex justify-center gap-3">
|
||||
{row.participants.map((userIndex) => (
|
||||
<div
|
||||
key={userIndex}
|
||||
className="flex items-center justify-center rounded-xl bg-red-500"
|
||||
style={{
|
||||
width: layout.tileWidth,
|
||||
height: layout.tileHeight,
|
||||
}}
|
||||
>
|
||||
Test {users[userIndex]?.identity}
|
||||
</div>
|
||||
))}
|
||||
{row.participants.map((userIndex) => {
|
||||
const tile = tiles[userIndex];
|
||||
|
||||
if (!tile) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={tile.key}
|
||||
className="flex items-center justify-center rounded-xl"
|
||||
style={{
|
||||
width: layout.tileWidth,
|
||||
height: layout.tileHeight,
|
||||
}}
|
||||
>
|
||||
<Base type={tile.kind} participant={getParticipantById(tile.participantId)} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,9 +4,15 @@ import TopBar from "../../components/top";
|
|||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="w-full h-full flex flex-col">
|
||||
<TopBar />
|
||||
<div className="w-full h-full">{children}</div>
|
||||
<Actions />
|
||||
<div className="shrink-0">
|
||||
<TopBar />
|
||||
</div>
|
||||
<div className="min-h-0 flex-1 w-full flex justify-center items-center overflow-hidden">
|
||||
{children}
|
||||
</div>
|
||||
<div className="shrink-0 pb-4.5 pt-3">
|
||||
<Actions />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue