community: friends, two signed answers on one request carried over soulseek, one answer is only ever pending
This commit is contained in:
1 parent
6ed6f5b8f8
commit
984738135a
9 files changed
+967
-4
No files matched your search
@@ -64,6 +64,14 @@
|
||||
checkLink,
|
||||
linkFor,
|
||||
loadRemoteProfile,
|
||||
friendRequest,
|
||||
friendAccept,
|
||||
friendDecline,
|
||||
friendRemove,
|
||||
friendForget,
|
||||
friendshipFor,
|
||||
friendCount,
|
||||
pendingIn,
|
||||
} from "../lib/community.svelte.js";
|
||||
|
||||
onMount(() => {
|
||||
@@ -296,6 +304,8 @@
|
||||
? true
|
||||
: peopleGroup === "trusted"
|
||||
? p.meta.trusted
|
||||
: peopleGroup === "friends"
|
||||
? friendshipFor(p.u)?.state === "friends"
|
||||
: peopleGroup === "favorite"
|
||||
? p.meta.favorite
|
||||
: p.meta.group === peopleGroup,
|
||||
@@ -412,6 +422,8 @@
|
||||
);
|
||||
const drawerLink = $derived(drawerUser ? linkFor(drawerUser) : null);
|
||||
const drawerRemote = $derived(drawerLink ? community.remoteProfiles[drawerLink.persona_key] : null);
|
||||
const drawerFriend = $derived(drawerUser ? friendshipFor(drawerUser) : null);
|
||||
const isMe = (u) => !!me && (u ?? "").trim().toLowerCase() === me.trim().toLowerCase();
|
||||
$effect(() => {
|
||||
const lk = drawerLink;
|
||||
if (lk) untrack(() => loadRemoteProfile(lk.persona_key));
|
||||
@@ -493,7 +505,7 @@
|
||||
{:else}
|
||||
<span class="chip off">SoulStream identity off</span>
|
||||
{/if}
|
||||
<span class="hint">{slsk.buddies.length} contacts · {joinedNames.length} rooms · {convos.length} conversations</span>
|
||||
<span class="hint">{friendCount()} friends · {slsk.buddies.length} contacts · {joinedNames.length} rooms · {convos.length} conversations</span>
|
||||
</div>
|
||||
<button class="ax-ghost" onclick={() => setSection("you")}>
|
||||
<Icon name={key ? "pencil" : "sparkle"} size={13} />
|
||||
@@ -536,6 +548,27 @@
|
||||
|
||||
<!-- ── INBOX ──────────────────────────────────────────────────────── -->
|
||||
{:else if community.section === "inbox"}
|
||||
{#if pendingIn().length}
|
||||
<div class="freqs">
|
||||
{#each pendingIn() as f (f.id)}
|
||||
<div class="card inv">
|
||||
<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>
|
||||
</div>
|
||||
<div class="bot">
|
||||
<button class="ax-fill sm" onclick={() => friendAccept(f.id)} disabled={!online || !nodeUp || !!community.busy["friend:" + f.id]}>Accept</button>
|
||||
<button class="ax-ghost sm" onclick={() => friendDecline(f.id)} disabled={!!community.busy["friend:" + f.id]}>Decline</button>
|
||||
<span class="hint">friends are two signed answers — yours goes back to them over Soulseek</span>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{#if !convos.length}
|
||||
<div class="empty">
|
||||
<Icon name="inbox" size={30} />
|
||||
@@ -762,6 +795,7 @@
|
||||
{#each groups as g (g)}
|
||||
<button class="fchip" class:on={peopleGroup === g} onclick={() => (peopleGroup = g)}>{g}</button>
|
||||
{/each}
|
||||
<button class="fchip" class:on={peopleGroup === "friends"} onclick={() => (peopleGroup = "friends")}>Friends</button>
|
||||
<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>
|
||||
@@ -787,6 +821,7 @@
|
||||
{#if p.meta.petname}<span class="real">{p.u}</span>{/if}
|
||||
{#if p.meta.favorite}<span class="mark fav" title="Favorite"><Icon name="star" size={11} /></span>{/if}
|
||||
{#if p.meta.trusted}<span class="mark tr" title="Trusted — a local permission choice, not a verified identity"><Icon name="shield" size={11} /></span>{/if}
|
||||
{#if friendshipFor(p.u)?.state === "friends"}<span class="mark fr" title="Friend — a signed, reciprocal SoulStream relationship"><Icon name="link" size={11} /></span>{/if}
|
||||
</div>
|
||||
<div class="sub">
|
||||
<span class="src slsk"><Icon name="globe" size={10} />Soulseek</span>
|
||||
@@ -1170,6 +1205,7 @@
|
||||
<input class="pet" placeholder={drawerUser} bind:value={petDraft} onchange={() => saveMeta()} title="Your private petname for this person" />
|
||||
{#if drawerMeta?.favorite}<span class="mark fav"><Icon name="star" size={12} /></span>{/if}
|
||||
{#if drawerMeta?.trusted}<span class="mark tr"><Icon name="shield" size={12} /></span>{/if}
|
||||
{#if drawerFriend?.state === "friends"}<span class="mark fr" title="Friend"><Icon name="link" size={12} /></span>{/if}
|
||||
</div>
|
||||
<div class="sub">
|
||||
{#if petDraft.trim()}<span>{drawerUser}</span><span>·</span>{/if}
|
||||
@@ -1263,6 +1299,35 @@
|
||||
</div>
|
||||
</div>
|
||||
<p class="hint" style="margin-top:8px">Their client answered a one-time question over Soulseek with a claim signed by this key — the server vouched for the sender, the signature for the key. <button class="lnk" onclick={() => checkLink(drawerUser)} disabled={!online || !!community.busy["link:" + drawerUser]}>ask again</button></p>
|
||||
{#if !isMe(drawerUser)}
|
||||
<div class="friendrow">
|
||||
{#if !drawerFriend || drawerFriend.state === "expired" || drawerFriend.state === "revoked" && drawerFriend.direction === "in"}
|
||||
<button class="ax-fill sm" onclick={() => friendRequest(drawerUser)} disabled={!online || !nodeUp || !key || !!community.busy["friend:" + drawerUser]}><Icon name="link" size={12} /> {community.busy["friend:" + drawerUser] ? "Sending…" : "Add as friend"}</button>
|
||||
<span class="hint">{drawerFriend?.state === "revoked" ? "the earlier friendship was ended — a new request starts over" : "a signed request goes to them over Soulseek; friends are two reciprocal signed answers"}</span>
|
||||
{:else if drawerFriend.state === "friends"}
|
||||
<span class="chip ok"><Icon name="link" size={10} />friends since {fmtWhen(drawerFriend.since * 1000)}</span>
|
||||
<button class="ax-ghost sm" onclick={() => friendRemove(drawerFriend.id)} disabled={!!community.busy["friend:" + drawerFriend.id]}>Unfriend</button>
|
||||
{:else if drawerFriend.state === "pending-in"}
|
||||
<span class="chip acc">they asked to be friends</span>
|
||||
<button class="ax-fill sm" onclick={() => friendAccept(drawerFriend.id)} disabled={!online || !nodeUp || !!community.busy["friend:" + drawerFriend.id]}>Accept</button>
|
||||
<button class="ax-ghost sm" onclick={() => friendDecline(drawerFriend.id)} disabled={!!community.busy["friend:" + drawerFriend.id]}>Decline</button>
|
||||
{:else if drawerFriend.state === "pending-out"}
|
||||
<span class="chip acc"><span class="bl"></span>request sent · waiting for their answer</span>
|
||||
<button class="lnk" onclick={() => friendForget(drawerFriend.id)}>withdraw</button>
|
||||
{:else if drawerFriend.state === "accepted-waiting"}
|
||||
<span class="chip acc"><span class="bl"></span>you accepted · waiting for their signed answer</span>
|
||||
{:else if drawerFriend.state === "declined"}
|
||||
<span class="chip off">{drawerFriend.direction === "out" ? "they declined" : "you declined"}</span>
|
||||
<button class="lnk" onclick={() => friendForget(drawerFriend.id)}>forget</button>
|
||||
{:else if drawerFriend.state === "revoked"}
|
||||
<span class="chip warn">friendship ended</span>
|
||||
<button class="ax-ghost sm" onclick={() => friendRequest(drawerUser)} disabled={!online || !nodeUp}>Ask again</button>
|
||||
{:else if drawerFriend.state === "reacceptance-required"}
|
||||
<span class="chip warn">their key changed — must re-accept</span>
|
||||
<button class="ax-ghost sm" onclick={() => friendRequest(drawerUser)} disabled={!online || !nodeUp}>Ask again</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<h4>Signed SoulStream profile <span class="chip off">not asked</span></h4>
|
||||
<p class="hint">Ask their client, over Soulseek, for a signed SoulStream ID. One message goes out; a client that is not SoulStream sees a line saying it is safe to ignore.</p>
|
||||
@@ -1443,6 +1508,15 @@
|
||||
.sub { color: var(--text-secondary); font-size: 12.5px; max-width: 460px; line-height: 1.6; }
|
||||
.spico { width: 56px; height: 56px; border-radius: 16px; display: grid; place-items: center; background: color-mix(in srgb, var(--space) 16%, transparent); color: #d9a6ff; }
|
||||
|
||||
/* friend requests — the invitation card, parsed and signed, never pasted text */
|
||||
.freqs { display: flex; flex-direction: column; gap: 10px; margin-bottom: 14px; }
|
||||
.card { border-radius: 12px; background: var(--surf); border: 1px solid var(--hair); overflow: hidden; max-width: 560px; }
|
||||
.card .top { display: flex; gap: 12px; padding: 12px; align-items: center; }
|
||||
.card .t { font-size: 14px; font-weight: 800; }
|
||||
.card .s { font-size: 11.5px; color: var(--text-secondary); margin-top: 2px; }
|
||||
.card .prov { font-size: 10.5px; color: var(--text-muted); margin-top: 6px; display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
|
||||
.card .bot { display: flex; gap: 8px; padding: 0 12px 12px; align-items: center; flex-wrap: wrap; }
|
||||
.card .bot .hint { margin-left: auto; font-size: 10.5px; }
|
||||
/* ── thread / room ── */
|
||||
.thread { display: flex; flex-direction: column; height: calc(100vh - 330px); min-height: 480px; }
|
||||
.thead-bar, .roomhead { display: flex; align-items: center; gap: 12px; padding: 4px 4px 10px; border-bottom: 1px solid var(--hair); }
|
||||
@@ -1628,6 +1702,8 @@
|
||||
.rp { display: flex; gap: 12px; align-items: flex-start; }
|
||||
.rptx { min-width: 0; flex: 1; }
|
||||
.rp .nm { font-size: 15px; font-weight: 800; display: flex; align-items: center; gap: 8px; }
|
||||
.friendrow { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-top: 12px; }
|
||||
.mark.fr { color: var(--accent-hover); }
|
||||
.dsec { margin-top: 18px; padding-top: 14px; border-top: 1px solid var(--hair); }
|
||||
.dsec h4 { margin: 0 0 8px; font-size: 10.5px; font-weight: 900; letter-spacing: 0.1em; text-transform: uppercase; color: var(--text-muted); display: flex; align-items: center; gap: 8px; }
|
||||
.dsec h4 .src, .dsec h4 .chip { margin-left: auto; }
|
||||
|
||||
Reference in new issue
Block a user