diff --git a/src/components/feed/Feed.tsx b/src/components/feed/Feed.tsx index b1e1f56..90e15e6 100644 --- a/src/components/feed/Feed.tsx +++ b/src/components/feed/Feed.tsx @@ -4,7 +4,7 @@ import { useUserStore } from "../../stores/user"; import { useMuteStore } from "../../stores/mute"; import { useUIStore } from "../../stores/ui"; import { useWoTStore } from "../../stores/wot"; -import { fetchFollowFeed, getNDK, ensureConnected } from "../../lib/nostr"; +import { fetchFollowFeed, getNDK, ensureConnected, batchFetchProfileAges } from "../../lib/nostr"; import { diagWrapFetch, logDiag } from "../../lib/feedDiagnostics"; import { detectScript, getEventLanguageTag, FILTER_SCRIPTS } from "../../lib/language"; import { NoteCard } from "./NoteCard"; @@ -63,7 +63,10 @@ export function Feed() { useEffect(() => { // Show cached notes immediately, then fetch fresh ones once connected loadCachedFeed(); - connect().then(() => loadFeed()); + connect().then(() => loadFeed().then(() => { + const pubkeys = [...new Set(useFeedStore.getState().notes.map((e) => e.pubkey))]; + batchFetchProfileAges(pubkeys); + })); }, []); diff --git a/src/components/feed/NoteCard.tsx b/src/components/feed/NoteCard.tsx index 9dcd81d..f91bef6 100644 --- a/src/components/feed/NoteCard.tsx +++ b/src/components/feed/NoteCard.tsx @@ -6,7 +6,7 @@ import { useUserStore } from "../../stores/user"; import { useMuteStore } from "../../stores/mute"; import { useUIStore } from "../../stores/ui"; import { timeAgo, shortenPubkey } from "../../lib/utils"; -import { getNDK, fetchNoteById, ensureConnected } from "../../lib/nostr"; +import { getNDK, fetchNoteById, ensureConnected, getProfileAge } from "../../lib/nostr"; import { getParentEventId } from "../../lib/threadTree"; import { NoteContent } from "./NoteContent"; import { NoteActions, LoggedOutStats } from "./NoteActions"; @@ -34,7 +34,7 @@ export const NoteCard = memo(function NoteCard({ event, focused, onReplyInThread const nip05 = typeof profile?.nip05 === "string" ? profile.nip05 : null; const verified = useNip05Verified(event.pubkey, nip05); const time = event.created_at ? timeAgo(event.created_at) : ""; - const profileCreatedAt = typeof profile?._createdAt === "number" ? profile._createdAt : null; + const profileCreatedAt = getProfileAge(event.pubkey); const isNewAccount = profileCreatedAt !== null && (Date.now() / 1000 - profileCreatedAt) < 60 * 24 * 3600; const loggedIn = useUserStore((s) => s.loggedIn); diff --git a/src/components/thread/ThreadView.tsx b/src/components/thread/ThreadView.tsx index 8814fb2..49e7748 100644 --- a/src/components/thread/ThreadView.tsx +++ b/src/components/thread/ThreadView.tsx @@ -4,7 +4,7 @@ import { useAutoResize } from "../../hooks/useAutoResize"; import { useUIStore } from "../../stores/ui"; import { useUserStore } from "../../stores/user"; import { useMuteStore } from "../../stores/mute"; -import { fetchNoteById, fetchThreadEvents, fetchAncestors, publishReply, getNDK, ensureConnected } from "../../lib/nostr"; +import { fetchNoteById, fetchThreadEvents, fetchAncestors, publishReply, getNDK, ensureConnected, batchFetchProfileAges } from "../../lib/nostr"; import { buildThreadTree, getRootEventId } from "../../lib/threadTree"; import type { ThreadNode } from "../../lib/threadTree"; import { debug } from "../../lib/debug"; @@ -99,6 +99,7 @@ export function ThreadView() { const built = buildThreadTree(root.id, allEvents); setTree(built); + batchFetchProfileAges([...new Set(allEvents.map((e) => e.pubkey))]); } catch (err) { debug.error("Failed to load thread:", err); if (!cancelled) setLoadError(`Failed to load: ${err}`); diff --git a/src/lib/nostr/index.ts b/src/lib/nostr/index.ts index 6ccde72..0f992f6 100644 --- a/src/lib/nostr/index.ts +++ b/src/lib/nostr/index.ts @@ -1,6 +1,6 @@ export { getNDK, getNDKUptimeMs, connectToRelays, ensureConnected, resetNDK, getStoredRelayUrls, normalizeRelayUrl, addRelay, removeRelay, fetchWithTimeout, withTimeout, FEED_TIMEOUT, THREAD_TIMEOUT, SINGLE_TIMEOUT } from "./core"; export { fetchGlobalFeed, fetchMediaFeed, fetchFollowFeed, fetchUserNotes, fetchUserNotesNIP65, fetchNoteById, fetchReplies, publishNote, publishReply, publishRepost, publishQuote, fetchHashtagFeed, fetchThreadEvents, fetchAncestors } from "./notes"; -export { publishProfile, publishContactList, fetchProfile, fetchFollowSuggestions, fetchMentions, fetchFollowers, fetchNewFollowers } from "./social"; +export { publishProfile, publishContactList, fetchProfile, fetchFollowSuggestions, fetchMentions, fetchFollowers, fetchNewFollowers, batchFetchProfileAges, getProfileAge } from "./social"; export { publishArticle, fetchArticle, fetchAuthorArticles, fetchArticleFeed, searchArticles, fetchByAddr } from "./articles"; export { publishReaction, fetchReplyCount, fetchZapCount, fetchReactions, groupReactions, fetchBatchEngagement, fetchZapsReceived, fetchZapsSent } from "./engagement"; export type { GroupedReactions, BatchEngagement } from "./engagement"; diff --git a/src/lib/nostr/social.ts b/src/lib/nostr/social.ts index f631932..831d27b 100644 --- a/src/lib/nostr/social.ts +++ b/src/lib/nostr/social.ts @@ -33,14 +33,26 @@ export async function publishContactList(pubkeys: string[]): Promise { export async function fetchProfile(pubkey: string) { const instance = getNDK(); - const events = await fetchWithTimeout(instance, { kinds: [0], authors: [pubkey] }, FEED_TIMEOUT); - const event = [...events].sort((a, b) => (b.created_at ?? 0) - (a.created_at ?? 0))[0]; - if (!event) return null; - try { - const content = JSON.parse(event.content) as Record; - return { ...content, _createdAt: event.created_at ?? null }; - } catch { - return null; + const user = instance.getUser({ pubkey }); + await user.fetchProfile(); + return user.profile ?? null; +} + +// Bulk-fetch kind-0 events for many pubkeys in one relay query and populate +// the profile-age cache. Called once per feed/thread load — not per note. +const profileAgeCache = new Map(); + +export function getProfileAge(pubkey: string): number | null { + return profileAgeCache.get(pubkey) ?? null; +} + +export async function batchFetchProfileAges(pubkeys: string[]): Promise { + const needed = pubkeys.filter((pk) => !profileAgeCache.has(pk)); + if (needed.length === 0) return; + const instance = getNDK(); + const events = await fetchWithTimeout(instance, { kinds: [0], authors: needed }, FEED_TIMEOUT); + for (const event of events) { + if (event.created_at) profileAgeCache.set(event.pubkey, event.created_at); } }