75 lines
3.5 KiB
TypeScript
75 lines
3.5 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useRef, useState } from "react";
|
|
import { useSdk, useChannel } from "@lynkd/messaging-inbox-sdk";
|
|
import { useUi } from "@/lib/ui-state";
|
|
import { Avatar } from "@/components/ui/primitives";
|
|
import { Mic, MicOff, Video, VideoOff, MonitorUp, PhoneOff } from "lucide-react";
|
|
import clsx from "clsx";
|
|
|
|
export function HuddleBar() {
|
|
const { huddleChannelId, endHuddle } = useUi();
|
|
const channel = useChannel(huddleChannelId ?? null);
|
|
const { userById } = useSdk();
|
|
const [muted, setMuted] = useState(false);
|
|
const [video, setVideo] = useState(false);
|
|
const [sharing, setSharing] = useState(false);
|
|
const [seconds, setSeconds] = useState(0);
|
|
const startRef = useRef(0);
|
|
|
|
useEffect(() => {
|
|
if (!huddleChannelId) return;
|
|
setSeconds(0);
|
|
setMuted(false);
|
|
setVideo(false);
|
|
setSharing(false);
|
|
startRef.current = Date.now();
|
|
const t = setInterval(() => setSeconds(Math.floor((Date.now() - startRef.current) / 1000)), 1000);
|
|
return () => clearInterval(t);
|
|
}, [huddleChannelId]);
|
|
|
|
if (!huddleChannelId || !channel) return null;
|
|
|
|
const members = channel.memberIds.slice(0, 4).map((id) => userById(id)).filter(Boolean);
|
|
const total = channel.memberIds.length;
|
|
const mmss = `${String(Math.floor(seconds / 60)).padStart(2, "0")}:${String(seconds % 60).padStart(2, "0")}`;
|
|
|
|
return (
|
|
<div className="fixed bottom-4 left-1/2 z-50 w-[min(440px,92vw)] -translate-x-1/2 rounded-card border border-border bg-elevated p-3 shadow-pop animate-slide-up">
|
|
<div className="flex items-center gap-3">
|
|
<span className="relative flex h-9 w-9 items-center justify-center rounded-control bg-success/20 text-success">
|
|
<Video size={18} />
|
|
<span className="absolute inset-0 animate-ping rounded-control border border-success/40" />
|
|
</span>
|
|
<div className="min-w-0 flex-1">
|
|
<div className="truncate text-[13.5px] font-bold">Huddle · {channel.kind === "channel" ? `#${channel.name}` : channel.name}</div>
|
|
<div className="text-[12px] text-muted">{total} {total === 1 ? "person" : "people"} · {mmss}</div>
|
|
</div>
|
|
<div className="flex -space-x-2">
|
|
{members.map((u) => (u ? <Avatar key={u.id} user={u} size={26} showPresence={false} /> : null))}
|
|
</div>
|
|
</div>
|
|
<div className="mt-3 flex items-center justify-center gap-2">
|
|
<HuddleBtn icon={muted ? MicOff : Mic} label={muted ? "Unmute" : "Mute"} active={muted} onClick={() => setMuted((m) => !m)} />
|
|
<HuddleBtn icon={video ? Video : VideoOff} label={video ? "Stop video" : "Start video"} active={video} onClick={() => setVideo((v) => !v)} />
|
|
<HuddleBtn icon={MonitorUp} label={sharing ? "Stop sharing" : "Share screen"} active={sharing} onClick={() => setSharing((s) => !s)} />
|
|
<button onClick={endHuddle} className="focus-ring flex items-center gap-2 rounded-control bg-danger px-4 py-2 text-[13px] font-semibold text-white">
|
|
<PhoneOff size={15} /> Leave
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function HuddleBtn({ icon: Icon, label, onClick, active }: { icon: typeof Mic; label: string; onClick: () => void; active?: boolean }) {
|
|
return (
|
|
<button
|
|
title={label}
|
|
onClick={onClick}
|
|
className={clsx("focus-ring grid h-9 w-11 place-items-center rounded-control border transition-colors", active ? "border-accent bg-accent-soft text-accent" : "border-border bg-panel text-ink hover:bg-hover")}
|
|
>
|
|
<Icon size={16} />
|
|
</button>
|
|
);
|
|
}
|