diff --git a/package-lock.json b/package-lock.json index ff820ef0..199b30f2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "imwald", - "version": "23.12.1", + "version": "23.13.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "imwald", - "version": "23.12.1", + "version": "23.13.0", "license": "MIT", "dependencies": { "@asciidoctor/core": "^3.0.4", diff --git a/package.json b/package.json index f036f0ba..1a142f8a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "imwald", - "version": "23.12.1", + "version": "23.13.0", "description": "Imwald — a user-friendly Nostr client focused on relay feed browsing, publications, and relay discovery", "private": true, "type": "module", diff --git a/src/components/KindFilter/index.tsx b/src/components/KindFilter/index.tsx index 3ae5368d..991b3d3e 100644 --- a/src/components/KindFilter/index.tsx +++ b/src/components/KindFilter/index.tsx @@ -21,7 +21,6 @@ const KIND_FILTER_OPTIONS = [ { kindGroup: [kinds.LongFormArticle, ExtendedKind.WIKI_ARTICLE, ExtendedKind.WIKI_ARTICLE_MARKDOWN], label: 'Articles' }, { kindGroup: [kinds.Highlights], label: 'Highlights' }, { kindGroup: [ExtendedKind.POLL], label: 'Polls' }, - { kindGroup: [ExtendedKind.ZAP_POLL], label: 'Zap polls' }, { kindGroup: [ExtendedKind.VOICE, ExtendedKind.VOICE_COMMENT], label: 'Voice Posts' }, { kindGroup: [ExtendedKind.PICTURE], label: 'Photo Posts' }, { kindGroup: [...NIP71_VIDEO_KINDS], label: 'Video Posts' }, diff --git a/src/components/Note/ZapPoll.tsx b/src/components/Note/ZapPoll.tsx deleted file mode 100644 index dde527c3..00000000 --- a/src/components/Note/ZapPoll.tsx +++ /dev/null @@ -1,304 +0,0 @@ -import { Button } from '@/components/ui/button' -import { Input } from '@/components/ui/input' -import { Label } from '@/components/ui/label' -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue -} from '@/components/ui/select' -import { cn } from '@/lib/utils' -import { - isZapPollPastDeadline, - isZapPollVoteEligible, - userHasZappedPoll, - userZapPollVoteOption -} from '@/lib/zap-poll' -import { useZapPollMeta, useZapPollTally } from '@/hooks/useZapPollTally' -import { useNostrOptional } from '@/providers/nostr-context' -import lightning from '@/services/lightning.service' -import { Zap } from 'lucide-react' -import { Event } from 'nostr-tools' -import { useEffect, useMemo, useState } from 'react' -import { useTranslation } from 'react-i18next' -import { toast } from 'sonner' -import dayjs from 'dayjs' - -export default function ZapPoll({ - event, - className, - voteHighlightOptionIndex -}: { - event: Event - className?: string - /** When showing this poll because the profile user voted, highlight that option. */ - voteHighlightOptionIndex?: number -}) { - const { t } = useTranslation() - const nostr = useNostrOptional() - const pubkey = nostr?.pubkey ?? null - const meta = useZapPollMeta(event) - /** Same pubkey can appear on multiple `p` tags; Select keys/values must be unique. */ - const payToRecipients = useMemo(() => { - if (!meta) return [] - const seen = new Set() - return meta.recipients.filter((r) => { - if (seen.has(r.pubkey)) return false - seen.add(r.pubkey) - return true - }) - }, [meta]) - const { receipts, tally, loading, error, reload } = useZapPollTally(event, meta) - - const [recipientPk, setRecipientPk] = useState('') - const [optionIndex, setOptionIndex] = useState(null) - const [sats, setSats] = useState(21) - const [zapping, setZapping] = useState(false) - /** Show sat/zap breakdown without having voted (card UX). */ - const [tallyRevealed, setTallyRevealed] = useState(false) - - useEffect(() => { - if (meta?.valueMinimum != null) { - setSats(Math.max(meta.valueMinimum, 1)) - } else { - setSats(21) - } - }, [meta?.valueMinimum, event.id]) - - const defaultRecipient = payToRecipients[0]?.pubkey ?? '' - const effectiveRecipient = recipientPk || defaultRecipient - - const closed = meta ? isZapPollPastDeadline(event, meta) : false - const viewerZapped = pubkey && meta ? userHasZappedPoll(event.id, pubkey, receipts) : false - const myVoteOption = - pubkey && meta ? userZapPollVoteOption(event, meta, pubkey, receipts) : undefined - - const showTally = - !!meta && - (closed || viewerZapped || event.pubkey === pubkey || tallyRevealed) - - /** When results are visible, list options by total sats (largest first). */ - const optionsDisplayOrder = useMemo(() => { - if (!meta) return [] - if (!showTally || !tally) return meta.options - return [...meta.options].sort((a, b) => { - const sa = tally.satsByOption.get(a.index) ?? 0 - const sb = tally.satsByOption.get(b.index) ?? 0 - if (sb !== sa) return sb - sa - return a.index - b.index - }) - }, [meta, showTally, tally]) - - const satsBounds = useMemo(() => { - if (!meta) return { min: 1, max: undefined as number | undefined } - return { - min: Math.max(1, meta.valueMinimum ?? 1), - max: meta.valueMaximum - } - }, [meta]) - - if (!meta) { - return ( -
- {t('Invalid zap poll')} -
- ) - } - - const handleZapVote = async () => { - if (!pubkey) { - nostr?.startLogin() - return - } - if (optionIndex === null) { - toast.error(t('Select an option')) - return - } - const eligible = isZapPollVoteEligible(event, meta, pubkey, sats) - if (!eligible.ok) { - toast.error(eligible.reason) - return - } - setZapping(true) - try { - const result = await lightning.zapPollVote( - pubkey, - event, - meta, - effectiveRecipient, - optionIndex, - sats, - '', - undefined - ) - if (result) { - toast.success(t('Zap sent')) - await reload() - } - } catch (e) { - toast.error((e as Error).message) - } finally { - setZapping(false) - } - } - - return ( -
-
- - {t('Zap poll (paid votes)')} -
- {voteHighlightOptionIndex != null && ( -

{t('You voted on this poll (zap receipt)')}

- )} - {meta.closedAt && ( -

- {closed - ? t('Poll closed {{time}}', { - time: dayjs.unix(meta.closedAt).format('lll') - }) - : t('Closes {{time}}', { time: dayjs.unix(meta.closedAt).format('lll') })} -

- )} - {!closed && (meta.valueMinimum != null || meta.valueMaximum != null) && ( -

- {t('Vote size')}:{' '} - {meta.valueMinimum != null && meta.valueMaximum != null - ? meta.valueMinimum === meta.valueMaximum - ? t('{{n}} sats (fixed)', { n: meta.valueMinimum }) - : t('{{min}}–{{max}} sats', { min: meta.valueMinimum, max: meta.valueMaximum }) - : meta.valueMinimum != null - ? t('≥ {{n}} sats', { n: meta.valueMinimum }) - : t('≤ {{n}} sats', { n: meta.valueMaximum! })} -

- )} - {loading ? ( -

{t('Loading tally…')}

- ) : null} - {error &&

{error}

} - {!loading && showTally && tally && tally.totalSats === 0 && ( -

{t('Zap poll no votes yet')}

- )} - {meta && !closed && !showTally && ( - - )} -
- {optionsDisplayOrder.map((opt) => { - const satsOpt = tally?.satsByOption.get(opt.index) ?? 0 - const pct = tally && tally.totalSats > 0 ? (100 * satsOpt) / tally.totalSats : 0 - const counts = tally?.receiptCountByOption.get(opt.index) ?? 0 - const isMine = - myVoteOption === opt.index || voteHighlightOptionIndex === opt.index - return ( -
- {showTally && tally && ( -
- )} -
- {opt.label} - {showTally && tally && ( - - {`${Math.round(satsOpt)} sats · ${t('{{n}} zaps', { n: counts })} (${pct.toFixed(0)}%)`} - - )} -
-
- ) - })} -
- {meta.consensusThreshold != null && showTally && tally && ( -

- {t('Consensus threshold')}: {meta.consensusThreshold}% -

- )} - {!closed && pubkey && event.pubkey !== pubkey && ( -
-
- - -
-
- - -
-
- - setSats(parseInt(e.target.value, 10) || 0)} - className="h-9" - /> -
- -
- )} - {showTally && ( - - )} -
- ) -} diff --git a/src/components/Note/index.tsx b/src/components/Note/index.tsx index 1c8f1f1a..fca3e8b1 100644 --- a/src/components/Note/index.tsx +++ b/src/components/Note/index.tsx @@ -71,7 +71,6 @@ import MutedNote from './MutedNote' import NsfwNote from './NsfwNote' import PictureNote from './PictureNote' import Poll from './Poll' -import ZapPoll from './ZapPoll' import NotificationEventCard from './NotificationEventCard' import ReactionEmojiDisplay from './ReactionEmojiDisplay' import UnknownNote from './UnknownNote' @@ -224,7 +223,6 @@ export default function Note({ /** From {@link MainNoteCard}: embedded cards need eager poll results (viewport IO often misses nested scrollers). */ embedded, fullCalendarInvite, - zapPollVoteHighlightOption, nip84HighlightEvents, deferAuthorAvatar = false, pinned = false @@ -241,8 +239,6 @@ export default function Note({ pinned?: boolean /** When viewing a kind-24 invite, use this to replace the embedded calendar with the full card (RSVP) in content */ fullCalendarInvite?: { event: Event; naddr: string } - /** Profile: highlight option when this row is from a zap vote receipt. */ - zapPollVoteHighlightOption?: number /** Kind-9802 events that cite this note; when spans match {@link displayEvent.content}, render green marks (note page OP). */ nip84HighlightEvents?: Event[] /** When true, defer remote profile avatars until near-viewport (dense lists e.g. merged NIP-50 search). */ @@ -533,17 +529,6 @@ export default function Note({ ) - } else if (event.kind === ExtendedKind.ZAP_POLL) { - content = ( - <> - {renderEventContent({ hideMetadata: true })} - - - ) } else if (event.kind === ExtendedKind.VOICE) { content = } else if (event.kind === ExtendedKind.VOICE_COMMENT) { diff --git a/src/components/NoteCard/MainNoteCard.tsx b/src/components/NoteCard/MainNoteCard.tsx index 3cc734a2..499b9031 100644 --- a/src/components/NoteCard/MainNoteCard.tsx +++ b/src/components/NoteCard/MainNoteCard.tsx @@ -21,7 +21,6 @@ export default function MainNoteCard({ originalNoteId, pinned = false, hideParentNotePreview = false, - zapPollVoteHighlightOption, bottomNoteLabel, showFull = false, fetchNoteStatsIfMissing = true, @@ -37,7 +36,6 @@ export default function MainNoteCard({ pinned?: boolean /** Hide the parent note preview (e.g. when showing quotes of current note). */ hideParentNotePreview?: boolean - zapPollVoteHighlightOption?: number bottomNoteLabel?: string showFull?: boolean /** When false, skip relay-backed stats prefetch (e.g. merged NIP-50 search lists). */ @@ -121,7 +119,6 @@ export default function MainNoteCard({ originalNoteId={originalNoteId} disableClick={true} hideParentNotePreview={hideParentNotePreview} - zapPollVoteHighlightOption={zapPollVoteHighlightOption} showFull={showFull} deferAuthorAvatar={deferAuthorAvatar} pinned={pinned} diff --git a/src/components/NoteCard/index.tsx b/src/components/NoteCard/index.tsx index a3ed6cd8..7ce571fc 100644 --- a/src/components/NoteCard/index.tsx +++ b/src/components/NoteCard/index.tsx @@ -15,7 +15,6 @@ const NoteCard = memo(function NoteCard({ filterMutedNotes = true, pinned = false, hideParentNotePreview = false, - zapPollVoteHighlightOption, bottomNoteLabel, fetchNoteStatsIfMissing = true, deferAuthorAvatar = true, @@ -27,7 +26,6 @@ const NoteCard = memo(function NoteCard({ pinned?: boolean /** When true, hide the parent/root note preview (e.g. when showing quotes of the current note). */ hideParentNotePreview?: boolean - zapPollVoteHighlightOption?: number /** Optional label rendered at the bottom of the card (e.g. why this event is in a composed feed). */ bottomNoteLabel?: string fetchNoteStatsIfMissing?: boolean @@ -68,7 +66,6 @@ const NoteCard = memo(function NoteCard({ className={className} pinned={pinned} hideParentNotePreview={hideParentNotePreview} - zapPollVoteHighlightOption={zapPollVoteHighlightOption} bottomNoteLabel={bottomNoteLabel} fetchNoteStatsIfMissing={fetchNoteStatsIfMissing} deferAuthorAvatar={deferAuthorAvatar} @@ -84,7 +81,6 @@ const NoteCard = memo(function NoteCard({ prevProps.filterMutedNotes === nextProps.filterMutedNotes && prevProps.pinned === nextProps.pinned && prevProps.hideParentNotePreview === nextProps.hideParentNotePreview && - prevProps.zapPollVoteHighlightOption === nextProps.zapPollVoteHighlightOption && prevProps.bottomNoteLabel === nextProps.bottomNoteLabel && prevProps.fetchNoteStatsIfMissing === nextProps.fetchNoteStatsIfMissing && prevProps.deferAuthorAvatar === nextProps.deferAuthorAvatar && diff --git a/src/components/NoteStats/index.tsx b/src/components/NoteStats/index.tsx index f72915a1..3403010a 100644 --- a/src/components/NoteStats/index.tsx +++ b/src/components/NoteStats/index.tsx @@ -66,8 +66,6 @@ export default function NoteStats({ const statsRelayFetchTier = isRssArticleRoot ? relayMergeTier : hintRelays.length > 0 ? 1 : 0 const statsRelaysRef = useRef(statsRelays) statsRelaysRef.current = statsRelays - const isZapPoll = event.kind === ExtendedKind.ZAP_POLL - const shouldDeferStatsFetch = deferFetchUntilNearViewport ?? (fetchIfNotExisting && !foregroundStats) const containerRef = useRef(null) @@ -110,7 +108,7 @@ export default function NoteStats({ isReplyToDiscussion={isReplyToDiscussion} useIconOnlyLikeTrigger={useIconOnlyLikeTrigger} /> - {!isRssArticleRoot && !isZapPoll && ( + {!isRssArticleRoot && ( )} diff --git a/src/components/ReplyNoteList/index.tsx b/src/components/ReplyNoteList/index.tsx index 5f9b3c1b..b764691d 100644 --- a/src/components/ReplyNoteList/index.tsx +++ b/src/components/ReplyNoteList/index.tsx @@ -302,11 +302,6 @@ function isPollVoteKind(evt: Pick): boolean { return evt.kind === ExtendedKind.POLL_RESPONSE } -/** Zap-poll (6969): kind 9735 receipts are paid votes — hide from “Antworten” so amounts/options are not tied to identities here. */ -function isZapPollThreadZapReceipt(evt: Pick, op: Pick): boolean { - return op.kind === ExtendedKind.ZAP_POLL && evt.kind === kinds.Zap -} - function threadBacklinkRelationLabel(item: NEvent, t: TFunction): string { if (item.kind === kinds.Highlights) return t('highlighted this note') if (item.kind === kinds.ShortTextNote) return t('quoted this note') @@ -418,7 +413,6 @@ function ReplyNoteList({ events.forEach((evt) => { if (replyIdSet.has(evt.id)) return if (isPollVoteKind(evt)) return - if (isZapPollThreadZapReceipt(evt, event)) return if ( shouldHideThreadResponseEvent( evt, @@ -450,10 +444,7 @@ function ReplyNoteList({ const { zaps: zapsPartitioned, nonZaps } = partitionZapReceipts(replyEvents) - const zaps = - event.kind === ExtendedKind.ZAP_POLL - ? [] - : filterZapReceiptsByReplyThreshold(zapsPartitioned, zapReplyThreshold) + const zaps = filterZapReceiptsByReplyThreshold(zapsPartitioned, zapReplyThreshold) const replyScoreById = sort === 'top' || sort === 'controversial' || sort === 'most-zapped' ? new Map( @@ -568,7 +559,7 @@ function ReplyNoteList({ /** Quotes + time-sorted feeds must not interleave zap receipts chronologically */ const zapsThenTimeSorted = (merged: NEvent[], direction: 'asc' | 'desc') => { const { zaps, nonZaps } = partitionZapReceipts(merged) - const zapsShown = event.kind === ExtendedKind.ZAP_POLL ? [] : zaps + const zapsShown = zaps const sortedNon = [...nonZaps].sort((a, b) => direction === 'asc' ? a.created_at - b.created_at : b.created_at - a.created_at ) @@ -580,7 +571,7 @@ function ReplyNoteList({ // E/A: zaps (sats desc) → thread replies (1 / 1111 / 1244, excluding #q-only) → tail (quotes, highlights, long-form refs) if (rootInfo?.type === 'E' || rootInfo?.type === 'A') { const { zaps, nonZaps } = partitionZapReceipts(replies) - const zapsShown = event.kind === ExtendedKind.ZAP_POLL ? [] : zaps + const zapsShown = zaps const middle = nonZaps.filter((e) => !isEaThreadTailBacklinkCandidate(e, rootInfo)) const tailFromReplies = nonZaps.filter((e) => isEaThreadTailBacklinkCandidate(e, rootInfo)) const tailSeen = new Set() @@ -598,7 +589,7 @@ function ReplyNoteList({ // Web article / URL thread (NIP-22): same zaps → middle → tail layout as E/A if (rootInfo?.type === 'I') { const { zaps, nonZaps } = partitionZapReceipts(replies) - const zapsShownI = event.kind === ExtendedKind.ZAP_POLL ? [] : zaps + const zapsShownI = zaps const middle = nonZaps.filter((e) => !isWebThreadTailKind(e.kind)) const tailFromReplies = nonZaps.filter((e) => isWebThreadTailKind(e.kind)) const tailSeen = new Set() @@ -919,7 +910,7 @@ function ReplyNoteList({ try { const ev = await eventService.fetchEvent(id) if (cancelled) return - if (ev && replyMatchesThreadForList(ev, event, threadRoot, true) && !isPollVoteKind(ev) && !isZapPollThreadZapReceipt(ev, event)) { + if (ev && replyMatchesThreadForList(ev, event, threadRoot, true) && !isPollVoteKind(ev)) { batch.push(ev) } else { discussionStatsHydratedReplyIdsRef.current.delete(id) @@ -960,7 +951,6 @@ function ReplyNoteList({ const onNewReply = useCallback( (evt: NEvent) => { if (isPollVoteKind(evt)) return - if (isZapPollThreadZapReceipt(evt, event)) return if (isNip18RepostKind(evt.kind)) { if ( rootInfo && @@ -1025,12 +1015,8 @@ function ReplyNoteList({ // Session LRU (timeline / note-stats / prior panels): thread replies before relay round-trip if (rootInfo.type === 'E' || rootInfo.type === 'A') { const fromSession = eventService.getSessionThreadInteractionEvents(rootInfo) - const fromSessionForUi = - event.kind === ExtendedKind.ZAP_POLL - ? fromSession.filter((e) => !isZapPollThreadZapReceipt(e, event)) - : fromSession - if (fromSessionForUi.length > 0) { - addReplies(fromSessionForUi) + if (fromSession.length > 0) { + addReplies(fromSession) } } @@ -1039,11 +1025,7 @@ function ReplyNoteList({ const hasCache = cachedData !== null if (hasCache && cachedData) { - const cachedForUi = - event.kind === ExtendedKind.ZAP_POLL - ? cachedData.filter((e) => !isZapPollThreadZapReceipt(e, event)) - : cachedData - addReplies(cachedForUi) + addReplies(cachedData) setLoading(false) } else { setLoading(true) @@ -1121,7 +1103,6 @@ function ReplyNoteList({ ? (evt: NEvent) => { if (fetchGeneration !== replyFetchGenRef.current) return if (isPollVoteKind(evt)) return - if (isZapPollThreadZapReceipt(evt, event)) return if (!isRssArticleUrlThreadInteraction(evt, urlThreadRootInfo.id)) return if (shouldHideThreadResponseEvent(evt, mutePubkeySet, hideContentMentioningMutedUsers)) return @@ -1148,7 +1129,7 @@ function ReplyNoteList({ // Filter and add replies (URL threads include kind 9802 highlights of this page) const regularReplies = allReplies.filter((evt) => { if (isPollVoteKind(evt)) return false - if (isZapPollThreadZapReceipt(evt, event)) return false + false const match = replyMatchesThreadForList(evt, event, rootInfo, isDiscussionRoot, threadWalkFromBatch) if (!match) return false return !shouldHideThreadResponseEvent( @@ -1170,10 +1151,7 @@ function ReplyNoteList({ logger.warn('[ReplyNoteList] Cache returned null after store, using fetched replies only') mergedForUi = regularReplies } else { - mergedForUi = - event.kind === ExtendedKind.ZAP_POLL - ? mergedCachedReplies.filter((e) => !isZapPollThreadZapReceipt(e, event)) - : mergedCachedReplies + mergedForUi = mergedCachedReplies } const repliesForStatsPrime = mergedForUi addReplies(mergedForUi) @@ -1420,7 +1398,6 @@ function ReplyNoteList({ const shouldShowFeedItem = useCallback( (item: NEvent) => { if (isPollVoteKind(item)) return false - if (isZapPollThreadZapReceipt(item, event)) return false if (shouldHideThreadResponseEvent(item, mutePubkeySet, hideContentMentioningMutedUsers)) { return false } diff --git a/src/constants.ts b/src/constants.ts index 74b49ae4..47d31184 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -880,7 +880,6 @@ export const SUPPORTED_KINDS = [ ExtendedKind.SHORT_VIDEO, ExtendedKind.VIDEO_ADDRESSABLE, ExtendedKind.POLL, - ExtendedKind.ZAP_POLL, ExtendedKind.COMMENT, ExtendedKind.VOICE, ExtendedKind.VOICE_COMMENT, diff --git a/src/hooks/useZapPollTally.tsx b/src/hooks/useZapPollTally.tsx deleted file mode 100644 index 802ac901..00000000 --- a/src/hooks/useZapPollTally.tsx +++ /dev/null @@ -1,138 +0,0 @@ -import { FAST_READ_RELAY_URLS, SEARCHABLE_RELAY_URLS } from '@/constants' -import { - parseZapPollEvent, - tallyZapPollFromReceipts, - type TZapPollMeta, - type TZapPollTally -} from '@/lib/zap-poll' -import { peekZapPollTallyReceipts, storeZapPollTallyReceipts } from '@/lib/zap-poll-tally-cache' -import { normalizeUrl } from '@/lib/url' -import client, { eventService } from '@/services/client.service' -import { Event, kinds } from 'nostr-tools' -import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react' - -/** Zap receipts for a poll often live on relays hinted on the poll’s `p` tags, not only the global read set. */ -function tallyRelayUrls(meta: TZapPollMeta): string[] { - const seen = new Set() - const out: string[] = [] - const push = (raw: string) => { - const n = normalizeUrl(raw) || raw?.trim() - if (!n || seen.has(n)) return - seen.add(n) - out.push(n) - } - for (const r of meta.recipients) { - push(r.relay) - } - for (const u of [...SEARCHABLE_RELAY_URLS, ...FAST_READ_RELAY_URLS]) { - push(u) - } - return out.slice(0, 28) -} - -function normalizePollHexId(id: string): string | null { - const k = id.trim().toLowerCase() - return /^[0-9a-f]{64}$/.test(k) ? k : null -} - -function dedupeReceipts(lists: Event[]): Event[] { - const byId = new Map() - for (const ev of lists) { - if (!byId.has(ev.id)) byId.set(ev.id, ev) - } - return [...byId.values()] -} - -function seedReceiptsFromSession(pollKey: string): { seeded: Event[]; hadWarmList: boolean } { - const cached = peekZapPollTallyReceipts(pollKey) - const sessionEvs = eventService.getSessionZapReceiptsForTargetEventId(pollKey) - const seeded = dedupeReceipts([...(cached ?? []), ...sessionEvs]) - const hadWarmList = cached !== undefined || sessionEvs.length > 0 - return { seeded, hadWarmList } -} - -export function useZapPollTally(poll: Event, meta: TZapPollMeta | null) { - const [receipts, setReceipts] = useState([]) - const [loading, setLoading] = useState(false) - const [error, setError] = useState(null) - /** Ignore stale fetch results when `poll.id` changes mid-request. */ - const activePollKeyRef = useRef(null) - activePollKeyRef.current = normalizePollHexId(poll.id) - - /** Before paint: session tally cache + session LRU zaps so drawer matches feed immediately. */ - useLayoutEffect(() => { - if (!meta) { - setReceipts([]) - setLoading(false) - setError(null) - return - } - const pollKey = normalizePollHexId(poll.id) - if (!pollKey) { - setLoading(false) - return - } - const { seeded, hadWarmList } = seedReceiptsFromSession(pollKey) - setReceipts(seeded) - setLoading(!hadWarmList && seeded.length === 0) - setError(null) - }, [poll.id, meta]) - - const load = useCallback(async () => { - if (!meta) { - setLoading(false) - return - } - const pollKey = normalizePollHexId(poll.id) - if (!pollKey) { - setLoading(false) - return - } - - const { seeded, hadWarmList } = seedReceiptsFromSession(pollKey) - setReceipts(seeded) - if (!hadWarmList && seeded.length === 0) { - setLoading(true) - } - setError(null) - - try { - const urls = tallyRelayUrls(meta) - const evs = await client.fetchEvents(urls, { - kinds: [kinds.Zap], - '#e': [poll.id], - limit: 500 - }) - if (activePollKeyRef.current !== pollKey) return - const merged = dedupeReceipts([...seeded, ...evs]) - setReceipts(merged) - storeZapPollTallyReceipts(pollKey, merged) - } catch (e) { - if (activePollKeyRef.current !== pollKey) return - if (!hadWarmList && seeded.length === 0) { - setError(e instanceof Error ? e.message : String(e)) - } - } finally { - if (activePollKeyRef.current === pollKey) { - setLoading(false) - } - } - }, [poll.id, meta]) - - useEffect(() => { - if (!meta) return - if (!normalizePollHexId(poll.id)) return - void load() - }, [load, meta, poll.id]) - - const tally = useMemo((): TZapPollTally | null => { - if (!meta) return null - return tallyZapPollFromReceipts(poll, meta, receipts) - }, [poll, meta, receipts]) - - return { receipts, tally, loading, error, reload: load } -} - -export function useZapPollMeta(event: Event) { - return useMemo(() => parseZapPollEvent(event), [event]) -} diff --git a/src/lib/note-renderable-kinds.ts b/src/lib/note-renderable-kinds.ts index 11b1078c..4c155f4e 100644 --- a/src/lib/note-renderable-kinds.ts +++ b/src/lib/note-renderable-kinds.ts @@ -22,7 +22,6 @@ const RENDERABLE_NOTE_KINDS = new Set([ ExtendedKind.CITATION_EXTERNAL, ExtendedKind.CITATION_HARDCOPY, ExtendedKind.CITATION_PROMPT, - ExtendedKind.ZAP_POLL, ExtendedKind.WEB_BOOKMARK ]) diff --git a/src/lib/thread-interaction-req.ts b/src/lib/thread-interaction-req.ts index b06b0a75..ffebb30c 100644 --- a/src/lib/thread-interaction-req.ts +++ b/src/lib/thread-interaction-req.ts @@ -29,7 +29,6 @@ export type BuildThreadInteractionFiltersInput = { */ export function buildThreadInteractionFilters(input: BuildThreadInteractionFiltersInput): Filter[] { const { root, opEventKind, limit } = input - const isZapPoll = opEventKind === ExtendedKind.ZAP_POLL const kindsNoteCommentVoiceZap = sortedUniqueKinds([ kinds.ShortTextNote, @@ -37,17 +36,12 @@ export function buildThreadInteractionFilters(input: BuildThreadInteractionFilte ExtendedKind.VOICE_COMMENT, kinds.Zap ]) - const kindsNoteCommentVoice = sortedUniqueKinds([ - kinds.ShortTextNote, + const kindsPrimaryThread = kindsNoteCommentVoiceZap + const kindsUpperEThread = sortedUniqueKinds([ ExtendedKind.COMMENT, - ExtendedKind.VOICE_COMMENT + ExtendedKind.VOICE_COMMENT, + kinds.Zap ]) - const kindsPrimaryThread = isZapPoll ? kindsNoteCommentVoice : kindsNoteCommentVoiceZap - const kindsUpperEThread = sortedUniqueKinds( - isZapPoll - ? [ExtendedKind.COMMENT, ExtendedKind.VOICE_COMMENT] - : [ExtendedKind.COMMENT, ExtendedKind.VOICE_COMMENT, kinds.Zap] - ) const kindsOnETag = sortedUniqueKinds([ ...kindsPrimaryThread, diff --git a/src/lib/wisp-trending-relay.ts b/src/lib/wisp-trending-relay.ts index 8ac91955..22a9e5d2 100644 --- a/src/lib/wisp-trending-relay.ts +++ b/src/lib/wisp-trending-relay.ts @@ -22,7 +22,6 @@ export const WISP_TRENDING_FEED_KINDS: readonly number[] = [ 1, 6, 1068, - 6969, 30023, ExtendedKind.PICTURE, ExtendedKind.VIDEO, diff --git a/src/lib/zap-poll-tally-cache.ts b/src/lib/zap-poll-tally-cache.ts deleted file mode 100644 index 056a4a8d..00000000 --- a/src/lib/zap-poll-tally-cache.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { Event } from 'nostr-tools' - -/** In-memory: successful tally fetches this tab session (incl. empty tallies). */ -const receiptsByPollId = new Map() - -function cacheKey(pollHexId: string): string | null { - const k = pollHexId.trim().toLowerCase() - return /^[0-9a-f]{64}$/.test(k) ? k : null -} - -export function peekZapPollTallyReceipts(pollHexId: string): Event[] | undefined { - const k = cacheKey(pollHexId) - if (!k || !receiptsByPollId.has(k)) return undefined - return receiptsByPollId.get(k)! -} - -export function storeZapPollTallyReceipts(pollHexId: string, receipts: Event[]) { - const k = cacheKey(pollHexId) - if (k) receiptsByPollId.set(k, receipts) -} diff --git a/src/lib/zap-poll.ts b/src/lib/zap-poll.ts deleted file mode 100644 index 6faaccbe..00000000 --- a/src/lib/zap-poll.ts +++ /dev/null @@ -1,420 +0,0 @@ -import { ExtendedKind, FAST_READ_RELAY_URLS } from '@/constants' -import { getAmountFromInvoice } from '@/lib/lightning' -import { userIdToPubkey } from '@/lib/pubkey' -import { tagNameEquals } from '@/lib/tag' -import { normalizeUrl } from '@/lib/url' -import type { Event, EventTemplate } from 'nostr-tools' -import { kinds } from 'nostr-tools' - -export type TZapPollOption = { index: number; label: string } - -export type TZapPollMeta = { - options: TZapPollOption[] - recipients: { pubkey: string; relay: string }[] - valueMinimum?: number - valueMaximum?: number - consensusThreshold?: number - closedAt?: number - primaryRelay: string -} - -/** `wss` / `ws` relay URL on `r` or `relay` tags (Primal megaFeed / zap vote relay list). */ -function firstWsRelayFromEventTags(tags: string[][]): string | undefined { - for (const t of tags) { - const u = t[1]?.trim() - if (!u || (t[0] !== 'r' && t[0] !== 'relay')) continue - if (u.startsWith('wss://') || u.startsWith('ws://')) { - return normalizeUrl(u) || u - } - } - return undefined -} - -/** - * Relay hint on a `p` tag: Primal web publishes `['p', pubkey, relay]`; `zapVote` also reads index 3. - * Only treat values that look like relay URLs as relays (pubkey-only `p` tags stay pubkey-no-relay). - */ -function relayHintFromPTag(t: string[]): string | undefined { - for (const i of [2, 3] as const) { - const c = t[i]?.trim() - if (!c || c === 'mention') continue - if (c.startsWith('wss://') || c.startsWith('ws://')) { - return normalizeUrl(c) || c - } - } - return undefined -} - -function defaultZapPollReadRelay(tags: string[][]): string { - return ( - firstWsRelayFromEventTags(tags) ?? - FAST_READ_RELAY_URLS[0] ?? - 'wss://relay.damus.io' - ) -} - -/** Parse NIP-B9 kind 6969 into structured metadata. */ -export function parseZapPollEvent(event: Event): TZapPollMeta | null { - if (event.kind !== ExtendedKind.ZAP_POLL) return null - const tags = event.tags - const authorPk = event.pubkey.trim().toLowerCase() - if (!/^[0-9a-f]{64}$/.test(authorPk)) return null - - const hintRelay = firstWsRelayFromEventTags(tags) - const fallbackRelay = hintRelay ?? defaultZapPollReadRelay(tags) - - const pTags = tags.filter(tagNameEquals('p')) - const recipients: { pubkey: string; relay: string }[] = [] - const withRelay: { pubkey: string; relay: string }[] = [] - const pubkeyNoRelay: string[] = [] - for (const t of pTags) { - const pk = t[1]?.trim().toLowerCase() - if (!pk || !/^[0-9a-f]{64}$/.test(pk)) continue - const relay = relayHintFromPTag(t) - if (relay) { - withRelay.push({ pubkey: pk, relay }) - } else { - pubkeyNoRelay.push(pk) - } - } - - if (withRelay.length > 0) { - recipients.push(...withRelay) - const primary = withRelay[0]!.relay - for (const pk of pubkeyNoRelay) { - if (!recipients.some((r) => r.pubkey === pk)) { - recipients.push({ pubkey: pk, relay: primary }) - } - } - } else if (pubkeyNoRelay.length > 0) { - for (const pk of pubkeyNoRelay) { - if (!recipients.some((r) => r.pubkey === pk)) { - recipients.push({ pubkey: pk, relay: fallbackRelay }) - } - } - } else { - // Primal: no `p` on poll → zap the poll author (see primal-web-app src/lib/zap.ts zapVote). - recipients.push({ pubkey: authorPk, relay: fallbackRelay }) - } - - const options: TZapPollOption[] = [] - for (const t of tags) { - const name = t[0] - // `poll_option` everywhere in megaFeed; some paths used `option` (same shape). - if ((name !== 'poll_option' && name !== 'option') || t[1] == null || t[2] == null) continue - const idx = parseInt(String(t[1]), 10) - if (Number.isNaN(idx)) continue - options.push({ index: idx, label: t[2] }) - } - options.sort((a, b) => a.index - b.index) - if (options.length < 2) return null - - const vmin = tags.find(tagNameEquals('value_minimum'))?.[1] - const vmax = tags.find(tagNameEquals('value_maximum'))?.[1] - const consensus = tags.find(tagNameEquals('consensus_threshold'))?.[1] - const closed = tags.find(tagNameEquals('closed_at'))?.[1] - - const valueMinimum = vmin != null && vmin !== '' ? parseInt(vmin, 10) : undefined - const valueMaximum = vmax != null && vmax !== '' ? parseInt(vmax, 10) : undefined - let consensusThreshold = - consensus != null && consensus !== '' ? parseInt(consensus, 10) : undefined - if (consensusThreshold === 0) consensusThreshold = undefined - - let closedAt = closed != null && closed !== '' ? parseInt(closed, 10) : undefined - if (closedAt != null && closedAt <= event.created_at) closedAt = undefined - - return { - options, - recipients, - valueMinimum: Number.isFinite(valueMinimum) ? valueMinimum : undefined, - valueMaximum: Number.isFinite(valueMaximum) ? valueMaximum : undefined, - consensusThreshold: Number.isFinite(consensusThreshold) ? consensusThreshold : undefined, - closedAt: Number.isFinite(closedAt) ? closedAt : undefined, - primaryRelay: recipients[0]!.relay - } -} - -export function isZapPollPastDeadline(_poll: Event, meta: TZapPollMeta, nowSec = Math.floor(Date.now() / 1000)): boolean { - if (!meta.closedAt) return false - return nowSec > meta.closedAt -} - -export function isZapPollVoteEligible( - poll: Event, - meta: TZapPollMeta, - voterPubkey: string, - amountSats: number -): { ok: true } | { ok: false; reason: string } { - const v = voterPubkey.trim().toLowerCase() - if (v === poll.pubkey) return { ok: false, reason: 'Poll authors cannot vote on their own poll' } - if (meta.closedAt && Math.floor(Date.now() / 1000) > meta.closedAt) { - return { ok: false, reason: 'Poll is closed' } - } - if (meta.valueMinimum != null && amountSats < meta.valueMinimum) { - return { ok: false, reason: `Minimum ${meta.valueMinimum} sats` } - } - if (meta.valueMaximum != null && amountSats > meta.valueMaximum) { - return { ok: false, reason: `Maximum ${meta.valueMaximum} sats` } - } - return { ok: true } -} - -/** Build kind 9734 template for a NIP-B9 vote (after validation). */ -export function buildZapPollVoteRequestTemplate(params: { - poll: Event - meta: TZapPollMeta - recipientPubkey: string - optionIndex: number - amountMillisats: number - relays: string[] - comment?: string -}): EventTemplate { - const { poll, meta, recipientPubkey, optionIndex, amountMillisats, relays, comment } = params - const relay = meta.primaryRelay - const pk = recipientPubkey.trim().toLowerCase() - const tags: string[][] = [ - ['p', pk, relay], - ['e', poll.id, relay], - ['relays', ...relays], - ['amount', String(amountMillisats)], - ['k', '6969'], - ['poll_option', String(optionIndex)] - ] - return { - kind: ExtendedKind.ZAP_REQUEST, - created_at: Math.round(Date.now() / 1000), - content: comment ?? '', - tags - } -} - -export type TZapPollTally = { - satsByOption: Map - totalSats: number - receiptCountByOption: Map -} - -function getPollOptionFromZapRequestTags(tags: unknown): number | undefined { - if (!Array.isArray(tags)) return undefined - const po = (tags as string[][]).find((t) => t[0] === 'poll_option' && t[1] != null) - if (!po) return undefined - const n = parseInt(String(po[1]), 10) - return Number.isNaN(n) ? undefined : n -} - -function getKindFromZapRequestTags(tags: unknown): string | undefined { - if (!Array.isArray(tags)) return undefined - const k = (tags as string[][]).find((t) => t[0] === 'k' && t[1] != null && String(t[1]).length > 0) - if (!k) return undefined - return String(k[1]) -} - -/** - * NIP-57 `k` is often missing; some clients wrongly send `1` when zapping a poll. - * We only reject kinds that clearly point at another event class (not exhaustive). - */ -function zapTargetKindAllowsPollTally(tags: string[][] | undefined): boolean { - const k = getKindFromZapRequestTags(tags) - if (k == null || k === '') return true - if (k === '6969' || k === String(ExtendedKind.ZAP_POLL)) return true - if (k === '1' || k === String(kinds.ShortTextNote)) return true - return false -} - -function normalizeZapRequestPTagPubkey(raw: string | undefined): string | undefined { - if (!raw) return undefined - const pk = userIdToPubkey(raw).trim().toLowerCase() - return /^[0-9a-f]{64}$/.test(pk) ? pk : undefined -} - -/** Every `p` on the embedded zap request (some clients put author first, LN recipient second). */ -function zapRequestPayeePubkeys(tags: string[][] | undefined): string[] { - if (!tags) return [] - const out: string[] = [] - const seen = new Set() - for (const t of tags) { - if (t[0] !== 'p' || !t[1]) continue - const pk = normalizeZapRequestPTagPubkey(t[1]) - if (!pk || seen.has(pk)) continue - seen.add(pk) - out.push(pk) - } - return out -} - -/** - * Resolve vote option: explicit `poll_option` tag, or infer from which poll candidate (`p`) was paid. - * Matches clients (e.g. Primal) that omit `poll_option` but pay the option’s pubkey. - */ -export function extractVoteOptionFromZapRequest( - poll: Event, - meta: TZapPollMeta, - tags: string[][] | undefined -): number | undefined { - const payees = zapRequestPayeePubkeys(tags) - if (payees.length === 0) return undefined - const payeeSet = new Set(payees) - const pollAuthor = poll.pubkey.trim().toLowerCase() - const paidAuthor = payeeSet.has(pollAuthor) - const hasCandidatePayee = meta.recipients.some((r) => payeeSet.has(r.pubkey)) - - const explicit = getPollOptionFromZapRequestTags(tags) - const explicitOk = - explicit !== undefined && meta.options.some((o) => o.index === explicit) ? explicit : undefined - if (explicitOk !== undefined && (paidAuthor || hasCandidatePayee)) { - return explicitOk - } - - const j = meta.recipients.findIndex((r) => payeeSet.has(r.pubkey)) - if (j < 0 || j >= meta.options.length) return undefined - return meta.options[j]!.index -} - -/** - * Tally NIP-B9 results from zap receipts (kind 9735) per NIP-B9 rules (sats only). - */ -export function tallyZapPollFromReceipts(poll: Event, meta: TZapPollMeta, receipts: Event[]): TZapPollTally { - const satsByOption = new Map() - const receiptCountByOption = new Map() - const equalMinMax = - meta.valueMinimum != null && - meta.valueMaximum != null && - meta.valueMinimum === meta.valueMaximum - const oneVotePerOptionPerUser = equalMinMax - const seenUserOption = new Set() - - let totalSats = 0 - - for (const opt of meta.options) { - satsByOption.set(opt.index, 0) - receiptCountByOption.set(opt.index, 0) - } - - for (const r of receipts) { - if (r.kind !== kinds.Zap) continue - const desc = r.tags.find(tagNameEquals('description'))?.[1] - if (!desc) continue - let zapReq: { pubkey?: string; tags?: string[][] } - try { - zapReq = JSON.parse(desc) as { pubkey?: string; tags?: string[][] } - } catch { - continue - } - if (!zapTargetKindAllowsPollTally(zapReq.tags)) continue - const eTag = zapReq.tags?.find((t) => t[0] === 'e' && t[1]) - if (!eTag || eTag[1] !== poll.id) continue - const voterPk = (zapReq.pubkey ?? '').trim().toLowerCase() - if (!voterPk || voterPk === poll.pubkey) continue - const optIdx = extractVoteOptionFromZapRequest(poll, meta, zapReq.tags) - if (optIdx === undefined || !satsByOption.has(optIdx)) continue - - const bolt11 = r.tags.find(tagNameEquals('bolt11'))?.[1] - if (!bolt11) continue - let amountSats: number - try { - amountSats = getAmountFromInvoice(bolt11) - } catch { - continue - } - if (!Number.isFinite(amountSats) || amountSats <= 0) continue - - if (meta.valueMaximum != null && amountSats > meta.valueMaximum) continue - if (meta.valueMinimum != null && amountSats < meta.valueMinimum) continue - - if (meta.closedAt != null) { - if (r.created_at < poll.created_at || r.created_at > meta.closedAt) continue - } - - if (oneVotePerOptionPerUser) { - const key = `${voterPk}:${optIdx}` - if (seenUserOption.has(key)) continue - seenUserOption.add(key) - } - - satsByOption.set(optIdx, (satsByOption.get(optIdx) ?? 0) + amountSats) - receiptCountByOption.set(optIdx, (receiptCountByOption.get(optIdx) ?? 0) + 1) - totalSats += amountSats - } - - return { satsByOption, totalSats, receiptCountByOption } -} - -export function userHasZappedPoll( - pollId: string, - userPubkey: string, - receipts: Event[] -): boolean { - const pk = userPubkey.trim().toLowerCase() - for (const r of receipts) { - if (r.kind !== kinds.Zap) continue - const desc = r.tags.find(tagNameEquals('description'))?.[1] - if (!desc) continue - try { - const zapReq = JSON.parse(desc) as { pubkey?: string; tags?: string[][] } - const eTag = zapReq.tags?.find((t) => t[0] === 'e' && t[1]) - if (eTag?.[1] !== pollId) continue - if ((zapReq.pubkey ?? '').trim().toLowerCase() === pk) return true - const pSender = r.tags.find(tagNameEquals('P'))?.[1] - if (pSender && pSender.trim().toLowerCase() === pk) return true - } catch { - continue - } - } - return false -} - -export function userZapPollVoteOption( - poll: Event, - meta: TZapPollMeta, - userPubkey: string, - receipts: Event[] -): number | undefined { - const pk = userPubkey.trim().toLowerCase() - for (const r of receipts) { - if (r.kind !== kinds.Zap) continue - const desc = r.tags.find(tagNameEquals('description'))?.[1] - if (!desc) continue - try { - const zapReq = JSON.parse(desc) as { pubkey?: string; tags?: string[][] } - if (!zapTargetKindAllowsPollTally(zapReq.tags)) continue - const eTag = zapReq.tags?.find((t) => t[0] === 'e' && t[1]) - if (eTag?.[1] !== poll.id) continue - if ((zapReq.pubkey ?? '').trim().toLowerCase() !== pk) continue - return extractVoteOptionFromZapRequest(poll, meta, zapReq.tags) - } catch { - continue - } - } - return undefined -} - -/** Receipts where user is the zapper and zap request looks like a vote on some event (kind 6969 or unspecified `k`). */ -export function filterZapPollVoteReceiptsForVoter(receipts: Event[], profilePubkey: string): Event[] { - const pk = profilePubkey.trim().toLowerCase() - return receipts.filter((r) => { - if (r.kind !== kinds.Zap) return false - const pSender = r.tags.find(tagNameEquals('P'))?.[1]?.trim().toLowerCase() - if (pSender !== pk) return false - const desc = r.tags.find(tagNameEquals('description'))?.[1] - if (!desc) return false - try { - const zapReq = JSON.parse(desc) as { tags?: string[][] } - if (!zapReq.tags?.some((t) => t[0] === 'e' && t[1])) return false - return zapTargetKindAllowsPollTally(zapReq.tags) - } catch { - return false - } - }) -} - -export function getPollIdFromZapReceipt(receipt: Event): string | undefined { - const desc = receipt.tags.find(tagNameEquals('description'))?.[1] - if (!desc) return undefined - try { - const zapReq = JSON.parse(desc) as { tags?: string[][] } - const eTag = zapReq.tags?.find((t) => t[0] === 'e' && t[1]) - return eTag?.[1] - } catch { - return undefined - } -} diff --git a/src/pages/secondary/NotePage/index.tsx b/src/pages/secondary/NotePage/index.tsx index 332a60a3..0a6465d9 100644 --- a/src/pages/secondary/NotePage/index.tsx +++ b/src/pages/secondary/NotePage/index.tsx @@ -256,8 +256,6 @@ const NotePage = forwardRef(({ id, index, hideTitlebar = false, initialEvent }: return 'Note: Highlight' case 1068: // ExtendedKind.POLL return 'Note: Poll' - case 6969: // ExtendedKind.ZAP_POLL - return 'Note: Zap Poll' case 31987: // ExtendedKind.RELAY_REVIEW return 'Note: Relay Review' case 31922: // ExtendedKind.CALENDAR_EVENT_DATE diff --git a/src/services/lightning.service.ts b/src/services/lightning.service.ts index e1c058d9..e30ac4bd 100644 --- a/src/services/lightning.service.ts +++ b/src/services/lightning.service.ts @@ -1,10 +1,5 @@ import { FAST_READ_RELAY_URLS, FAST_WRITE_RELAY_URLS, CODY_PUBKEY, IMWALD_MAINTAINER_PUBKEY } from '@/constants' import { getZapInfoFromEvent } from '@/lib/event-metadata' -import { - buildZapPollVoteRequestTemplate, - isZapPollVoteEligible, - type TZapPollMeta -} from '@/lib/zap-poll' import { TProfile } from '@/types' import { init, launchPaymentModal } from '@getalby/bitcoin-connect-react' import { Invoice } from '@getalby/lightning-tools' @@ -164,133 +159,6 @@ class LightningService { }) } - /** NIP-B9: pay-to-vote on a zap poll (kind 6969). */ - async zapPollVote( - sender: string, - pollEvent: NostrEvent, - meta: TZapPollMeta, - recipientPubkey: string, - optionIndex: number, - sats: number, - comment: string, - closeOuterModel?: () => void, - includePublicReceipt: boolean = storage.getIncludePublicZapReceipt() - ): Promise<{ preimage: string; invoice: string } | null> { - if (!client.signer) { - throw new Error('You need to be logged in to zap') - } - const eligible = isZapPollVoteEligible(pollEvent, meta, sender, sats) - if (!eligible.ok) { - throw new Error(eligible.reason) - } - const rec = recipientPubkey.trim().toLowerCase() - if (!meta.recipients.some((r) => r.pubkey === rec)) { - throw new Error('Recipient is not a poll payout pubkey') - } - - const [profile, senderRelayList] = await Promise.all([ - (async () => { - const profileEvent = await replaceableEventService.fetchReplaceableEvent(rec, kinds.Metadata) - return profileEvent ? getProfileFromEvent(profileEvent) : undefined - })(), - client.fetchRelayList(sender) - ]) - if (!profile) { - throw new Error('Recipient not found') - } - const zapEndpoint = await this.getZapEndpoint(profile) - if (!zapEndpoint) { - throw new Error("Recipient's lightning address is invalid") - } - const { callback, lnurl } = zapEndpoint - const amount = sats * 1000 - const zapRelays = includePublicReceipt - ? senderRelayList.write.slice(0, 4).concat(FAST_READ_RELAY_URLS) - : [] - const zapRequestDraft = buildZapPollVoteRequestTemplate({ - poll: pollEvent, - meta, - recipientPubkey: rec, - optionIndex, - amountMillisats: amount, - relays: zapRelays, - comment - }) - const zapRequest = await client.signer.signEvent(zapRequestDraft) - const zapRequestRes = await fetchWithTimeout( - `${callback}?amount=${amount}&nostr=${encodeURI(JSON.stringify(zapRequest))}&lnurl=${lnurl}`, - { timeoutMs: 25_000 } - ) - const zapRequestResBody = await zapRequestRes.json() - if (zapRequestResBody.error) { - throw new Error(zapRequestResBody.message) - } - const { pr, verify, reason } = zapRequestResBody - if (!pr) { - throw new Error(reason ?? 'Failed to create invoice') - } - - if (this.provider) { - const { preimage } = await this.provider.sendPayment(pr) - closeOuterModel?.() - return { preimage, invoice: pr } - } - - return new Promise((resolve) => { - runAfterReleasingRadixScrollLock(closeOuterModel, () => { - let checkPaymentInterval: ReturnType | undefined - let subCloser: SubCloser | undefined - const { setPaid } = launchPaymentModal({ - invoice: pr, - onPaid: (response) => { - clearInterval(checkPaymentInterval) - subCloser?.close() - resolve({ preimage: response.preimage, invoice: pr }) - }, - onCancelled: () => { - clearInterval(checkPaymentInterval) - subCloser?.close() - resolve(null) - } - }) - - if (verify) { - checkPaymentInterval = setInterval(async () => { - const invoice = new Invoice({ pr, verify }) - const paid = await invoice.verifyPayment() - - if (paid && invoice.preimage) { - setPaid({ - preimage: invoice.preimage - }) - } - }, 1000) - } else { - const filter: Filter = { - kinds: [kinds.Zap], - '#p': [rec], - '#e': [pollEvent.id], - since: dayjs().subtract(1, 'minute').unix() - } - subCloser = client.subscribe( - senderRelayList.write.concat(FAST_READ_RELAY_URLS).slice(0, 4), - filter, - { - onevent: (evt) => { - const info = getZapInfoFromEvent(evt) - if (!info) return - - if (info.invoice === pr) { - setPaid({ preimage: info.preimage ?? '' }) - } - } - } - ) - } - }) - }) - } - async payInvoice( invoice: string, closeOuterModel?: () => void diff --git a/src/services/local-storage.service.ts b/src/services/local-storage.service.ts index ffc664f9..48212f27 100644 --- a/src/services/local-storage.service.ts +++ b/src/services/local-storage.service.ts @@ -320,11 +320,6 @@ class LocalStorageService { } } } - if (showKindsVersion < 10) { - if (showKinds.includes(ExtendedKind.POLL) && !showKinds.includes(ExtendedKind.ZAP_POLL)) { - showKinds.push(ExtendedKind.ZAP_POLL) - } - } if (showKindsVersion < 11) { if (!showKinds.includes(ExtendedKind.GIT_RELEASE)) { showKinds.push(ExtendedKind.GIT_RELEASE) diff --git a/src/services/mention-event-search.service.ts b/src/services/mention-event-search.service.ts index ffb5b6fa..87729ff1 100644 --- a/src/services/mention-event-search.service.ts +++ b/src/services/mention-event-search.service.ts @@ -26,7 +26,6 @@ export const NEVENT_KINDS = [ ExtendedKind.PICTURE, ...NIP71_VIDEO_KINDS, ExtendedKind.POLL, - ExtendedKind.ZAP_POLL, ExtendedKind.COMMENT, ExtendedKind.VOICE, ExtendedKind.VOICE_COMMENT,