/** * The client column of the query tables reads as a name wherever one is known, * with the same precedence the Clients page applies: a hand-typed `name` wins, * the reverse-DNS `learned_name` stands in muted behind it, and an address with * neither — including one the loaded list has never seen — stays bare. * * The muted colour is the whole of the affordance here. The Clients page pairs * it with an outlined "learned" tag, and keeps it: one mention per client is * information. Repeating that tag down every row of a query table is noise, so * the tables carry the name alone. */ import { useMemo } from "react"; import { useQuery } from "@tanstack/react-query"; import * as stylex from "@stylexjs/stylex"; import { clientsQuery } from "@/lib/queries"; import type { Client } from "@/lib/types"; import { styles as shared } from "@/ui/styles"; import { colors } from "@/ui/tokens.stylex"; const styles = stylex.create({ /** Secondary to the name it qualifies, and never the only thing in the cell. */ address: { color: colors.textMuted, }, }); export type ClientNames = ReadonlyMap>; /** * The live stream names clients the loaded list has never seen. Polling folds * them in on the next tick, which keeps the lookup a single cached query * instead of a fetch fired per unknown address. */ const CLIENTS_POLL_MS = 30_000; export function useClientNames(): ClientNames { const { data } = useQuery({ ...clientsQuery(), refetchInterval: CLIENTS_POLL_MS }); return useMemo( () => new Map( (data ?? []).map((client) => [client.ip, { name: client.name, learned_name: client.learned_name }]), ), [data], ); } /** * The name the loaded list gives this address right now, or null when it gives * none. Callers that must distinguish "named" from "bare address" — rather than * just render whichever applies — read this instead of re-deriving precedence. */ export function clientLabel(ip: string, names: ClientNames): { text: string; learned: boolean } | null { const client = names.get(ip); if (client === undefined) return null; if (client.name !== "") return { text: client.name, learned: false }; if (client.learned_name !== "") return { text: client.learned_name, learned: true }; return null; } export function ClientName({ ip, names }: { ip: string; names: ClientNames }) { const label = clientLabel(ip, names); if (label === null) return {ip}; // The name replaces the address, so the address follows it as real text that // anyone can read and copy. A `title` carried it before, which reaches // neither a keyboard nor a touch screen. return ( {label.text} ({ip}) ); }