import { useState, useEffect, useCallback, useRef, useMemo } from "react"; import type { ServerInfo } from "../../relay/types"; import styles from "./ServerBrowser.module.css"; export function ServerBrowser({ open, onClose, servers, loading, onRefresh, onJoin, wsPing, }: { open: boolean; onClose: () => void; servers: ServerInfo[]; loading: boolean; onRefresh: () => void; onJoin: (address: string) => void; /** Browser↔relay RTT to add to server pings for effective latency. */ wsPing?: number | null; }) { const [selectedAddress, setSelectedAddress] = useState(null); const [sortKey, setSortKey] = useState("ping"); const [sortDir, setSortDir] = useState<"asc" | "desc">("asc"); const dialogRef = useRef(null); const onRefreshRef = useRef(onRefresh); onRefreshRef.current = onRefresh; const didAutoRefreshRef = useRef(false); useEffect(() => { if (open) { dialogRef.current?.focus(); try { document.exitPointerLock(); } catch { /* expected */ } } else { didAutoRefreshRef.current = false; } }, [open]); // Refresh on open if no servers cached useEffect(() => { if (open && servers.length === 0 && !didAutoRefreshRef.current) { didAutoRefreshRef.current = true; onRefreshRef.current(); } }, [open]); // eslint-disable-line react-hooks/exhaustive-deps // Block keyboard events from reaching Three.js while open useEffect(() => { if (!open) return; const handleKeyDown = (e: KeyboardEvent) => { e.stopPropagation(); if (e.key === "Escape") { onClose(); } }; window.addEventListener("keydown", handleKeyDown, true); return () => window.removeEventListener("keydown", handleKeyDown, true); }, [open, onClose]); const handleSort = useCallback( (key: keyof ServerInfo) => { if (sortKey === key) { setSortDir((d) => (d === "asc" ? "desc" : "asc")); } else { setSortKey(key); setSortDir("desc"); } }, [sortKey], ); const sorted = useMemo(() => { return [...servers].sort((a, b) => { const av = a[sortKey]; const bv = b[sortKey]; const cmp = typeof av === "number" && typeof bv === "number" ? av - bv : String(av).localeCompare(String(bv)); return sortDir === "asc" ? cmp : -cmp; }); }, [servers, sortDir, sortKey]); const handleJoin = useCallback(() => { if (selectedAddress) { onJoin(selectedAddress); onClose(); } }, [selectedAddress, onJoin, onClose]); if (!open) return null; return (
e.stopPropagation()} >

Server Browser

{servers.length} server{servers.length !== 1 ? "s" : ""}
{sorted.map((server) => ( setSelectedAddress(server.address)} onDoubleClick={() => { setSelectedAddress(server.address); onJoin(server.address); onClose(); }} > ))} {sorted.length === 0 && !loading && ( )} {loading && sorted.length === 0 && ( )}
handleSort("name")}>Server Name handleSort("playerCount")}>Players handleSort("ping")}>Ping handleSort("mapName")}>Map handleSort("gameType")}>Type handleSort("mod")}>Mod
{server.passwordRequired && ( 🔒 )} {server.name} {server.playerCount}/{server.maxPlayers} {wsPing != null ? (server.ping + wsPing).toLocaleString() : "\u2014"} {server.mapName} {server.gameType} {server.mod}
No servers found
Querying master server...
Double-click a server to join
); }