// Round twenty-two: your own Soulseek interests, and who the server says is // like you. Community grows a fourth tab, Taste — the likes and hates this // account publishes (slsk_interests_mine, slsk_interest_set) and the people // the server ranks on them (slsk_similar_users), each a row into the peer // sheet. Every declaration asks the server again: the ranking only moves // when its input does. 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 ins = (f) => fs.readFileSync(SCRATCH + f, 'utf8'); const NL = String.fromCharCode(10); 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: the lists, the match, and one rule ── fs.writeFileSync(root + 'src/mobile/taste.svelte.js', ins('ins-r22-taste-store.txt')); // ── Community: the fourth tab ── load('src/mobile/screens/Community.svelte'); rep(` /* Community — the Soulseek side: rooms, chat, buddies. Signed out it is the sign-in card; signed in it is the hub, the desktop's seats drawn with the phone's components. The private layer (Spaces, holdings, the sealed inbox) waits for the Freenet story on the phone. */`, ` /* Community — the Soulseek side: rooms, chat, buddies, taste. Signed out it is the sign-in card; signed in it is the hub, the desktop's seats drawn with the phone's components. The private layer (Spaces, holdings, the sealed inbox) waits for the Freenet story on the phone. */`); rep(` import { ago } from "../util.js";`, ` import { ago } from "../util.js"; import { taste, loadTaste, setInterest, askSimilar } from "../taste.svelte.js";`); rep(`["buddies", "Buddies"]`, `["buddies", "Buddies"], ["taste", "Taste"]`); rep(` async function signIn() {`, ` /* your own declared interests and the people the server ranks close to them — read once per account, when the tab is opened */ $effect(() => { if (online && tab === "taste") void loadTaste(); }); function declare(like) { openHalf("prompt", { title: like ? "Declare a like" : "Declare a hate", placeholder: like ? "an artist, a label, a genre" : "an artist or a genre you would rather not hear", ok: "Declare", onsubmit: (v) => { void setInterest(v, like, true); }, }); } async function signIn() {`); { const a = ` {/if}
`; rep(a, ins('ins-r22-community.txt') + a); } save(); // ── the chips wrap: a declared list is read, not swiped ── load('src/mobile/shell.css'); s = s.replace(/\s*$/, NL) + ` /* Community → Taste: the interests you publish, wrapped rather than scrolled — a declared list is read, not swiped */ .tastechips { flex-wrap: wrap; overflow: visible; } .tastechips .tchip svg { opacity: 0.62; margin-right: -3px; } .tastechips .chip:disabled { opacity: 0.5; } `; save(); // ── mocks: a fake account's declarations, and a match that moves with them ── load('src/lib/mockLibrary.js'); rep(`function mockNet(cmd, args) {`, `/* the interests this fake account has declared, and the server's match on them — a declaration moves both, the way the real server does */ const MY_INTERESTS = { likes: ["Daft Punk", "french house", "Warp Records"], hates: ["loudness war"] }; function mockNet(cmd, args) {`); rep(` if (cmd === "stop_search") return Promise.resolve();`, ` if (cmd === "stop_search") return Promise.resolve(); if (cmd === "slsk_interests_mine") return Promise.resolve({ likes: MY_INTERESTS.likes.slice(), hates: MY_INTERESTS.hates.slice() }); if (cmd === "slsk_interest_set") { const thing = String(args?.thing || "").trim(); if (!thing) return Promise.reject(new Error("nothing to declare")); const list = args?.like ? MY_INTERESTS.likes : MY_INTERESTS.hates; const at = list.findIndex((t) => t.toLowerCase() === thing.toLowerCase()); if (args?.on && at < 0) list.push(thing); if (!args?.on && at >= 0) list.splice(at, 1); return new Promise((r) => setTimeout(() => r({ likes: MY_INTERESTS.likes.slice(), hates: MY_INTERESTS.hates.slice() }), 200)); } if (cmd === "slsk_similar_users") { /* the server ranks on the likes we declared: one more name per like */ const pool = ["hifi_hank", "dusty_crates", "mp3_marge", "ada", "cy", "bo"]; const n = MY_INTERESTS.likes.length; return new Promise((r) => setTimeout(() => r(pool.slice(0, Math.min(pool.length, n + 1)).map((username, i) => ({ username, rating: Math.max(1, n - i) }))), 300)); }`); save(); // ── the walk: declared, taken back, and the match moving with it ── load('tests/mobile/smoke.mjs'); rep(` cardPhs: document.querySelectorAll('.rail .acard svg').length,`, ` cardPhs: document.querySelectorAll('.rail .acard svg').length, likes: [].map.call(document.querySelectorAll('.tastechips.likes .tchip'), function (c) { return c.textContent.trim(); }).join('|'), hates: [].map.call(document.querySelectorAll('.tastechips.hates .tchip'), function (c) { return c.textContent.trim(); }).join('|'), similar: document.querySelectorAll('.screen .row.similar').length,`); { const a = ` { do: \`__click('.segtabs button', 'Rooms')\`, wait: 500 },`; rep(a, ins('ins-r22-smoke-steps.txt') + a); } rep(` "home-more": (i) => i.half.includes("up"),`, ` taste: (i) => i.likes === "Daft Punk|french house|Warp Records" && i.hates === "loudness war" && i.similar === 4, "taste-declared": (i) => /Boards of Canada/.test(i.likes) && i.similar === 5, "taste-hated": (i) => i.hates === "loudness war|loudness war 2", "taste-dropped": (i) => !/Boards of Canada/.test(i.likes) && i.similar === 4 && !i.toast, "taste-peer": (i) => i.half.includes("up") && /hifi_hank/.test(i.halfText), "home-more": (i) => i.half.includes("up"),`); save(); // ── the doc ── load('ANDROID.md'); rep(`Community (sign in; rooms, chat, buddies`, `Community (sign in; rooms, chat, buddies, taste`); { const a = `The server's room list on Community is filtered by name and shows 80 at a time with a way to the rest (Soulseek lists hundreds).`; rep(a, a + NL + NL + ins('ins-r22-doc.txt').replace(/\s*$/, '')); } save(); console.log('round twenty-two wired');