From 30b5bb8d429560284114b1e7f00ac7fa532e9ab4 Mon Sep 17 00:00:00 2001 From: Jure <44338+hoornet@users.noreply.github.com> Date: Sun, 8 Mar 2026 18:52:26 +0100 Subject: [PATCH] Add following feed + persist likes to localStorage - Following tab in feed header (visible when logged in) - Fetches kind 1 notes from followed pubkeys via NDK - fetchFollows on login using NDK user.follows() - fetchFollowFeed added to nostr lib - Liked note IDs persisted in localStorage so likes survive refresh Co-Authored-By: Claude Sonnet 4.6 --- src/components/feed/Feed.tsx | 91 +++++++++++++++++++++++++------- src/components/feed/NoteCard.tsx | 10 +++- src/lib/nostr/client.ts | 17 ++++++ src/lib/nostr/index.ts | 2 +- src/stores/user.ts | 24 ++++++++- 5 files changed, 121 insertions(+), 23 deletions(-) diff --git a/src/components/feed/Feed.tsx b/src/components/feed/Feed.tsx index baafd8b..270e4c5 100644 --- a/src/components/feed/Feed.tsx +++ b/src/components/feed/Feed.tsx @@ -1,22 +1,78 @@ -import { useEffect } from "react"; +import { useEffect, useState } from "react"; import { useFeedStore } from "../../stores/feed"; import { useUserStore } from "../../stores/user"; +import { fetchFollowFeed } from "../../lib/nostr"; import { NoteCard } from "./NoteCard"; import { ComposeBox } from "./ComposeBox"; +import { NDKEvent } from "@nostr-dev-kit/ndk"; + +type FeedTab = "global" | "following"; export function Feed() { const { notes, loading, connected, error, connect, loadFeed } = useFeedStore(); - const { loggedIn } = useUserStore(); + const { loggedIn, follows } = useUserStore(); + + const [tab, setTab] = useState("global"); + const [followNotes, setFollowNotes] = useState([]); + const [followLoading, setFollowLoading] = useState(false); useEffect(() => { connect().then(() => loadFeed()); }, []); + useEffect(() => { + if (tab === "following" && loggedIn && follows.length > 0) { + loadFollowFeed(); + } + }, [tab, follows]); + + const loadFollowFeed = async () => { + setFollowLoading(true); + try { + const events = await fetchFollowFeed(follows); + setFollowNotes(events); + } finally { + setFollowLoading(false); + } + }; + + const isFollowing = tab === "following"; + const activeNotes = isFollowing ? followNotes : notes; + const isLoading = isFollowing ? followLoading : loading; + + const filteredNotes = activeNotes.filter((event) => { + const c = event.content.trim(); + return c.length > 0 && !c.startsWith("{") && !c.startsWith("["); + }); + return (
{/* Header */}
-

Global Feed

+
+ + {loggedIn && ( + + )} +
{connected && ( @@ -25,11 +81,11 @@ export function Feed() { )}
@@ -39,32 +95,29 @@ export function Feed() { {/* Feed */}
- {error && ( + {error && !isFollowing && (
{error}
)} - {loading && notes.length === 0 && ( + {isLoading && filteredNotes.length === 0 && (
- Connecting to relays… + {isFollowing ? "Loading notes from people you follow…" : "Connecting to relays…"}
)} - {!loading && notes.length === 0 && !error && ( + {!isLoading && filteredNotes.length === 0 && (
- No notes yet. + {isFollowing && follows.length === 0 + ? "You're not following anyone yet." + : "No notes yet."}
)} - {notes - .filter((event) => { - const c = event.content.trim(); - return c.length > 0 && !c.startsWith("{") && !c.startsWith("["); - }) - .map((event) => ( - - ))} + {filteredNotes.map((event) => ( + + ))}
); diff --git a/src/components/feed/NoteCard.tsx b/src/components/feed/NoteCard.tsx index 883c4fe..9872eb1 100644 --- a/src/components/feed/NoteCard.tsx +++ b/src/components/feed/NoteCard.tsx @@ -20,7 +20,12 @@ export function NoteCard({ event }: NoteCardProps) { const { loggedIn } = useUserStore(); const { openProfile } = useUIStore(); - const [liked, setLiked] = useState(false); + const likedKey = "wrystr_liked"; + const getLiked = () => { + try { return new Set(JSON.parse(localStorage.getItem(likedKey) || "[]")); } + catch { return new Set(); } + }; + const [liked, setLiked] = useState(() => getLiked().has(event.id)); const [liking, setLiking] = useState(false); const [showReply, setShowReply] = useState(false); const [replyText, setReplyText] = useState(""); @@ -34,6 +39,9 @@ export function NoteCard({ event }: NoteCardProps) { setLiking(true); try { await publishReaction(event.id, event.pubkey); + const liked = getLiked(); + liked.add(event.id); + localStorage.setItem(likedKey, JSON.stringify(Array.from(liked))); setLiked(true); } finally { setLiking(false); diff --git a/src/lib/nostr/client.ts b/src/lib/nostr/client.ts index 1b4f581..e78cd7b 100644 --- a/src/lib/nostr/client.ts +++ b/src/lib/nostr/client.ts @@ -99,6 +99,23 @@ export async function publishNote(content: string): Promise { await event.publish(); } +export async function fetchFollowFeed(pubkeys: string[], limit = 80): Promise { + if (pubkeys.length === 0) return []; + const instance = getNDK(); + + const filter: NDKFilter = { + kinds: [NDKKind.Text], + authors: pubkeys, + limit, + }; + + const events = await instance.fetchEvents(filter, { + cacheUsage: NDKSubscriptionCacheUsage.ONLY_RELAY, + }); + + return Array.from(events).sort((a, b) => (b.created_at ?? 0) - (a.created_at ?? 0)); +} + export async function fetchUserNotes(pubkey: string, limit = 30): Promise { const instance = getNDK(); const filter: NDKFilter = { diff --git a/src/lib/nostr/index.ts b/src/lib/nostr/index.ts index 07bfd2d..0991dc9 100644 --- a/src/lib/nostr/index.ts +++ b/src/lib/nostr/index.ts @@ -1 +1 @@ -export { getNDK, connectToRelays, fetchGlobalFeed, publishNote, publishReaction, publishReply, fetchUserNotes, fetchProfile } from "./client"; +export { getNDK, connectToRelays, fetchGlobalFeed, fetchFollowFeed, publishNote, publishReaction, publishReply, fetchUserNotes, fetchProfile } from "./client"; diff --git a/src/stores/user.ts b/src/stores/user.ts index 13259b2..5e5927a 100644 --- a/src/stores/user.ts +++ b/src/stores/user.ts @@ -7,6 +7,7 @@ interface UserState { pubkey: string | null; npub: string | null; profile: any | null; + follows: string[]; loggedIn: boolean; loginError: string | null; @@ -14,12 +15,14 @@ interface UserState { loginWithPubkey: (pubkey: string) => Promise; logout: () => void; fetchOwnProfile: () => Promise; + fetchFollows: () => Promise; } export const useUserStore = create((set, get) => ({ pubkey: null, npub: null, profile: null, + follows: [], loggedIn: false, loginError: null, @@ -54,8 +57,9 @@ export const useUserStore = create((set, get) => ({ localStorage.setItem("wrystr_pubkey", pubkey); localStorage.setItem("wrystr_login_type", "nsec"); - // Fetch profile + // Fetch profile and follows get().fetchOwnProfile(); + get().fetchFollows(); } catch (err) { set({ loginError: `Login failed: ${err}` }); } @@ -84,6 +88,7 @@ export const useUserStore = create((set, get) => ({ localStorage.setItem("wrystr_login_type", "pubkey"); get().fetchOwnProfile(); + get().fetchFollows(); } catch (err) { set({ loginError: `Login failed: ${err}` }); } @@ -94,7 +99,7 @@ export const useUserStore = create((set, get) => ({ ndk.signer = undefined; localStorage.removeItem("wrystr_pubkey"); localStorage.removeItem("wrystr_login_type"); - set({ pubkey: null, npub: null, profile: null, loggedIn: false, loginError: null }); + set({ pubkey: null, npub: null, profile: null, follows: [], loggedIn: false, loginError: null }); }, fetchOwnProfile: async () => { @@ -110,4 +115,19 @@ export const useUserStore = create((set, get) => ({ // Profile fetch is non-critical } }, + + fetchFollows: async () => { + const { pubkey } = get(); + if (!pubkey) return; + + try { + const ndk = getNDK(); + const user = ndk.getUser({ pubkey }); + const followSet = await user.follows(); + const follows = Array.from(followSet).map((u) => u.pubkey); + set({ follows }); + } catch { + // Non-critical + } + }, }));