diff --git a/src/components/NoteList/index.tsx b/src/components/NoteList/index.tsx index 44a0d75a..c39f43f6 100644 --- a/src/components/NoteList/index.tsx +++ b/src/components/NoteList/index.tsx @@ -798,7 +798,9 @@ const NoteList = forwardRef( */ alexandriaEmptyUrl = null, /** Notifications feed: show attest-superchat bar on incoming payment cards. */ - showPaymentAttestationAction = false + showPaymentAttestationAction = false, + /** Notifications feed: show unattested kind 9734 / 9735 / 9740 / 9736 / 1814 addressed to this pubkey. */ + incomingPaymentRecipientPubkey = null }: { subRequests: TFeedSubRequest[] showKinds: number[] @@ -861,6 +863,7 @@ const NoteList = forwardRef( /** Optional Alexandria `/events` URL when this feed’s timeline is empty (search / tag browse). */ alexandriaEmptyUrl?: string | null showPaymentAttestationAction?: boolean + incomingPaymentRecipientPubkey?: string | null }, ref ) => { @@ -1363,8 +1366,14 @@ const NoteList = forwardRef( // Filter out expired events if (shouldFilterEvent(evt)) return true - // Attested superchats only (9741), same as threads / profile walls. - if (!shouldIncludePaymentInFeed(evt, feedAttestedSuperchatIds)) { + // Attested superchats only (9741), except incoming payments in notifications. + if ( + !shouldIncludePaymentInFeed( + evt, + feedAttestedSuperchatIds, + incomingPaymentRecipientPubkey + ) + ) { return true } @@ -1395,6 +1404,7 @@ const NoteList = forwardRef( pinnedEventIds, isEventDeleted, feedAttestedSuperchatIds, + incomingPaymentRecipientPubkey, extraShouldHideEvent, homeFeedActiveSeenOnAllowlist, homeFeedListMode diff --git a/src/components/ReplyNoteList/index.tsx b/src/components/ReplyNoteList/index.tsx index 31af57c8..268820a4 100644 --- a/src/components/ReplyNoteList/index.tsx +++ b/src/components/ReplyNoteList/index.tsx @@ -1,15 +1,9 @@ import { ExtendedKind } from '@/constants' import { isDiscussionDownvoteEmoji, isDiscussionUpvoteEmoji } from '@/lib/discussion-votes' -import { - canonicalizeRssArticleUrl, - getArticleUrlFromCommentITags -} from '@/lib/rss-article' import { getParentETag, - getReplaceableCoordinateFromEvent, isMentioningMutedUsers, - isNip18RepostKind, - isReplaceableEvent + isNip18RepostKind } from '@/lib/event' import logger from '@/lib/logger' import { diff --git a/src/lib/superchat.test.ts b/src/lib/superchat.test.ts index caecd4b1..b18af9cd 100644 --- a/src/lib/superchat.test.ts +++ b/src/lib/superchat.test.ts @@ -9,6 +9,7 @@ import { getSuperchatPaytoType, getSuperchatReferenceFetchId, canUserAttestSuperchatPayment, + isIncomingNotificationsPaymentEvent, isProfileWallPaymentNotification, isProfileWallZapReceipt, isNestedThreadReplyParentKind, @@ -289,6 +290,96 @@ describe('shouldIncludePaymentInFeed', () => { expect(shouldIncludePaymentInFeed(zap, new Set())).toBe(false) expect(shouldIncludePaymentInFeed(note, attested)).toBe(true) }) + + it('includes unattested incoming payments for the notifications recipient only', () => { + const zap = fakeEvent({ + id: ZAP_ID, + kind: kinds.Zap, + tags: [ + ['P', SENDER], + ['p', RECIPIENT], + ['bolt11', 'lnbc210n1p0fake'], + [ + 'description', + JSON.stringify({ + pubkey: SENDER, + content: 'Zap!', + tags: [['p', RECIPIENT], ['amount', '21000']] + }) + ] + ] + }) + const payment = fakeEvent({ + id: PAYMENT_ID, + kind: ExtendedKind.PAYMENT_NOTIFICATION, + tags: [['p', RECIPIENT], ['amount', '100000']] + }) + const moneroDisclosure = fakeEvent({ + id: 'a'.repeat(64), + kind: ExtendedKind.MONERO_TIP_DISCLOSURE, + tags: [['p', RECIPIENT], ['amount', '0.01']] + }) + const moneroReceipt = fakeEvent({ + id: 'b'.repeat(64), + kind: ExtendedKind.MONERO_TIP_RECEIPT, + tags: [['p', SENDER], ['p', RECIPIENT]], + content: JSON.stringify({ txid: 'abc', message: 'tip' }) + }) + const zapRequest = fakeEvent({ + id: 'c'.repeat(64), + kind: ExtendedKind.ZAP_REQUEST, + pubkey: SENDER, + tags: [['p', RECIPIENT], ['amount', '21000']] + }) + const empty = new Set() + + expect(shouldIncludePaymentInFeed(zap, empty, RECIPIENT)).toBe(true) + expect(shouldIncludePaymentInFeed(payment, empty, RECIPIENT)).toBe(true) + expect(shouldIncludePaymentInFeed(moneroDisclosure, empty, RECIPIENT)).toBe(true) + expect(shouldIncludePaymentInFeed(moneroReceipt, empty, RECIPIENT)).toBe(true) + expect(shouldIncludePaymentInFeed(zapRequest, empty, RECIPIENT)).toBe(true) + expect(shouldIncludePaymentInFeed(zap, empty, SENDER)).toBe(false) + expect(shouldIncludePaymentInFeed(zap, empty)).toBe(false) + }) +}) + +describe('isIncomingNotificationsPaymentEvent', () => { + it('matches all payment kinds addressed to the recipient', () => { + expect( + isIncomingNotificationsPaymentEvent( + fakeEvent({ + kind: ExtendedKind.ZAP_REQUEST, + tags: [['p', RECIPIENT], ['amount', '21000']] + }), + RECIPIENT + ) + ).toBe(true) + expect( + isIncomingNotificationsPaymentEvent( + fakeEvent({ + kind: ExtendedKind.MONERO_TIP_DISCLOSURE, + tags: [['p', RECIPIENT], ['amount', '0.01']] + }), + RECIPIENT + ) + ).toBe(true) + expect( + isIncomingNotificationsPaymentEvent( + fakeEvent({ + kind: ExtendedKind.MONERO_TIP_RECEIPT, + tags: [['p', SENDER], ['p', RECIPIENT]], + content: '{}' + }), + RECIPIENT + ) + ).toBe(true) + expect( + isIncomingNotificationsPaymentEvent( + fakeEvent({ kind: kinds.ShortTextNote, tags: [['p', RECIPIENT]] }), + RECIPIENT + ) + ).toBe(false) + }) }) describe('getPaymentNotificationInfo', () => { diff --git a/src/lib/superchat.ts b/src/lib/superchat.ts index 28e97e60..292c277e 100644 --- a/src/lib/superchat.ts +++ b/src/lib/superchat.ts @@ -205,17 +205,46 @@ export function canUserAttestSuperchatPayment( if (!isAttestableSuperchatPayment(event)) return false const resolved = attestationRecipientPubkey ?? getSuperchatPaymentRecipientPubkey(event) if (resolved && hexPubkeysEqual(resolved, userPubkey)) return true - const pTag = firstTagValue(event.tags, ['p']) - return Boolean(pTag && hexPubkeysEqual(pTag, userPubkey)) + return event.tags.some((t) => t[0] === 'p' && t[1] && hexPubkeysEqual(t[1], userPubkey)) } -/** Incoming payment notification or zap receipt addressed to `userPubkey`. */ +/** Payment / tip kinds that may appear in the notifications feed (9734–9736, 1814, 9740). */ +export function isIncomingNotificationsPaymentKind(kind: number): boolean { + return ( + kind === ExtendedKind.ZAP_REQUEST || + kind === kinds.Zap || + kind === ExtendedKind.ZAP_RECEIPT || + kind === ExtendedKind.PAYMENT_NOTIFICATION || + isMoneroTipKind(kind) + ) +} + +/** + * Incoming payment or tip addressed to `userPubkey` — shown unattested in notifications only. + * Covers kind 9734 (zap request), 9735, 9740, 9736, and 1814. + */ +export function isIncomingNotificationsPaymentEvent( + event: Event, + userPubkey: string, + attestationRecipientPubkey?: string | null +): boolean { + if (!isIncomingNotificationsPaymentKind(event.kind)) return false + if (event.kind === ExtendedKind.ZAP_REQUEST) { + return event.tags.some((t) => t[0] === 'p' && t[1] && hexPubkeysEqual(t[1], userPubkey)) + } + if (isAttestableSuperchatPayment(event)) { + return canUserAttestSuperchatPayment(event, userPubkey, attestationRecipientPubkey) + } + return false +} + +/** @deprecated Use {@link isIncomingNotificationsPaymentEvent}. */ export function isIncomingPaymentNotificationOrZapReceipt( event: Event, userPubkey: string, attestationRecipientPubkey?: string | null ): boolean { - return canUserAttestSuperchatPayment(event, userPubkey, attestationRecipientPubkey) + return isIncomingNotificationsPaymentEvent(event, userPubkey, attestationRecipientPubkey) } /** Target `k` tag value for a kind 9741 attestation pointing at this event. */ @@ -324,13 +353,25 @@ export function buildGlobalAttestedSuperchatIdSet(attestations: Event[]): Set + attestedIds: ReadonlySet, + incomingPaymentRecipientPubkey?: string | null ): boolean { if (!isSuperchatKind(event.kind)) return true - return isAttestedSuperchat(event, attestedIds) + if (isAttestedSuperchat(event, attestedIds)) return true + if ( + incomingPaymentRecipientPubkey && + isIncomingNotificationsPaymentEvent(event, incomingPaymentRecipientPubkey) + ) { + return true + } + return false } export function replyFeedSuperchatsFirst(sortedNonSuperchatReplies: Event[], superchats: Event[]) { diff --git a/src/pages/primary/SpellsPage/index.tsx b/src/pages/primary/SpellsPage/index.tsx index 8ff686db..d37d76fd 100644 --- a/src/pages/primary/SpellsPage/index.tsx +++ b/src/pages/primary/SpellsPage/index.tsx @@ -1102,6 +1102,9 @@ const SpellsPage = forwardRef(function SpellsPage( : undefined } showPaymentAttestationAction={selectedFauxSpell === 'notifications'} + incomingPaymentRecipientPubkey={ + selectedFauxSpell === 'notifications' ? notificationsFeedPubkey : null + } /> diff --git a/src/pages/primary/SpellsPage/useSpellsPageFeed.ts b/src/pages/primary/SpellsPage/useSpellsPageFeed.ts index 7c1f6f2b..0089025c 100644 --- a/src/pages/primary/SpellsPage/useSpellsPageFeed.ts +++ b/src/pages/primary/SpellsPage/useSpellsPageFeed.ts @@ -17,6 +17,7 @@ import { parseThreadWatchListRefs, threadWatchMatchesRefs } from '@/lib/notification-thread-watch' +import { isIncomingNotificationsPaymentEvent } from '@/lib/superchat' import { decodeFollowSetSpellId, getFollowSetDTag, @@ -600,6 +601,8 @@ export function useSpellsPageFeed(a: UseSpellsPageFeedArgs) { return true } + if (isIncomingNotificationsPaymentEvent(evt, pk)) return false + if (isUserInEventMentions(evt, pk)) return false if (