who has this: a gap card says who holds it, the sheet and the open album name them, a Space grows a Holdings tab, and Get it is the hub's own grab or a wait for their presence

This commit is contained in:
KeelrLeek committed 2026-09-08 12:10:49 -05:00
1 parent a98c750908
commit b8e6fb7396
11 files changed
+454 -6

No files matched your search

+8
View File
@@ -196,6 +196,14 @@ export const spaceBoard = (owner, space) => invoke("space_board", { owner, space
export const spacePropose = (owner, space, base, ops, note) => invoke("space_propose", { owner, space, base, ops, note });
export const spaceDecide = (owner, space, proposal, accept, reason) => invoke("space_decide", { owner, space, proposal, accept, reason });
export const overlapList = () => invoke("overlap_list");
export const holdingsSetShare = (owner, space, on) => invoke("holdings_set_share", { owner, space, on });
export const holdingsRoom = (owner, space, refresh = false) => invoke("holdings_room", { owner, space, refresh });
export const holdingsAsk = (items) => invoke("holdings_ask", { items });
export const holdingsFindFolder = (username, artist, album) => invoke("holdings_find_folder", { username, artist, album });
export const holdingsRepublish = () => invoke("holdings_republish");
export const holdingsWaits = () => invoke("holdings_waits");
export const holdingsWaitAdd = (username, artist, album, via = null) => invoke("holdings_wait_add", { username, artist, album, via });
export const holdingsWaitRemove = (username, artist, album) => invoke("holdings_wait_remove", { username, artist, album });
export const overlapCompute = (username) => invoke("overlap_compute", { username });
export const overlapForget = (username) => invoke("overlap_forget", { username });
export const buddyList = () => invoke("buddy_list");
+85 -2
View File
@@ -91,6 +91,11 @@ export const community = $state({
boardError: "",
/** overlap_list: library comparisons computed on this device, best first. */
overlap: [],
/** `owner:space` → holdings_room: who in that room publishes what they
* hold, and whether this device does. Read when a Space opens. */
holdings: {},
/** holdings_waits: albums to grab from someone the moment they come online. */
waits: [],
/** persona key → ProfileView | {none} | {error} | {pending}: other
* people's signed profiles, read once per session when surfaced. */
remoteProfiles: {},
@@ -350,6 +355,18 @@ function wireEvents() {
void refreshFollowing();
});
api.onEvent("community:overlap", () => void refreshOverlap());
api.onEvent("community:holdings", () => {
// A read finished, a publish landed, a wait changed: re-read the
// open room's lists from the cache (fresh members cost no network).
if (community.space) void refreshHoldings(community.space.owner, community.space.space_id);
void refreshWaits();
});
// The library changed: every room with the switch on gets the new
// list, coalesced so a scan's burst of changes is one publish.
api.onEvent("soulseek:library-changed", () => {
clearTimeout(republishTimer);
republishTimer = setTimeout(() => void api.holdingsRepublish().catch(() => {}), REPUBLISH_DEBOUNCE_MS);
});
api.onEvent("community:proposal", (p) => {
announceProposal(p.from, p.playlist);
// If the board is already on screen, show what it is talking about.
@@ -357,6 +374,8 @@ function wireEvents() {
});
api.onEvent("soulseek:user-state", (p) => {
if (p.status === 1 || p.status === 2) noteSeen(p.username);
// Someone we are waiting on came online: the grab runs now.
if (p.status === 2) void drainWaits(p.username);
});
}
}
@@ -364,7 +383,7 @@ function wireEvents() {
/** Idempotent: pulls the state the Community page draws. */
export async function initCommunity() {
wireEvents();
await Promise.all([refreshFreenet(), loadContacts(), refreshLinks(), refreshFriends(), refreshShares(), refreshPublished(), refreshFollowing(), refreshInbox(), refreshSpaces(), refreshOverlap()]);
await Promise.all([refreshFreenet(), loadContacts(), refreshLinks(), refreshFriends(), refreshShares(), refreshPublished(), refreshFollowing(), refreshInbox(), refreshSpaces(), refreshOverlap(), refreshWaits()]);
await refreshPersona();
}
@@ -379,6 +398,9 @@ export async function initCommunity() {
* that comes up later — or an inbox published later — simply starts working
* without anything having to restart the timer. */
const INBOX_POLL_MS = 90_000;
/** A library scan raises many change events in a row; one publish serves them all. */
const REPUBLISH_DEBOUNCE_MS = 30_000;
let republishTimer = 0;
/** Followed playlists are read far less often: every pointer read is a ring
* search that can take seconds, and an owner's next revision is a matter
* of days, not minutes. */
@@ -705,6 +727,67 @@ export async function refreshSpaces() {
}
}
/** Read a Space through the node; the view is what the checkpoint says. */
// ---- holdings: who has this, and get it from them ---------------------------------
const holdingsId = (owner, space) => `${owner}:${space}`;
/** The room's lists as the cache has them; the backend reads stale members
* in the background and the event brings the result back here. */
export async function refreshHoldings(owner, space, refresh = false) {
try {
community.holdings[holdingsId(owner, space)] = await api.holdingsRoom(owner, space, refresh);
} catch {
// stays as it was
}
}
export const holdingsFor = (owner, space) => community.holdings[holdingsId(owner, space)] ?? null;
/** The switch: publish this device's list into the room, or withdraw it. */
export const setShareHoldings = (owner, space, on) =>
run("holdings:share", async () => {
community.holdings[holdingsId(owner, space)] = await api.holdingsSetShare(owner, space, on);
if (community.space?.owner === owner && community.space?.space_id === space) await openSpace(owner, space);
});
/** Who holds each (artist, album), across every room — local, instant. */
export const askHoldings = (items) => api.holdingsAsk(items);
/** Get it: browse the holder, find the folder the room said they hold, and
* queue every audio file through the hub's own download — the transfers
* page shows them like any album grab. */
export const holdingsGrab = (username, artist, album) =>
run(`grab:${username}:${album}`, async () => {
const m = await api.holdingsFindFolder(username, artist, album);
if (!m) throw new Error(`${username}'s share does not name ${album} any more`);
for (const f of m.files) await api.downloadFile(username, f.path, f.size);
return m;
});
export async function refreshWaits() {
try {
community.waits = await api.holdingsWaits();
} catch {
// stays as it was
}
}
export const waitFor = (username, artist, album, via = null) =>
run(`wait:${username}:${album}`, async () => {
community.waits = await api.holdingsWaitAdd(username, artist, album, via);
});
export const forgetWait = (username, artist, album) =>
run(`wait:${username}:${album}`, async () => {
community.waits = await api.holdingsWaitRemove(username, artist, album);
});
export const waitingOn = (username, artist, album) =>
community.waits.some((w) => w.username.toLowerCase() === (username ?? "").toLowerCase() && w.artist === artist && w.album === album);
/** A waited-on person came online: run their grabs, each once. */
async function drainWaits(username) {
const mine = community.waits.filter((w) => w.username.toLowerCase() === (username ?? "").toLowerCase());
for (const w of mine) {
try {
await holdingsGrab(w.username, w.artist, w.album);
await forgetWait(w.username, w.artist, w.album);
} catch {
// their share no longer names it, or the browse failed: the wait
// stays, and the row says what happened when it is looked at
}
}
}
export async function openSpace(owner, space) {
community.spaceError = "";
community.busy["space:" + space] = true;
@@ -814,4 +897,4 @@ export const decideProposal = (owner, space, proposal, accept, reason) =>
// Dev handle beside `__soulstream` (state.svelte.js): probes reach the store
// through this, never through a bare import (which would be a second copy).
if (import.meta.env.DEV) globalThis.__community = community;
if (import.meta.env.DEV) globalThis.__communityActions = { friendRequest, friendRequestByKey, friendAccept, friendDecline, friendRemove, friendForget, checkLink, refreshFriends, sharedPublish, sharedWithdraw, shareSend, sharePreview, shareImport, refreshShares, refreshPublished, refreshFollowing, followFor, followNews, checkFollows, followUpdate, unfollow, publishInbox, checkInbox, refreshInbox, refreshSpaces, openSpace, createSpace, postToSpace, inviteToSpace, removeFromSpace, pinToSpace, leaveSpace, setShareListens, openBoard, proposeToBoard, decideProposal, refreshOverlap, computeOverlap, forgetOverlap, refreshInterests, setInterest };
if (import.meta.env.DEV) globalThis.__communityActions = { refreshHoldings, holdingsFor, setShareHoldings, askHoldings, holdingsGrab, refreshWaits, waitFor, forgetWait, waitingOn, friendRequest, friendRequestByKey, friendAccept, friendDecline, friendRemove, friendForget, checkLink, refreshFriends, sharedPublish, sharedWithdraw, shareSend, sharePreview, shareImport, refreshShares, refreshPublished, refreshFollowing, followFor, followNews, checkFollows, followUpdate, unfollow, publishInbox, checkInbox, refreshInbox, refreshSpaces, openSpace, createSpace, postToSpace, inviteToSpace, removeFromSpace, pinToSpace, leaveSpace, setShareListens, openBoard, proposeToBoard, decideProposal, refreshOverlap, computeOverlap, forgetOverlap, refreshInterests, setInterest };
Binary file not shown.