Browse Source

more bug-fixes

imwald
Silberengel 3 weeks ago
parent
commit
dc33d85cfd
  1. 20
      src/components/Note/Superchat.tsx
  2. 24
      src/components/Note/Zap.tsx
  3. 2
      src/components/ReplyNote/index.tsx
  4. 56
      src/hooks/usePaymentAttestationStatus.test.ts
  5. 59
      src/hooks/usePaymentAttestationStatus.tsx
  6. 8
      src/lib/event-metadata.ts
  7. 6
      src/lib/superchat.ts

20
src/components/Note/Superchat.tsx

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
import { useFetchEvent } from '@/hooks'
import { usePaymentAttestationStatus } from '@/hooks/usePaymentAttestationStatus'
import { openNoteFromFetchOrCache } from '@/lib/navigation-related-events'
import { parsePaytoTagType } from '@/lib/payto'
import { relayHintsFromEventTags } from '@/lib/relay-list-builder'
@ -20,17 +21,17 @@ export type SuperchatLayoutVariant = 'notification' | 'profileWall' | 'thread' @@ -20,17 +21,17 @@ export type SuperchatLayoutVariant = 'notification' | 'profileWall' | 'thread'
export default function Superchat({
event,
className,
showAttestationAction = false,
variant = 'thread'
}: {
event: Event
className?: string
/** Notifications feed only — attest incoming payments. */
/** @deprecated Attestation button is shown automatically for payment recipients. */
showAttestationAction?: boolean
/** `notification`: recipient + view links; `profileWall`: sender row; `thread`: body only. */
variant?: SuperchatLayoutVariant
}) {
const { t } = useTranslation()
const { attested } = usePaymentAttestationStatus(event)
const info = useMemo(() => getPaymentNotificationInfo(event), [event])
const paytoType = useMemo(
() => (info?.payto ? parsePaytoTagType(info.payto) : 'unknown'),
@ -64,6 +65,7 @@ export default function Superchat({ @@ -64,6 +65,7 @@ export default function Superchat({
const hasThreadTarget = Boolean(targetEvent || referencedFetchId)
const isNotification = variant === 'notification'
const isProfileWall = variant === 'profileWall'
const showAsSuperchat = isProfileWall || attested
const hasTarget = isNotification && (hasThreadTarget || Boolean(recipientPubkey))
const hasMetaLine =
isProfileWall ||
@ -128,19 +130,29 @@ export default function Superchat({ @@ -128,19 +130,29 @@ export default function Superchat({
hasMetaLine && 'mt-1'
)}
>
{showAsSuperchat ? (
<>
<SuperchatPaymentMethodLabel
paytoType={paytoType}
className="px-2.5 py-1.5 text-lg"
imgClassName="size-5"
/>
<span className="text-xl font-semibold text-yellow-400/90">{t('Superchat')}</span>
</>
) : (
<SuperchatPaymentMethodLabel
paytoType={paytoType}
className="px-2.5 py-1.5 text-lg"
imgClassName="size-5"
/>
)}
</div>
) : null}
{comment ? (
<SuperchatCommentMarkdown event={event} comment={comment} className="mt-2" />
) : null}
{showAttestationAction ? (
<TurnIntoSuperchatButton event={event} prominent className="mt-3" />
{!isProfileWall ? (
<TurnIntoSuperchatButton event={event} prominent={isNotification} className="mt-3" />
) : null}
</div>
)

24
src/components/Note/Zap.tsx

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
import { useFetchEvent } from '@/hooks'
import { usePaymentAttestationStatus } from '@/hooks/usePaymentAttestationStatus'
import { getZapInfoFromEvent } from '@/lib/event-metadata'
import { shouldHideInteractions } from '@/lib/event-filtering'
import { formatAmount } from '@/lib/lightning'
@ -8,6 +9,7 @@ import { getSuperchatPaytoType } from '@/lib/superchat' @@ -8,6 +9,7 @@ import { getSuperchatPaytoType } from '@/lib/superchat'
import { toProfile } from '@/lib/link'
import { cn } from '@/lib/utils'
import { Event } from 'nostr-tools'
import { Zap as ZapIcon } from 'lucide-react'
import { useMemo, type MouseEvent } from 'react'
import { useTranslation } from 'react-i18next'
import { useSmartNoteNavigationOptional, useSecondaryPageOptional } from '@/PageManager'
@ -21,11 +23,11 @@ import type { SuperchatLayoutVariant } from './Superchat' @@ -21,11 +23,11 @@ import type { SuperchatLayoutVariant } from './Superchat'
export default function Zap({
event,
className,
showAttestationAction = false,
variant = 'thread'
}: {
event: Event
className?: string
/** @deprecated Attestation button is shown automatically for payment recipients. */
showAttestationAction?: boolean
variant?: SuperchatLayoutVariant
}) {
@ -52,6 +54,7 @@ export default function Zap({ @@ -52,6 +54,7 @@ export default function Zap({
}, [isEventZap, isProfileZap, targetEvent, zapInfo?.recipientPubkey])
const paytoType = useMemo(() => getSuperchatPaytoType(event), [event])
const { attested } = usePaymentAttestationStatus(event)
const { navigateToNote } = useSmartNoteNavigationOptional()
const secondaryPage = useSecondaryPageOptional()
const push = secondaryPage?.push ?? ((url: string) => { window.location.href = url })
@ -83,6 +86,7 @@ export default function Zap({ @@ -83,6 +86,7 @@ export default function Zap({
const isNotification = variant === 'notification'
const isProfileWall = variant === 'profileWall'
const showAsSuperchat = isProfileWall || attested
const hasMetaLine =
isProfileWall ||
(isNotification &&
@ -147,6 +151,8 @@ export default function Zap({ @@ -147,6 +151,8 @@ export default function Zap({
hasMetaLine && 'mt-1'
)}
>
{showAsSuperchat ? (
<>
<SuperchatPaymentMethodLabel
paytoType={paytoType}
className="px-2.5 py-1.5 text-lg"
@ -158,13 +164,25 @@ export default function Zap({ @@ -158,13 +164,25 @@ export default function Zap({
{formatAmount(amount)} {t('sats')}
</span>
) : null}
</>
) : (
<>
<ZapIcon className="size-5 shrink-0 text-primary" aria-hidden />
<span className="text-lg font-semibold text-foreground">{t('Zap')}</span>
{amount != null ? (
<span className="text-lg font-bold tabular-nums tracking-tight text-foreground">
{formatAmount(amount)} {t('sats')}
</span>
) : null}
</>
)}
</div>
) : null}
{comment ? (
<SuperchatCommentMarkdown event={event} comment={comment} className="mt-2" />
) : null}
{showAttestationAction ? (
<TurnIntoSuperchatButton event={event} prominent className="mt-3" />
{!isProfileWall ? (
<TurnIntoSuperchatButton event={event} prominent={isNotification} className="mt-3" />
) : null}
</div>
)

2
src/components/ReplyNote/index.tsx

@ -203,7 +203,7 @@ export default function ReplyNote({ @@ -203,7 +203,7 @@ export default function ReplyNote({
<span className="text-sm text-foreground/85">{t(notificationReactionSummaryKey(reactionDisplay))}</span>
)}
</div>
) : event.kind === kinds.Zap ? (
) : event.kind === kinds.Zap || event.kind === ExtendedKind.ZAP_RECEIPT ? (
<Zap className="mt-1.5" event={event} variant="thread" />
) : event.kind === ExtendedKind.PAYMENT_NOTIFICATION ? (
<Superchat className="mt-1.5" event={event} variant="thread" />

56
src/hooks/usePaymentAttestationStatus.test.ts

@ -0,0 +1,56 @@ @@ -0,0 +1,56 @@
import { ExtendedKind } from '@/constants'
import { describe, expect, it } from 'vitest'
import { isPaymentAttestationForTarget } from './usePaymentAttestationStatus'
import type { Event } from 'nostr-tools'
const recipient = 'a'.repeat(64)
const targetId = 'b'.repeat(64)
function attestationEvent(overrides: Partial<Event> = {}): Event {
return {
id: 'c'.repeat(64),
kind: ExtendedKind.PAYMENT_ATTESTATION,
pubkey: recipient,
created_at: 1,
tags: [['e', targetId], ['k', '9735']],
content: '',
sig: 'sig',
...overrides
}
}
describe('isPaymentAttestationForTarget', () => {
it('accepts a matching kind 9741 attestation', () => {
expect(isPaymentAttestationForTarget(attestationEvent(), targetId, recipient)).toBe(true)
})
it('rejects zap receipts and other kinds', () => {
expect(
isPaymentAttestationForTarget(
attestationEvent({ kind: ExtendedKind.ZAP_RECEIPT, tags: [['e', targetId]] }),
targetId,
recipient
)
).toBe(false)
})
it('rejects attestations for a different payment target', () => {
expect(
isPaymentAttestationForTarget(
attestationEvent({ tags: [['e', 'd'.repeat(64)], ['k', '9735']] }),
targetId,
recipient
)
).toBe(false)
})
it('rejects attestations from a different author', () => {
expect(
isPaymentAttestationForTarget(
attestationEvent({ pubkey: 'e'.repeat(64) }),
targetId,
recipient
)
).toBe(false)
})
})

59
src/hooks/usePaymentAttestationStatus.tsx

@ -6,9 +6,7 @@ import { @@ -6,9 +6,7 @@ import {
import { hexPubkeysEqual } from '@/lib/pubkey'
import {
hydrateAttestationsForAuthor,
isLocallyMarkedAttested,
loadPaymentAttestationLocal,
markLocalAttestationTarget,
peekCachedPaymentAttestation,
refreshPaymentAttestationFromRelays,
rememberPaymentAttestationFromPublish
@ -26,6 +24,17 @@ function attestationFilter(recipientPubkey: string, targetEventId: string) { @@ -26,6 +24,17 @@ function attestationFilter(recipientPubkey: string, targetEventId: string) {
}
}
export function isPaymentAttestationForTarget(
attestation: NostrEvent,
targetEventId: string,
recipientPubkey: string
): boolean {
if (attestation.kind !== ExtendedKind.PAYMENT_ATTESTATION) return false
if (!hexPubkeysEqual(attestation.pubkey, recipientPubkey)) return false
const attestedId = getPaymentAttestationTargetId(attestation)
return Boolean(attestedId && attestedId.toLowerCase() === targetEventId.trim().toLowerCase())
}
function readAttestedFromLocalSources(
targetEventId: string | undefined,
recipientPubkey: string | null
@ -34,9 +43,8 @@ function readAttestedFromLocalSources( @@ -34,9 +43,8 @@ function readAttestedFromLocalSources(
return { attested: false, attestationEvent: null }
}
const hit = peekCachedPaymentAttestation(targetEventId, recipientPubkey)
const locallyMarked = isLocallyMarkedAttested(recipientPubkey, targetEventId)
return {
attested: Boolean(hit) || locallyMarked,
attested: Boolean(hit),
attestationEvent: hit ?? null
}
}
@ -64,23 +72,25 @@ export function usePaymentAttestationStatus(targetEvent: NostrEvent | undefined) @@ -64,23 +72,25 @@ export function usePaymentAttestationStatus(targetEvent: NostrEvent | undefined)
)
const [checking, setChecking] = useState(false)
const applyMatch = useCallback((match: NostrEvent | undefined) => {
if (!match) return
const applyMatch = useCallback(
(match: NostrEvent | undefined) => {
if (!match || !targetEvent?.id || !recipientPubkey) return
if (!isPaymentAttestationForTarget(match, targetEvent.id, recipientPubkey)) return
setAttestationEvent(match)
setAttested(true)
},
[recipientPubkey, targetEvent?.id]
)
const clearAttested = useCallback(() => {
setAttestationEvent(null)
setAttested(false)
}, [])
const markAttested = useCallback(
(attestation: NostrEvent) => {
if (!targetEvent?.id || !recipientPubkey) return
markLocalAttestationTarget(recipientPubkey, targetEvent.id)
if (attestation.kind !== ExtendedKind.PAYMENT_ATTESTATION) {
setAttested(true)
return
}
if (!hexPubkeysEqual(attestation.pubkey, recipientPubkey)) return
const attestedId = getPaymentAttestationTargetId(attestation)
if (!attestedId || attestedId.toLowerCase() !== targetEvent.id.toLowerCase()) return
if (!isPaymentAttestationForTarget(attestation, targetEvent.id, recipientPubkey)) return
rememberPaymentAttestationFromPublish(attestation)
applyMatch(attestation)
},
@ -101,10 +111,6 @@ export function usePaymentAttestationStatus(targetEvent: NostrEvent | undefined) @@ -101,10 +111,6 @@ export function usePaymentAttestationStatus(targetEvent: NostrEvent | undefined)
useEffect(() => {
if (!targetEvent?.id || !recipientPubkey || !filter) return
if (isLocallyMarkedAttested(recipientPubkey, targetEvent.id)) {
setAttested(true)
}
let cancelled = false
setChecking(true)
@ -116,16 +122,17 @@ export function usePaymentAttestationStatus(targetEvent: NostrEvent | undefined) @@ -116,16 +122,17 @@ export function usePaymentAttestationStatus(targetEvent: NostrEvent | undefined)
applyMatch(local)
return
}
if (isLocallyMarkedAttested(recipientPubkey, targetEvent.id)) {
setAttested(true)
return
}
const relay = await refreshPaymentAttestationFromRelays(
targetEvent.id,
recipientPubkey,
filter
)
if (!cancelled) applyMatch(relay)
if (cancelled) return
if (relay) {
applyMatch(relay)
} else {
clearAttested()
}
} catch {
/* optional */
} finally {
@ -136,13 +143,15 @@ export function usePaymentAttestationStatus(targetEvent: NostrEvent | undefined) @@ -136,13 +143,15 @@ export function usePaymentAttestationStatus(targetEvent: NostrEvent | undefined)
return () => {
cancelled = true
}
}, [applyMatch, filter, recipientPubkey, targetEvent?.id, targetId])
}, [applyMatch, clearAttested, filter, recipientPubkey, targetEvent?.id, targetId])
useEffect(() => {
if (!targetEvent?.id || !recipientPubkey) return
const handleAttestation = (data: globalThis.Event) => {
markAttested((data as CustomEvent<NostrEvent>).detail)
const attestation = (data as CustomEvent<NostrEvent>).detail
if (attestation.kind !== ExtendedKind.PAYMENT_ATTESTATION) return
markAttested(attestation)
}
client.addEventListener('newEvent', handleAttestation)

8
src/lib/event-metadata.ts

@ -465,7 +465,13 @@ export function getRelaySetFromEvent(event: Event, blockedRelays?: string[]): TR @@ -465,7 +465,13 @@ export function getRelaySetFromEvent(event: Event, blockedRelays?: string[]): TR
}
export function getZapInfoFromEvent(receiptEvent: Event) {
if (receiptEvent.kind !== kinds.Zap && receiptEvent.kind !== ExtendedKind.ZAP_REQUEST) return null
if (
receiptEvent.kind !== kinds.Zap &&
receiptEvent.kind !== ExtendedKind.ZAP_RECEIPT &&
receiptEvent.kind !== ExtendedKind.ZAP_REQUEST
) {
return null
}
// Kind 9734 — zap request: all data is directly on the event (no bolt11, no description wrapper).
if (receiptEvent.kind === ExtendedKind.ZAP_REQUEST) {

6
src/lib/superchat.ts

@ -101,7 +101,7 @@ export function getPaymentNotificationInfo(event: Event): PaymentNotificationInf @@ -101,7 +101,7 @@ export function getPaymentNotificationInfo(event: Event): PaymentNotificationInf
/** Payment category for superchat display (9735 → lightning). */
export function getSuperchatPaytoType(event: Event): string {
if (event.kind === kinds.Zap) return 'lightning'
if (event.kind === kinds.Zap || event.kind === ExtendedKind.ZAP_RECEIPT) return 'lightning'
if (event.kind === ExtendedKind.PAYMENT_NOTIFICATION) {
const payto = getPaymentNotificationInfo(event)?.payto
return payto ? parsePaytoTagType(payto) : 'unknown'
@ -119,7 +119,7 @@ export function getSuperchatReferenceFetchId(info: PaymentNotificationInfo): str @@ -119,7 +119,7 @@ export function getSuperchatReferenceFetchId(info: PaymentNotificationInfo): str
}
export function getSuperchatAmountSats(event: Event): number {
if (event.kind === kinds.Zap) {
if (event.kind === kinds.Zap || event.kind === ExtendedKind.ZAP_RECEIPT) {
return getZapInfoFromEvent(event)?.amount ?? 0
}
if (event.kind === ExtendedKind.PAYMENT_NOTIFICATION) {
@ -129,7 +129,7 @@ export function getSuperchatAmountSats(event: Event): number { @@ -129,7 +129,7 @@ export function getSuperchatAmountSats(event: Event): number {
}
export function isSuperchatKind(kind: number): boolean {
return kind === kinds.Zap || kind === ExtendedKind.PAYMENT_NOTIFICATION
return kind === kinds.Zap || kind === ExtendedKind.ZAP_RECEIPT || kind === ExtendedKind.PAYMENT_NOTIFICATION
}
/** Recipient pubkey for a kind 9735 or 9740 payment the user may attest to. */

Loading…
Cancel
Save