community: the encrypted inbox, friend requests and shares travel sealed to a box key when the other side has published one
This commit is contained in:
1 parent
13c44f731a
commit
4378c4c0cc
14 files changed
+1387
-145
No files matched your search
@@ -158,6 +158,7 @@ export const slskLinkCheck = (username) => invoke("slsk_link_check", { username
|
||||
export const slskLinkForget = (username) => invoke("slsk_link_forget", { username });
|
||||
export const friendsList = () => invoke("friends_list");
|
||||
export const friendRequest = (username) => invoke("friend_request", { username });
|
||||
export const friendRequestByKey = (personaKey) => invoke("friend_request_by_key", { personaKey });
|
||||
export const friendAccept = (id) => invoke("friend_accept", { id });
|
||||
export const friendDecline = (id) => invoke("friend_decline", { id });
|
||||
export const friendRemove = (id) => invoke("friend_remove", { id });
|
||||
@@ -170,6 +171,9 @@ export const sharedPlaylistImport = (owner, uuid) => invoke("shared_playlist_imp
|
||||
export const shareSend = (username, playlistId) => invoke("share_send", { username, playlistId });
|
||||
export const sharesInbox = () => invoke("shares_inbox");
|
||||
export const shareDismiss = (id) => invoke("share_dismiss", { id });
|
||||
export const inboxStatus = () => invoke("inbox_status");
|
||||
export const inboxPublish = () => invoke("inbox_publish");
|
||||
export const inboxCheck = () => invoke("inbox_check");
|
||||
export const buddyList = () => invoke("buddy_list");
|
||||
export const buddyAdd = (username) => invoke("buddy_add", { username });
|
||||
export const buddyRemove = (username) => invoke("buddy_remove", { username });
|
||||
|
||||
@@ -71,6 +71,10 @@ export const community = $state({
|
||||
sharePreviews: {},
|
||||
/** shared_playlists_mine: what this device has published. */
|
||||
published: [],
|
||||
/** inbox_status: the encrypted inbox's state on this device. */
|
||||
inbox: null,
|
||||
/** What the last check found ({new_items, quarantined, pruned}). */
|
||||
inboxLast: null,
|
||||
/** persona key → ProfileView | {none} | {error} | {pending}: other
|
||||
* people's signed profiles, read once per session when surfaced. */
|
||||
remoteProfiles: {},
|
||||
@@ -323,7 +327,7 @@ export async function initCommunity() {
|
||||
if (p.status === 1 || p.status === 2) noteSeen(p.username);
|
||||
});
|
||||
}
|
||||
await Promise.all([refreshFreenet(), loadContacts(), refreshLinks(), refreshFriends(), refreshShares(), refreshPublished()]);
|
||||
await Promise.all([refreshFreenet(), loadContacts(), refreshLinks(), refreshFriends(), refreshShares(), refreshPublished(), refreshInbox()]);
|
||||
await refreshPersona();
|
||||
}
|
||||
|
||||
@@ -489,7 +493,33 @@ export const shareImport = (card) =>
|
||||
});
|
||||
|
||||
|
||||
// ---- the encrypted inbox --------------------------------------------------------------
|
||||
export async function refreshInbox() {
|
||||
try {
|
||||
community.inbox = await api.inboxStatus();
|
||||
} catch {
|
||||
// stays as it was
|
||||
}
|
||||
}
|
||||
export const publishInbox = () =>
|
||||
run("inbox", async () => {
|
||||
community.inbox = await api.inboxPublish();
|
||||
});
|
||||
/** Read our inbox through the node, act on sealed cargo, prune it. */
|
||||
export const checkInbox = () =>
|
||||
run("inboxcheck", async () => {
|
||||
community.inboxLast = await api.inboxCheck();
|
||||
await Promise.all([refreshInbox(), refreshFriends(), refreshShares()]);
|
||||
return community.inboxLast;
|
||||
});
|
||||
export const friendRequestByKey = (personaKey) => friendAction("friend:" + personaKey, () => api.friendRequestByKey(personaKey));
|
||||
export const friendshipForKey = (key) => {
|
||||
const mine = community.friends.filter((f) => f.persona_key === key);
|
||||
return mine.find((f) => f.state === "friends") ?? mine[0] ?? null;
|
||||
};
|
||||
|
||||
|
||||
// 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, friendAccept, friendDecline, friendRemove, friendForget, checkLink, refreshFriends, sharedPublish, sharedWithdraw, shareSend, sharePreview, shareImport, refreshShares, refreshPublished };
|
||||
if (import.meta.env.DEV) globalThis.__communityActions = { friendRequest, friendRequestByKey, friendAccept, friendDecline, friendRemove, friendForget, checkLink, refreshFriends, sharedPublish, sharedWithdraw, shareSend, sharePreview, shareImport, refreshShares, refreshPublished, publishInbox, checkInbox, refreshInbox };
|
||||
@@ -77,6 +77,9 @@
|
||||
shareDismiss,
|
||||
sharedPublish,
|
||||
sharedWithdraw,
|
||||
publishInbox,
|
||||
checkInbox,
|
||||
friendRequestByKey,
|
||||
} from "../lib/community.svelte.js";
|
||||
|
||||
onMount(() => {
|
||||
@@ -432,11 +435,24 @@
|
||||
const id = await shareImport(card);
|
||||
if (id) goToView("playlist", { playlistId: id, label: card.name });
|
||||
}
|
||||
// The inbox is polled while this page is open and the node is up: on
|
||||
// arrival and every 90 s. Sealed cargo becomes friend requests and share
|
||||
// cards through the same stores the PM carry feeds.
|
||||
$effect(() => {
|
||||
if (!nodeUp || !key || !community.inbox?.published) return;
|
||||
untrack(() => void checkInbox());
|
||||
const t = setInterval(() => void checkInbox(), 90_000);
|
||||
return () => clearInterval(t);
|
||||
});
|
||||
let addKey = $state("");
|
||||
const isMe = (u) => !!me && (u ?? "").trim().toLowerCase() === me.trim().toLowerCase();
|
||||
$effect(() => {
|
||||
const lk = drawerLink;
|
||||
if (lk) untrack(() => loadRemoteProfile(lk.persona_key));
|
||||
});
|
||||
$effect(() => {
|
||||
for (const f of pendingIn()) if (!f.username) untrack(() => loadRemoteProfile(f.persona_key));
|
||||
});
|
||||
// Discover's similar-users shelf asks the server once per session.
|
||||
$effect(() => {
|
||||
if (community.section === "discover" && online) untrack(() => loadSimilar());
|
||||
@@ -564,9 +580,9 @@
|
||||
<div class="top">
|
||||
<button class="avbtn" onclick={() => (community.drawer = f.username)}><Identicon seed={f.persona_key} size={44} presence={presenceOf(f.username)} /></button>
|
||||
<div>
|
||||
<div class="t">{display(f.username)} wants to be friends</div>
|
||||
<div class="s">{f.username} · SoulStream ID {fpShort(f.persona_key)} · asked {fmtWhen(f.updated_at * 1000)}</div>
|
||||
<div class="prov"><span class="src ss"><Icon name="disc" size={10} />SoulStream</span><span>signed request · their ID proven over Soulseek</span><span>·</span><span>open until {fmtWhen(f.expires_at * 1000)}</span></div>
|
||||
<div class="t">{f.username ? display(f.username) : community.remoteProfiles[f.persona_key]?.display_name || `SoulStream ID ${fpShort(f.persona_key)}`} wants to be friends</div>
|
||||
<div class="s">{f.username ? `${f.username} · ` : ""}SoulStream ID {fpShort(f.persona_key)} · asked {fmtWhen(f.updated_at * 1000)}</div>
|
||||
<div class="prov"><span class="src ss"><Icon name="disc" size={10} />SoulStream</span><span>{f.carry === "inbox" ? "sealed · via your encrypted inbox · signed by their key" : "signed request · their ID proven over Soulseek"}</span><span>·</span><span>open until {fmtWhen(f.expires_at * 1000)}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bot">
|
||||
@@ -587,10 +603,11 @@
|
||||
<span class="mosaic-ph"><Icon name="playlist" size={22} /></span>
|
||||
<div>
|
||||
<div class="t">{c.name}</div>
|
||||
<div class="s">{c.entry_count} {c.entry_count === 1 ? "track" : "tracks"} · playlist · shared by <button class="lnkname" onclick={() => (community.drawer = c.from_username)}>{display(c.from_username)}</button> {fmtWhen(c.received_at * 1000)}</div>
|
||||
<div class="s">{c.entry_count} {c.entry_count === 1 ? "track" : "tracks"} · playlist · shared by {#if c.from_username}<button class="lnkname" onclick={() => (community.drawer = c.from_username)}>{display(c.from_username)}</button>{:else}SoulStream ID {fpShort(c.from_key)}{/if} {fmtWhen(c.received_at * 1000)}</div>
|
||||
<div class="prov">
|
||||
<span class="src ss"><Icon name="disc" size={10} />SoulStream</span>
|
||||
<span>signed by {fpShort(c.owner_key)}</span>
|
||||
{#if c.carry === "inbox"}<span>·</span><span><Icon name="lock" size={9} /> sealed · via your encrypted inbox</span>{/if}
|
||||
{#if pv && !pv.pending && !pv.error && !pv.withdrawn}<span>·</span><span class="chip ok"><Icon name="check" size={9} />verified · revision {pv.revision_seq}</span>{/if}
|
||||
{#if pv?.withdrawn}<span>·</span><span class="chip warn">withdrawn by the owner</span>{/if}
|
||||
{#if pv?.error}<span>·</span><span class="chip err">{pv.error}</span>{/if}
|
||||
@@ -837,6 +854,10 @@
|
||||
<button class="fchip" class:on={peopleGroup === "favorite"} onclick={() => (peopleGroup = "favorite")}>Favorites</button>
|
||||
<button class="fchip" class:on={peopleGroup === "trusted"} onclick={() => (peopleGroup = "trusted")}>Trusted</button>
|
||||
<span class="sp"></span>
|
||||
<form class="addrow" onsubmit={(e) => { e.preventDefault(); const k = addKey.trim(); if (k) { void friendRequestByKey(k); addKey = ""; } }}>
|
||||
<div class="qbox slim"><Icon name="link" size={13} /><input placeholder="Friend by SoulStream ID" bind:value={addKey} disabled={!nodeUp || !key} title="Sends a sealed friend request through their encrypted inbox — they must have published one" /></div>
|
||||
<button class="ax-ghost sm" type="submit" disabled={!nodeUp || !key || !addKey.trim() || !!community.busy["friend:" + addKey.trim()]}>Ask</button>
|
||||
</form>
|
||||
<form class="addrow" onsubmit={(e) => { e.preventDefault(); void addContact(); }}>
|
||||
<div class="qbox slim"><Icon name="plus" size={13} /><input placeholder="Add by Soulseek username" bind:value={addName} disabled={!online} /></div>
|
||||
<button class="ax-ghost sm" type="submit" disabled={!online || !addName.trim() || adding}>Add</button>
|
||||
@@ -1237,6 +1258,18 @@
|
||||
<span>Load remote profile images</span>
|
||||
<span class="hint r">verified, resized, cached by hash</span>
|
||||
</label>
|
||||
{#if key}
|
||||
<div class="noderow">
|
||||
{#if community.inbox?.published}
|
||||
<span class="chip ok"><Icon name="lock" size={10} />encrypted inbox · key {community.inbox.box_key_id}</span>
|
||||
<button class="ax-ghost sm" onclick={() => checkInbox()} disabled={!nodeUp || !!community.busy.inboxcheck}>{community.busy.inboxcheck ? "Checking…" : "Check now"}</button>
|
||||
<span class="hint">{community.inbox.last_check ? `checked ${fmtWhen(community.inbox.last_check * 1000)} · ` : ""}friend requests and shares arrive sealed to a key kept on this device; a persona restored elsewhere publishes a new one</span>
|
||||
{:else}
|
||||
<button class="ax-fill sm" onclick={() => publishInbox()} disabled={!nodeUp || !!community.busy.inbox}><Icon name="lock" size={12} /> {community.busy.inbox ? "Publishing…" : "Publish an encrypted inbox"}</button>
|
||||
<span class="hint">lets other SoulStream users send you friend requests and shares sealed, without Soulseek in between</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
<button class="tog big" class:on={freenetOn} role="switch" aria-checked={freenetOn} title={freenetOn ? "Turn the Freenet layer off" : "Turn the Freenet layer on"} onclick={() => setFreenetEnabled(!freenetOn)} disabled={!!community.busy.freenet}></button>
|
||||
|
||||
Reference in new issue
Block a user