forked from Keelrleek/SoulStream
community: declare what you like on soulseek, so the similar-users shelf finally has something to rank on
This commit is contained in:
1 parent
8093adc89e
commit
7b7f48ebc6
8 files changed
+5671
-5519
No files matched your search
@@ -625,6 +625,19 @@ impl Client {
|
||||
}
|
||||
}
|
||||
|
||||
/// Declare, or take back, one interest on the server. Fire and forget:
|
||||
/// the server sends nothing back, and reading our own interests is how you
|
||||
/// confirm it landed.
|
||||
pub fn declare_interest(&self, thing: &str, like: bool, on: bool) -> Result<()> {
|
||||
let msg = match (like, on) {
|
||||
(true, true) => server::add_thing_i_like(thing),
|
||||
(true, false) => server::remove_thing_i_like(thing),
|
||||
(false, true) => server::add_thing_i_hate(thing),
|
||||
(false, false) => server::remove_thing_i_hate(thing),
|
||||
};
|
||||
self.inner.send_server(msg)
|
||||
}
|
||||
|
||||
/// Users the server ranks as similar to us by shared interests. Spawn-shaped.
|
||||
pub fn similar_users(
|
||||
&self,
|
||||
|
||||
@@ -31,7 +31,11 @@ pub mod code {
|
||||
pub const PING: u32 = 32;
|
||||
pub const SHARED_FOLDERS_AND_FILES: u32 = 35;
|
||||
pub const GET_USER_STATS: u32 = 36;
|
||||
pub const ADD_THING_I_LIKE: u32 = 51;
|
||||
pub const REMOVE_THING_I_LIKE: u32 = 52;
|
||||
pub const USER_INTERESTS: u32 = 57;
|
||||
pub const ADD_THING_I_HATE: u32 = 117;
|
||||
pub const REMOVE_THING_I_HATE: u32 = 118;
|
||||
pub const SIMILAR_USERS: u32 = 110;
|
||||
pub const ROOM_LIST: u32 = 64;
|
||||
pub const HAVE_NO_PARENTS: u32 = 71;
|
||||
@@ -138,6 +142,28 @@ pub fn get_user_stats(username: &str) -> Vec<u8> {
|
||||
.finish_u32(code::GET_USER_STATS)
|
||||
}
|
||||
|
||||
/// Declare a like (code 51). These are what the server matches on when it
|
||||
/// ranks similar users, so a client that never sends one is invisible to that
|
||||
/// ranking however much it reads.
|
||||
pub fn add_thing_i_like(thing: &str) -> Vec<u8> {
|
||||
Writer::new().string(thing).finish_u32(code::ADD_THING_I_LIKE)
|
||||
}
|
||||
|
||||
/// Take a like back (code 52).
|
||||
pub fn remove_thing_i_like(thing: &str) -> Vec<u8> {
|
||||
Writer::new().string(thing).finish_u32(code::REMOVE_THING_I_LIKE)
|
||||
}
|
||||
|
||||
/// Declare a dislike (code 117).
|
||||
pub fn add_thing_i_hate(thing: &str) -> Vec<u8> {
|
||||
Writer::new().string(thing).finish_u32(code::ADD_THING_I_HATE)
|
||||
}
|
||||
|
||||
/// Take a dislike back (code 118).
|
||||
pub fn remove_thing_i_hate(thing: &str) -> Vec<u8> {
|
||||
Writer::new().string(thing).finish_u32(code::REMOVE_THING_I_HATE)
|
||||
}
|
||||
|
||||
/// A user's declared likes and hates (code 57).
|
||||
pub fn user_interests(username: &str) -> Vec<u8> {
|
||||
Writer::new()
|
||||
|
||||
+5571
-5517
File diff suppressed because it is too large.
Load diff
@@ -148,6 +148,14 @@ pub struct AppConfig {
|
||||
/// their status and stats stream in while connected.
|
||||
#[serde(default)]
|
||||
pub buddies: Vec<String>,
|
||||
/// What we have told the Soulseek server we like, and what we do not.
|
||||
/// Kept here because the server's copy is per account, not per client:
|
||||
/// signing in from anywhere else can change it under us, so this device's
|
||||
/// list is re-declared on every login.
|
||||
#[serde(default)]
|
||||
pub interests_like: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub interests_hate: Vec<String>,
|
||||
/// The experimental Freenet social layer (crates/freenet-adapter).
|
||||
/// None → false: strictly opt-in, and flipping it off is the kill
|
||||
/// switch — the adapter drops its connection and stops retrying. A
|
||||
|
||||
@@ -132,6 +132,8 @@ pub fn run() {
|
||||
commands::slsk_user_info,
|
||||
commands::slsk_user_interests,
|
||||
commands::slsk_similar_users,
|
||||
commands::slsk_interests_mine,
|
||||
commands::slsk_interest_set,
|
||||
slsk_link::slsk_link_status,
|
||||
slsk_link::slsk_link_self,
|
||||
slsk_link::slsk_link_unlink,
|
||||
|
||||
@@ -151,6 +151,8 @@ export const slskBrowseUser = (username) => invoke("slsk_browse_user", { usernam
|
||||
export const slskUserInfo = (username) => invoke("slsk_user_info", { username });
|
||||
export const slskUserInterests = (username) => invoke("slsk_user_interests", { username });
|
||||
export const slskSimilarUsers = () => invoke("slsk_similar_users");
|
||||
export const slskInterestsMine = () => invoke("slsk_interests_mine");
|
||||
export const slskInterestSet = (thing, like, on) => invoke("slsk_interest_set", { thing, like, on });
|
||||
export const slskLinkStatus = () => invoke("slsk_link_status");
|
||||
export const slskLinkSelf = () => invoke("slsk_link_self");
|
||||
export const slskLinkUnlink = () => invoke("slsk_link_unlink");
|
||||
|
||||
@@ -60,6 +60,8 @@ export const community = $state({
|
||||
interests: {},
|
||||
/** The server's similar-users answer, or null before it was asked. */
|
||||
similar: null,
|
||||
/** What this device has declared on Soulseek — the shelf's only input. */
|
||||
interests: null,
|
||||
/** slsk_link_status: our own verified link and everyone we verified. */
|
||||
links: null,
|
||||
/** username → the last failed ask's reason. */
|
||||
@@ -419,6 +421,24 @@ export async function loadUserInfo(username) {
|
||||
.catch((e) => (community.interests[username] = { error: String(e) }));
|
||||
}
|
||||
}
|
||||
/** What we have told the server we like. The shelf ranks on exactly this. */
|
||||
export async function refreshInterests() {
|
||||
try {
|
||||
community.interests = await api.slskInterestsMine();
|
||||
} catch {
|
||||
// not connected; the panel says so
|
||||
}
|
||||
}
|
||||
|
||||
/** Declare an interest, or take it back, then ask the server again — the
|
||||
* ranking only moves when its input does. */
|
||||
export const setInterest = (thing, like, on) =>
|
||||
run("interest", async () => {
|
||||
community.interests = await api.slskInterestSet(thing, like, on);
|
||||
community.similar = null;
|
||||
await loadSimilar(true);
|
||||
});
|
||||
|
||||
export async function loadSimilar(force = false) {
|
||||
if (community.similar && !force) return;
|
||||
community.busy.similar = true;
|
||||
@@ -706,4 +726,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, publishInbox, checkInbox, refreshInbox, refreshSpaces, openSpace, createSpace, postToSpace, inviteToSpace, removeFromSpace, pinToSpace, leaveSpace, openBoard, proposeToBoard, decideProposal, refreshOverlap, computeOverlap, forgetOverlap };
|
||||
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, refreshSpaces, openSpace, createSpace, postToSpace, inviteToSpace, removeFromSpace, pinToSpace, leaveSpace, openBoard, proposeToBoard, decideProposal, refreshOverlap, computeOverlap, forgetOverlap, refreshInterests, setInterest };
|
||||
@@ -58,6 +58,8 @@
|
||||
personaKey,
|
||||
loadUserInfo,
|
||||
loadSimilar,
|
||||
refreshInterests,
|
||||
setInterest,
|
||||
refreshLinks,
|
||||
linkSelf,
|
||||
unlinkSelf,
|
||||
@@ -603,9 +605,13 @@
|
||||
$effect(() => {
|
||||
for (const f of pendingIn()) if (!f.username) untrack(() => loadRemoteProfile(f.persona_key));
|
||||
});
|
||||
let likeDraft = $state("");
|
||||
// Discover's similar-users shelf asks the server once per session.
|
||||
$effect(() => {
|
||||
if (community.section === "discover" && online) untrack(() => loadSimilar());
|
||||
if (community.section === "discover" && online) {
|
||||
untrack(() => loadSimilar());
|
||||
untrack(() => refreshInterests());
|
||||
}
|
||||
});
|
||||
function saveMeta(patch = {}) {
|
||||
if (!drawerUser) return;
|
||||
@@ -1121,6 +1127,17 @@
|
||||
<h2 class="ax-title">Similar Soulseek interests</h2>
|
||||
<span class="prov"><Icon name="heart" size={12} /> the server's match on declared likes · not follows</span>
|
||||
</div>
|
||||
{#if online}
|
||||
<div class="likes">
|
||||
{#each community.interests?.likes ?? [] as thing (thing)}
|
||||
<span class="chip acc">{thing}<button class="x" title="Stop declaring {thing}" onclick={() => void setInterest(thing, true, false)} disabled={!!community.busy.interest}>×</button></span>
|
||||
{/each}
|
||||
<form class="inl" onsubmit={(e) => { e.preventDefault(); const t = likeDraft.trim(); if (t) { void setInterest(t, true, true); likeDraft = ""; } }}>
|
||||
<input placeholder="Declare a like — an artist, a label, a genre" maxlength="64" bind:value={likeDraft} />
|
||||
<button class="ax-ghost sm" type="submit" disabled={!likeDraft.trim() || !!community.busy.interest}>{community.busy.interest ? "Telling the server…" : "Declare"}</button>
|
||||
</form>
|
||||
</div>
|
||||
{/if}
|
||||
{#if community.similar?.users?.length}
|
||||
<div class="track">
|
||||
{#each community.similar.users.slice(0, 24) as su (su.username)}
|
||||
@@ -2177,6 +2194,16 @@
|
||||
.field label span.over { color: var(--err); }
|
||||
.field input, .field textarea { background: var(--surf-hi); border: 1px solid transparent; border-radius: 8px; color: #fff; padding: 8px 12px; font-size: 13px; outline: none; resize: none; font-family: inherit; }
|
||||
.field input:focus, .field textarea:focus { border-color: rgba(255, 255, 255, 0.28); }
|
||||
.likes { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin: 2px 0 12px; }
|
||||
.likes .inl { flex: 1; min-width: 240px; }
|
||||
.likes .chip .x {
|
||||
all: unset;
|
||||
cursor: pointer;
|
||||
margin-left: 6px;
|
||||
opacity: 0.65;
|
||||
line-height: 1;
|
||||
}
|
||||
.likes .chip .x:hover { opacity: 1; }
|
||||
.inl { display: flex; gap: 8px; align-items: center; }
|
||||
.inl input { flex: 1; min-width: 0; }
|
||||
.chk { display: flex; align-items: center; gap: 7px; font-size: 11.5px; color: var(--text-muted); }
|
||||
|
||||
Reference in new issue
Block a user