feat: one-click join + live pod presence

Join UX: click a member's name to join directly, or type a name and "Join"
(adds to roster + joins) — removes the redundant dropdown step. "Add" still
adds to the roster without joining.

Presence: new GET /api/presence (LiveKit RoomServiceClient) reports who's
connected per pod. The pod list polls it and shows a "N in room" badge plus a
green dot on members currently connected, so people can see active pods and jump in.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Kartikeya
2026-06-27 16:46:47 -07:00
parent 8271188cf1
commit 0f3b55a4f9
5 changed files with 146 additions and 49 deletions
+38
View File
@@ -14,6 +14,7 @@ export default function App() {
const [loading, setLoading] = useState(true);
const [pending, setPending] = useState<Set<string>>(new Set());
const [error, setError] = useState<string | null>(null);
const [presence, setPresence] = useState<Record<string, string[]>>({});
// join state — keep the id and derive the pod so it never goes stale
const [joinedPodId, setJoinedPodId] = useState<string | null>(null);
@@ -59,6 +60,27 @@ export default function App() {
void refresh();
}, []);
// Poll live presence while browsing the pod list (not while joined — PodView
// tracks the room live itself).
useEffect(() => {
if (joinedPodId) return;
let alive = true;
const tick = async () => {
try {
const p = await api.getPresence();
if (alive) setPresence(p);
} catch {
/* presence is best-effort */
}
};
void tick();
const id = window.setInterval(() => void tick(), 5000);
return () => {
alive = false;
window.clearInterval(id);
};
}, [joinedPodId]);
// Resume a joined session across a page refresh (auto-reconnect, no re-prompt).
useEffect(() => {
const raw = sessionStorage.getItem(SESSION_KEY);
@@ -134,6 +156,20 @@ export default function App() {
}
}
// Add your name to the roster (if new) and join in one step.
async function handleAddAndJoin(pod: Pod, name: string) {
startPending(pod.id);
setError(null);
try {
upsert(await api.addMember(pod.id, name));
await connectToPod(pod.id, name);
} catch (e) {
setError((e as Error).message);
} finally {
endPending(pod.id);
}
}
function handleLeave() {
room?.disconnect();
setRoom(null);
@@ -203,7 +239,9 @@ export default function App() {
key={pod.id}
pod={pod}
busy={pending.has(pod.id)}
presence={presence[pod.id] ?? []}
onJoin={handleJoin}
onAddAndJoin={handleAddAndJoin}
onAddMember={handleAddMember}
onRemoveMember={handleRemoveMember}
onUpdate={handleUpdate}