From a0b9217d1bff034102fe769a49a00b32af6d36a9 Mon Sep 17 00:00:00 2001 From: Silberengel Date: Sat, 23 May 2026 14:25:55 +0200 Subject: [PATCH] show superchats on header --- src/components/Note/ReactionEmojiDisplay.tsx | 52 ++++++---- src/components/Note/Superchat.tsx | 60 ++++++++---- src/components/Note/Zap.tsx | 63 ++++++++---- src/components/Note/index.tsx | 8 +- src/components/NoteList/index.tsx | 15 +++ src/components/Profile/ProfileBadges.tsx | 11 ++- .../Profile/ProfileWallSuperchats.tsx | 4 +- src/components/ReplyNote/index.tsx | 4 +- src/hooks/useProfileWall.tsx | 67 ++++++++++++- src/lib/reaction-display.test.ts | 96 +++++++++++++++++++ src/lib/reaction-display.ts | 41 +++++++- src/lib/superchat.test.ts | 32 +++++++ src/lib/superchat.ts | 13 ++- 13 files changed, 384 insertions(+), 82 deletions(-) create mode 100644 src/lib/reaction-display.test.ts diff --git a/src/components/Note/ReactionEmojiDisplay.tsx b/src/components/Note/ReactionEmojiDisplay.tsx index f5d0f549..b668ebeb 100644 --- a/src/components/Note/ReactionEmojiDisplay.tsx +++ b/src/components/Note/ReactionEmojiDisplay.tsx @@ -1,11 +1,17 @@ import Emoji from '@/components/Emoji' import { ExtendedKind } from '@/constants' -import { fetchAuthorNip30EmojiInfos } from '@/lib/nip30-author-emojis' -import { resolveReactionEmojiSync } from '@/lib/reaction-display' +import { + EMPTY_AUTHOR_NIP30_EMOJIS, + fetchAuthorNip30EmojiInfos, + fetchAuthorNip30EmojiInfosFromIndexedDb, + getAuthorNip30EmojiCache, + subscribeAuthorNip30EmojiCache +} from '@/lib/nip30-author-emojis' +import { resolveAuthorEmojiForReactionShortcode, resolveReactionEmojiSync } from '@/lib/reaction-display' import { cn } from '@/lib/utils' import { TEmoji } from '@/types' import { Event, kinds } from 'nostr-tools' -import { useEffect, useMemo, useState } from 'react' +import { useEffect, useMemo, useSyncExternalStore } from 'react' /** * Renders a reaction glyph (Unicode, standard :shortcode:, or NIP-30 custom image from reactor profile). @@ -28,28 +34,32 @@ export default function ReactionEmojiDisplay({ [event, maxRawLength] ) - const initial: TEmoji | string = - sync.mode === 'display' ? sync.value : sync.placeholder + const reactorPubkey = event.pubkey?.trim().toLowerCase() ?? '' + const needsAuthorLookup = sync.mode === 'profile' - const [value, setValue] = useState(initial) + const authorEmojis = useSyncExternalStore( + (onStoreChange) => + needsAuthorLookup && /^[0-9a-f]{64}$/.test(reactorPubkey) + ? subscribeAuthorNip30EmojiCache(reactorPubkey, onStoreChange) + : () => {}, + () => + needsAuthorLookup && /^[0-9a-f]{64}$/.test(reactorPubkey) + ? getAuthorNip30EmojiCache(reactorPubkey) + : EMPTY_AUTHOR_NIP30_EMOJIS, + () => EMPTY_AUTHOR_NIP30_EMOJIS + ) useEffect(() => { - setValue(initial) - }, [initial, event.id]) + if (!needsAuthorLookup || !/^[0-9a-f]{64}$/.test(reactorPubkey)) return + void fetchAuthorNip30EmojiInfosFromIndexedDb(reactorPubkey) + void fetchAuthorNip30EmojiInfos(reactorPubkey) + }, [needsAuthorLookup, reactorPubkey]) - useEffect(() => { - if (sync.mode !== 'profile' || (event.kind !== kinds.Reaction && event.kind !== ExtendedKind.EXTERNAL_REACTION)) - return - let cancelled = false - void fetchAuthorNip30EmojiInfos(event.pubkey).then((infos) => { - if (cancelled) return - const hit = infos.find((i) => i.shortcode === sync.shortcode) - if (hit) setValue(hit) - }) - return () => { - cancelled = true - } - }, [event.pubkey, event.kind, sync]) + const value: TEmoji | string = useMemo(() => { + if (sync.mode === 'display') return sync.value + const hit = resolveAuthorEmojiForReactionShortcode(authorEmojis, sync.shortcode) + return hit ?? sync.placeholder + }, [sync, authorEmojis]) if ( (event.kind !== kinds.Reaction && event.kind !== ExtendedKind.EXTERNAL_REACTION) || diff --git a/src/components/Note/Superchat.tsx b/src/components/Note/Superchat.tsx index e49586e8..ea2f0c9e 100644 --- a/src/components/Note/Superchat.tsx +++ b/src/components/Note/Superchat.tsx @@ -13,16 +13,22 @@ import Username from '../Username' import SuperchatPaymentMethodLabel from './SuperchatPaymentMethodLabel' import SuperchatCommentMarkdown from './SuperchatCommentMarkdown' import TurnIntoSuperchatButton from '../TurnIntoSuperchatButton' +import UserAvatar from '../UserAvatar' + +export type SuperchatLayoutVariant = 'notification' | 'profileWall' | 'thread' export default function Superchat({ event, className, - showAttestationAction = false + showAttestationAction = false, + variant = 'thread' }: { event: Event className?: string /** Notifications feed only — attest incoming payments. */ showAttestationAction?: boolean + /** `notification`: recipient + view links; `profileWall`: sender row; `thread`: body only. */ + variant?: SuperchatLayoutVariant }) { const { t } = useTranslation() const info = useMemo(() => getPaymentNotificationInfo(event), [event]) @@ -56,9 +62,12 @@ export default function Superchat({ const { senderPubkey, recipientPubkey, comment } = info const hasThreadTarget = Boolean(targetEvent || referencedFetchId) - const hasTarget = hasThreadTarget || Boolean(recipientPubkey) + const isNotification = variant === 'notification' + const isProfileWall = variant === 'profileWall' + const hasTarget = isNotification && (hasThreadTarget || Boolean(recipientPubkey)) const hasMetaLine = - (recipientPubkey && recipientPubkey !== senderPubkey) || hasTarget + isProfileWall || + (isNotification && ((recipientPubkey && recipientPubkey !== senderPubkey) || hasTarget)) const openTarget = (e: MouseEvent) => { e.stopPropagation() @@ -73,24 +82,37 @@ export default function Superchat({
{hasMetaLine ? (
- {recipientPubkey && recipientPubkey !== senderPubkey ? ( - - {t('to')}{' '} + {isProfileWall ? ( +
+ - - ) : null} - {hasTarget ? ( - - ) : null} +
+ ) : ( + <> + {recipientPubkey && recipientPubkey !== senderPubkey ? ( + + {t('to')}{' '} + + + ) : null} + {hasTarget ? ( + + ) : null} + + )}
) : null}
getZapInfoFromEvent(event), [event]) @@ -77,34 +81,51 @@ export default function Zap({ } } + const isNotification = variant === 'notification' + const isProfileWall = variant === 'profileWall' const hasMetaLine = - (recipientPubkey && recipientPubkey !== senderPubkey) || isEventZap || isProfileZap + isProfileWall || + (isNotification && + ((recipientPubkey && recipientPubkey !== senderPubkey) || isEventZap || isProfileZap)) return (
{hasMetaLine ? (
- {recipientPubkey && recipientPubkey !== senderPubkey && ( - - {t('zapped')}{' '} + {isProfileWall ? ( +
+ - - )} - {(isEventZap || isProfileZap) && ( - +
+ ) : ( + <> + {recipientPubkey && recipientPubkey !== senderPubkey && ( + + {t('zapped')}{' '} + + + )} + {(isNotification && (isEventZap || isProfileZap)) && ( + + )} + )}
) : null} diff --git a/src/components/Note/index.tsx b/src/components/Note/index.tsx index 3f074608..a6d3dbe0 100644 --- a/src/components/Note/index.tsx +++ b/src/components/Note/index.tsx @@ -574,7 +574,12 @@ export default function Note({ content = renderEventContent({ hideMetadata: true }) } else if (event.kind === ExtendedKind.ZAP_REQUEST || event.kind === ExtendedKind.ZAP_RECEIPT) { content = ( - + ) } else if (event.kind === ExtendedKind.PAYMENT_NOTIFICATION) { content = ( @@ -582,6 +587,7 @@ export default function Note({ className="mt-2" event={displayEvent} showAttestationAction={showPaymentAttestationAction} + variant={showPaymentAttestationAction ? 'notification' : 'thread'} /> ) } else if (event.kind === ExtendedKind.FOLLOW_PACK) { diff --git a/src/components/NoteList/index.tsx b/src/components/NoteList/index.tsx index bf506a60..08b3c8d8 100644 --- a/src/components/NoteList/index.tsx +++ b/src/components/NoteList/index.tsx @@ -17,6 +17,8 @@ import { isReplyNoteEvent, normalizeReplaceableCoordinateString } from '@/lib/event' +import { collectReactionAuthorPubkeysForEmojiPrefetch } from '@/lib/reaction-display' +import { prefetchAuthorNip30EmojisForPubkeys } from '@/lib/nip30-author-emojis' import { shouldFilterEvent } from '@/lib/event-filtering' import { isRelayUrlStrictSupersetIdentityKey, @@ -1112,11 +1114,17 @@ const NoteList = forwardRef( /** Pending pubkeys sync with rows so useFetchProfile skips per-note fetches before the debounced batch. */ useLayoutEffect(() => { const candidates = new Set() + const emojiAuthors = new Set() for (const e of timelineEventsForFilter) { collectProfilePrefetchPubkeysFromEvent(e, candidates) + collectReactionAuthorPubkeysForEmojiPrefetch([e], emojiAuthors) } for (const e of newEvents) { collectProfilePrefetchPubkeysFromEvent(e, candidates) + collectReactionAuthorPubkeysForEmojiPrefetch([e], emojiAuthors) + } + if (emojiAuthors.size > 0) { + prefetchAuthorNip30EmojisForPubkeys([...emojiAuthors]) } const pubkeysKey = [...candidates].sort().join('\n') if (pubkeysKey === lastProfilePrefetchPubkeysKeyRef.current) return @@ -1712,16 +1720,23 @@ const NoteList = forwardRef( useEffect(() => { const handle = window.setTimeout(() => { const candidates = new Set() + const emojiAuthors = new Set() for (const e of timelineEventsForFilter) { collectProfilePrefetchPubkeysFromEvent(e, candidates) + collectReactionAuthorPubkeysForEmojiPrefetch([e], emojiAuthors) } for (const e of newEvents) { collectProfilePrefetchPubkeysFromEvent(e, candidates) + collectReactionAuthorPubkeysForEmojiPrefetch([e], emojiAuthors) } for (const e of clientFilteredEvents.slice(0, Math.min(120, Math.max(showCount + 64, 64)))) { collectProfilePrefetchPubkeysFromNoteStats(noteStatsService.getNoteStats(e.id), candidates) } + if (emojiAuthors.size > 0) { + prefetchAuthorNip30EmojisForPubkeys([...emojiAuthors]) + } + const need = [...candidates].filter((pk) => !feedProfileLoadedRef.current.has(pk)) enqueueFeedProfilePubkeys(need) }, FEED_PROFILE_BATCH_DEBOUNCE_MS) diff --git a/src/components/Profile/ProfileBadges.tsx b/src/components/Profile/ProfileBadges.tsx index 2241329f..49e1217c 100644 --- a/src/components/Profile/ProfileBadges.tsx +++ b/src/components/Profile/ProfileBadges.tsx @@ -25,14 +25,17 @@ export default function ProfileBadges({ if (isLoading && badges.length === 0 && superchats.length === 0) { return ( -
- - +
+
+ + +
+
) } - if (badges.length === 0 && superchats.length === 0) return null + if (badges.length === 0 && superchats.length === 0 && !isLoading) return null return (
diff --git a/src/components/Profile/ProfileWallSuperchats.tsx b/src/components/Profile/ProfileWallSuperchats.tsx index 9c0ed80a..778a5a15 100644 --- a/src/components/Profile/ProfileWallSuperchats.tsx +++ b/src/components/Profile/ProfileWallSuperchats.tsx @@ -32,9 +32,9 @@ export default function ProfileWallSuperchats({
{superchats.map((event) => event.kind === ExtendedKind.PAYMENT_NOTIFICATION ? ( - + ) : ( - + ) )}
diff --git a/src/components/ReplyNote/index.tsx b/src/components/ReplyNote/index.tsx index 3ed80c6d..e7dda355 100644 --- a/src/components/ReplyNote/index.tsx +++ b/src/components/ReplyNote/index.tsx @@ -204,9 +204,9 @@ export default function ReplyNote({ )}
) : event.kind === kinds.Zap ? ( - + ) : event.kind === ExtendedKind.PAYMENT_NOTIFICATION ? ( - + ) : isNip18RepostKind(event.kind) ? null : ( , + relayUrls: string[] +): Promise { + const ids = [...attestedIds].filter((id) => /^[0-9a-f]{64}$/i.test(id)) + if (ids.length === 0) return [] + + const byId = new Map() + try { + const local = await client.getLocalFeedEvents( + [{ urls: [], filter: { ids, limit: ids.length } }], + { maxMatches: ids.length } + ) + for (const e of local) byId.set(e.id.toLowerCase(), e) + } catch { + /* optional */ + } + + for (const id of ids) { + const key = id.toLowerCase() + if (byId.has(key)) continue + try { + const fromPublication = await indexedDb.getEventFromPublicationStore(id) + if (fromPublication) byId.set(fromPublication.id.toLowerCase(), fromPublication) + } catch { + /* optional */ + } + } + + const missing = ids.filter((id) => !byId.has(id.toLowerCase())) + if (missing.length > 0 && relayUrls.length > 0) { + try { + const fetched = await client.fetchEvents( + relayUrls, + { ids: missing, limit: missing.length }, + { cache: true, eoseTimeout: 4500, globalTimeout: 12_000, foreground: true } + ) + for (const e of fetched) byId.set(e.id.toLowerCase(), e) + } catch { + /* optional */ + } + } + + return [...byId.values()] +} + const wallCacheByKey = new Map< string, { badges: ResolvedProfileBadge[]; comments: Event[]; superchats: Event[]; lastUpdated: number } @@ -349,6 +397,12 @@ export function useProfileWall(pubkey: string, profileEventId: string | undefine ) } const pool = new Map() + try { + const idbPayments = await indexedDb.getPaymentNotificationsForRecipient(pkNorm, 200) + for (const e of idbPayments) pool.set(e.id, e) + } catch { + /* optional */ + } try { const localMatches = await client.getLocalFeedEvents( filters.map((filter) => ({ urls: [], filter: filter as TSubRequestFilter })), @@ -376,6 +430,13 @@ export function useProfileWall(pubkey: string, profileEventId: string | undefine /* ignore */ } + const attestations = [...pool.values()].filter( + (e) => e.kind === ExtendedKind.PAYMENT_ATTESTATION + ) + const attestedIds = await resolveAttestedPaymentIdSet(pkNorm, attestations) + const hydratedTargets = await hydrateProfileWallSuperchatTargets(attestedIds, relayUrls) + for (const e of hydratedTargets) pool.set(e.id, e) + if (profileId) { wallComments = [...pool.values()] .filter( @@ -394,14 +455,12 @@ export function useProfileWall(pubkey: string, profileEventId: string | undefine e.kind === ExtendedKind.ZAP_RECEIPT) && !isEventDeletedRef.current(e) ) - const attestations = [...pool.values()].filter( - (e) => e.kind === ExtendedKind.PAYMENT_ATTESTATION - ) wallSuperchats = filterAttestedProfileWallSuperchats( paymentEvents, attestations, pkNorm, - profileId + profileId, + attestedIds ) } diff --git a/src/lib/reaction-display.test.ts b/src/lib/reaction-display.test.ts new file mode 100644 index 00000000..1744f749 --- /dev/null +++ b/src/lib/reaction-display.test.ts @@ -0,0 +1,96 @@ +import { describe, expect, it } from 'vitest' +import { kinds } from 'nostr-tools' +import { + collectReactionAuthorPubkeysForEmojiPrefetch, + reactionNeedsAuthorEmojiLookup, + resolveAuthorEmojiForReactionShortcode, + resolveReactionEmojiSync +} from './reaction-display' + +function reactionEvent( + content: string, + tags: string[][] = [], + pubkey = 'aa'.repeat(32) +) { + return { + kind: kinds.Reaction, + id: 'bb'.repeat(32), + pubkey, + content, + tags, + created_at: 1, + sig: 'cc'.repeat(32) + } +} + +describe('resolveReactionEmojiSync', () => { + it('uses emoji tag when content is a custom shortcode', () => { + const event = reactionEvent(':jumble:', [ + ['emoji', 'jumble', 'https://cdn.example/jumble.png'] + ]) + const result = resolveReactionEmojiSync(event, 64) + expect(result).toEqual({ + mode: 'display', + value: { shortcode: 'jumble', url: 'https://cdn.example/jumble.png' } + }) + }) + + it('matches emoji tags case-insensitively', () => { + const event = reactionEvent(':Jumble:', [ + ['emoji', 'jumble', 'https://cdn.example/jumble.png'] + ]) + const result = resolveReactionEmojiSync(event, 64) + expect(result.mode).toBe('display') + if (result.mode === 'display' && typeof result.value === 'object') { + expect(result.value.url).toBe('https://cdn.example/jumble.png') + } + }) + + it('defers unknown custom shortcodes to author lookup', () => { + const event = reactionEvent(':unknown_custom:', []) + expect(reactionNeedsAuthorEmojiLookup(event)).toBe(true) + expect(resolveReactionEmojiSync(event, 64)).toEqual({ + mode: 'profile', + shortcode: 'unknown_custom', + placeholder: ':unknown_custom:' + }) + }) + + it('resolves URL content from emoji tag', () => { + const url = 'https://cdn.example/emoji.png' + const event = reactionEvent(url, [['emoji', 'pic', url]]) + const result = resolveReactionEmojiSync(event, 64) + expect(result).toEqual({ + mode: 'display', + value: { shortcode: 'pic', url } + }) + }) +}) + +describe('collectReactionAuthorPubkeysForEmojiPrefetch', () => { + it('collects reactor pubkeys for profile-lookup reactions', () => { + const pk = 'dd'.repeat(32) + const event = reactionEvent(':custom:', [], pk) + const set = new Set() + collectReactionAuthorPubkeysForEmojiPrefetch([event], set) + expect(set.has(pk)).toBe(true) + }) + + it('skips reactions with inline emoji tags', () => { + const pk = 'ee'.repeat(32) + const event = reactionEvent(':custom:', [['emoji', 'custom', 'https://x/y.png']], pk) + const set = new Set() + collectReactionAuthorPubkeysForEmojiPrefetch([event], set) + expect(set.size).toBe(0) + }) +}) + +describe('resolveAuthorEmojiForReactionShortcode', () => { + it('finds shortcodes case-insensitively', () => { + const hit = resolveAuthorEmojiForReactionShortcode( + [{ shortcode: 'Firefly', url: 'https://cdn.example/f.png' }], + 'firefly' + ) + expect(hit?.url).toBe('https://cdn.example/f.png') + }) +}) diff --git a/src/lib/reaction-display.ts b/src/lib/reaction-display.ts index 1e85dab1..6a100804 100644 --- a/src/lib/reaction-display.ts +++ b/src/lib/reaction-display.ts @@ -12,6 +12,28 @@ export type TReactionEmojiSync = | { mode: 'display'; value: TEmoji | string } | { mode: 'profile'; shortcode: string; placeholder: string } +function findEmojiByShortcode(infos: readonly TEmoji[], shortcode: string): TEmoji | undefined { + const lower = shortcode.toLowerCase() + return infos.find((e) => e.shortcode === shortcode || e.shortcode.toLowerCase() === lower) +} + +/** True when the reaction glyph must be resolved from the reactor’s NIP-30 inventory. */ +export function reactionNeedsAuthorEmojiLookup(event: Event): boolean { + return resolveReactionEmojiSync(event, 64).mode === 'profile' +} + +/** Collect reactor pubkeys whose custom reaction emoji should be prefetched for feed/notification rows. */ +export function collectReactionAuthorPubkeysForEmojiPrefetch( + events: readonly Event[], + candidates: Set +): void { + for (const e of events) { + if (!reactionNeedsAuthorEmojiLookup(e)) continue + const pk = e.pubkey?.trim().toLowerCase() + if (pk && /^[0-9a-f]{64}$/.test(pk)) candidates.add(pk) + } +} + /** * Resolve reaction display without network: emoji tags on the reaction, standard :shortcode: → Unicode, * or defer to profile (reactor kind 0) for custom shortcodes. @@ -32,10 +54,19 @@ export function resolveReactionEmojiSync(event: Event, maxRawLength: number): TR const fromReactionTags = getEmojiInfosFromEmojiTags(event.tags) const customShortcodes = fromReactionTags.map((e) => e.shortcode) + if (/^https?:\/\//i.test(raw)) { + const hit = fromReactionTags.find((e) => e.url === raw) + if (hit) return { mode: 'display', value: hit } + } + + if (fromReactionTags.length === 1 && raw === fromReactionTags[0].shortcode) { + return { mode: 'display', value: fromReactionTags[0] } + } + const whole = raw.match(WHOLE_SHORTCODE) if (whole) { const shortcode = whole[1] - const hit = fromReactionTags.find((e) => e.shortcode === shortcode) + const hit = findEmojiByShortcode(fromReactionTags, shortcode) if (hit) { return { mode: 'display', value: hit } } @@ -52,3 +83,11 @@ export function resolveReactionEmojiSync(event: Event, maxRawLength: number): TR return { mode: 'display', value: raw } } + +/** Match a custom shortcode from a loaded author NIP-30 inventory. */ +export function resolveAuthorEmojiForReactionShortcode( + infos: readonly TEmoji[], + shortcode: string +): TEmoji | undefined { + return findEmojiByShortcode(infos, shortcode) +} diff --git a/src/lib/superchat.test.ts b/src/lib/superchat.test.ts index ef20e08c..377bc36f 100644 --- a/src/lib/superchat.test.ts +++ b/src/lib/superchat.test.ts @@ -63,6 +63,15 @@ describe('buildAttestedPaymentIdSet', () => { expect(ids.has(PAYMENT_ID)).toBe(true) expect(ids.size).toBe(2) }) + + it('collects attested ids without a k tag', () => { + const attestation = fakeEvent({ + kind: ExtendedKind.PAYMENT_ATTESTATION, + pubkey: RECIPIENT, + tags: [['e', PAYMENT_ID]] + }) + expect(buildAttestedPaymentIdSet([attestation], RECIPIENT).has(PAYMENT_ID)).toBe(true) + }) }) describe('partitionAttestedSuperchats', () => { @@ -233,6 +242,29 @@ describe('profile wall payment notifications', () => { expect(getPaymentNotificationInfo(evt)?.amountSats).toBe(50) }) + it('rejects 9740 with a thread reference on the profile wall', () => { + const evt = fakeEvent({ + id: PAYMENT_ID, + kind: ExtendedKind.PAYMENT_NOTIFICATION, + tags: [ + ['p', RECIPIENT], + ['e', 'f'.repeat(64)], + ['amount', '50000'] + ] + }) + const attestation = fakeEvent({ + kind: ExtendedKind.PAYMENT_ATTESTATION, + pubkey: RECIPIENT, + tags: [ + ['e', PAYMENT_ID], + ['k', '9740'] + ] + }) + expect(isProfileWallPaymentNotification(evt, RECIPIENT)).toBe(false) + const out = filterAttestedProfileWallSuperchats([evt], [attestation], RECIPIENT) + expect(out).toHaveLength(0) + }) + it('filters to attested profile wall superchats', () => { const paymentId = PAYMENT_ID const payment = fakeEvent({ diff --git a/src/lib/superchat.ts b/src/lib/superchat.ts index a207445a..646787a3 100644 --- a/src/lib/superchat.ts +++ b/src/lib/superchat.ts @@ -50,13 +50,11 @@ export function buildAttestedPaymentIdSet( attestations: Event[], recipientPubkey: string ): Set { - const recipient = recipientPubkey.trim().toLowerCase() const out = new Set() for (const attestation of attestations) { - if (attestation.pubkey.toLowerCase() !== recipient) continue + if (!hexPubkeysEqual(attestation.pubkey, recipientPubkey)) continue const targetId = getPaymentAttestationTargetId(attestation) - const targetKind = getPaymentAttestationTargetKind(attestation) - if (!targetId || !targetKind) continue + if (!targetId) continue out.add(targetId) } return out @@ -169,7 +167,7 @@ export function isIncomingPaymentNotificationOrZapReceipt( return recipient != null && hexPubkeysEqual(recipient, userPubkey) } -export function isAttestedSuperchat(event: Event, attestedIds: Set): boolean { +export function isAttestedSuperchat(event: Event, attestedIds: ReadonlySet): boolean { if (!isSuperchatKind(event.kind)) return false return attestedIds.has(event.id.toLowerCase()) } @@ -277,9 +275,10 @@ export function filterAttestedProfileWallSuperchats( paymentEvents: Event[], attestations: Event[], profilePubkey: string, - profileEventId?: string + profileEventId?: string, + attestedIdsOverride?: ReadonlySet ): Event[] { - const attestedIds = buildAttestedPaymentIdSet(attestations, profilePubkey) + const attestedIds = attestedIdsOverride ?? buildAttestedPaymentIdSet(attestations, profilePubkey) return sortSuperchatsByAmountDesc( paymentEvents.filter((e) => { if (e.kind === ExtendedKind.PAYMENT_NOTIFICATION) {