/* The local view of a person on the network: a petname, a group, a private note and the two marks. All of it lives in this app's own config and is published nowhere — Soulseek never learns it, and neither does the person it describes. The desktop keeps the same index by username (src/lib/community.svelte.js), and the same rule: the backend drops an entry with nothing set, so the list it answers with is the one we take. */ import * as api from "../lib/api.js"; import { player } from "./player.svelte.js"; export const contacts = $state({ /** username → { username, petname, group, note, trusted, favorite } */ by: {}, /** the list came back; false until it does, and after a failure */ loaded: false, error: "", }); const BLANK = { petname: null, group: null, note: null, trusted: false, favorite: false }; /** what the backend stores: a trimmed string, or nothing at all */ const trimmed = (v) => { const s = typeof v === "string" ? v.trim() : v; return s ? s : null; }; function index(list) { contacts.by = Object.fromEntries((list || []).map((c) => [c.username, c])); contacts.loaded = true; contacts.error = ""; } let inflight = null; /** Read the whole list — once at boot, and again whenever anyone asks. A * failure is quiet: a petname is an enhancement, not the screen. */ export function loadContacts() { if (inflight) return inflight; inflight = api .contactsMetaList() .then(index) .catch((e) => { contacts.loaded = false; contacts.error = String(e); }) .finally(() => { inflight = null; }); return inflight; } export const metaOf = (u) => contacts.by[u] ?? { username: u, ...BLANK }; /** The name to show: the petname when there is one, the username otherwise. */ export const display = (u) => contacts.by[u]?.petname || u; /** The username, but only while a petname stands in front of it — a stranger * is never quietly turned into someone you know. */ export const realName = (u) => (contacts.by[u]?.petname ? u : ""); /** Upsert one person. The answer is the whole list after the write, so it is * taken as it comes rather than patched into our own copy: that is how an * entry with nothing left in it disappears. A failure goes to the bar. */ export async function setMeta(username, patch) { const m = { ...metaOf(username), ...patch }; try { index( await api.contactMetaSet({ username, petname: trimmed(m.petname), group: trimmed(m.group), note: trimmed(m.note), trusted: !!m.trusted, favorite: !!m.favorite, }), ); } catch (e) { player.error = String(e); } } /** Everything blank: the entry leaves the list rather than being stored empty. */ export const clearMeta = (u) => setMeta(u, { petname: "", group: "", note: "", trusted: false, favorite: false });