// 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');