"use client"; import { Username } from "./Username"; import { useCallback, useEffect, useRef, useState } from "react"; import { MessageCircle, Send, X } from "lucide-react"; import { api } from "@/lib/client-api"; import { chatMessageSchema, type RoomChat as ChatView, } from "@/shared/multiplayer"; import { useI18n } from "./I18nProvider"; export function RoomChat({ id, playerId, available, playing, endpoint, }: { id: string; playerId: string | null; available: boolean; playing: boolean; endpoint?: string; }) { const { t } = useI18n(); const [open, setOpen] = useState(false); const [messages, setMessages] = useState([]); const [text, setText] = useState(""); const [error, setError] = useState(""); const [sending, setSending] = useState(false); const [lastRead, setLastRead] = useState(0n); const version = useRef(0); const inFlight = useRef(false); const initialized = useRef(false); const pending = useRef<{ text: string; clientId: string } | null>(null); const input = useRef(null); const toggle = useRef(null); const log = useRef(null); const followLatest = useRef(true); const enabled = !!playerId && available; const accept = useCallback((view: ChatView) => { setMessages(view.messages); if (!initialized.current) { initialized.current = true; setLastRead(BigInt(view.messages.at(-1)?.id ?? "0")); } setError(""); }, []); const load = useCallback(async () => { if (inFlight.current) return; const current = ++version.current; try { const view = await api(endpoint ?? `rooms/${id}/chat`); if (current === version.current) accept(view); } catch (failure) { if (current === version.current) setError((failure as Error).message); } }, [id, endpoint, accept]); useEffect(() => { if (!enabled) return; let stopped = false; let timer: ReturnType; const poll = async () => { await load(); if (!stopped) timer = setTimeout(poll, document.hidden ? 5000 : 2000); }; void poll(); const resume = () => { if (!document.hidden) void load(); }; document.addEventListener("visibilitychange", resume); return () => { stopped = true; version.current++; clearTimeout(timer); document.removeEventListener("visibilitychange", resume); }; }, [enabled, load]); useEffect(() => { if (open) { setLastRead(BigInt(messages.at(-1)?.id ?? "0")); if (log.current && followLatest.current) log.current.scrollTop = log.current.scrollHeight; } }, [open, messages]); useEffect(() => { if (open) { followLatest.current = true; if (log.current) log.current.scrollTop = log.current.scrollHeight; input.current?.focus(); } }, [open]); async function send() { if (inFlight.current || !enabled) return; const draft = text.trim(); const body = pending.current?.text === draft ? pending.current : { text: draft, clientId: crypto.randomUUID() }; if (!chatMessageSchema.safeParse(body).success) { setError("Write a message of 1–500 characters without hidden controls"); return; } pending.current = body; inFlight.current = true; setSending(true); setError(""); const current = ++version.current; try { const view = await api(endpoint ?? `rooms/${id}/chat`, body); if (current === version.current) { accept(view); followLatest.current = true; setText(""); pending.current = null; } } catch (failure) { if (current === version.current) setError((failure as Error).message); } finally { inFlight.current = false; setSending(false); input.current?.focus(); } } if (!enabled) return null; const unread = messages.filter( (message) => message.playerId !== playerId && BigInt(message.id) > lastRead, ).length; return ( ); }