Everything written this session that has not been through the device: rounds 43 to 48 (each patch adversarially verified, 43's transient-notice expectation fixed), a partial round 49, the pipeline scripts, the Android Auto design and the icon comparison mock. Also the two harness changes that were never A/B'd — adaptive waits, which may only shorten a declared wait and never lengthen it, and a ship that runs the mock walk and the deploy at the same time. The A/B and its 255-step baseline are in handoff/tools; main stays at what the phone has actually walked.
71 lines
2.9 KiB
Plaintext
71 lines
2.9 KiB
Plaintext
/* 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 });
|