Browse Source

get payment methods and badges displayed faster

imwald
Silberengel 4 weeks ago
parent
commit
9a197f9bf2
  1. 14
      src/components/Profile/ProfileBadges.tsx
  2. 8
      src/components/Profile/ProfileFeed.tsx
  3. 46
      src/components/Profile/index.tsx
  4. 135
      src/hooks/useProfileWall.tsx
  5. 107
      src/lib/nip58-profile-badges-list.ts
  6. 38
      src/lib/profile-author-replaceables-cache.ts

14
src/components/Profile/ProfileBadges.tsx

@ -5,13 +5,23 @@ import { useTranslation } from 'react-i18next' @@ -5,13 +5,23 @@ import { useTranslation } from 'react-i18next'
export default function ProfileBadges({
pubkey,
profileEventId
profileEventId,
onRefresh
}: {
pubkey: string
profileEventId?: string
/** Full author replaceables refresh (profile, payment, badges from relays). */
onRefresh?: () => void | Promise<void>
}) {
const { t } = useTranslation()
const { badges, isLoading, refresh } = useProfileWall(pubkey, profileEventId)
const handleRefresh = () => {
if (onRefresh) {
void onRefresh()
return
}
refresh()
}
if (isLoading && badges.length === 0) {
return (
@ -27,7 +37,7 @@ export default function ProfileBadges({ @@ -27,7 +37,7 @@ export default function ProfileBadges({
return (
<section className="mt-3 min-w-0" aria-label={t('Badges')}>
<div className="mb-1 flex items-center justify-end gap-2">
<RefreshButton onClick={refresh} onLongPress={null} />
<RefreshButton onClick={handleRefresh} onLongPress={null} />
</div>
<div className="flex flex-wrap gap-2">
{badges.map((badge) => (

8
src/components/Profile/ProfileFeed.tsx

@ -15,7 +15,10 @@ import { useTranslation } from 'react-i18next' @@ -15,7 +15,10 @@ import { useTranslation } from 'react-i18next'
const profileFeedKinds = [...PROFILE_FEED_KINDS]
const ProfileFeed = forwardRef<{ refresh: () => void }, { pubkey: string }>(({ pubkey }, ref) => {
const ProfileFeed = forwardRef<
{ refresh: () => void },
{ pubkey: string; /** Payment methods, badges, and other author replaceables. */ onRefreshExtras?: () => void }
>(({ pubkey, onRefreshExtras }, ref) => {
const { t } = useTranslation()
const { isEventDeleted } = useDeletedEvent()
const { showKinds, showKind1OPs, showKind1Replies, showKind1111, feedKindFilterBypass } =
@ -55,7 +58,8 @@ const ProfileFeed = forwardRef<{ refresh: () => void }, { pubkey: string }>(({ p @@ -55,7 +58,8 @@ const ProfileFeed = forwardRef<{ refresh: () => void }, { pubkey: string }>(({ p
refreshAuthorRelayLayers()
noteListRef.current?.refresh()
void client.fetchDeletionEventsForPubkey(pubkey)
}, [refreshPins, refreshAuthorRelayLayers, pubkey])
onRefreshExtras?.()
}, [refreshPins, refreshAuthorRelayLayers, pubkey, onRefreshExtras])
useImperativeHandle(ref, () => ({ refresh: refreshAll }), [refreshAll])

46
src/components/Profile/index.tsx

@ -12,6 +12,7 @@ import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' @@ -12,6 +12,7 @@ import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { Button } from '@/components/ui/button'
import { Skeleton } from '@/components/ui/skeleton'
import { useFetchProfile } from '@/hooks'
import { requestProfileWallRefresh } from '@/hooks/useProfileWall'
import { kinds, type NostrEvent } from 'nostr-tools'
import { createReactionDraftEvent } from '@/lib/draft-event'
import { getPaymentInfoFromEvent } from '@/lib/event-metadata'
@ -80,6 +81,7 @@ import { FAST_READ_RELAY_URLS, FAST_WRITE_RELAY_URLS } from '@/constants' @@ -80,6 +81,7 @@ import { FAST_READ_RELAY_URLS, FAST_WRITE_RELAY_URLS } from '@/constants'
import { nip66Service } from '@/services/nip66.service'
import PaymentMethodsSection from '@/components/PaymentMethodsSection'
import { buildRecipientZapPaymentData } from '@/hooks/useRecipientAlternativePayments'
import { loadAuthorReplaceablesFromLocalCache } from '@/lib/profile-author-replaceables-cache'
import ZapDialog from '@/components/ZapDialog'
import {
groupPaymentMethodsByDisplayType,
@ -184,14 +186,33 @@ export default function Profile({ @@ -184,14 +186,33 @@ export default function Profile({
setProfileEvent(undefined)
return
}
let cancelled = false
void loadAuthorReplaceablesFromLocalCache(profile.pubkey).then(({ paymentInfo: pi, profileEvent: pe }) => {
if (cancelled) return
setPaymentInfo(pi)
setProfileEvent(pe)
})
void syncAuthorReplaceablesFromCache(profile.pubkey)
return () => {
cancelled = true
}
}, [profile?.pubkey, syncAuthorReplaceablesFromCache])
const refreshAuthorReplaceables = useCallback(async (pubkey: string) => {
await client.forceRefreshProfileAndPaymentInfoCache(pubkey)
await syncAuthorReplaceablesFromCache(pubkey)
requestProfileWallRefresh(pubkey)
try {
await client.forceRefreshProfileAndPaymentInfoCache(pubkey)
await syncAuthorReplaceablesFromCache(pubkey, { bustCache: true })
} catch (error) {
logger.error('Failed to refresh author replaceables', { error, pubkey })
}
}, [syncAuthorReplaceablesFromCache])
const refreshAuthorExtrasForCurrentProfile = useCallback(() => {
const pk = profilePubkeyRef.current
if (pk) void refreshAuthorReplaceables(pk)
}, [refreshAuthorReplaceables])
useEffect(() => {
if (!profile?.pubkey || profile.batchPlaceholder) return
const pk = profile.pubkey
@ -216,7 +237,7 @@ export default function Profile({ @@ -216,7 +237,7 @@ export default function Profile({
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,
@ -294,18 +315,13 @@ export default function Profile({ @@ -294,18 +315,13 @@ export default function Profile({
if (typeof r === 'function') return
const m = r as MutableRefObject<{ refresh: () => void } | null>
m.current = {
refresh: () => {
internalFeedRef.current?.refresh()
const pk = profilePubkeyRef.current
if (pk) {
void refreshAuthorReplaceables(pk)
}
}
// ProfileFeed.refresh already runs onRefreshExtras (payment + badges).
refresh: () => internalFeedRef.current?.refresh()
}
return () => {
m.current = null
}
}, [refreshAuthorReplaceables])
}, [])
if (!profile && isFetching) {
return (
@ -596,11 +612,15 @@ export default function Profile({ @@ -596,11 +612,15 @@ export default function Profile({
<SmartRelays pubkey={pubkey} />
{isSelf && <SmartMuteLink />}
</div>
<ProfileBadges pubkey={pubkey} profileEventId={effectiveProfileEvent?.id} />
<ProfileBadges
pubkey={pubkey}
profileEventId={effectiveProfileEvent?.id}
onRefresh={refreshAuthorExtrasForCurrentProfile}
/>
</div>
</div>
</div>
<ProfileFeed ref={profileFeedRef} pubkey={pubkey} />
<ProfileFeed ref={profileFeedRef} pubkey={pubkey} onRefreshExtras={refreshAuthorExtrasForCurrentProfile} />
<ProfileReportsDialog
open={openReportsDialog}
onOpenChange={setOpenReportsDialog}

135
src/hooks/useProfileWall.tsx

@ -8,7 +8,8 @@ import { buildProfilePageReadRelayUrls } from '@/lib/favorites-feed-relays' @@ -8,7 +8,8 @@ import { buildProfilePageReadRelayUrls } from '@/lib/favorites-feed-relays'
import { getReplaceableCoordinate } from '@/lib/event'
import {
fetchLegacyProfileBadgesListEvent,
fetchProfileBadgesListEvent
fetchProfileBadgesListEvent,
hydrateProfileBadgesFromLocalCache
} from '@/lib/nip58-profile-badges-list'
import {
isNip58ProfileBadgesListEvent,
@ -23,6 +24,7 @@ import { normalizeAnyRelayUrl } from '@/lib/url' @@ -23,6 +24,7 @@ import { normalizeAnyRelayUrl } from '@/lib/url'
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import { useDeletedEvent } from '@/providers/DeletedEventProvider'
import client, { replaceableEventService } from '@/services/client.service'
import { ReplaceableEventService } from '@/services/client-replaceable-events.service'
import indexedDb from '@/services/indexed-db.service'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Event, kinds, type Filter } from 'nostr-tools'
@ -77,6 +79,22 @@ async function fetchBadgeDefinitionOnRelays( @@ -77,6 +79,22 @@ async function fetchBadgeDefinitionOnRelays(
const CACHE_DURATION = 5 * 60 * 1000
const wallCacheByKey = new Map<string, { badges: ResolvedProfileBadge[]; comments: Event[]; lastUpdated: number }>()
const wallRefreshListenersByPubkey = new Map<string, Set<() => void>>()
function normalizeWallRefreshPubkey(pubkey: string): string | null {
const pk = (userIdToPubkey(pubkey) || pubkey).trim().toLowerCase()
return /^[0-9a-f]{64}$/.test(pk) ? pk : null
}
/** Invalidate in-memory wall cache and schedule a badge re-fetch (avoids sync window events during React updates). */
export function requestProfileWallRefresh(pubkey: string): void {
const pk = normalizeWallRefreshPubkey(pubkey)
if (!pk) return
const listeners = wallRefreshListenersByPubkey.get(pk)
if (!listeners?.size) return
for (const listener of listeners) listener()
}
function relayListsContentKey(favoriteRelays: string[], blockedRelays: string[]): string {
const fav = [...favoriteRelays].map((u) => normalizeAnyRelayUrl(u) || u).filter(Boolean).sort().join('\u0001')
const blk = [...blockedRelays].map((u) => normalizeAnyRelayUrl(u) || u).filter(Boolean).sort().join('\u0001')
@ -97,6 +115,7 @@ export function useProfileWall(pubkey: string, profileEventId: string | undefine @@ -97,6 +115,7 @@ export function useProfileWall(pubkey: string, profileEventId: string | undefine
cached.badges.length > 0 &&
Date.now() - cached.lastUpdated < CACHE_DURATION
const pkNormForHydrate = useMemo(() => userIdToPubkey(pubkey) || pubkey, [pubkey])
const [badges, setBadges] = useState<ResolvedProfileBadge[]>(
hasUsefulWallCache ? cached!.badges : []
)
@ -115,6 +134,75 @@ export function useProfileWall(pubkey: string, profileEventId: string | undefine @@ -115,6 +134,75 @@ export function useProfileWall(pubkey: string, profileEventId: string | undefine
const useGlobalRelayBootstrapRef = useRef(useGlobalRelayBootstrap)
useGlobalRelayBootstrapRef.current = useGlobalRelayBootstrap
const runGenRef = useRef(0)
const manualRefreshBumpScheduledRef = useRef(false)
const relayListsKeyRef = useRef(relayListsKey)
const bumpWallRefetch = useCallback(() => {
wallCacheByKey.delete(cacheKey)
queueMicrotask(() => {
setIsLoading(true)
setRefreshToken((t) => t + 1)
})
}, [cacheKey])
const scheduleManualWallRefetch = useCallback(() => {
if (manualRefreshBumpScheduledRef.current) return
manualRefreshBumpScheduledRef.current = true
wallCacheByKey.delete(cacheKey)
queueMicrotask(() => {
manualRefreshBumpScheduledRef.current = false
setIsLoading(true)
setRefreshToken((t) => t + 1)
})
}, [cacheKey])
useEffect(() => {
if (!isValidPubkey(pkNormForHydrate)) return
let cancelled = false
void hydrateProfileBadgesFromLocalCache(pkNormForHydrate).then((local) => {
if (cancelled || local.length === 0) return
setBadges((prev) => (prev.length > 0 ? prev : local))
setIsLoading(false)
})
return () => {
cancelled = true
}
}, [pkNormForHydrate])
useEffect(() => {
const pk = normalizeWallRefreshPubkey(pkNormForHydrate)
if (!pk) return
const listeners = wallRefreshListenersByPubkey.get(pk) ?? new Set()
listeners.add(scheduleManualWallRefetch)
wallRefreshListenersByPubkey.set(pk, listeners)
const onAuthorReplaceablesRefreshed: EventListener = (domEvt) => {
const detailPk = (domEvt as CustomEvent<{ pubkey?: string }>).detail?.pubkey?.toLowerCase()
if (detailPk !== pk) return
bumpWallRefetch()
}
window.addEventListener(
ReplaceableEventService.AUTHOR_REPLACEABLES_REFRESHED_EVENT,
onAuthorReplaceablesRefreshed
)
return () => {
listeners.delete(scheduleManualWallRefetch)
if (listeners.size === 0) {
wallRefreshListenersByPubkey.delete(pk)
}
window.removeEventListener(
ReplaceableEventService.AUTHOR_REPLACEABLES_REFRESHED_EVENT,
onAuthorReplaceablesRefreshed
)
}
}, [pkNormForHydrate, scheduleManualWallRefetch, bumpWallRefetch])
useEffect(() => {
if (relayListsKeyRef.current === relayListsKey) return
relayListsKeyRef.current = relayListsKey
bumpWallRefetch()
}, [relayListsKey, bumpWallRefetch])
useEffect(() => {
let cancelled = false
@ -129,16 +217,17 @@ export function useProfileWall(pubkey: string, profileEventId: string | undefine @@ -129,16 +217,17 @@ export function useProfileWall(pubkey: string, profileEventId: string | undefine
Date.now() - mem.lastUpdated < CACHE_DURATION &&
refreshToken === 0
) {
setBadges(mem.badges)
setComments(mem.comments)
if (runGen === runGenRef.current) setIsLoading(false)
if (runGen === runGenRef.current) {
setBadges((prev) => (prev === mem.badges ? prev : mem.badges))
setComments((prev) => (prev === mem.comments ? prev : mem.comments))
setIsLoading((prev) => (prev ? false : prev))
}
return
}
if (mem?.badges.length === 0) {
wallCacheByKey.delete(cacheKey)
}
setIsLoading(true)
try {
const pkNorm = userIdToPubkey(pubkey) || pubkey
if (!isValidPubkey(pkNorm)) {
@ -164,10 +253,23 @@ export function useProfileWall(pubkey: string, profileEventId: string | undefine @@ -164,10 +253,23 @@ export function useProfileWall(pubkey: string, profileEventId: string | undefine
useGlobalRelayBootstrapRef.current
)
// --- Badges (NIP-58): IndexedDB + profile read relays (favorites / fast-read), not inbox-only ---
let listEvent = await fetchProfileBadgesListEvent(pkNorm, relayUrls, { foreground: true })
const localBadges = await hydrateProfileBadgesFromLocalCache(pkNorm)
if (!cancelled && localBadges.length > 0) {
setBadges(localBadges)
setIsLoading(false)
} else if (!cancelled) {
setIsLoading(true)
}
// --- Badges (NIP-58): show cache first; relay refresh may upgrade list/definitions ---
let listEvent = await fetchProfileBadgesListEvent(pkNorm, relayUrls, {
foreground: true,
cacheFirst: false
})
if (!listEvent || !isNip58ProfileBadgesListEvent(listEvent)) {
const legacy = await fetchLegacyProfileBadgesListEvent(pkNorm, relayUrls)
const legacy = await fetchLegacyProfileBadgesListEvent(pkNorm, relayUrls, {
cacheFirst: false
})
if (legacy && isNip58ProfileBadgesListEvent(legacy)) listEvent = legacy
}
@ -185,7 +287,13 @@ export function useProfileWall(pubkey: string, profileEventId: string | undefine @@ -185,7 +287,13 @@ export function useProfileWall(pubkey: string, profileEventId: string | undefine
resolveBadgeDisplayFromDefinition(entry, defByCoord.get(entry.definitionCoordinate))
)
// --- Wall comments (kind 1111 on profile kind 0) ---
if (cancelled) return
if (resolvedBadges.length > 0 || localBadges.length === 0) {
setBadges(resolvedBadges)
}
setIsLoading(false)
// --- Wall comments (kind 1111): after badges so payment UI is not blocked ---
let wallComments: Event[] = []
const profileId = profileEventId?.trim().toLowerCase()
if (profileId && /^[0-9a-f]{64}$/.test(profileId) && relayUrls.length > 0) {
@ -223,7 +331,6 @@ export function useProfileWall(pubkey: string, profileEventId: string | undefine @@ -223,7 +331,6 @@ export function useProfileWall(pubkey: string, profileEventId: string | undefine
}
if (cancelled) return
setBadges(resolvedBadges)
setComments(wallComments)
if (resolvedBadges.length > 0 || wallComments.length > 0) {
wallCacheByKey.set(cacheKey, {
@ -244,13 +351,11 @@ export function useProfileWall(pubkey: string, profileEventId: string | undefine @@ -244,13 +351,11 @@ export function useProfileWall(pubkey: string, profileEventId: string | undefine
return () => {
cancelled = true
}
}, [pubkey, profileEventId, cacheKey, refreshToken, relayListsKey])
}, [pubkey, profileEventId, cacheKey, refreshToken])
const refresh = useCallback(() => {
wallCacheByKey.delete(cacheKey)
setIsLoading(true)
setRefreshToken((t) => t + 1)
}, [cacheKey])
scheduleManualWallRefetch()
}, [scheduleManualWallRefetch])
return { badges, comments, isLoading, refresh }
}

107
src/lib/nip58-profile-badges-list.ts

@ -6,8 +6,11 @@ import { @@ -6,8 +6,11 @@ import {
import {
isNip58ProfileBadgesListEvent,
LEGACY_PROFILE_BADGES_D_TAG,
parseAddressableCoordinate,
parseProfileBadgeEntries,
type ProfileBadgeEntry
resolveBadgeDisplayFromDefinition,
type ProfileBadgeEntry,
type ResolvedProfileBadge
} from '@/lib/nip58-profile-badges'
import { normalizeHexPubkey } from '@/lib/pubkey'
import { fetchLatestReplaceableListEvent } from '@/lib/replaceable-list-latest'
@ -41,13 +44,8 @@ export function profileBadgeListTagsAfterRemovingEntry( @@ -41,13 +44,8 @@ export function profileBadgeListTagsAfterRemovingEntry(
return profileBadgeEntriesToTags(next)
}
export async function fetchProfileBadgesListEvent(
pubkeyHex: string,
relayUrls: string[],
options?: { foreground?: boolean }
): Promise<Event | undefined> {
async function loadProfileBadgesListFromLocalCache(pubkeyHex: string): Promise<Event | undefined> {
const pk = normalizeHexPubkey(pubkeyHex)
const foreground = options?.foreground === true
let cached: Event | undefined
try {
const disk = await indexedDb.getReplaceableEvent(pk, ExtendedKind.PROFILE_BADGES_LIST)
@ -55,6 +53,70 @@ export async function fetchProfileBadgesListEvent( @@ -55,6 +53,70 @@ export async function fetchProfileBadgesListEvent(
} catch {
cached = undefined
}
const sessionHits = client.eventService.listSessionEventsAuthoredBy(pk, {
kinds: [ExtendedKind.PROFILE_BADGES_LIST],
limit: 8
})
for (const ev of sessionHits) {
if (!isNip58ProfileBadgesListEvent(ev)) continue
if (!cached || ev.created_at >= cached.created_at) cached = ev
}
if (cached && isNip58ProfileBadgesListEvent(cached)) return cached
try {
const legacy =
(await indexedDb.getReplaceableEvent(pk, ExtendedKind.PROFILE_BADGES, LEGACY_PROFILE_BADGES_D_TAG)) ??
undefined
if (legacy && isNip58ProfileBadgesListEvent(legacy)) return legacy
} catch {
/* best-effort */
}
return undefined
}
async function loadBadgeDefinitionFromLocalCache(coordinate: string): Promise<Event | undefined> {
const parsed = parseAddressableCoordinate(coordinate)
if (!parsed || parsed.kind !== ExtendedKind.BADGE_DEFINITION) return undefined
try {
const disk = await indexedDb.getReplaceableEvent(parsed.pubkey, parsed.kind, parsed.d)
if (disk) return disk
} catch {
/* best-effort */
}
return undefined
}
/** Resolve NIP-58 badges from IndexedDB/session only (no relay REQ). */
export async function hydrateProfileBadgesFromLocalCache(
pubkeyHex: string
): Promise<ResolvedProfileBadge[]> {
let listEvent = await loadProfileBadgesListFromLocalCache(pubkeyHex)
if (!listEvent || !isNip58ProfileBadgesListEvent(listEvent)) return []
const entries = parseProfileBadgeEntries(listEvent)
const defCoords = [...new Set(entries.map((e) => e.definitionCoordinate))]
const defByCoord = new Map<string, Event | undefined>()
await Promise.all(
defCoords.map(async (coord) => {
defByCoord.set(coord, await loadBadgeDefinitionFromLocalCache(coord))
})
)
return entries.map((entry) =>
resolveBadgeDisplayFromDefinition(entry, defByCoord.get(entry.definitionCoordinate))
)
}
export async function fetchProfileBadgesListEvent(
pubkeyHex: string,
relayUrls: string[],
options?: { foreground?: boolean; /** When true and local cache exists, return cache immediately and skip relay wait. */ cacheFirst?: boolean }
): Promise<Event | undefined> {
const pk = normalizeHexPubkey(pubkeyHex)
const foreground = options?.foreground === true
const cacheFirst = options?.cacheFirst !== false
let cached = await loadProfileBadgesListFromLocalCache(pk)
if (cacheFirst && cached) {
return cached
}
try {
const fromService =
(await replaceableEventService.fetchReplaceableEvent(pk, ExtendedKind.PROFILE_BADGES_LIST)) ??
@ -77,20 +139,37 @@ export async function fetchProfileBadgesListEvent( @@ -77,20 +139,37 @@ export async function fetchProfileBadgesListEvent(
/** Deprecated NIP-58 profile badges (kind 30008, d=profile_badges). */
export async function fetchLegacyProfileBadgesListEvent(
pubkeyHex: string,
relayUrls: string[]
relayUrls: string[],
options?: { cacheFirst?: boolean }
): Promise<Event | undefined> {
const pk = normalizeHexPubkey(pubkeyHex)
const cacheFirst = options?.cacheFirst !== false
let cached: Event | undefined
try {
cached =
(await replaceableEventService.fetchReplaceableEvent(
if (cacheFirst) {
try {
const legacyDisk = await indexedDb.getReplaceableEvent(
pk,
ExtendedKind.PROFILE_BADGES,
LEGACY_PROFILE_BADGES_D_TAG
)) ?? undefined
} catch {
cached = undefined
)
if (legacyDisk && isNip58ProfileBadgesListEvent(legacyDisk)) cached = legacyDisk
} catch {
cached = undefined
}
}
if (!cached) {
try {
cached =
(await replaceableEventService.fetchReplaceableEvent(
pk,
ExtendedKind.PROFILE_BADGES,
LEGACY_PROFILE_BADGES_D_TAG
)) ?? undefined
} catch {
cached = undefined
}
}
if (cacheFirst && cached) return cached
const allUrls = [...new Set(relayUrls.map((u) => normalizeAnyRelayUrl(u) || u).filter(Boolean))]
if (!allUrls.length) return cached

38
src/lib/profile-author-replaceables-cache.ts

@ -0,0 +1,38 @@ @@ -0,0 +1,38 @@
import { ExtendedKind } from '@/constants'
import { getPaymentInfoFromEvent } from '@/lib/event-metadata'
import { shouldDropEventOnIngest } from '@/lib/event-ingest-filter'
import client from '@/services/client.service'
import indexedDb from '@/services/indexed-db.service'
import { kinds, type Event } from 'nostr-tools'
function pickNewestEvent(...candidates: (Event | undefined | null)[]): Event | undefined {
let best: Event | undefined
for (const e of candidates) {
if (!e || shouldDropEventOnIngest(e)) continue
if (!best || e.created_at >= best.created_at) best = e
}
return best
}
/** IndexedDB + session only — no relay round-trip (for instant profile payment UI). */
export async function loadAuthorReplaceablesFromLocalCache(pubkey: string): Promise<{
paymentInfo: ReturnType<typeof getPaymentInfoFromEvent> | null
profileEvent: Event | undefined
}> {
const pk = pubkey.trim().toLowerCase()
const [idbPayment, idbMeta] = await Promise.all([
indexedDb.getReplaceableEvent(pk, ExtendedKind.PAYMENT_INFO).catch(() => undefined),
indexedDb.getReplaceableEvent(pk, kinds.Metadata).catch(() => undefined)
])
const sesPayment = client.eventService.listSessionEventsAuthoredBy(pk, {
kinds: [ExtendedKind.PAYMENT_INFO],
limit: 8
})[0]
const sesMeta = client.eventService.getSessionMetadataForPubkey(pk)
const paymentEvent = pickNewestEvent(idbPayment, sesPayment)
const profileEvent = pickNewestEvent(idbMeta, sesMeta)
return {
paymentInfo: paymentEvent ? getPaymentInfoFromEvent(paymentEvent) : null,
profileEvent
}
}
Loading…
Cancel
Save