diff --git a/src/PageManager.tsx b/src/PageManager.tsx index 9c80d694..3af61752 100644 --- a/src/PageManager.tsx +++ b/src/PageManager.tsx @@ -8,9 +8,8 @@ import { Button } from '@/components/ui/button' import { cn } from '@/lib/utils' import logger from '@/lib/logger' import { - MOBILE_SWIPE_BACK_DOMINANCE, MOBILE_SWIPE_BACK_EDGE_PX, - MOBILE_SWIPE_BACK_MIN_PX, + tryMobileSwipeBackFromGesture, useMobileSwipeBackOnElement } from '@/lib/mobile-swipe-back' import { preventRadixSheetCloseForPortaledOverlay } from '@/lib/sheet-dismiss-guard' @@ -1132,8 +1131,10 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { }, [currentPrimaryPage]) const navigationCounterRef = useRef(0) const goBackRef = useRef<() => void>(() => {}) + const popSecondaryPageRef = useRef<() => void>(() => {}) const drawerOpenRef = useRef(drawerOpen) const [mobilePrimarySwipeRoot, setMobilePrimarySwipeRoot] = useState(null) + const [mobileSecondarySwipeRoot, setMobileSecondarySwipeRoot] = useState(null) useLayoutEffect(() => { drawerOpenRef.current = drawerOpen }, [drawerOpen]) @@ -2219,6 +2220,16 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { hardCloseSecondaryPanel() } + popSecondaryPageRef.current = popSecondaryPage + + const mobileSecondaryPanelOpen = + isSmallScreen && secondaryStack.length > 0 && !primaryNoteView + useMobileSwipeBackOnElement(mobileSecondaryPanelOpen ? mobileSecondarySwipeRoot : null, () => + popSecondaryPageRef.current() + , { + enabled: mobileSecondaryPanelOpen + }) + const mobileSecondaryOpen = isSmallScreen && (drawerOpen || secondaryStack.length > 0) useEffect(() => { if (!mobileSecondaryOpen) return @@ -2226,36 +2237,30 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { let grab: { x: number; y: number; pointerId: number } | null = null const onPointerDown = (e: PointerEvent) => { - if (e.button !== 0 || e.clientX > MOBILE_SWIPE_BACK_EDGE_PX) return + if ((e.button !== 0 && e.button !== -1) || e.clientX > MOBILE_SWIPE_BACK_EDGE_PX) return grab = { x: e.clientX, y: e.clientY, pointerId: e.pointerId } } const onPointerUp = (e: PointerEvent) => { if (!grab || grab.pointerId !== e.pointerId) return - const dx = e.clientX - grab.x - const dy = e.clientY - grab.y + tryMobileSwipeBackFromGesture(grab, e.clientX, e.clientY, e.pointerId, () => + popSecondaryPageRef.current() + ) grab = null - const ax = Math.abs(dx) - const ay = Math.abs(dy) - if (dx < MOBILE_SWIPE_BACK_MIN_PX || ax < ay * MOBILE_SWIPE_BACK_DOMINANCE) return - if (secondaryStackRef.current.length > 1) { - window.history.back() - } else { - hardCloseSecondaryPanel() - } } const onPointerCancel = () => { grab = null } - document.addEventListener('pointerdown', onPointerDown, { capture: true }) - document.addEventListener('pointerup', onPointerUp, { capture: true }) - document.addEventListener('pointercancel', onPointerCancel, { capture: true }) + const capture = { capture: true } as const + document.addEventListener('pointerdown', onPointerDown, capture) + document.addEventListener('pointerup', onPointerUp, capture) + document.addEventListener('pointercancel', onPointerCancel, capture) return () => { - document.removeEventListener('pointerdown', onPointerDown, { capture: true }) - document.removeEventListener('pointerup', onPointerUp, { capture: true }) - document.removeEventListener('pointercancel', onPointerCancel, { capture: true }) + document.removeEventListener('pointerdown', onPointerDown, capture) + document.removeEventListener('pointerup', onPointerUp, capture) + document.removeEventListener('pointercancel', onPointerCancel, capture) } }, [mobileSecondaryOpen]) @@ -2368,7 +2373,12 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { ) : ( <> {secondaryStack.length > 0 ? ( - +
+ +
) : null} {secondaryStack.length === 0 ? (
diff --git a/src/components/Note/Superchat.tsx b/src/components/Note/Superchat.tsx index a4cb6e16..c82c8dc3 100644 --- a/src/components/Note/Superchat.tsx +++ b/src/components/Note/Superchat.tsx @@ -152,7 +152,12 @@ export default function Superchat({ ) : null} {!isProfileWall ? ( - + ) : null}
) diff --git a/src/components/Note/Zap.tsx b/src/components/Note/Zap.tsx index 16872808..cafe6e47 100644 --- a/src/components/Note/Zap.tsx +++ b/src/components/Note/Zap.tsx @@ -75,6 +75,8 @@ export default function Zap({ const { senderPubkey, recipientPubkey, amount, comment } = zapInfo + const attestationRecipientPubkey = actualRecipientPubkey ?? recipientPubkey ?? null + const openZapTarget = (e: MouseEvent) => { e.stopPropagation() if (isEventZap && zapInfo?.eventId) { @@ -182,7 +184,12 @@ export default function Zap({ ) : null} {!isProfileWall ? ( - + ) : null} ) diff --git a/src/components/Note/index.tsx b/src/components/Note/index.tsx index a6d3dbe0..9fe6e363 100644 --- a/src/components/Note/index.tsx +++ b/src/components/Note/index.tsx @@ -572,12 +572,15 @@ export default function Note({ ) } else if (event.kind === ExtendedKind.PUBLIC_MESSAGE) { content = renderEventContent({ hideMetadata: true }) - } else if (event.kind === ExtendedKind.ZAP_REQUEST || event.kind === ExtendedKind.ZAP_RECEIPT) { + } else if ( + event.kind === ExtendedKind.ZAP_REQUEST || + event.kind === ExtendedKind.ZAP_RECEIPT || + event.kind === kinds.Zap + ) { content = ( ) @@ -586,7 +589,6 @@ export default function Note({ ) diff --git a/src/components/TurnIntoSuperchatButton/index.tsx b/src/components/TurnIntoSuperchatButton/index.tsx index a1e821c8..4ef18bed 100644 --- a/src/components/TurnIntoSuperchatButton/index.tsx +++ b/src/components/TurnIntoSuperchatButton/index.tsx @@ -3,10 +3,10 @@ import { createPaymentAttestationDraftEvent } from '@/lib/draft-event' import { LoginRequiredError } from '@/lib/nostr-errors' import { showSimplePublishSuccess } from '@/lib/publishing-feedback' import { + canUserAttestSuperchatPayment, getSuperchatAttestationTargetKindValue, getSuperchatPaymentRecipientPubkey, - isAttestableSuperchatPayment, - isIncomingPaymentNotificationOrZapReceipt + isAttestableSuperchatPayment } from '@/lib/superchat' import { cn } from '@/lib/utils' import { requestProfileWallRefresh } from '@/hooks/useProfileWall' @@ -22,48 +22,59 @@ import { toast } from 'sonner' export default function TurnIntoSuperchatButton({ event, className, - prominent = false + prominent = false, + attestationRecipientPubkey: attestationRecipientPubkeyProp }: { event: Event className?: string - /** Full-width call-to-action styling for note cards. */ + /** Full-width call-to-action styling for note cards (notifications feed). */ prominent?: boolean + /** Note author for note zaps; defaults to payment `p` / zap metadata. */ + attestationRecipientPubkey?: string | null }) { const { pubkey } = useNostr() + const attestationRecipientPubkey = + attestationRecipientPubkeyProp ?? getSuperchatPaymentRecipientPubkey(event) if ( !isAttestableSuperchatPayment(event) || !getSuperchatAttestationTargetKindValue(event) || !pubkey || - !isIncomingPaymentNotificationOrZapReceipt(event, pubkey) + !attestationRecipientPubkey || + !canUserAttestSuperchatPayment(event, pubkey, attestationRecipientPubkey) ) { return null } return ( - + ) } function TurnIntoSuperchatButtonInner({ event, + attestationRecipientPubkey, className, prominent = false }: { event: Event + attestationRecipientPubkey: string className?: string prominent?: boolean }) { const { t } = useTranslation() const { publish, checkLogin } = useNostr() - const recipientPubkey = getSuperchatPaymentRecipientPubkey(event) - const { attested, checking, markAttested } = usePaymentAttestationStatus(event) + const { attested, checking, markAttested } = usePaymentAttestationStatus( + event, + attestationRecipientPubkey + ) const [publishing, setPublishing] = useState(false) - if (!recipientPubkey) { - return null - } - if (attested) { return (

+ paymentContext?.amountMsat ? clampZapSats(Math.floor(paymentContext.amountMsat / 1000)) : 0 + ) const [minPow, setMinPow] = useState(0) const [sending, setSending] = useState(false) const textareaRef = useRef(null) + const amountMsat = amountSats > 0 ? clampZapSats(amountSats) * 1000 : undefined + useEffect(() => { const id = requestAnimationFrame(() => textareaRef.current?.focus()) return () => cancelAnimationFrame(id) @@ -43,8 +50,8 @@ export default function SuperchatRequestForm({ const previewEvent = useMemo(() => { const tags: string[][] = [['p', recipientPubkey]] - if (paymentContext?.amountMsat) { - tags.push(['amount', String(paymentContext.amountMsat)]) + if (amountMsat) { + tags.push(['amount', String(amountMsat)]) } if (paymentContext?.payto) { tags.push(['payto', paymentContext.payto]) @@ -56,7 +63,7 @@ export default function SuperchatRequestForm({ content: message, tags }) - }, [message, paymentContext, recipientPubkey, selfPubkey]) + }, [amountMsat, message, paymentContext, recipientPubkey, selfPubkey]) const handleSend = () => { const trimmed = message.trim() @@ -65,7 +72,7 @@ export default function SuperchatRequestForm({ setSending(true) try { const draft = await createPaymentNotificationDraftEvent(trimmed, recipientPubkey, { - amountMsat: paymentContext?.amountMsat, + amountMsat, payto: paymentContext?.payto, referencedEvent: paymentContext?.referencedEvent, addClientTag: true @@ -96,6 +103,25 @@ export default function SuperchatRequestForm({ ) : null} +

+ +
+ 0 ? formatSatsGrouped(amountSats) : ''} + onChange={(e) => setAmountSats(parseGroupedIntegerInput(e.target.value))} + placeholder="0" + disabled={sending} + className="min-w-0 flex-1 tabular-nums" + aria-describedby="superchat-amount-hint" + /> + {t('sats')} +
+

+ {t('Superchat estimated amount hint')} +

+