import { toneText } from '@/components/Data/tone'; import { useState, useEffect } from 'react'; import { useSearchParams } from 'react-router'; import { toast } from 'sonner'; import { Users, Search, CircleCheck, CircleSlash, Clock, Loader2, Star, X, Download } from 'lucide-react'; import { useSoulseekFavorites } from './useSoulseekFavorites'; import { useSoulseekUser } from './useSoulseekUser'; import { useSoulseekBrowseSnapshots } from './useSoulseekBrowse'; import { SharesBrowser, BrowseStateChip } from './SharesBrowser'; import { USER_PARAM } from './shared'; // Users panel — look up a peer: their presence (online/away/offline) and profile info (description, // upload slots, queue), both cheap live calls (GET /users/{u}/status + /info). Their shared folders come // from Officer's own cache instead, via — see that file for why it isn't browsed live. // // This is also where the workspace's peer actions land: the username dropdown in search results and // downloads links here with `?user=`, which is the whole mechanism — a peer profile is now // linkable and survives a reload. // Favourites — Officer's own data, since slskd has no such concept — get their own section at the top, // which also serves as this panel's landing content before any lookup. const presenceStyle = (p?: string): { label: string; dot: string; icon: typeof CircleCheck } => { switch ((p ?? '').toLowerCase()) { case 'online': return { label: 'Online', dot: toneText.success, icon: CircleCheck }; case 'away': return { label: 'Away', dot: toneText.warning, icon: Clock }; default: return { label: 'Offline', dot: toneText.neutral, icon: CircleSlash }; } }; export const SoulseekUsers = () => { const [params, setParams] = useSearchParams(); const selected = params.get(USER_PARAM)?.trim() ?? ''; const [query, setQuery] = useState(selected); const { favorites, isFavorite, toggle } = useSoulseekFavorites(); const { snapshotOf, fetchShares } = useSoulseekBrowseSnapshots(); // Selecting a peer is now just naming one — the query owns the fetching, and its cache owns the answer. const user = useSoulseekUser(selected); const peer = user.data ?? null; // Navigating, not setting state: the peer is `?user=`. Shares are still NOT fetched automatically — // that's a multi-minute server-side job, so it stays an explicit click in the shares card. const select = (name: string) => { const username = name.trim(); if (!username) return; setParams((prev) => { const next = new URLSearchParams(prev); next.set(USER_PARAM, username); return next; }); }; // Arriving by link (or Back) should show the name in the box that produced it. useEffect(() => { if (selected) setQuery(selected); }, [selected]); useEffect(() => { if (user.error) toast.error(user.error.message); }, [user.error]); const pres = presenceStyle(peer?.status?.presence); const PresIcon = pres.icon; // Only a peer we have nothing cached for is worth showing a pending state for. const pending = !!selected && user.isPending; return (
{ ev.preventDefault(); select(query); }} className="flex flex-1 items-center gap-2" > setQuery(ev.target.value)} placeholder="Look up a username…" className="h-9 min-w-0 flex-1 rounded-lg border bg-background px-3 text-sm outline-none focus:border-primary/50 focus:ring-2 focus:ring-primary/20" />
{/* Favorites — Officer's own data (slskd has no such concept). Always present, so the section doubles as the panel's landing content before any lookup. */}
Favorites {favorites.length > 0 && · {favorites.length}}
{favorites.length === 0 ? (

No favorites yet — add one from a peer's profile below, or from the username menu in search results and downloads.

) : (
{favorites.map((u) => { const current = selected === u; return (
{!snapshotOf(u) && ( )}
); })}
)}
{!selected ? (

Enter a Soulseek username to see their profile and shares.

) : ( <> {/* Profile. Keyed off the selected name, not the loaded peer, so a lookup already in cache paints instantly and a fresh one fills in around a card that's already on screen. */}
{selected.charAt(0).toUpperCase()}
{selected}
{pending ? (
Checking…
) : (
{pres.label} {peer?.status?.isPrivileged && · privileged}
)}
{peer?.info && ( <>
{peer.info.description && (

{peer.info.description}

)} )}
)}
); }; type StatProps = { label: string; value: string | number; accent?: string }; const Stat = ({ label, value, accent = 'text-foreground' }: StatProps) => (
{label} {value}
);