diff --git a/src/components/Profile/ProfileHeaderInteractions.tsx b/src/components/Profile/ProfileHeaderInteractions.tsx
index 0c1a3085..ea8bf60d 100644
--- a/src/components/Profile/ProfileHeaderInteractions.tsx
+++ b/src/components/Profile/ProfileHeaderInteractions.tsx
@@ -8,7 +8,8 @@ import Emoji from '@/components/Emoji'
import { getEmojiInfosFromEmojiTags } from '@/lib/tag'
import type { TProfileZap } from '@/hooks/useProfileInteractions'
import type { TProfileBadge } from '@/hooks/useProfileBadges'
-import { Zap, MessageCircle, ThumbsUp } from 'lucide-react'
+import type { TProfileFollowPack } from '@/hooks/useProfileFollowPacks'
+import { Zap, MessageCircle, ThumbsUp, Users } from 'lucide-react'
import { Skeleton } from '@/components/ui/skeleton'
import { useTranslation } from 'react-i18next'
import { Event } from 'nostr-tools'
@@ -18,8 +19,10 @@ type Props = {
reactions: Event[]
comments: Event[]
badges: TProfileBadge[]
+ followPacks: TProfileFollowPack[]
loading: boolean
badgesLoading: boolean
+ followPacksLoading: boolean
}
const ZAPS_PER_ROW = 4
@@ -28,6 +31,7 @@ const MAX_ZAPS = ZAPS_PER_ROW * ZAP_ROWS
const BADGES_PER_ROW = 4
const BADGE_ROWS = 2
const MAX_BADGES = BADGES_PER_ROW * BADGE_ROWS
+const MAX_FOLLOW_PACKS = 8
function ZapBadge({ zap }: { zap: TProfileZap }) {
const { push } = useSecondaryPage()
@@ -85,6 +89,21 @@ function CommentBadge({ event }: { event: Event }) {
)
}
+function FollowPackBadge({ pack }: { pack: TProfileFollowPack }) {
+ const { push } = useSecondaryPage()
+ return (
+
+ )
+}
+
function BadgeItem({ badge }: { badge: TProfileBadge }) {
const imageUrl = badge.thumb ?? badge.image
const label = badge.name ?? badge.a.split(':').pop() ?? ''
@@ -116,10 +135,20 @@ function BadgeItem({ badge }: { badge: TProfileBadge }) {
)
}
-export default function ProfileHeaderInteractions({ zaps, reactions, comments, badges, loading, badgesLoading }: Props) {
+export default function ProfileHeaderInteractions({
+ zaps,
+ reactions,
+ comments,
+ badges,
+ followPacks,
+ loading,
+ badgesLoading,
+ followPacksLoading
+}: Props) {
const { t } = useTranslation()
const displayZaps = zaps.slice(0, MAX_ZAPS)
const displayBadges = badges.slice(0, MAX_BADGES)
+ const displayFollowPacks = followPacks.slice(0, MAX_FOLLOW_PACKS)
const Section = ({ title, isEmpty, isLoading, children, skeletonCount = 6 }: {
title: string
@@ -174,6 +203,13 @@ export default function ProfileHeaderInteractions({ zaps, reactions, comments, b
))}
+
+
+ {displayFollowPacks.map((pack) => (
+
+ ))}
+
+
)
}
diff --git a/src/components/Profile/ProfileInteractionsAccordion.tsx b/src/components/Profile/ProfileInteractionsAccordion.tsx
new file mode 100644
index 00000000..1b5c742d
--- /dev/null
+++ b/src/components/Profile/ProfileInteractionsAccordion.tsx
@@ -0,0 +1,110 @@
+import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'
+import { Skeleton } from '@/components/ui/skeleton'
+import { ChevronDown } from 'lucide-react'
+import { useTranslation } from 'react-i18next'
+import { cn } from '@/lib/utils'
+import { useEffect } from 'react'
+import { useProfileRelayUrls } from '@/hooks/useProfileRelayUrls'
+import { useProfileInteractions } from '@/hooks/useProfileInteractions'
+import { useProfileBadges } from '@/hooks/useProfileBadges'
+import { useProfileFollowPacks } from '@/hooks/useProfileFollowPacks'
+import ProfileHeaderInteractions from './ProfileHeaderInteractions'
+
+type Props = {
+ pubkey: string | undefined
+ isExpanded: boolean
+ onExpandedChange: (open: boolean) => void
+ onRefreshReady?: (refresh: (() => void) | null) => void
+}
+
+function ProfileInteractionsContent({ pubkey, relayUrls, onRefreshReady }: {
+ pubkey: string
+ relayUrls: string[] | undefined
+ onRefreshReady?: (refresh: (() => void) | null) => void
+}) {
+ const { zaps, reactions, comments, loading, refresh } = useProfileInteractions(pubkey, relayUrls)
+ const { badges, loading: badgesLoading, refresh: refreshBadges } = useProfileBadges(pubkey, relayUrls)
+ const { packs, loading: followPacksLoading, refresh: refreshFollowPacks } = useProfileFollowPacks(pubkey, relayUrls)
+
+ useEffect(() => {
+ const doRefresh = () => {
+ refresh()
+ refreshBadges()
+ refreshFollowPacks()
+ }
+ onRefreshReady?.(doRefresh)
+ return () => { onRefreshReady?.(null) }
+ }, [refresh, refreshBadges, refreshFollowPacks, onRefreshReady])
+
+ return (
+
+ )
+}
+
+function ProfileInteractionsSkeleton() {
+ return (
+
+ {[6, 4, 4, 8, 6].map((count, i) => (
+
+
+
+ {Array.from({ length: count }).map((_, j) => (
+
+ ))}
+
+
+ ))}
+
+ )
+}
+
+export default function ProfileInteractionsAccordion({
+ pubkey,
+ isExpanded,
+ onExpandedChange,
+ onRefreshReady
+}: Props) {
+ const { t } = useTranslation()
+ const { relayUrls, loading: relayUrlsLoading } = useProfileRelayUrls(pubkey, isExpanded)
+ const relaysReady = !relayUrlsLoading
+ const hasContent = isExpanded && pubkey
+
+ return (
+
+
+
+ {t('Zaps')}, {t('Likes')}, {t('Comments')}, {t('Badges')}, {t('In Follow Packs')}
+
+
+
+
+ {hasContent ? (
+ !relaysReady ? (
+
+ ) : (
+
+
0 ? relayUrls : undefined}
+ onRefreshReady={onRefreshReady}
+ />
+
+ )
+ ) : null}
+
+
+ )
+}
diff --git a/src/components/Profile/index.tsx b/src/components/Profile/index.tsx
index 7e68a454..19b3596a 100644
--- a/src/components/Profile/index.tsx
+++ b/src/components/Profile/index.tsx
@@ -49,7 +49,7 @@ import ProfileFeedWithPins from './ProfileFeedWithPins'
import ProfileMediaFeed from './ProfileMediaFeed'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import type { TNoteListRef } from '@/components/NoteList'
-import ProfileHeaderInteractions from './ProfileHeaderInteractions'
+import ProfileInteractionsAccordion from './ProfileInteractionsAccordion'
import SmartFollowings from './SmartFollowings'
import SmartMuteLink from './SmartMuteLink'
import SmartRelays from './SmartRelays'
@@ -62,8 +62,6 @@ import {
} from '@/components/ScheduleVideoCallDialog'
import RawEventDialog from '@/components/NoteOptions/RawEventDialog'
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
-import { useProfileInteractions } from '@/hooks/useProfileInteractions'
-import { useProfileBadges } from '@/hooks/useProfileBadges'
import { useCurrentRelays } from '@/providers/CurrentRelaysProvider'
import { FAST_READ_RELAY_URLS, FAST_WRITE_RELAY_URLS } from '@/constants'
import { nip66Service } from '@/services/nip66.service'
@@ -287,10 +285,8 @@ export default function Profile({
[profile]
)
const isSelf = accountPubkey === profile?.pubkey
- const { zaps: profileZaps, reactions: profileReactions, comments: profileComments, loading: profileInteractionsLoading, refresh: refreshProfileInteractions } =
- useProfileInteractions(profile?.pubkey, profileEvent)
- const { badges: profileBadges, loading: profileBadgesLoading, refresh: refreshProfileBadges } =
- useProfileBadges(profile?.pubkey)
+ const [profileInteractionsExpanded, setProfileInteractionsExpanded] = useState(false)
+ const profileInteractionsRefreshRef = useRef<(() => void) | null>(null)
/** All available relays: current feed, favorites, relay sets, defaults (FAST_READ, FAST_WRITE). */
const allAvailableRelayUrls = useMemo(() => {
@@ -354,8 +350,7 @@ export default function Profile({
const m = r as MutableRefObject<{ refresh: () => void } | null>
m.current = {
refresh: () => {
- refreshProfileInteractions()
- refreshProfileBadges()
+ profileInteractionsRefreshRef.current?.()
postsFeedRef.current?.refresh()
mediaFeedRef.current?.refresh()
}
@@ -363,7 +358,7 @@ export default function Profile({
return () => {
m.current = null
}
- }, [refreshProfileInteractions, refreshProfileBadges])
+ }, [])
useEffect(() => {
if (!profile?.pubkey) return
@@ -427,7 +422,7 @@ export default function Profile({
? (url) => setOpenCallInviteTo({ pubkey, url })
: undefined
}
- onProfileInteractionsRefresh={refreshProfileInteractions}
+ onProfileInteractionsRefresh={() => profileInteractionsRefreshRef.current?.()}
/>
{isSelf ? (
@@ -454,7 +449,7 @@ export default function Profile({
const evt = await publish(reaction)
if (evt) {
showSimplePublishSuccess(t('Reaction published'))
- refreshProfileInteractions()
+ profileInteractionsRefreshRef.current?.()
}
} finally {
setSelfReacting(false)
@@ -510,7 +505,7 @@ export default function Profile({
parentEvent={profileEvent}
open={openSelfReply}
setOpen={setOpenSelfReply}
- onPublishSuccess={refreshProfileInteractions}
+ onPublishSuccess={() => profileInteractionsRefreshRef.current?.()}
/>
)}
{!isSelf ? (
@@ -536,18 +531,18 @@ export default function Profile({
{nip05List && nip05List.length > 1 && (
)}
-
-
+
-
+
+
{ profileInteractionsRefreshRef.current = refresh ?? null }}
+ />
+
(pubkey ? nip19.npubEncode(pubkey) : ''), [pubkey])
const [copied, setCopied] = useState(false)
@@ -17,10 +17,10 @@ export default function PubkeyCopy({ pubkey }: { pubkey: string }) {
return (
copyNpub()}
>
-
{formatNpub(npub, 24)}
+
{formatNpub(npub, showFull ? 99 : 24)}
{copied ?
:
}
)
diff --git a/src/constants.ts b/src/constants.ts
index 3655d7fe..6848f8a2 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -239,7 +239,8 @@ export const PROFILE_RELAY_URLS = [
'wss://nos.lol',
'wss://relay.damus.io',
'wss://profiles.nostr1.com',
- 'wss://purplepag.es'
+ 'wss://purplepag.es',
+ 'wss://thecitadel.nostr1.com'
]
// Combined relay URLs for profile fetching - includes both FAST_READ_RELAY_URLS and SEARCHABLE_RELAY_URLS
diff --git a/src/hooks/useProfileBadges.tsx b/src/hooks/useProfileBadges.tsx
index 2a859375..2f4a4d71 100644
--- a/src/hooks/useProfileBadges.tsx
+++ b/src/hooks/useProfileBadges.tsx
@@ -1,10 +1,9 @@
-import { E_TAG_FILTER_BLOCKED_RELAY_URLS, ExtendedKind } from '@/constants'
+import { ExtendedKind } from '@/constants'
import { queryService, replaceableEventService } from '@/services/client.service'
import { useCallback, useEffect, useRef, useState } from 'react'
import { tagNameEquals } from '@/lib/tag'
-import { buildComprehensiveRelayList } from '@/lib/relay-list-builder'
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
-import { useNostr } from '@/providers/NostrProvider'
+import { buildProfileRelayUrls } from '@/lib/profile-relay-urls'
export type TProfileBadge = {
/** Badge definition coordinate (e.g. "30009:alice:bravery") */
@@ -29,8 +28,8 @@ function parseATag(aTag: string): { kind: number; pubkey: string; d: string } |
}
/** NIP-58: Fetches profile badges (kind 30008) and resolves badge definitions (kind 30009). */
-export function useProfileBadges(pubkey: string | undefined) {
- const { pubkey: accountPubkey } = useNostr()
+/** Pass relayUrls to share with other profile fetches. */
+export function useProfileBadges(pubkey: string | undefined, relayUrls?: string[]) {
const { blockedRelays } = useFavoriteRelays()
const [badges, setBadges] = useState([])
const [loading, setLoading] = useState(false)
@@ -46,20 +45,12 @@ export function useProfileBadges(pubkey: string | undefined) {
setLoading(true)
try {
- const relayUrls = await buildComprehensiveRelayList({
- authorPubkey: pubkey,
- userPubkey: accountPubkey ?? undefined,
- blockedRelays: [...blockedRelays, ...E_TAG_FILTER_BLOCKED_RELAY_URLS],
- includeFastReadRelays: true,
- includeSearchableRelays: true,
- includeProfileFetchRelays: true,
- includeLocalRelays: true
- })
+ const urls = relayUrls ?? (await buildProfileRelayUrls(pubkey, blockedRelays))
const events = await queryService.fetchEvents(
- relayUrls,
+ urls,
{ authors: [pubkey], kinds: [ExtendedKind.PROFILE_BADGES], '#d': ['profile_badges'] },
- undefined
+ { eoseTimeout: 2000, globalTimeout: 15000, firstRelayResultGraceMs: false }
)
const profileBadgesEvent = events.sort((a, b) => b.created_at - a.created_at)[0]
@@ -118,7 +109,7 @@ export function useProfileBadges(pubkey: string | undefined) {
} finally {
if (myFetchId === fetchIdRef.current) setLoading(false)
}
- }, [pubkey, accountPubkey, blockedRelays])
+ }, [pubkey, blockedRelays, relayUrls])
useEffect(() => {
fetchBadges()
diff --git a/src/hooks/useProfileFollowPacks.tsx b/src/hooks/useProfileFollowPacks.tsx
new file mode 100644
index 00000000..26bd8345
--- /dev/null
+++ b/src/hooks/useProfileFollowPacks.tsx
@@ -0,0 +1,70 @@
+import { ExtendedKind } from '@/constants'
+import { queryService } from '@/services/client.service'
+import { Event } from 'nostr-tools'
+import { useCallback, useEffect, useRef, useState } from 'react'
+import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
+import { buildProfileRelayUrls } from '@/lib/profile-relay-urls'
+
+export type TProfileFollowPack = {
+ event: Event
+ title: string
+}
+
+function getPackTitle(event: Event): string {
+ const titleTag = event.tags.find((tag) => tag[0] === 'title' || tag[0] === 'name')
+ return titleTag?.[1] || 'Follow Pack'
+}
+
+/** Fetches follow packs (kind 39089) that contain this pubkey in #p tags. */
+export function useProfileFollowPacks(
+ pubkey: string | undefined,
+ relayUrls?: string[]
+) {
+ const { blockedRelays } = useFavoriteRelays()
+ const [packs, setPacks] = useState([])
+ const [loading, setLoading] = useState(false)
+ const fetchIdRef = useRef(0)
+
+ const fetchPacks = useCallback(async () => {
+ if (!pubkey) {
+ setPacks([])
+ return
+ }
+
+ const myFetchId = (fetchIdRef.current += 1)
+ setLoading(true)
+
+ try {
+ const urls = relayUrls ?? (await buildProfileRelayUrls(pubkey, blockedRelays))
+ if (urls.length === 0) {
+ if (myFetchId === fetchIdRef.current) setPacks([])
+ return
+ }
+
+ const events = await queryService.fetchEvents(
+ urls,
+ [{ '#p': [pubkey], kinds: [ExtendedKind.FOLLOW_PACK], limit: 50 }],
+ { eoseTimeout: 2000, globalTimeout: 15000, firstRelayResultGraceMs: false }
+ )
+
+ if (myFetchId !== fetchIdRef.current) return
+
+ const result: TProfileFollowPack[] = events.map((evt) => ({
+ event: evt,
+ title: getPackTitle(evt)
+ }))
+ setPacks(result)
+ } catch {
+ if (myFetchId !== fetchIdRef.current) return
+ setPacks([])
+ } finally {
+ if (myFetchId === fetchIdRef.current) setLoading(false)
+ }
+ }, [pubkey, blockedRelays, relayUrls])
+
+ useEffect(() => {
+ fetchPacks()
+ }, [fetchPacks])
+
+ return { packs, loading, refresh: fetchPacks }
+}
diff --git a/src/hooks/useProfileInteractions.tsx b/src/hooks/useProfileInteractions.tsx
index 8b098a48..e454bee8 100644
--- a/src/hooks/useProfileInteractions.tsx
+++ b/src/hooks/useProfileInteractions.tsx
@@ -1,11 +1,11 @@
-import { E_TAG_FILTER_BLOCKED_RELAY_URLS, ExtendedKind } from '@/constants'
+import { ExtendedKind } from '@/constants'
import { getZapInfoFromEvent } from '@/lib/event-metadata'
import { queryService } from '@/services/client.service'
+import { hexPubkeysEqual } from '@/lib/pubkey'
import { Event, Filter, kinds } from 'nostr-tools'
import { useCallback, useEffect, useRef, useState } from 'react'
-import { buildComprehensiveRelayList } from '@/lib/relay-list-builder'
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
-import { useNostr } from '@/providers/NostrProvider'
+import { buildProfileRelayUrls } from '@/lib/profile-relay-urls'
export type TProfileZap = {
pr: string
@@ -15,9 +15,11 @@ export type TProfileZap = {
comment?: string
}
-/** Fetches zaps, reactions (likes), and comments for a profile. */
-export function useProfileInteractions(pubkey: string | undefined, profileEvent: Event | undefined) {
- const { pubkey: accountPubkey } = useNostr()
+const NOTE_IDS_FOR_REACTIONS = 50
+
+/** Fetches zaps, reactions (likes on profile's notes), and comments (on profile's notes). */
+/** Uses profile owner's outboxes + PROFILE_FETCH_RELAY_URLS. Pass relayUrls to share with other profile fetches. */
+export function useProfileInteractions(pubkey: string | undefined, relayUrls?: string[]) {
const { blockedRelays } = useFavoriteRelays()
const [zaps, setZaps] = useState([])
const [reactions, setReactions] = useState([])
@@ -37,60 +39,110 @@ export function useProfileInteractions(pubkey: string | undefined, profileEvent:
setLoading(true)
try {
- const relayUrls = await buildComprehensiveRelayList({
- authorPubkey: pubkey,
- userPubkey: accountPubkey ?? undefined,
- blockedRelays: [...blockedRelays, ...E_TAG_FILTER_BLOCKED_RELAY_URLS],
- includeFastReadRelays: true,
- includeSearchableRelays: true,
- includeProfileFetchRelays: true,
- includeLocalRelays: true
- })
-
- const filters: Filter[] = [{ '#p': [pubkey], kinds: [kinds.Zap], limit: 100 }]
- if (profileEvent) {
- filters.push({
- '#e': [profileEvent.id],
- kinds: [kinds.Reaction, ExtendedKind.COMMENT],
- limit: 50
- })
- }
+ const urls = relayUrls ?? (await buildProfileRelayUrls(pubkey, blockedRelays))
const collectedZaps: TProfileZap[] = []
- const collectedReactions: Event[] = []
+ const reactionsByPubkey = new Map() // one reaction per npub, newest kept
const collectedComments: Event[] = []
const seenZaps = new Set()
const seenReactions = new Set()
+ let noteIds: string[] = []
+
+ // Phase 1: zaps + profile's recent notes (to find reactions/comments on their content)
+ const phase1Filters: Filter[] = [
+ { '#p': [pubkey], kinds: [kinds.Zap], limit: 100 },
+ { authors: [pubkey], kinds: [kinds.ShortTextNote], limit: NOTE_IDS_FOR_REACTIONS }
+ ]
- await queryService.fetchEvents(relayUrls, filters, {
+ const flushZaps = () => {
+ if (myFetchId !== fetchIdRef.current) return
+ const sorted = [...collectedZaps].sort((a, b) => b.amount - a.amount)
+ setZaps(sorted)
+ }
+ await queryService.fetchEvents(urls, phase1Filters, {
+ eoseTimeout: 2000,
+ globalTimeout: 15000,
+ firstRelayResultGraceMs: false,
onevent: (evt) => {
if (evt.kind === kinds.Zap) {
const info = getZapInfoFromEvent(evt)
- if (!info || info.recipientPubkey !== pubkey || !info.amount || info.amount <= 0) return
+ if (!info || !hexPubkeysEqual(info.recipientPubkey ?? '', pubkey) || !info.amount || info.amount <= 0) return
+ const sender = info.senderPubkey ?? evt.pubkey
+ if (hexPubkeysEqual(sender, pubkey)) return // skip self-zaps (likely tests)
if (seenZaps.has(evt.id)) return
seenZaps.add(evt.id)
collectedZaps.push({
pr: evt.id,
- pubkey: info.senderPubkey ?? evt.pubkey,
+ pubkey: sender,
amount: info.amount,
created_at: evt.created_at,
comment: info.comment
})
- } else if (evt.kind === kinds.Reaction || evt.kind === ExtendedKind.COMMENT) {
- if (seenReactions.has(evt.id)) return
- seenReactions.add(evt.id)
- if (evt.kind === kinds.Reaction) {
- collectedReactions.push(evt)
- } else {
- collectedComments.push(evt)
- }
+ flushZaps() // render incrementally as events arrive from slow relays
+ } else if (evt.kind === kinds.ShortTextNote) {
+ noteIds.push(evt.id)
}
}
})
+ noteIds = [...new Set(noteIds)].slice(0, NOTE_IDS_FOR_REACTIONS)
+ if (myFetchId !== fetchIdRef.current) return
+
+ const flushReactions = () => {
+ if (myFetchId !== fetchIdRef.current) return
+ setReactions(Array.from(reactionsByPubkey.values()).sort((a, b) => b.created_at - a.created_at))
+ }
+ const flushComments = () => {
+ if (myFetchId !== fetchIdRef.current) return
+ setComments([...collectedComments].sort((a, b) => b.created_at - a.created_at))
+ }
+ const handleReactionOrComment = (evt: Event) => {
+ if (hexPubkeysEqual(evt.pubkey, pubkey)) return // skip self-reactions/self-comments (likely tests)
+ if (seenReactions.has(evt.id)) return
+ seenReactions.add(evt.id)
+ if (evt.kind === kinds.Reaction) {
+ const existing = reactionsByPubkey.get(evt.pubkey)
+ if (!existing || evt.created_at > existing.created_at) {
+ reactionsByPubkey.set(evt.pubkey, evt)
+ }
+ flushReactions()
+ } else {
+ collectedComments.push(evt)
+ flushComments()
+ }
+ }
+
+ const phase2Opts = {
+ eoseTimeout: 2000,
+ globalTimeout: 15000,
+ firstRelayResultGraceMs: false as const,
+ onevent: (evt: Event) => {
+ if (evt.kind === kinds.Reaction || evt.kind === ExtendedKind.COMMENT) {
+ handleReactionOrComment(evt)
+ }
+ }
+ }
+
+ // Phase 2a: reactions and comments on profile's notes (#e)
+ if (noteIds.length > 0) {
+ await queryService.fetchEvents(urls, [{
+ '#e': noteIds,
+ kinds: [kinds.Reaction, ExtendedKind.COMMENT],
+ limit: 50
+ }], phase2Opts)
+ }
+
+ // Phase 2b: comments ON the profile itself (kind 0) - use #a (required), p is optional
+ const profileAddrs = [`0:${pubkey}:`, `0:${pubkey}:profile`]
+ await queryService.fetchEvents(urls, [{
+ '#a': profileAddrs,
+ kinds: [ExtendedKind.COMMENT],
+ limit: 50
+ }], phase2Opts)
+
if (myFetchId !== fetchIdRef.current) return
collectedZaps.sort((a, b) => b.amount - a.amount)
- collectedReactions.sort((a, b) => b.created_at - a.created_at)
+ const collectedReactions = Array.from(reactionsByPubkey.values()).sort((a, b) => b.created_at - a.created_at)
collectedComments.sort((a, b) => b.created_at - a.created_at)
setZaps(collectedZaps)
setReactions(collectedReactions)
@@ -100,7 +152,7 @@ export function useProfileInteractions(pubkey: string | undefined, profileEvent:
} finally {
if (myFetchId === fetchIdRef.current) setLoading(false)
}
- }, [pubkey, profileEvent?.id, accountPubkey, blockedRelays])
+ }, [pubkey, blockedRelays, relayUrls])
useEffect(() => {
fetchAll()
@@ -111,6 +163,6 @@ export function useProfileInteractions(pubkey: string | undefined, profileEvent:
/** @deprecated Use useProfileInteractions instead. Returns zaps only for compatibility. */
export function useProfileZaps(pubkey: string | undefined) {
- const result = useProfileInteractions(pubkey, undefined)
+ const result = useProfileInteractions(pubkey)
return { zaps: result.zaps, loading: result.loading, refresh: result.refresh }
}
diff --git a/src/hooks/useProfileRelayUrls.tsx b/src/hooks/useProfileRelayUrls.tsx
new file mode 100644
index 00000000..2f4ca085
--- /dev/null
+++ b/src/hooks/useProfileRelayUrls.tsx
@@ -0,0 +1,33 @@
+import { buildProfileRelayUrls } from '@/lib/profile-relay-urls'
+import { useCallback, useEffect, useState } from 'react'
+import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
+
+/** Returns profile relay URLs (outboxes + PROFILE_FETCH). Use for sharing relays across profile fetches. */
+export function useProfileRelayUrls(pubkey: string | undefined, enabled: boolean) {
+ const { blockedRelays } = useFavoriteRelays()
+ const [relayUrls, setRelayUrls] = useState([])
+ const [loading, setLoading] = useState(false)
+
+ const fetch = useCallback(async () => {
+ if (!pubkey || !enabled) {
+ setRelayUrls([])
+ setLoading(false)
+ return
+ }
+ setLoading(true)
+ try {
+ const urls = await buildProfileRelayUrls(pubkey, blockedRelays)
+ setRelayUrls(urls)
+ } catch {
+ setRelayUrls([])
+ } finally {
+ setLoading(false)
+ }
+ }, [pubkey, enabled, blockedRelays])
+
+ useEffect(() => {
+ fetch()
+ }, [fetch])
+
+ return { relayUrls, loading, refresh: fetch }
+}
diff --git a/src/i18n/locales/de.ts b/src/i18n/locales/de.ts
index 328102ef..43b65617 100644
--- a/src/i18n/locales/de.ts
+++ b/src/i18n/locales/de.ts
@@ -479,6 +479,7 @@ export default {
Bookmarks: 'Lesezeichen',
'Follow Packs': 'Follow-Packs',
'Follow Pack': 'Follow-Pack',
+ 'In Follow Packs': 'In Follow-Packs',
'Please log in to follow': 'Zum Folgen bitte anmelden',
'Following All': 'Allen gefolgt',
'Followed {{count}} users': '{{count}} Nutzer:innen gefolgt',
diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts
index 052d0b72..a0a02e55 100644
--- a/src/i18n/locales/en.ts
+++ b/src/i18n/locales/en.ts
@@ -471,6 +471,7 @@ export default {
Bookmarks: 'Bookmarks',
'Follow Packs': 'Follow Packs',
'Follow Pack': 'Follow Pack',
+ 'In Follow Packs': 'In Follow Packs',
'Please log in to follow': 'Please log in to follow',
'Following All': 'Following All',
'Followed {{count}} users': 'Followed {{count}} users',
diff --git a/src/i18n/locales/fr.ts b/src/i18n/locales/fr.ts
index 472f0d0f..9975010a 100644
--- a/src/i18n/locales/fr.ts
+++ b/src/i18n/locales/fr.ts
@@ -442,6 +442,7 @@ export default {
Bookmarks: 'Favoris',
'Follow Packs': 'Follow Packs',
'Follow Pack': 'Follow Pack',
+ 'In Follow Packs': 'Dans les Follow Packs',
'Please log in to follow': 'Please log in to follow',
'Following All': 'Following All',
'Followed {{count}} users': 'Followed {{count}} users',
diff --git a/src/i18n/locales/pl.ts b/src/i18n/locales/pl.ts
index 7ce8be41..cb2c29bb 100644
--- a/src/i18n/locales/pl.ts
+++ b/src/i18n/locales/pl.ts
@@ -440,6 +440,7 @@ export default {
Bookmarks: 'Zakładki',
'Follow Packs': 'Follow Packs',
'Follow Pack': 'Follow Pack',
+ 'In Follow Packs': 'In Follow Packs',
'Please log in to follow': 'Please log in to follow',
'Following All': 'Following All',
'Followed {{count}} users': 'Followed {{count}} users',
diff --git a/src/lib/profile-relay-urls.ts b/src/lib/profile-relay-urls.ts
new file mode 100644
index 00000000..a42165f8
--- /dev/null
+++ b/src/lib/profile-relay-urls.ts
@@ -0,0 +1,29 @@
+/**
+ * Build relay URLs for profile-related fetches (zaps, likes, comments, badges, follow packs).
+ * Uses profile owner's outboxes + PROFILE_FETCH_RELAY_URLS.
+ */
+
+import { E_TAG_FILTER_BLOCKED_RELAY_URLS, PROFILE_FETCH_RELAY_URLS } from '@/constants'
+import client from '@/services/client.service'
+import { normalizeUrl } from '@/lib/url'
+
+export async function buildProfileRelayUrls(
+ pubkey: string,
+ blockedRelays: string[] = []
+): Promise {
+ const blocked = new Set(
+ [...blockedRelays, ...E_TAG_FILTER_BLOCKED_RELAY_URLS].map((u) => (normalizeUrl(u) || u).toLowerCase())
+ )
+ const addRelay = (url: string | undefined, out: Set) => {
+ if (!url) return
+ const n = normalizeUrl(url) || url
+ if (!n || blocked.has(n.toLowerCase())) return
+ out.add(n)
+ }
+
+ const relayUrlsSet = new Set()
+ const relayList = await client.fetchRelayList(pubkey).catch(() => ({ write: [] as string[], read: [] as string[] }))
+ ;(relayList?.write ?? []).filter((u): u is string => !!u).forEach((u) => addRelay(u, relayUrlsSet))
+ PROFILE_FETCH_RELAY_URLS.forEach((u) => addRelay(u, relayUrlsSet))
+ return Array.from(relayUrlsSet)
+}