diff --git a/src/components/ContentPreview/ZapPreview.tsx b/src/components/ContentPreview/ZapPreview.tsx
new file mode 100644
index 0000000..329b649
--- /dev/null
+++ b/src/components/ContentPreview/ZapPreview.tsx
@@ -0,0 +1,54 @@
+import { useFetchEvent } from '@/hooks'
+import { getZapInfoFromEvent } from '@/lib/event-metadata'
+import { formatAmount } from '@/lib/lightning'
+import { cn } from '@/lib/utils'
+import { Zap } from 'lucide-react'
+import { Event } from 'nostr-tools'
+import { useMemo } from 'react'
+import { useTranslation } from 'react-i18next'
+import Username from '../Username'
+
+export default function ZapPreview({ event, className }: { event: Event; className?: string }) {
+ const { t } = useTranslation()
+ const zapInfo = useMemo(() => getZapInfoFromEvent(event), [event])
+ const { event: targetEvent } = useFetchEvent(zapInfo?.eventId)
+
+ if (!zapInfo || !zapInfo.senderPubkey || !zapInfo.amount) {
+ return (
+
+ [{t('Invalid zap receipt')}]
+
+ )
+ }
+
+ const { senderPubkey, recipientPubkey, amount, comment } = zapInfo
+
+ return (
+
+
+
+
+
+ {t('zapped')}
+ {recipientPubkey && recipientPubkey !== senderPubkey && (
+
+ )}
+
+
+ {formatAmount(amount)} {t('sats')}
+
+ {comment && (
+
+ {comment}
+
+ )}
+ {targetEvent && (
+
+ {t('on note')} {targetEvent.id.substring(0, 8)}...
+
+ )}
+
+
+ )
+}
+
diff --git a/src/components/ContentPreview/index.tsx b/src/components/ContentPreview/index.tsx
index 2c90fac..8b4b8bd 100644
--- a/src/components/ContentPreview/index.tsx
+++ b/src/components/ContentPreview/index.tsx
@@ -15,6 +15,7 @@ import NormalContentPreview from './NormalContentPreview'
import PictureNotePreview from './PictureNotePreview'
import PollPreview from './PollPreview'
import VideoNotePreview from './VideoNotePreview'
+import ZapPreview from './ZapPreview'
import DiscussionNote from '../DiscussionNote'
export default function ContentPreview({
@@ -106,5 +107,9 @@ export default function ContentPreview({
return
}
+ if (event.kind === ExtendedKind.ZAP_REQUEST || event.kind === ExtendedKind.ZAP_RECEIPT) {
+ return
+ }
+
return [{t('Cannot handle event of kind k', { k: event.kind })}]
}
diff --git a/src/components/KindFilter/index.tsx b/src/components/KindFilter/index.tsx
index 041af8a..78d146c 100644
--- a/src/components/KindFilter/index.tsx
+++ b/src/components/KindFilter/index.tsx
@@ -21,7 +21,8 @@ const KIND_FILTER_OPTIONS = [
{ kindGroup: [ExtendedKind.VOICE, ExtendedKind.VOICE_COMMENT], label: 'Voice Posts' },
{ kindGroup: [ExtendedKind.PICTURE], label: 'Photo Posts' },
{ kindGroup: [ExtendedKind.VIDEO, ExtendedKind.SHORT_VIDEO], label: 'Video Posts' },
- { kindGroup: [ExtendedKind.DISCUSSION], label: 'Discussions' }
+ { kindGroup: [ExtendedKind.DISCUSSION], label: 'Discussions' },
+ { kindGroup: [ExtendedKind.ZAP_RECEIPT], label: 'Zaps' }
]
export default function KindFilter({
diff --git a/src/components/Note/Zap.tsx b/src/components/Note/Zap.tsx
new file mode 100644
index 0000000..a238935
--- /dev/null
+++ b/src/components/Note/Zap.tsx
@@ -0,0 +1,80 @@
+import { useFetchEvent } from '@/hooks'
+import { getZapInfoFromEvent } from '@/lib/event-metadata'
+import { formatAmount } from '@/lib/lightning'
+import { toNote, toProfile } from '@/lib/link'
+import { cn } from '@/lib/utils'
+import { Zap as ZapIcon } from 'lucide-react'
+import { Event } from 'nostr-tools'
+import { useMemo } from 'react'
+import { useTranslation } from 'react-i18next'
+import { useSecondaryPage } from '@/PageManager'
+import Username from '../Username'
+import UserAvatar from '../UserAvatar'
+
+export default function Zap({ event, className }: { event: Event; className?: string }) {
+ const { t } = useTranslation()
+ const { push } = useSecondaryPage()
+ const zapInfo = useMemo(() => getZapInfoFromEvent(event), [event])
+ const { event: targetEvent } = useFetchEvent(zapInfo?.eventId)
+
+ if (!zapInfo || !zapInfo.senderPubkey || !zapInfo.amount) {
+ return (
+
+ [{t('Invalid zap receipt')}]
+
+ )
+ }
+
+ const { senderPubkey, recipientPubkey, amount, comment } = zapInfo
+
+ return (
+
+ {/* Zapped note/profile link in top-right corner */}
+ {(targetEvent || recipientPubkey) && (
+
+ )}
+
+
+
+
+
+
+
+ {t('zapped')}
+ {recipientPubkey && recipientPubkey !== senderPubkey && (
+ <>
+
+
+ >
+ )}
+
+
+
+ {formatAmount(amount)}
+
+
+ {t('sats')}
+
+
+ {comment && (
+
+ {comment}
+
+ )}
+
+
+
+ )
+}
+
diff --git a/src/components/Note/index.tsx b/src/components/Note/index.tsx
index a169818..f5dfd47 100644
--- a/src/components/Note/index.tsx
+++ b/src/components/Note/index.tsx
@@ -32,6 +32,7 @@ import Poll from './Poll'
import UnknownNote from './UnknownNote'
import VideoNote from './VideoNote'
import RelayReview from './RelayReview'
+import Zap from './Zap'
export default function Note({
event,
@@ -67,7 +68,9 @@ export default function Note({
kinds.CommunityDefinition,
kinds.LiveEvent,
ExtendedKind.GROUP_METADATA,
- ExtendedKind.PUBLIC_MESSAGE
+ ExtendedKind.PUBLIC_MESSAGE,
+ ExtendedKind.ZAP_REQUEST,
+ ExtendedKind.ZAP_RECEIPT
].includes(event.kind)
) {
content =
@@ -115,6 +118,8 @@ export default function Note({
content =
} else if (event.kind === ExtendedKind.PUBLIC_MESSAGE) {
content =
+ } else if (event.kind === ExtendedKind.ZAP_REQUEST || event.kind === ExtendedKind.ZAP_RECEIPT) {
+ content =
} else {
content =
}
diff --git a/src/components/NoteList/index.tsx b/src/components/NoteList/index.tsx
index 5e79512..0cd8c8c 100644
--- a/src/components/NoteList/index.tsx
+++ b/src/components/NoteList/index.tsx
@@ -6,16 +6,18 @@ import {
isReplaceableEvent,
isReplyNoteEvent
} from '@/lib/event'
+import { getZapInfoFromEvent } from '@/lib/event-metadata'
import { isTouchDevice } from '@/lib/utils'
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
import { useDeletedEvent } from '@/providers/DeletedEventProvider'
import { useMuteList } from '@/providers/MuteListProvider'
import { useNostr } from '@/providers/NostrProvider'
import { useUserTrust } from '@/providers/UserTrustProvider'
+import { useZap } from '@/providers/ZapProvider'
import client from '@/services/client.service'
import { TFeedSubRequest } from '@/types'
import dayjs from 'dayjs'
-import { Event } from 'nostr-tools'
+import { Event, kinds } from 'nostr-tools'
import {
forwardRef,
useCallback,
@@ -61,6 +63,7 @@ const NoteList = forwardRef(
const { mutePubkeySet } = useMuteList()
const { hideContentMentioningMutedUsers } = useContentPolicy()
const { isEventDeleted } = useDeletedEvent()
+ const { zapReplyThreshold } = useZap()
const [events, setEvents] = useState([])
const [newEvents, setNewEvents] = useState([])
const [hasMore, setHasMore] = useState(true)
@@ -75,7 +78,24 @@ const NoteList = forwardRef(
const shouldHideEvent = useCallback(
(evt: Event) => {
if (isEventDeleted(evt)) return true
- if (hideReplies && isReplyNoteEvent(evt)) return true
+
+ // Special handling for zaps - always check threshold, then check hideReplies for non-zap replies
+ if (evt.kind === kinds.Zap) {
+ console.log(`[NoteList] Processing zap ${evt.id.slice(0, 8)}: isReply=${isReplyNoteEvent(evt)}, hideReplies=${hideReplies}`)
+ const zapInfo = getZapInfoFromEvent(evt)
+ console.log(`[NoteList] Zap ${evt.id.slice(0, 8)}: amount=${zapInfo?.amount} sats, threshold=${zapReplyThreshold}`)
+
+ // Always filter zaps by threshold regardless of hideReplies setting
+ if (zapInfo && zapInfo.amount < zapReplyThreshold) {
+ console.log(`[NoteList] HIDING zap ${evt.id.slice(0, 8)}: ${zapInfo.amount} < ${zapReplyThreshold} (threshold filter)`)
+ return true
+ } else {
+ console.log(`[NoteList] SHOWING zap ${evt.id.slice(0, 8)}: ${zapInfo?.amount} >= ${zapReplyThreshold}`)
+ }
+ } else if (hideReplies && isReplyNoteEvent(evt)) {
+ return true
+ }
+
if (hideUntrustedNotes && !isUserTrusted(evt.pubkey)) return true
if (filterMutedNotes && mutePubkeySet.has(evt.pubkey)) return true
if (
@@ -88,7 +108,7 @@ const NoteList = forwardRef(
return false
},
- [hideReplies, hideUntrustedNotes, mutePubkeySet, isEventDeleted]
+ [hideReplies, hideUntrustedNotes, mutePubkeySet, isEventDeleted, zapReplyThreshold]
)
const filteredEvents = useMemo(() => {
diff --git a/src/components/ReplyNoteList/index.tsx b/src/components/ReplyNoteList/index.tsx
index e587d83..e187948 100644
--- a/src/components/ReplyNoteList/index.tsx
+++ b/src/components/ReplyNoteList/index.tsx
@@ -9,6 +9,7 @@ import {
isReplaceableEvent,
isReplyNoteEvent
} from '@/lib/event'
+import { getZapInfoFromEvent } from '@/lib/event-metadata'
import { toNote } from '@/lib/link'
import { generateBech32IdFromETag, tagNameEquals } from '@/lib/tag'
import { useSecondaryPage } from '@/PageManager'
@@ -17,6 +18,7 @@ import { useMuteList } from '@/providers/MuteListProvider'
import { useNostr } from '@/providers/NostrProvider'
import { useReply } from '@/providers/ReplyProvider'
import { useUserTrust } from '@/providers/UserTrustProvider'
+import { useZap } from '@/providers/ZapProvider'
import client from '@/services/client.service'
import noteStatsService from '@/services/note-stats.service'
import { Filter, Event as NEvent, kinds } from 'nostr-tools'
@@ -33,7 +35,7 @@ type TRootInfo =
const LIMIT = 100
const SHOW_COUNT = 10
-export default function ReplyNoteList({ index, event, sort = 'oldest' }: { index?: number; event: NEvent; sort?: 'newest' | 'oldest' | 'top' | 'controversial' | 'most-zapped' }) {
+function ReplyNoteList({ index, event, sort = 'oldest' }: { index?: number; event: NEvent; sort?: 'newest' | 'oldest' | 'top' | 'controversial' | 'most-zapped' }) {
const { t } = useTranslation()
const { push, currentIndex } = useSecondaryPage()
const { hideUntrustedInteractions, isUserTrusted } = useUserTrust()
@@ -42,6 +44,8 @@ export default function ReplyNoteList({ index, event, sort = 'oldest' }: { index
const { relayList: userRelayList } = useNostr()
const [rootInfo, setRootInfo] = useState(undefined)
const { repliesMap, addReplies } = useReply()
+ const [zapEvents, setZapEvents] = useState([])
+ const { zapReplyThreshold } = useZap()
// Helper function to get vote score for a reply
const getReplyVoteScore = (reply: NEvent) => {
@@ -83,6 +87,10 @@ export default function ReplyNoteList({ index, event, sort = 'oldest' }: { index
return totalAmount
}
const replies = useMemo(() => {
+ console.log(`[ReplyNoteList] Processing replies for event ${event.id.slice(0, 8)}...`)
+ console.log(`[ReplyNoteList] zapEvents.length: ${zapEvents.length}`)
+ console.log(`[ReplyNoteList] zapReplyThreshold: ${zapReplyThreshold}`)
+
const replyIdSet = new Set()
const replyEvents: NEvent[] = []
const currentEventKey = isReplaceableEvent(event.kind)
@@ -101,6 +109,46 @@ export default function ReplyNoteList({ index, event, sort = 'oldest' }: { index
})
parentEventKeys = events.map((evt) => evt.id)
}
+
+ // Add zap receipts that are above the threshold
+ console.log(`========== ZAP FILTERING START ==========`)
+ console.log(`Processing ${zapEvents.length} zap events with threshold ${zapReplyThreshold} sats`)
+ zapEvents.forEach((zapEvt) => {
+ console.log(`\n--- Processing zap: ${zapEvt.id.slice(0, 8)}... ---`)
+ console.log(`Created: ${new Date(zapEvt.created_at * 1000).toISOString()}`)
+
+ if (replyIdSet.has(zapEvt.id)) {
+ console.log(`❌ Already in set, skipping`)
+ return
+ }
+ if (mutePubkeySet.has(zapEvt.pubkey)) {
+ console.log(`❌ From muted user, skipping`)
+ return
+ }
+
+ const zapInfo = getZapInfoFromEvent(zapEvt)
+
+ if (!zapInfo) {
+ console.log(`❌ No valid zapInfo`)
+ return
+ }
+
+ console.log(`💰 Zap amount: ${zapInfo.amount} sats`)
+ console.log(`🎯 Threshold: ${zapReplyThreshold} sats`)
+ console.log(`🔢 Comparison: ${zapInfo.amount} >= ${zapReplyThreshold} = ${zapInfo.amount >= zapReplyThreshold}`)
+
+ if (zapInfo.amount >= zapReplyThreshold) {
+ console.log(`✅ PASSED - Adding to replies`)
+ replyIdSet.add(zapEvt.id)
+ replyEvents.push(zapEvt)
+ } else {
+ console.log(`❌ FILTERED OUT - ${zapInfo.amount} < ${zapReplyThreshold}`)
+ }
+ })
+ console.log(`\n========== ZAP FILTERING END ==========`)
+ console.log(`Total zaps that passed: ${replyEvents.filter(e => e.kind === kinds.Zap).length}`)
+ console.log(`Total reply events: ${replyEvents.length}`)
+
// Apply sorting based on the sort parameter
switch (sort) {
case 'oldest':
@@ -140,7 +188,7 @@ export default function ReplyNoteList({ index, event, sort = 'oldest' }: { index
default:
return replyEvents.sort((a, b) => b.created_at - a.created_at)
}
- }, [event.id, repliesMap, sort])
+ }, [event.id, repliesMap, zapEvents, zapReplyThreshold, mutePubkeySet, hideContentMentioningMutedUsers, sort])
const [timelineKey, setTimelineKey] = useState(undefined)
const [until, setUntil] = useState(undefined)
const [loading, setLoading] = useState(false)
@@ -215,16 +263,18 @@ export default function ReplyNoteList({ index, event, sort = 'oldest' }: { index
useEffect(() => {
if (loading || !rootInfo || currentIndex !== index) return
- const init = async () => {
- setLoading(true)
+ const init = async () => {
+ setLoading(true)
- try {
- // Privacy: Only use user's own relays + defaults, never connect to other users' relays
- const userRelays = userRelayList?.read || []
- const finalRelayUrls = Array.from(new Set([
- ...FAST_READ_RELAY_URLS, // Fast, well-connected relays
- ...userRelays // User's mailbox relays
- ]))
+ try {
+ console.log(`[ReplyNoteList] Starting init with rootInfo:`, rootInfo)
+
+ // Privacy: Only use user's own relays + defaults, never connect to other users' relays
+ const userRelays = userRelayList?.read || []
+ const finalRelayUrls = Array.from(new Set([
+ ...FAST_READ_RELAY_URLS, // Fast, well-connected relays
+ ...userRelays // User's mailbox relays
+ ]))
const filters: (Omit & {
limit: number
@@ -273,6 +323,29 @@ export default function ReplyNoteList({ index, event, sort = 'oldest' }: { index
limit: LIMIT
})
}
+
+ // Fetch zap receipts for the event
+ if (rootInfo.type === 'E') {
+ console.log(`[ReplyNoteList] Adding zap filter for E type: #e=[${rootInfo.id}], kinds=[${kinds.Zap}]`)
+ filters.push({
+ '#e': [rootInfo.id],
+ kinds: [kinds.Zap],
+ limit: LIMIT
+ })
+ } else if (rootInfo.type === 'A') {
+ console.log(`[ReplyNoteList] Adding zap filter for A type: #a=[${rootInfo.id}], kinds=[${kinds.Zap}]`)
+ filters.push({
+ '#a': [rootInfo.id],
+ kinds: [kinds.Zap],
+ limit: LIMIT
+ })
+ }
+
+ console.log(`[ReplyNoteList] Total filters: ${filters.length}`)
+ filters.forEach((filter, i) => {
+ console.log(`[ReplyNoteList] Filter ${i}:`, filter)
+ })
+
const { closer, timelineKey } = await client.subscribeTimeline(
filters.map((filter) => ({
urls: finalRelayUrls.slice(0, 8), // Increased from 5 to 8 for better coverage
@@ -281,7 +354,21 @@ export default function ReplyNoteList({ index, event, sort = 'oldest' }: { index
{
onEvents: (evts, eosed) => {
if (evts.length > 0) {
- addReplies(evts.filter((evt) => isReplyNoteEvent(evt)))
+ const regularReplies = evts.filter((evt) => isReplyNoteEvent(evt))
+ const zaps = evts.filter((evt) => evt.kind === kinds.Zap)
+
+ console.log(`[ReplyNoteList] Received ${evts.length} events: ${regularReplies.length} regular replies, ${zaps.length} zaps`)
+
+ addReplies(regularReplies)
+ if (zaps.length > 0) {
+ console.log(`[ReplyNoteList] Adding ${zaps.length} new zap events`)
+ setZapEvents(prev => {
+ const zapIdSet = new Set(prev.map(z => z.id))
+ const newZaps = zaps.filter(z => !zapIdSet.has(z.id))
+ console.log(`[ReplyNoteList] ${newZaps.length} are actually new (not duplicates)`)
+ return [...prev, ...newZaps]
+ })
+ }
}
if (eosed) {
setUntil(evts.length >= LIMIT ? evts[evts.length - 1].created_at - 1 : undefined)
@@ -289,8 +376,19 @@ export default function ReplyNoteList({ index, event, sort = 'oldest' }: { index
}
},
onNew: (evt) => {
- if (!isReplyNoteEvent(evt)) return
- addReplies([evt])
+ if (evt.kind === kinds.Zap) {
+ console.log(`[ReplyNoteList] New zap event received: ${evt.id.slice(0, 8)}...`)
+ setZapEvents(prev => {
+ if (prev.some(z => z.id === evt.id)) {
+ console.log(`[ReplyNoteList] Zap ${evt.id.slice(0, 8)} already exists, skipping`)
+ return prev
+ }
+ console.log(`[ReplyNoteList] Adding new zap: ${evt.id.slice(0, 8)}...`)
+ return [...prev, evt]
+ })
+ } else if (isReplyNoteEvent(evt)) {
+ addReplies([evt])
+ }
}
}
)
@@ -426,3 +524,5 @@ export default function ReplyNoteList({ index, event, sort = 'oldest' }: { index
)
}
+
+export default ReplyNoteList
diff --git a/src/constants.ts b/src/constants.ts
index 47c87a6..ae14853 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -28,6 +28,7 @@ export const StorageKey = {
DEFAULT_ZAP_SATS: 'defaultZapSats',
DEFAULT_ZAP_COMMENT: 'defaultZapComment',
QUICK_ZAP: 'quickZap',
+ ZAP_REPLY_THRESHOLD: 'zapReplyThreshold',
LAST_READ_NOTIFICATION_TIME_MAP: 'lastReadNotificationTimeMap',
ACCOUNT_FEED_INFO_MAP: 'accountFeedInfoMap',
AUTOPLAY: 'autoplay',
@@ -125,7 +126,9 @@ export const ExtendedKind = {
FAVORITE_RELAYS: 10012,
BLOSSOM_SERVER_LIST: 10063,
RELAY_REVIEW: 31987,
- GROUP_METADATA: 39000
+ GROUP_METADATA: 39000,
+ ZAP_REQUEST: 9734,
+ ZAP_RECEIPT: 9735
}
export const SUPPORTED_KINDS = [
@@ -142,7 +145,8 @@ export const SUPPORTED_KINDS = [
kinds.Highlights,
kinds.LongFormArticle,
ExtendedKind.RELAY_REVIEW,
- ExtendedKind.DISCUSSION
+ ExtendedKind.DISCUSSION,
+ ExtendedKind.ZAP_RECEIPT
]
export const URL_REGEX =
diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts
index 89d28f4..968c95e 100644
--- a/src/i18n/locales/en.ts
+++ b/src/i18n/locales/en.ts
@@ -390,6 +390,12 @@ export default {
'reposted your note': 'reposted your note',
'zapped your note': 'zapped your note',
'zapped you': 'zapped you',
+ zapped: 'zapped',
+ 'Invalid zap receipt': 'Invalid zap receipt',
+ 'Zapped note': 'Zapped note',
+ 'Zapped profile': 'Zapped profile',
+ 'Zap reply threshold': 'Zap reply threshold',
+ 'Zaps above this amount will appear as replies in threads': 'Zaps above this amount will appear as replies in threads',
'Mark as read': 'Mark as read',
Report: 'Report',
'Successfully report': 'Successfully reported',
diff --git a/src/lib/event-metadata.ts b/src/lib/event-metadata.ts
index f417f7e..8ef8174 100644
--- a/src/lib/event-metadata.ts
+++ b/src/lib/event-metadata.ts
@@ -5,7 +5,7 @@ import { buildATag } from './draft-event'
import { getReplaceableEventIdentifier } from './event'
import { getAmountFromInvoice, getLightningAddressFromProfile } from './lightning'
import { formatPubkey, pubkeyToNpub } from './pubkey'
-import { generateBech32IdFromETag, tagNameEquals } from './tag'
+import { generateBech32IdFromATag, generateBech32IdFromETag, tagNameEquals } from './tag'
import { isWebsocketUrl, normalizeHttpUrl, normalizeUrl } from './url'
import { isTorBrowser } from './utils'
@@ -121,6 +121,10 @@ export function getZapInfoFromEvent(receiptEvent: Event) {
originalEventId = tag[1]
eventId = generateBech32IdFromETag(tag)
break
+ case 'a':
+ originalEventId = tag[1]
+ eventId = generateBech32IdFromATag(tag)
+ break
case 'bolt11':
invoice = tagValue
break
@@ -133,7 +137,14 @@ export function getZapInfoFromEvent(receiptEvent: Event) {
}
})
if (!recipientPubkey || !invoice) return null
- amount = invoice ? getAmountFromInvoice(invoice) : 0
+
+ // Try to parse amount from invoice, fallback to description if invoice is invalid
+ try {
+ amount = getAmountFromInvoice(invoice)
+ } catch {
+ amount = 0
+ }
+
if (description) {
try {
const zapRequest = JSON.parse(description)
@@ -141,6 +152,17 @@ export function getZapInfoFromEvent(receiptEvent: Event) {
if (!senderPubkey) {
senderPubkey = zapRequest.pubkey
}
+ // If invoice parsing failed, try to get amount from zap request tags
+ if (amount === 0 && zapRequest.tags) {
+ const amountTag = zapRequest.tags.find((tag: string[]) => tag[0] === 'amount')
+ if (amountTag && amountTag[1]) {
+ const millisats = parseInt(amountTag[1])
+ amount = millisats / 1000 // Convert millisats to sats
+ console.log(`📝 Parsed amount from description tag: ${amountTag[1]} millisats = ${amount} sats`)
+ }
+ } else if (amount > 0) {
+ console.log(`📝 Parsed amount from invoice: ${amount} sats`)
+ }
} catch {
// ignore
}
diff --git a/src/lib/event.ts b/src/lib/event.ts
index 858867f..efa7821 100644
--- a/src/lib/event.ts
+++ b/src/lib/event.ts
@@ -26,6 +26,12 @@ export function isReplyNoteEvent(event: Event) {
if ([ExtendedKind.COMMENT, ExtendedKind.VOICE_COMMENT].includes(event.kind)) {
return true
}
+
+ // Zap receipts are considered replies if they have an 'e' tag (zapping a note) or 'a' tag (zapping an addressable event)
+ if (event.kind === kinds.Zap) {
+ return event.tags.some(tag => tag[0] === 'e' || tag[0] === 'a')
+ }
+
if (event.kind !== kinds.ShortTextNote) return false
const cache = EVENT_IS_REPLY_NOTE_CACHE.get(event.id)
diff --git a/src/pages/secondary/WalletPage/ZapReplyThresholdInput.tsx b/src/pages/secondary/WalletPage/ZapReplyThresholdInput.tsx
new file mode 100644
index 0000000..6f13860
--- /dev/null
+++ b/src/pages/secondary/WalletPage/ZapReplyThresholdInput.tsx
@@ -0,0 +1,46 @@
+import { Input } from '@/components/ui/input'
+import { Label } from '@/components/ui/label'
+import { useZap } from '@/providers/ZapProvider'
+import { useState } from 'react'
+import { useTranslation } from 'react-i18next'
+
+export default function ZapReplyThresholdInput() {
+ const { t } = useTranslation()
+ const { zapReplyThreshold, updateZapReplyThreshold } = useZap()
+ const [zapReplyThresholdInput, setZapReplyThresholdInput] = useState(zapReplyThreshold)
+
+ return (
+
+
+
+ {
+ setZapReplyThresholdInput((pre) => {
+ if (e.target.value === '') {
+ return 0
+ }
+ let num = parseInt(e.target.value, 10)
+ if (isNaN(num) || num < 0) {
+ num = pre
+ }
+ return num
+ })
+ }}
+ onBlur={() => {
+ updateZapReplyThreshold(zapReplyThresholdInput)
+ }}
+ />
+ {t('sats')}
+
+
+ )
+}
+
diff --git a/src/pages/secondary/WalletPage/index.tsx b/src/pages/secondary/WalletPage/index.tsx
index 154e88e..0f824b4 100644
--- a/src/pages/secondary/WalletPage/index.tsx
+++ b/src/pages/secondary/WalletPage/index.tsx
@@ -21,6 +21,7 @@ import DefaultZapAmountInput from './DefaultZapAmountInput'
import DefaultZapCommentInput from './DefaultZapCommentInput'
import LightningAddressInput from './LightningAddressInput'
import QuickZapSwitch from './QuickZapSwitch'
+import ZapReplyThresholdInput from './ZapReplyThresholdInput'
const WalletPage = forwardRef(({ index }: { index?: number }, ref) => {
const { t } = useTranslation()
@@ -29,55 +30,60 @@ const WalletPage = forwardRef(({ index }: { index?: number }, ref) => {
return (
- {isWalletConnected ? (
-
-
- {walletInfo?.node.alias && (
-
- {t('Connected to')} {walletInfo.node.alias}
-
- )}
-
-
-
-
-
-
- {t('Are you absolutely sure?')}
-
- {t('You will not be able to send zaps to others.')}
-
-
-
- {t('Cancel')}
- disconnect()}>
- {t('Disconnect')}
-
-
-
-
+
+ {isWalletConnected ? (
+ <>
+
+ {walletInfo?.node.alias && (
+
+ {t('Connected to')} {walletInfo.node.alias}
+
+ )}
+
+
+
+
+
+
+ {t('Are you absolutely sure?')}
+
+ {t('You will not be able to send zaps to others.')}
+
+
+
+ {t('Cancel')}
+ disconnect()}>
+ {t('Disconnect')}
+
+
+
+
+
+
+
+
+
+ >
+ ) : (
+
+
+
-
-
-
-
-
- ) : (
-
-
-
-
- )}
+ )}
+
+ {/* Zap Reply Threshold - always visible as it's just a display setting */}
+
+
)
})
diff --git a/src/providers/ZapProvider.tsx b/src/providers/ZapProvider.tsx
index 51fa519..da0b9e9 100644
--- a/src/providers/ZapProvider.tsx
+++ b/src/providers/ZapProvider.tsx
@@ -14,6 +14,8 @@ type TZapContext = {
updateDefaultComment: (comment: string) => void
quickZap: boolean
updateQuickZap: (quickZap: boolean) => void
+ zapReplyThreshold: number
+ updateZapReplyThreshold: (sats: number) => void
}
const ZapContext = createContext
(undefined)
@@ -30,6 +32,7 @@ export function ZapProvider({ children }: { children: React.ReactNode }) {
const [defaultZapSats, setDefaultZapSats] = useState(storage.getDefaultZapSats())
const [defaultZapComment, setDefaultZapComment] = useState(storage.getDefaultZapComment())
const [quickZap, setQuickZap] = useState(storage.getQuickZap())
+ const [zapReplyThreshold, setZapReplyThreshold] = useState(storage.getZapReplyThreshold())
const [isWalletConnected, setIsWalletConnected] = useState(false)
const [provider, setProvider] = useState(null)
const [walletInfo, setWalletInfo] = useState(null)
@@ -69,6 +72,11 @@ export function ZapProvider({ children }: { children: React.ReactNode }) {
setQuickZap(quickZap)
}
+ const updateZapReplyThreshold = (sats: number) => {
+ storage.setZapReplyThreshold(sats)
+ setZapReplyThreshold(sats)
+ }
+
return (
{children}
diff --git a/src/services/local-storage.service.ts b/src/services/local-storage.service.ts
index c04f395..ea128bc 100644
--- a/src/services/local-storage.service.ts
+++ b/src/services/local-storage.service.ts
@@ -6,6 +6,7 @@ import {
SUPPORTED_KINDS,
StorageKey
} from '@/constants'
+import { kinds } from 'nostr-tools'
import { isSameAccount } from '@/lib/account'
import { randomString } from '@/lib/random'
import {
@@ -33,6 +34,7 @@ class LocalStorageService {
private defaultZapSats: number = 21
private defaultZapComment: string = 'Zap!'
private quickZap: boolean = false
+ private zapReplyThreshold: number = 210
private accountFeedInfoMap: Record = {}
private mediaUploadService: string = DEFAULT_NIP_96_SERVICE
private autoplay: boolean = true
@@ -106,6 +108,14 @@ class LocalStorageService {
this.defaultZapComment = window.localStorage.getItem(StorageKey.DEFAULT_ZAP_COMMENT) ?? 'Zap!'
this.quickZap = window.localStorage.getItem(StorageKey.QUICK_ZAP) === 'true'
+ const zapReplyThresholdStr = window.localStorage.getItem(StorageKey.ZAP_REPLY_THRESHOLD)
+ if (zapReplyThresholdStr) {
+ const num = parseInt(zapReplyThresholdStr)
+ if (!isNaN(num)) {
+ this.zapReplyThreshold = num
+ }
+ }
+
const accountFeedInfoMapStr =
window.localStorage.getItem(StorageKey.ACCOUNT_FEED_INFO_MAP) ?? '{}'
this.accountFeedInfoMap = JSON.parse(accountFeedInfoMapStr)
@@ -156,7 +166,8 @@ class LocalStorageService {
const showKindsStr = window.localStorage.getItem(StorageKey.SHOW_KINDS)
if (!showKindsStr) {
- this.showKinds = SUPPORTED_KINDS
+ // Default: show all supported kinds except reposts
+ this.showKinds = SUPPORTED_KINDS.filter(kind => kind !== kinds.Repost)
} else {
const showKindsVersionStr = window.localStorage.getItem(StorageKey.SHOW_KINDS_VERSION)
const showKindsVersion = showKindsVersionStr ? parseInt(showKindsVersionStr) : 0
@@ -164,10 +175,13 @@ class LocalStorageService {
if (showKindsVersion < 1) {
showKinds.push(ExtendedKind.VIDEO, ExtendedKind.SHORT_VIDEO)
}
+ if (showKindsVersion < 2) {
+ showKinds.push(ExtendedKind.ZAP_RECEIPT)
+ }
this.showKinds = showKinds
}
window.localStorage.setItem(StorageKey.SHOW_KINDS, JSON.stringify(this.showKinds))
- window.localStorage.setItem(StorageKey.SHOW_KINDS_VERSION, '1')
+ window.localStorage.setItem(StorageKey.SHOW_KINDS_VERSION, '2')
this.hideContentMentioningMutedUsers =
window.localStorage.getItem(StorageKey.HIDE_CONTENT_MENTIONING_MUTED_USERS) === 'true'
@@ -310,6 +324,15 @@ class LocalStorageService {
window.localStorage.setItem(StorageKey.QUICK_ZAP, quickZap.toString())
}
+ getZapReplyThreshold() {
+ return this.zapReplyThreshold
+ }
+
+ setZapReplyThreshold(sats: number) {
+ this.zapReplyThreshold = sats
+ window.localStorage.setItem(StorageKey.ZAP_REPLY_THRESHOLD, sats.toString())
+ }
+
getLastReadNotificationTime(pubkey: string) {
return this.lastReadNotificationTimeMap[pubkey] ?? 0
}