// Round twenty-one: a name of your own. The desktop's local contact
// metadata on the phone — a petname, a group, a private note and the two
// marks (Favorite, Trusted), kept in the app's own config and published
// nowhere. The petname stands in for the username wherever Community shows
// one, with the real name on the line beneath it.
const fs = require('fs');
const root = process.env.SS_ROOT || 'F:/Coding/SoulStream/';
const scratch = 'C:/Users/Kyle/AppData/Local/Temp/claude/F--Coding-SoulStream/5488514a-11e8-482a-b0a0-20cb511ee110/scratchpad/';
const NL = String.fromCharCode(10);
const ins = (name) => fs.readFileSync(scratch + name, 'utf8').replace(/\r\n/g, NL).replace(/\s+$/, '');
let p, s;
const load = (file) => { p = file; s = fs.readFileSync(root + p, 'utf8'); };
const save = () => fs.writeFileSync(root + p, s);
const once = (a) => {
const i = s.indexOf(a);
if (i < 0) throw new Error('missing in ' + p + ': ' + a.slice(0, 90));
if (s.indexOf(a, i + 1) >= 0) throw new Error('ambiguous in ' + p + ': ' + a.slice(0, 90));
return i;
};
const rep = (a, b) => { once(a); s = s.replace(a, () => b); };
// ── the store ──
fs.writeFileSync(root + 'src/mobile/contacts.svelte.js', ins('ins-r21-store.txt') + NL);
// ── the shell: the list read at boot, the petname on a thread's bar ──
load('src/mobile/Mobile.svelte');
rep(` import { downloadsAllowed, cellular } from "./network.svelte.js";`,
` import { downloadsAllowed, cellular } from "./network.svelte.js";
import { contacts, loadContacts, display } from "./contacts.svelte.js";`);
rep(` case "thread": return { title: e.c.username, kicker: "Chat", flush: true };`,
` case "thread": return { title: display(e.c.username), kicker: "Chat", flush: true };`);
rep(` bindImporter();
bindShare();`,
` bindImporter();
bindShare();
loadContacts();`);
rep(`, patch };`, `, patch, contacts, loadContacts };`);
save();
// ── the sheets: the peer's head and its new door, the editor, the roster ──
load('src/mobile/HalfSheet.svelte');
rep(` import { art, wantArt } from "./art.svelte.js";`,
` import { art, wantArt } from "./art.svelte.js";
import { metaOf, display, realName, setMeta, clearMeta } from "./contacts.svelte.js";`);
rep(` {@const buddy = slsk.buddies.includes(u)}`,
` {@const buddy = slsk.buddies.includes(u)}
{@const m = metaOf(u)}`);
rep(` {u}{g ? (g.slot ? "slot free" : "queue " + g.queue) + " · " + speedLabel(g.speed) + " · " + g.folderName : "on Soulseek"}`,
` {display(u)}{#if m.favorite}{/if}{#if m.trusted}{/if}{[realName(u), m.group, g ? (g.slot ? "slot free" : "queue " + g.queue) + " · " + speedLabel(g.speed) + " · " + g.folderName : "on Soulseek"].filter(Boolean).join(" · ")}`);
{
const a = ` {@render row(buddy ? "x" : "heart", buddy ? "Remove buddy" : "Add buddy", buddy ? "" : "buddies stay on the Community screen", act(() => (buddy ? removeBuddy : addBuddy)(u).catch(oops)))}`;
rep(a, a + NL
+ `
` + NL
+ ` {@render row("pencil", "Name and notes", "a petname, a group, a note — this phone only", () => openHalf("contact", { username: u }))}`);
}
{
const a = ` /* the desktop's per-record cover refresh; the thumbnail is asked for again */`;
rep(a, ins('ins-r21-prompt.txt') + NL + a);
}
{
const a = ` {:else if (kind === "catalogAlbum-more" || kind === "catalogAlbum") && catPage}`;
rep(a, ins('ins-r21-sheet.txt') + NL + a);
}
rep(` {u.username}{#if buddy} BUDDY{/if}{u.file_count ? u.file_count.toLocaleString() + " files" : ""}`,
` {display(u.username)}{#if buddy} BUDDY{/if}{[realName(u.username), u.file_count ? u.file_count.toLocaleString() + " files" : ""].filter(Boolean).join(" · ")}`);
save();
// ── Community: favourites first, the name you gave them on the row ──
load('src/mobile/screens/Community.svelte');
rep(` import { push, patch, openHalf } from "../nav.svelte.js";`,
` import { push, patch, openHalf } from "../nav.svelte.js";
import { metaOf, display, realName } from "../contacts.svelte.js";`);
rep(` const buddies = $derived(slsk.buddies.map((u) => ({ u, st: slsk.buddyState[u] || {} })));`,
` /* favourites at the top, then by the name you gave them */
const buddies = $derived(
slsk.buddies
.map((u) => ({ u, st: slsk.buddyState[u] || {}, m: metaOf(u) }))
.sort((a, b) => (b.m.favorite ? 1 : 0) - (a.m.favorite ? 1 : 0) || display(a.u).localeCompare(display(b.u))),
);`);
rep(` {/if}`,
` {#if !me}{/if}`);
save();
// ── the marks' style ──
load('src/mobile/shell.css');
{
const a = `/* the nav's badges: transfers in flight on You, unread chats on Community */`;
rep(a, ins('ins-r21-css.txt') + NL + NL + a);
}
save();
// ── the mock: one person already named, and the backend's drop rule ──
load('src/lib/mockLibrary.js');
rep(`const LINES = ["anyone got the 2024 remaster?", "the vinyl rip is warmer imo", "that folder is complete btw", "brb"];`,
`const LINES = ["anyone got the 2024 remaster?", "the vinyl rip is warmer imo", "that folder is complete btw", "brb"];
/* one person already named, so a cold read of the list has something in it */
const CONTACTS = [{ username: "dusty_crates", petname: "Dusty the digger", group: "Traders", note: "always has the vinyl rips", trusted: true, favorite: false }];`);
rep(` if (cmd === "buddy_remove") return Promise.resolve(["ada", "cy"].filter((u) => u !== args?.username));`,
` if (cmd === "buddy_remove") return Promise.resolve(["ada", "cy"].filter((u) => u !== args?.username));
if (cmd === "contacts_meta_list") return Promise.resolve(CONTACTS.slice());
if (cmd === "contact_meta_set") {
/* the backend's own rule: an entry with nothing set is dropped */
const meta = args?.meta || {};
const txt = (v) => (typeof v === "string" && v.trim() ? v.trim() : null);
const i = CONTACTS.findIndex((c) => c.username === meta.username);
if (i >= 0) CONTACTS.splice(i, 1);
const kept = { username: meta.username, petname: txt(meta.petname), group: txt(meta.group), note: txt(meta.note), trusted: !!meta.trusted, favorite: !!meta.favorite };
if (kept.petname || kept.group || kept.note || kept.trusted || kept.favorite) CONTACTS.push(kept);
return Promise.resolve(CONTACTS.slice());
}`);
save();
// ── the walks ──
load('tests/mobile/smoke.mjs');
{
const a = ` cardPhs: document.querySelectorAll('.rail .acard svg').length,`;
rep(a, a + NL + ins('ins-r21-probe.txt'));
}
{
const a = ` { do: \`document.querySelectorAll('.screen button.row')[1].dispatchEvent(new MouseEvent('contextmenu', { bubbles: true, cancelable: true }))\`, name: "buddy-sheet", wait: 600 },`;
rep(a, a + NL + ins('ins-r21-steps-b.txt'));
}
{
const a = ` { do: \`__click('.screen button.row', 'Lounge')\`, name: "room", wait: 2400 },`;
rep(a, a + NL + ins('ins-r21-steps-c.txt'));
}
{
const a = ` /* a search answered by five peers; the best one's first file streams,`;
rep(a, ins('ins-r21-steps-d.txt') + NL + a);
}
rep(` "buddy-sheet": (i) => i.half.includes("up") && /Browse their files/.test(i.halfText) && /Remove buddy/.test(i.halfText),`,
` "buddy-sheet": (i) => i.half.includes("up") && /Browse their files/.test(i.halfText) && /Remove buddy/.test(i.halfText) && /Name and notes/.test(i.halfText),`);
rep(` "user-lookup": (i) => i.half.includes("up") && /dusty_crates/.test(i.halfText) && /Browse their files/.test(i.halfText) && /Add buddy/.test(i.halfText),`,
ins('ins-r21-expect.txt'));
{
const a = ` { do: \`document.querySelectorAll('.nav button')[3].click()\`, name: "community", wait: 900 },`;
rep(a, a + NL
+ ` /* the real config's contact list, read back over the real IPC */` + NL
+ ` { do: \`__ss.loadContacts()\`, name: "contacts-read", wait: 800 },`);
}
{
const a = ` "artist-cold": (i) => i.screen.includes("has-hero"),`;
rep(a, ` "contacts-read": (i) => i.contactsLoaded === "yes" && !i.toast,` + NL + a);
}
save();
// ── the doc ──
load('ANDROID.md');
rep(`| \`src/mobile/slsk.svelte.js\` | the Soulseek hub: the connection, the rooms, the chats, the buddies — the desktop's \`slsk\` store and events |`,
`| \`src/mobile/slsk.svelte.js\` | the Soulseek hub: the connection, the rooms, the chats, the buddies — the desktop's \`slsk\` store and events |
| \`src/mobile/contacts.svelte.js\` | the local view of a person on the network: a petname, a group, a note and the two marks (\`contacts_meta_list\` / \`contact_meta_set\`) — this phone only, published nowhere |`);
{
const a = `More long presses: a catalog song in the search results opens the catalog`;
rep(a, ins('ins-r21-doc.txt') + NL + NL + a);
}
save();
console.log('round twenty-one wired');