From e156cb534784f9135ac0bacd86aa7e706dc4bfb7 Mon Sep 17 00:00:00 2001 From: Silberengel Date: Wed, 20 May 2026 14:04:20 +0200 Subject: [PATCH] bug-fix --- src/components/Profile/index.tsx | 67 +++++++++++++------ src/hooks/useProfileAuthorFeedSubRequests.ts | 12 +++- src/providers/NostrProvider/index.tsx | 19 +++++- .../client-replaceable-events.service.ts | 8 +++ 4 files changed, 80 insertions(+), 26 deletions(-) diff --git a/src/components/Profile/index.tsx b/src/components/Profile/index.tsx index cd4747fa..59657454 100644 --- a/src/components/Profile/index.tsx +++ b/src/components/Profile/index.tsx @@ -125,7 +125,7 @@ export default function Profile({ const { profile, isFetching } = useFetchProfile(id) profilePubkeyRef.current = profile?.pubkey ?? null - const { pubkey: accountPubkey, publish, checkLogin } = useNostr() + const { pubkey: accountPubkey, profileEvent: accountProfileEvent, publish, checkLogin } = useNostr() const [paymentInfo, setPaymentInfo] = useState | null>(null) const [profileEvent, setProfileEvent] = useState(undefined) const [openZapDialog, setOpenZapDialog] = useState(false) @@ -140,9 +140,20 @@ export default function Profile({ const { relayUrls: currentBrowsingRelayUrls } = useCurrentRelays() const { relaySets, favoriteRelays } = useFavoriteRelays() + const isSelf = accountPubkey === profile?.pubkey + + const effectiveProfileEvent = useMemo(() => { + if (!isSelf || !accountProfileEvent) return profileEvent + if (!profileEvent) return accountProfileEvent + return accountProfileEvent.created_at >= profileEvent.created_at ? accountProfileEvent : profileEvent + }, [isSelf, profileEvent, accountProfileEvent]) + const mergedPaymentMethods = useMemo( - () => sortMergedPaymentMethods(mergePaymentMethods(paymentInfo, profile ?? null, profileEvent)), - [paymentInfo, profile, profileEvent] + () => + sortMergedPaymentMethods( + mergePaymentMethods(paymentInfo, profile ?? null, effectiveProfileEvent) + ), + [paymentInfo, profile, effectiveProfileEvent] ) const paymentMethodsByType = useMemo( @@ -151,30 +162,36 @@ export default function Profile({ ) const hasTipDialog = useMemo( - () => recipientHasAnyPaymentOptions(paymentInfo, profile ?? null, profileEvent), - [paymentInfo, profile, profileEvent] + () => recipientHasAnyPaymentOptions(paymentInfo, profile ?? null, effectiveProfileEvent), + [paymentInfo, profile, effectiveProfileEvent] ) const prefetchedZapPayment = useMemo( () => profile?.pubkey - ? buildRecipientZapPaymentData(paymentInfo, profile ?? null, profileEvent ?? null) + ? buildRecipientZapPaymentData(paymentInfo, profile ?? null, effectiveProfileEvent ?? null) : null, - [paymentInfo, profile, profileEvent] + [paymentInfo, profile, effectiveProfileEvent] ) - const syncAuthorReplaceablesFromCache = useCallback(async (pubkey: string) => { - try { - const [paymentEvent, metaEvent] = await Promise.all([ - client.fetchPaymentInfoEvent(pubkey), - replaceableEventService.fetchReplaceableEvent(pubkey, kinds.Metadata) - ]) - setPaymentInfo(paymentEvent ? getPaymentInfoFromEvent(paymentEvent) : null) - setProfileEvent(metaEvent ?? undefined) - } catch (error) { - logger.error('Failed to sync author replaceables from cache', { error, pubkey }) - } - }, []) + const syncAuthorReplaceablesFromCache = useCallback( + async (pubkey: string, options?: { bustCache?: boolean }) => { + try { + if (options?.bustCache) { + replaceableEventService.clearAuthorViewPaymentAndMetadataLoaders(pubkey) + } + const [paymentEvent, metaEvent] = await Promise.all([ + client.fetchPaymentInfoEvent(pubkey), + replaceableEventService.fetchReplaceableEvent(pubkey, kinds.Metadata) + ]) + setPaymentInfo(paymentEvent ? getPaymentInfoFromEvent(paymentEvent) : null) + setProfileEvent(metaEvent ?? undefined) + } catch (error) { + logger.error('Failed to sync author replaceables from cache', { error, pubkey }) + } + }, + [] + ) useEffect(() => { if (!profile?.pubkey) { @@ -195,13 +212,21 @@ export default function Profile({ void client.refreshAuthorPublishedReplaceablesOnProfileView(profile.pubkey) }, [profile?.pubkey]) + useEffect(() => { + if (!isSelf || !profile?.pubkey || !accountProfileEvent) return + setProfileEvent((prev) => + !prev || accountProfileEvent.created_at >= prev.created_at ? accountProfileEvent : prev + ) + void syncAuthorReplaceablesFromCache(profile.pubkey, { bustCache: true }) + }, [isSelf, accountProfileEvent, profile?.pubkey, syncAuthorReplaceablesFromCache]) + useEffect(() => { if (!profile?.pubkey) return const pk = profile.pubkey.toLowerCase() const onAuthorReplaceablesRefreshed: EventListener = (domEvt) => { const detailPk = (domEvt as CustomEvent<{ pubkey?: string }>).detail?.pubkey?.toLowerCase() if (detailPk !== pk) return - void syncAuthorReplaceablesFromCache(profile.pubkey) + void syncAuthorReplaceablesFromCache(profile.pubkey, { bustCache: true }) } window.addEventListener( ReplaceableEventService.AUTHOR_REPLACEABLES_REFRESHED_EVENT, @@ -222,8 +247,6 @@ export default function Profile({ () => (profile?.pubkey ? generateImageByPubkey(profile?.pubkey) : ''), [profile] ) - const isSelf = accountPubkey === profile?.pubkey - /** All available relays: current feed, favorites, relay sets, defaults (FAST_READ, FAST_WRITE). */ const allAvailableRelayUrls = useMemo(() => { const urls = [ diff --git a/src/hooks/useProfileAuthorFeedSubRequests.ts b/src/hooks/useProfileAuthorFeedSubRequests.ts index 38d4eafc..2645133e 100644 --- a/src/hooks/useProfileAuthorFeedSubRequests.ts +++ b/src/hooks/useProfileAuthorFeedSubRequests.ts @@ -8,7 +8,7 @@ import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' import { useNostrOptional } from '@/providers/nostr-context' import client from '@/services/client.service' import type { TFeedSubRequest } from '@/types' -import { useCallback, useEffect, useMemo, useState } from 'react' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' function relayListsContentKey(favoriteRelays: string[], blockedRelays: string[]): string { const fav = [...favoriteRelays].map((u) => normalizeAnyRelayUrl(u) || u).filter(Boolean).sort().join('\u0001') @@ -75,10 +75,17 @@ export function useProfileAuthorFeedSubRequests({ const [refreshToken, setRefreshToken] = useState(0) /** Single emission per visit: provisional→full relay stacks used to restart NoteList and wipe rows mid-fetch. */ const [relayUrls, setRelayUrls] = useState(null) + const relayUrlsPubkeyRef = useRef(null) + + useEffect(() => { + if (relayUrlsPubkeyRef.current !== pubkey) { + relayUrlsPubkeyRef.current = pubkey + setRelayUrls(null) + } + }, [pubkey]) useEffect(() => { let cancelled = false - setRelayUrls(null) const socialKinds = kinds.some(isSocialKindBlockedKind) void client @@ -115,6 +122,7 @@ export function useProfileAuthorFeedSubRequests({ }, [authorHex, kindsKey, limit]) const refresh = useCallback(() => { + setRelayUrls(null) setRefreshToken((n) => n + 1) }, []) diff --git a/src/providers/NostrProvider/index.tsx b/src/providers/NostrProvider/index.tsx index 64d52130..45cb6b5a 100644 --- a/src/providers/NostrProvider/index.tsx +++ b/src/providers/NostrProvider/index.tsx @@ -562,8 +562,11 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { return controller } const sortedEvents = events.sort((a, b) => b.created_at - a.created_at) - const profileEvent = sortedEvents.find((e) => e.kind === kinds.Metadata) - const followListEvent = sortedEvents.find((e) => e.kind === kinds.Contacts) + const profileEvent = sortedEvents.find((e) => e.kind === kinds.Metadata) + const paymentInfoEvent = sortedEvents + .filter((e) => e.kind === ExtendedKind.PAYMENT_INFO) + .sort((a, b) => b.created_at - a.created_at)[0] + const followListEvent = sortedEvents.find((e) => e.kind === kinds.Contacts) const muteListEvent = sortedEvents.find((e) => e.kind === kinds.Mutelist) const bookmarkListEvent = sortedEvents.find((e) => e.kind === kinds.BookmarkList) const interestListEvent = sortedEvents.find((e) => e.kind === INTEREST_LIST_KIND) @@ -585,6 +588,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { const [ resolvedProfilePut, + resolvedPaymentPut, resolvedFollowPut, resolvedMutePut, resolvedBookmarkPut, @@ -594,6 +598,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { resolvedUserEmojiPut ] = await Promise.all([ safePutReplaceable(profileEvent), + safePutReplaceable(paymentInfoEvent), safePutReplaceable(followListEvent), safePutReplaceable(muteListEvent), safePutReplaceable(bookmarkListEvent), @@ -626,6 +631,16 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { username: formatPubkey(account.pubkey) }) } + if (paymentInfoEvent) { + const resolvedPayment = resolvedPaymentPut ?? paymentInfoEvent + try { + await replaceableEventService.updateReplaceableEventCache(resolvedPayment) + } catch { + try { + await replaceableEventService.updateReplaceableEventCache(paymentInfoEvent) + } catch {} + } + } if (followListEvent) { if (resolvedFollowPut && resolvedFollowPut.id === followListEvent.id) { setFollowListEvent(followListEvent) diff --git a/src/services/client-replaceable-events.service.ts b/src/services/client-replaceable-events.service.ts index 6bd4ce4c..dbc826e1 100644 --- a/src/services/client-replaceable-events.service.ts +++ b/src/services/client-replaceable-events.service.ts @@ -1373,6 +1373,14 @@ export class ReplaceableEventService { return await this.fetchReplaceableEvent(pubkey, ExtendedKind.PAYMENT_INFO) } + /** Drop in-memory kind 0 / 10133 loaders so the next read picks up IndexedDB after a profile-view refresh. */ + clearAuthorViewPaymentAndMetadataLoaders(pubkey: string): void { + const pk = pubkey.trim().toLowerCase() + if (!/^[0-9a-f]{64}$/.test(pk)) return + this.replaceableEventFromBigRelaysDataloader.clear({ pubkey: pk, kind: kinds.Metadata }) + this.replaceableEventFromBigRelaysDataloader.clear({ pubkey: pk, kind: ExtendedKind.PAYMENT_INFO }) + } + /** * Force refresh profile and payment info: clear in-memory loaders, pull from relays (incl. 10133), persist to IndexedDB. */