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:
1 parent
a98c750908
commit
b8e6fb7396
11 files changed
+454
-6
No files matched your search
@@ -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");
|
||||
|
||||
@@ -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.
Reference in new issue
Block a user