Follow source/topic — account-backed personalization (v1)
Per Codex — turn accounts into a real reason to return, without an algorithmic feed. Durable interests (sources + tags), not moods. * DB: user_follows (user_id, kind source|tag, value, unique). * queries.feed gains follow_sources/follow_tags → the Following feed is "articles from a followed source OR carrying a followed tag", still respecting calm filters/boundaries. * API: GET/POST/DELETE /api/follows (sign-in required; source ids validated); /api/feed?following=true resolves the user's follows (anon → empty, not error). * Frontend: follows store (followKeys + toggleFollow, mirrors savedIds); a Follow button on source + tag/topic views; a "Following" lane in the nav with a tailored empty state; a Following management section in Account (unfollow). Digest "From what you follow" deferred to v2 (brief stays first). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,10 @@ import { getJSON, postJSON, delJSON } from './api.js';
|
||||
|
||||
export const auth = $state({ user: null, ready: false });
|
||||
export const savedIds = new SvelteSet(); // reactive set of saved article ids
|
||||
export const followKeys = new SvelteSet(); // reactive set of "kind:value" follows
|
||||
|
||||
const fkey = (kind, value) => `${kind}:${String(value).toLowerCase()}`;
|
||||
export const isFollowing = (kind, value) => followKeys.has(fkey(kind, value));
|
||||
|
||||
export async function refresh() {
|
||||
try {
|
||||
@@ -13,8 +17,35 @@ export async function refresh() {
|
||||
} finally {
|
||||
auth.ready = true;
|
||||
}
|
||||
if (auth.user) await loadSaved();
|
||||
else savedIds.clear();
|
||||
if (auth.user) { await loadSaved(); await loadFollows(); }
|
||||
else { savedIds.clear(); followKeys.clear(); }
|
||||
}
|
||||
|
||||
export async function loadFollows() {
|
||||
try {
|
||||
const list = await getJSON('/api/follows');
|
||||
followKeys.clear();
|
||||
for (const f of list) followKeys.add(fkey(f.kind, f.value));
|
||||
} catch {
|
||||
/* not signed in / transient */
|
||||
}
|
||||
}
|
||||
|
||||
// Optimistic follow/unfollow of a source or tag; reverts on failure.
|
||||
export async function toggleFollow(kind, value) {
|
||||
if (!auth.user) return false;
|
||||
const key = fkey(kind, value);
|
||||
const willFollow = !followKeys.has(key);
|
||||
if (willFollow) followKeys.add(key);
|
||||
else followKeys.delete(key);
|
||||
try {
|
||||
if (willFollow) await postJSON('/api/follows', { kind, value: String(value) });
|
||||
else await delJSON(`/api/follows?kind=${kind}&value=${encodeURIComponent(value)}`);
|
||||
} catch {
|
||||
if (willFollow) followKeys.delete(key);
|
||||
else followKeys.add(key);
|
||||
}
|
||||
return followKeys.has(key);
|
||||
}
|
||||
|
||||
export async function loadSaved() {
|
||||
@@ -52,6 +83,7 @@ export async function verifyToken(token) {
|
||||
const res = await postJSON('/api/auth/email/verify', { token });
|
||||
auth.user = res.user;
|
||||
await loadSaved();
|
||||
await loadFollows();
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -68,4 +100,5 @@ export async function logout() {
|
||||
export function clearLocal() {
|
||||
auth.user = null;
|
||||
savedIds.clear();
|
||||
followKeys.clear();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user