import { useState, useEffect, useCallback, useRef, useMemo } from "react"; import type { ServerInfo } from "../../relay/types"; import styles from "./ServerBrowser.module.css"; import { useLiveSelector } from "../state/liveConnectionStore"; import { useSettings } from "./SettingsProvider"; import { LuUsers } from "react-icons/lu"; export function ServerBrowser({ onClose }: { onClose: () => void }) { const servers = useLiveSelector((s) => s.servers); const serversLoading = useLiveSelector((s) => s.serversLoading); const browserToRelayPing = useLiveSelector((s) => s.browserToRelayPing); const listServers = useLiveSelector((s) => s.listServers); const joinServer = useLiveSelector((s) => s.joinServer); const { warriorName, setWarriorName } = useSettings(); const [selectedAddress, setSelectedAddress] = useState(null); const handleJoinSelected = () => { if (selectedAddress) { joinServer(selectedAddress, warriorName); onClose(); } }; const handleJoin = (address: string) => { joinServer(address, warriorName); onClose(); }; const [sortKey, setSortKey] = useState("ping"); const [sortDir, setSortDir] = useState<"asc" | "desc">("asc"); const dialogRef = useRef(null); useEffect(() => { dialogRef.current?.focus(); try { document.exitPointerLock(); } catch { /* expected */ } }, []); useEffect(() => { listServers(); }, [listServers]); // Block keyboard events from reaching Three.js while open useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { e.stopPropagation(); if (e.key === "Escape") { onClose(); } }; window.addEventListener("keydown", handleKeyDown, true); return () => window.removeEventListener("keydown", handleKeyDown, true); }, [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]); return (
e.stopPropagation()} >

Server Browser

{servers.length} server{servers.length !== 1 ? "s" : ""}
{sorted.map((server) => ( { setSelectedAddress(server.address); const form = document.forms.namedItem("serverList")!; const inputs = form.elements.namedItem( "serverAddress", ) as RadioNodeList; const input = Array.from(inputs).find( (input) => input.value === server.address, ); input!.focus(); }} onDoubleClick={() => { setSelectedAddress(server.address); handleJoin(server.address); onClose(); }} > ))} {/* {sorted.length === 0 && !serversLoading && ( )} {serversLoading && sorted.length === 0 && ( )} */}
handleSort("name")}> Server Name handleSort("playerCount")} > handleSort("ping")}> Ping handleSort("mapName")}> Map handleSort("gameType")} > Type handleSort("mod")}> Mod
{ setSelectedAddress(event.target.value); }} /> {server.passwordRequired && ( 🔒 )} {server.name} {server.playerCount}  / {server.maxPlayers} {browserToRelayPing != null ? (server.ping + browserToRelayPing).toLocaleString() : "\u2014"} {server.mapName} {server.gameType} {server.mod}
No servers found
Querying master server…
setWarriorName(e.target.value)} placeholder="Name thyself…" maxLength={24} />
Double-click a server to join
); }