(feat): move call context to zustand

(feat): add actions, basic call page
This commit is contained in:
Alois 2026-04-29 14:49:53 +02:00
commit 413764f33e
17 changed files with 673 additions and 382 deletions

View file

@ -1,5 +1,5 @@
import { Lock, LockOpen } from "lucide-react";
import { useCall } from "../context";
import { openCallPage, useCall } from "../store";
import {
Button,
Card,
@ -10,16 +10,16 @@ import {
TooltipTrigger,
useIsMobile,
} from "@tensamin/ui";
import { LeaveIcon } from "@livekit/components-react";
import ScreenshareButton from "./buttons/screenshare";
import MuteButton from "./buttons/mute";
import DeafButton from "./buttons/deaf";
import { Room, Track } from "livekit-client";
import { useEffect, useState } from "react";
import { ResponsiveContainer, AreaChart, Area } from "recharts";
import LeaveButton from "./buttons/leave";
export default function SidebarBox() {
const { state, disconnect } = useCall();
const state = useCall((store) => store.state);
const isMobile = useIsMobile();
return state === "closed" ? null : (
@ -32,14 +32,7 @@ export default function SidebarBox() {
<MuteButton className="w-9 h-9" />
<DeafButton className="w-9 h-9" />
<ScreenshareButton className="w-9 h-9" />
<Button
size="lg"
className="w-9 h-9"
onClick={() => disconnect()}
variant="destructive"
>
<LeaveIcon />
</Button>
<LeaveButton className="w-9 h-9" />
</div>
</CardContent>
</Card>
@ -47,9 +40,9 @@ export default function SidebarBox() {
}
function ConnectionBar() {
const { state, room, openCallPage, callId } = useCall();
const encrypted =
room.localParticipant.isE2EEEnabled && room.localParticipant.isEncrypted;
const state = useCall((store) => store.state);
const isEncrypted = useCall((store) => store.isEncrypted);
const callId = useCall((store) => store.callId);
return (
<Tooltip>
@ -59,7 +52,7 @@ function ConnectionBar() {
onClick={() => openCallPage(callId || "")}
size="lg"
variant={
state === "open" && encrypted ? "subtleDefault" : "destructive"
state === "open" && isEncrypted ? "subtleDefault" : "destructive"
}
className="flex justify-between items-center"
>
@ -71,7 +64,7 @@ function ConnectionBar() {
<TinyPingGraph />
{encrypted ? (
{isEncrypted ? (
<Lock color="var(--primary-foreground-alt)" />
) : (
<LockOpen color="var(--destructive)" />
@ -85,11 +78,9 @@ function ConnectionBar() {
}
export function TinyPingGraph() {
const { room } = useCall();
const room = useCall((store) => store.room);
const [mapData, setMapData] = useState<Map<number, number>>(
() => new Map([[Date.now(), 0]]),
);
const [mapData, setMapData] = useState<Map<number, number>>(() => new Map());
const data = Array.from(mapData, ([time, ping]) => ({ time, ping }));
@ -102,7 +93,9 @@ export function TinyPingGraph() {
setMapData((prev) => {
const next = new Map(prev);
next.set(now, ping || 0);
if (ping != null && ping > 0) {
next.set(now, ping);
}
for (const time of next.keys()) {
if (time < cutoff) next.delete(time);
@ -128,48 +121,52 @@ export function TinyPingGraph() {
"linear-gradient(to right, transparent 0%, var(--primary) 15%, var(--primary) 85%, transparent 100%)",
}}
>
<ResponsiveContainer width="100%" height="100%">
<AreaChart
data={data}
margin={{ top: 2, right: 0, bottom: 2, left: 0 }}
>
<defs>
<linearGradient id="pingGradient" x1="0" y1="0" x2="0" y2="1">
<stop
offset="0%"
stopColor="currentColor"
stopOpacity={0.35}
/>
<stop
offset="70%"
stopColor="currentColor"
stopOpacity={0.08}
/>
<stop
offset="100%"
stopColor="currentColor"
stopOpacity={0}
/>
</linearGradient>
</defs>
{data.length > 1 && (
<ResponsiveContainer width="100%" height="100%">
<AreaChart
data={data}
margin={{ top: 2, right: 0, bottom: 2, left: 0 }}
>
<defs>
<linearGradient id="pingGradient" x1="0" y1="0" x2="0" y2="1">
<stop
offset="0%"
stopColor="currentColor"
stopOpacity={0.35}
/>
<stop
offset="70%"
stopColor="currentColor"
stopOpacity={0.08}
/>
<stop
offset="100%"
stopColor="currentColor"
stopOpacity={0}
/>
</linearGradient>
</defs>
<Area
type="monotone"
dataKey="ping"
stroke="currentColor"
strokeWidth={2}
fill="url(#pingGradient)"
fillOpacity={1}
dot={false}
activeDot={false}
isAnimationActive={false}
/>
</AreaChart>
</ResponsiveContainer>
<Area
type="monotone"
dataKey="ping"
stroke="currentColor"
strokeWidth={2}
fill="url(#pingGradient)"
fillOpacity={1}
dot={false}
activeDot={false}
isAnimationActive={false}
/>
</AreaChart>
</ResponsiveContainer>
)}
</div>
}
/>
<TooltipContent>{data.at(-1)?.ping} ms</TooltipContent>
<TooltipContent>
{data.length > 0 ? `${data.at(-1)?.ping} ms` : "Measuring ping..."}
</TooltipContent>
</Tooltip>
);
}
@ -197,5 +194,11 @@ async function getPing(room: Room): Promise<number | undefined> {
}
});
return Math.round(bestRtt || 0);
if (bestRtt == null || bestRtt <= 0) return;
const roundedRtt = Math.round(bestRtt);
if (roundedRtt <= 0) return;
return roundedRtt;
}