(feat): move call context to zustand
(feat): add actions, basic call page
This commit is contained in:
parent
d01df7c02c
commit
413764f33e
17 changed files with 673 additions and 382 deletions
|
|
@ -1,3 +1,20 @@
|
|||
import { Card, CardContent } from "@tensamin/ui";
|
||||
import MuteButton from "./buttons/mute";
|
||||
import DeafButton from "./buttons/deaf";
|
||||
import ScreenshareButton from "./buttons/screenshare";
|
||||
import LeaveButton from "./buttons/leave";
|
||||
|
||||
export default function Actions() {
|
||||
return <div>Actions</div>;
|
||||
return (
|
||||
<div className="w-full flex justify-center py-3">
|
||||
<Card className="p-1.75">
|
||||
<CardContent className="p-0! flex gap-1.75">
|
||||
<MuteButton className="w-14 h-10" iconSize={25} />
|
||||
<DeafButton className="w-14 h-10" iconSize={25} />
|
||||
<ScreenshareButton className="w-14 h-10" iconSize={25} />
|
||||
<LeaveButton className="w-14 h-10" iconSize={25} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,15 @@
|
|||
import { Button } from "@tensamin/ui";
|
||||
import { useCall } from "../../context";
|
||||
import { toggleDeaf, useCall } from "../../store";
|
||||
import { HeadphoneOff, Headphones } from "lucide-react";
|
||||
|
||||
export default function DeafButton({ className }: { className?: string }) {
|
||||
const { toggleDeaf, deaf } = useCall();
|
||||
export default function DeafButton({
|
||||
className,
|
||||
iconSize,
|
||||
}: {
|
||||
className?: string;
|
||||
iconSize?: number;
|
||||
}) {
|
||||
const deaf = useCall((state) => state.deaf);
|
||||
|
||||
return (
|
||||
<Button
|
||||
|
|
@ -11,7 +17,15 @@ export default function DeafButton({ className }: { className?: string }) {
|
|||
onClick={toggleDeaf}
|
||||
className={className}
|
||||
>
|
||||
{deaf ? <HeadphoneOff /> : <Headphones />}
|
||||
{deaf ? (
|
||||
<HeadphoneOff
|
||||
style={{ scale: (iconSize ? iconSize + 100 : 100) + "%" }}
|
||||
/>
|
||||
) : (
|
||||
<Headphones
|
||||
style={{ scale: (iconSize ? iconSize + 100 : 100) + "%" }}
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
21
packages/call/src/components/buttons/leave.tsx
Normal file
21
packages/call/src/components/buttons/leave.tsx
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { LeaveIcon } from "@livekit/components-react";
|
||||
import { Button } from "@tensamin/ui";
|
||||
import { disconnect } from "../../store";
|
||||
|
||||
export default function LeaveButton({
|
||||
className,
|
||||
iconSize,
|
||||
}: {
|
||||
className?: string;
|
||||
iconSize?: number;
|
||||
}) {
|
||||
return (
|
||||
<Button
|
||||
className={className}
|
||||
onClick={() => disconnect()}
|
||||
variant="destructive"
|
||||
>
|
||||
<LeaveIcon style={{ scale: (iconSize ? iconSize + 100 : 100) + "%" }} />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,17 +1,27 @@
|
|||
import { Button } from "@tensamin/ui";
|
||||
import { useCall } from "../../context";
|
||||
import { toggleMute, useCall } from "../../store";
|
||||
import { Mic, MicOff } from "lucide-react";
|
||||
|
||||
export default function MuteButton({ className }: { className?: string }) {
|
||||
const { room } = useCall();
|
||||
const micEnabled = room.localParticipant.isMicrophoneEnabled;
|
||||
export default function MuteButton({
|
||||
className,
|
||||
iconSize,
|
||||
}: {
|
||||
className?: string;
|
||||
iconSize?: number;
|
||||
}) {
|
||||
const micEnabled = useCall((state) => state.micEnabled);
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant={micEnabled ? "default" : "destructive"}
|
||||
className={className}
|
||||
onClick={() => void toggleMute()}
|
||||
>
|
||||
{micEnabled ? <Mic /> : <MicOff />}
|
||||
{micEnabled ? (
|
||||
<Mic style={{ scale: (iconSize ? iconSize + 100 : 100) + "%" }} />
|
||||
) : (
|
||||
<MicOff style={{ scale: (iconSize ? iconSize + 100 : 100) + "%" }} />
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,39 +1,47 @@
|
|||
import { Button, Popover, PopoverContent, PopoverTrigger } from "@tensamin/ui";
|
||||
import { MonitorDot, ScreenShare } from "lucide-react";
|
||||
import { useCall } from "../../context";
|
||||
import { setScreenShareEnabled, useCall } from "../../store";
|
||||
|
||||
export default function ScreenshareButton({
|
||||
className,
|
||||
iconSize,
|
||||
}: {
|
||||
className?: string;
|
||||
iconSize?: number;
|
||||
}) {
|
||||
const { room } = useCall();
|
||||
|
||||
const isScreensharing = room.localParticipant.isScreenShareEnabled;
|
||||
const isScreensharing = useCall((state) => state.screenShareEnabled);
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger
|
||||
render={
|
||||
<Button
|
||||
variant={isScreensharing ? "secondary" : "default"}
|
||||
variant={isScreensharing ? "subtleDefault" : "default"}
|
||||
className={className}
|
||||
>
|
||||
{isScreensharing ? <MonitorDot /> : <ScreenShare />}
|
||||
{isScreensharing ? (
|
||||
<MonitorDot
|
||||
style={{ scale: (iconSize ? iconSize + 100 : 100) + "%" }}
|
||||
/>
|
||||
) : (
|
||||
<ScreenShare
|
||||
style={{ scale: (iconSize ? iconSize + 100 : 100) + "%" }}
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<PopoverContent className="w-auto flex flex-col gap-2">
|
||||
<Button
|
||||
disabled={isScreensharing}
|
||||
onClick={() => room.localParticipant.setScreenShareEnabled(true)}
|
||||
onClick={() => void setScreenShareEnabled(true)}
|
||||
>
|
||||
Start
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
disabled={!isScreensharing}
|
||||
onClick={() => room.localParticipant.setScreenShareEnabled(false)}
|
||||
onClick={() => void setScreenShareEnabled(false)}
|
||||
>
|
||||
Stop
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
20
packages/call/src/components/top.tsx
Normal file
20
packages/call/src/components/top.tsx
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { useCall } from "../store";
|
||||
|
||||
export default function TopBar() {
|
||||
const room = useCall((state) => state.room);
|
||||
|
||||
const users = new Array(room.remoteParticipants.size).fill(0).map((_, i) => {
|
||||
const participant = Array.from(room.remoteParticipants.values())[i];
|
||||
return participant.identity;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="w-full flex justify-between h-12">
|
||||
<div className="flex gap-1">
|
||||
{users.map((user) => (
|
||||
<div key={user}>{user}</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue