From 2960e7b2793a7c9b80aab7128305dfdba1561a4a Mon Sep 17 00:00:00 2001 From: Jure <44338+hoornet@users.noreply.github.com> Date: Mon, 9 Mar 2026 17:25:34 +0100 Subject: [PATCH] Add follow/unfollow (NIP-02) from profile view MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - publishContactList (kind 3) in nostr lib — replaces full follow list on each change - follow() and unfollow() actions in user store with optimistic UI update - Follow/Unfollow button in ProfileView header (visible when logged in, not own profile) - Button shows "unfollow" in muted style with danger hover when already following Co-Authored-By: Claude Sonnet 4.6 --- src/components/profile/ProfileView.tsx | 31 +++++++++++++++++++++++++- src/lib/nostr/client.ts | 11 +++++++++ src/lib/nostr/index.ts | 2 +- src/stores/user.ts | 19 +++++++++++++++- 4 files changed, 60 insertions(+), 3 deletions(-) diff --git a/src/components/profile/ProfileView.tsx b/src/components/profile/ProfileView.tsx index e3f716f..9acd639 100644 --- a/src/components/profile/ProfileView.tsx +++ b/src/components/profile/ProfileView.tsx @@ -99,7 +99,7 @@ function EditProfileForm({ pubkey, onSaved }: { pubkey: string; onSaved: () => v export function ProfileView() { const { selectedPubkey, goBack } = useUIStore(); - const { pubkey: ownPubkey } = useUserStore(); + const { pubkey: ownPubkey, loggedIn, follows, follow, unfollow } = useUserStore(); const pubkey = selectedPubkey!; const isOwn = pubkey === ownPubkey; @@ -107,6 +107,22 @@ export function ProfileView() { const [notes, setNotes] = useState([]); const [loading, setLoading] = useState(true); const [editing, setEditing] = useState(false); + const [followPending, setFollowPending] = useState(false); + + const isFollowing = follows.includes(pubkey); + + const handleFollowToggle = async () => { + setFollowPending(true); + try { + if (isFollowing) { + await unfollow(pubkey); + } else { + await follow(pubkey); + } + } finally { + setFollowPending(false); + } + }; const name = profile?.displayName || profile?.name || shortenPubkey(pubkey); const avatar = profile?.picture; @@ -144,6 +160,19 @@ export function ProfileView() { edit profile )} + {!isOwn && loggedIn && ( + + )}
diff --git a/src/lib/nostr/client.ts b/src/lib/nostr/client.ts index af79490..3b80533 100644 --- a/src/lib/nostr/client.ts +++ b/src/lib/nostr/client.ts @@ -191,6 +191,17 @@ export async function fetchUserNotes(pubkey: string, limit = 30): Promise (b.created_at ?? 0) - (a.created_at ?? 0)); } +export async function publishContactList(pubkeys: string[]): Promise { + const instance = getNDK(); + if (!instance.signer) throw new Error("Not logged in"); + + const event = new NDKEvent(instance); + event.kind = 3; + event.content = ""; + event.tags = pubkeys.map((pk) => ["p", pk]); + await event.publish(); +} + export async function fetchProfile(pubkey: string) { const instance = getNDK(); const user = instance.getUser({ pubkey }); diff --git a/src/lib/nostr/index.ts b/src/lib/nostr/index.ts index 6ef2739..e6caab1 100644 --- a/src/lib/nostr/index.ts +++ b/src/lib/nostr/index.ts @@ -1 +1 @@ -export { getNDK, connectToRelays, fetchGlobalFeed, fetchFollowFeed, fetchReplies, publishNote, publishArticle, publishProfile, publishReaction, publishReply, fetchUserNotes, fetchProfile } from "./client"; +export { getNDK, connectToRelays, fetchGlobalFeed, fetchFollowFeed, fetchReplies, publishNote, publishArticle, publishProfile, publishReaction, publishReply, publishContactList, fetchUserNotes, fetchProfile } from "./client"; diff --git a/src/stores/user.ts b/src/stores/user.ts index 5e5927a..124a04b 100644 --- a/src/stores/user.ts +++ b/src/stores/user.ts @@ -1,6 +1,6 @@ import { create } from "zustand"; import { NDKPrivateKeySigner } from "@nostr-dev-kit/ndk"; -import { getNDK } from "../lib/nostr"; +import { getNDK, publishContactList } from "../lib/nostr"; import { nip19 } from "@nostr-dev-kit/ndk"; interface UserState { @@ -16,6 +16,8 @@ interface UserState { logout: () => void; fetchOwnProfile: () => Promise; fetchFollows: () => Promise; + follow: (pubkey: string) => Promise; + unfollow: (pubkey: string) => Promise; } export const useUserStore = create((set, get) => ({ @@ -130,4 +132,19 @@ export const useUserStore = create((set, get) => ({ // Non-critical } }, + + follow: async (pubkey: string) => { + const { follows } = get(); + if (follows.includes(pubkey)) return; + const updated = [...follows, pubkey]; + set({ follows: updated }); + await publishContactList(updated); + }, + + unfollow: async (pubkey: string) => { + const { follows } = get(); + const updated = follows.filter((pk) => pk !== pubkey); + set({ follows: updated }); + await publishContactList(updated); + }, }));