Add network reaction counts to note cards

- fetchReactionCount (kind 7 #e filter) in nostr lib
- useReactionCount hook with module-level cache to avoid refetching
- NoteCard shows count next to like button; increments optimistically on like
- Falls back to "like"/"liked" text when count is zero or still loading

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jure
2026-03-09 17:31:42 +01:00
parent 2960e7b279
commit d52cfa5f75
4 changed files with 46 additions and 2 deletions

View File

@@ -1,6 +1,7 @@
import { useState, useRef } from "react"; import { useState, useRef } from "react";
import { NDKEvent } from "@nostr-dev-kit/ndk"; import { NDKEvent } from "@nostr-dev-kit/ndk";
import { useProfile } from "../../hooks/useProfile"; import { useProfile } from "../../hooks/useProfile";
import { useReactionCount } from "../../hooks/useReactionCount";
import { useUserStore } from "../../stores/user"; import { useUserStore } from "../../stores/user";
import { useUIStore } from "../../stores/ui"; import { useUIStore } from "../../stores/ui";
import { timeAgo, shortenPubkey } from "../../lib/utils"; import { timeAgo, shortenPubkey } from "../../lib/utils";
@@ -27,6 +28,7 @@ export function NoteCard({ event }: NoteCardProps) {
}; };
const [liked, setLiked] = useState(() => getLiked().has(event.id)); const [liked, setLiked] = useState(() => getLiked().has(event.id));
const [liking, setLiking] = useState(false); const [liking, setLiking] = useState(false);
const [reactionCount, adjustReactionCount] = useReactionCount(event.id);
const [showReply, setShowReply] = useState(false); const [showReply, setShowReply] = useState(false);
const [replyText, setReplyText] = useState(""); const [replyText, setReplyText] = useState("");
const [replying, setReplying] = useState(false); const [replying, setReplying] = useState(false);
@@ -43,6 +45,7 @@ export function NoteCard({ event }: NoteCardProps) {
liked.add(event.id); liked.add(event.id);
localStorage.setItem(likedKey, JSON.stringify(Array.from(liked))); localStorage.setItem(likedKey, JSON.stringify(Array.from(liked)));
setLiked(true); setLiked(true);
adjustReactionCount(1);
} finally { } finally {
setLiking(false); setLiking(false);
} }
@@ -134,7 +137,7 @@ export function NoteCard({ event }: NoteCardProps) {
liked ? "text-accent" : "text-text-dim hover:text-accent" liked ? "text-accent" : "text-text-dim hover:text-accent"
} disabled:cursor-default`} } disabled:cursor-default`}
> >
{liked ? "♥ liked" : " like"} {liked ? "♥" : "♡"}{reactionCount !== null && reactionCount > 0 ? ` ${reactionCount}` : liked ? " liked" : " like"}
</button> </button>
</div> </div>
)} )}

View File

@@ -0,0 +1,29 @@
import { useEffect, useState } from "react";
import { fetchReactionCount } from "../lib/nostr";
const cache = new Map<string, number>();
export function useReactionCount(eventId: string): [number | null, (delta: number) => void] {
const [count, setCount] = useState<number | null>(() => cache.get(eventId) ?? null);
useEffect(() => {
if (cache.has(eventId)) {
setCount(cache.get(eventId)!);
return;
}
fetchReactionCount(eventId).then((n) => {
cache.set(eventId, n);
setCount(n);
});
}, [eventId]);
const adjust = (delta: number) => {
setCount((prev) => {
const next = (prev ?? 0) + delta;
cache.set(eventId, next);
return next;
});
};
return [count, adjust];
}

View File

@@ -191,6 +191,18 @@ export async function fetchUserNotes(pubkey: string, limit = 30): Promise<NDKEve
return Array.from(events).sort((a, b) => (b.created_at ?? 0) - (a.created_at ?? 0)); return Array.from(events).sort((a, b) => (b.created_at ?? 0) - (a.created_at ?? 0));
} }
export async function fetchReactionCount(eventId: string): Promise<number> {
const instance = getNDK();
const filter: NDKFilter = {
kinds: [NDKKind.Reaction],
"#e": [eventId],
};
const events = await instance.fetchEvents(filter, {
cacheUsage: NDKSubscriptionCacheUsage.ONLY_RELAY,
});
return events.size;
}
export async function publishContactList(pubkeys: string[]): Promise<void> { export async function publishContactList(pubkeys: string[]): Promise<void> {
const instance = getNDK(); const instance = getNDK();
if (!instance.signer) throw new Error("Not logged in"); if (!instance.signer) throw new Error("Not logged in");

View File

@@ -1 +1 @@
export { getNDK, connectToRelays, fetchGlobalFeed, fetchFollowFeed, fetchReplies, publishNote, publishArticle, publishProfile, publishReaction, publishReply, publishContactList, fetchUserNotes, fetchProfile } from "./client"; export { getNDK, connectToRelays, fetchGlobalFeed, fetchFollowFeed, fetchReplies, publishNote, publishArticle, publishProfile, publishReaction, publishReply, publishContactList, fetchReactionCount, fetchUserNotes, fetchProfile } from "./client";