Browse Source

get rid of zap thresholds

reorg feed filter and add superchats
imwald
Silberengel 3 weeks ago
parent
commit
c67c74112b
  1. 101
      src/components/KindFilter/index.tsx
  2. 26
      src/components/NoteList/index.tsx
  3. 29
      src/components/ReplyNoteList/index.tsx
  4. 1
      src/constants.ts
  5. 73
      src/hooks/useFeedAttestedSuperchatIds.ts
  6. 11
      src/i18n/locales/de.ts
  7. 11
      src/i18n/locales/en.ts
  8. 6
      src/lib/btc-usd-rate.ts
  9. 13
      src/lib/event-metadata.ts
  10. 91
      src/lib/feed-kind-filter.test.ts
  11. 88
      src/lib/feed-kind-filter.ts
  12. 8
      src/lib/payment-attestation-cache.ts
  13. 61
      src/lib/superchat.test.ts
  14. 39
      src/lib/superchat.ts
  15. 6
      src/lib/xmr-usd-rate.ts
  16. 50
      src/pages/secondary/WalletPage/ZapReplyThresholdInput.tsx
  17. 2
      src/pages/secondary/WalletPage/index.tsx
  18. 10
      src/providers/ZapProvider.tsx
  19. 24
      src/services/local-storage.service.ts

101
src/components/KindFilter/index.tsx

@ -5,6 +5,17 @@ import { Drawer, DrawerContent, DrawerHeader, DrawerTitle, DrawerTrigger } from @@ -5,6 +5,17 @@ import { Drawer, DrawerContent, DrawerHeader, DrawerTitle, DrawerTrigger } from
import { Label } from '@/components/ui/label'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { ExtendedKind, NIP71_VIDEO_KINDS, PROFILE_FEED_KINDS } from '@/constants'
import {
applyFeedGitGroupToggle,
applyFeedPostsGroupToggle,
applyFeedRepliesGroupToggle,
FEED_GIT_GROUP_KINDS,
FEED_POSTS_GROUP_KINDS,
FEED_REPLIES_GROUP_KINDS,
isFeedGitGroupEnabled,
isFeedPostsGroupEnabled,
isFeedRepliesGroupEnabled
} from '@/lib/feed-kind-filter'
import { LIVE_ACTIVITY_KINDS } from '@/lib/live-activities'
import { cn } from '@/lib/utils'
import { useKindFilterOrDefaults } from '@/providers/KindFilterProvider'
@ -19,26 +30,11 @@ const KIND_1111 = ExtendedKind.COMMENT @@ -19,26 +30,11 @@ const KIND_1111 = ExtendedKind.COMMENT
const KIND_FILTER_OPTIONS = [
{ kindGroup: [kinds.LongFormArticle, ExtendedKind.WIKI_ARTICLE, ExtendedKind.NOSTR_SPECIFICATION], label: 'Articles' },
{ kindGroup: [kinds.Highlights], label: 'Highlights' },
{ kindGroup: [ExtendedKind.POLL], label: 'Polls' },
{ kindGroup: [ExtendedKind.VOICE, ExtendedKind.VOICE_COMMENT], label: 'Voice Posts' },
{ kindGroup: [ExtendedKind.PICTURE], label: 'Photo Posts' },
{ kindGroup: [...NIP71_VIDEO_KINDS], label: 'Video Posts' },
{ kindGroup: [ExtendedKind.DISCUSSION], label: 'Discussions' },
{ kindGroup: [ExtendedKind.CALENDAR_EVENT_DATE, ExtendedKind.CALENDAR_EVENT_TIME], label: 'Calendar Events' },
{ kindGroup: [...LIVE_ACTIVITY_KINDS], label: 'Live streams' },
{
kindGroup: [
ExtendedKind.ZAP_RECEIPT,
ExtendedKind.MONERO_TIP_DISCLOSURE,
ExtendedKind.MONERO_TIP_RECEIPT
],
label: 'Zaps'
},
{ kindGroup: [kinds.Repost, ExtendedKind.GENERIC_REPOST], label: 'Boosts' },
{ kindGroup: [ExtendedKind.GIT_REPO_ANNOUNCEMENT], label: 'Git repositories' },
{ kindGroup: [ExtendedKind.GIT_ISSUE], label: 'Git issues' },
{ kindGroup: [ExtendedKind.GIT_RELEASE], label: 'Git releases' }
{ kindGroup: [kinds.Repost, ExtendedKind.GENERIC_REPOST], label: 'Boosts' }
]
function buildShowKindsFromOptions(
@ -134,6 +130,14 @@ export default function KindFilter({ @@ -134,6 +130,14 @@ export default function KindFilter({
)
const canApply = temporarySeeAllEvents || appliedShowKinds.length > 0
const postsGroupEnabled = isFeedPostsGroupEnabled(temporaryShowKind1OPs, temporaryShowKinds)
const repliesGroupEnabled = isFeedRepliesGroupEnabled(
temporaryShowKind1Replies,
temporaryShowKind1111,
temporaryShowKinds
)
const gitGroupEnabled = isFeedGitGroupEnabled(temporaryShowKinds)
const handleApply = () => {
if (!canApply) return
@ -196,38 +200,64 @@ export default function KindFilter({ @@ -196,38 +200,64 @@ export default function KindFilter({
{temporarySeeAllEvents ? t('See all events hint') : t('Use filter hint')}
</p>
<div className={cn('grid grid-cols-2 gap-2', temporarySeeAllEvents && 'opacity-50')}>
{/* Posts (OPs) - kind 1 top-level only */}
<div
className={cn(
'cursor-pointer grid gap-1.5 rounded-lg border px-4 py-3',
temporaryShowKind1OPs ? 'border-primary/60 bg-primary/5' : 'clickable'
postsGroupEnabled ? 'border-primary/60 bg-primary/5' : 'clickable'
)}
onClick={() => setTemporaryShowKind1OPs((prev) => !prev)}
onClick={() => {
const next = !postsGroupEnabled
const { showKinds: nextKinds, showKind1OPs } = applyFeedPostsGroupToggle(
temporaryShowKinds,
next
)
setTemporaryShowKinds(nextKinds)
setTemporaryShowKind1OPs(showKind1OPs)
}}
>
<p className="leading-none font-medium">{t('Posts (OPs)')}</p>
<p className="text-muted-foreground text-xs">kind {KIND_1}</p>
<p className="leading-none font-medium">{t('Posts')}</p>
<p className="text-muted-foreground text-xs">
{t('Feed filter posts group kinds', {
kinds: [KIND_1, ...FEED_POSTS_GROUP_KINDS].join(', ')
})}
</p>
</div>
{/* Kind 1 replies - kind 1 that are replies */}
<div
className={cn(
'cursor-pointer grid gap-1.5 rounded-lg border px-4 py-3',
temporaryShowKind1Replies ? 'border-primary/60 bg-primary/5' : 'clickable'
repliesGroupEnabled ? 'border-primary/60 bg-primary/5' : 'clickable'
)}
onClick={() => setTemporaryShowKind1Replies((prev) => !prev)}
onClick={() => {
const next = !repliesGroupEnabled
const { showKinds: nextKinds, showKind1Replies, showKind1111 } = applyFeedRepliesGroupToggle(
temporaryShowKinds,
next
)
setTemporaryShowKinds(nextKinds)
setTemporaryShowKind1Replies(showKind1Replies)
setTemporaryShowKind1111(showKind1111)
}}
>
<p className="leading-none font-medium">{t('Kind 1 replies')}</p>
<p className="text-muted-foreground text-xs">kind {KIND_1}</p>
<p className="leading-none font-medium">{t('Replies')}</p>
<p className="text-muted-foreground text-xs">
{t('Feed filter replies group kinds', {
kinds: [KIND_1, KIND_1111, ...FEED_REPLIES_GROUP_KINDS].join(', ')
})}
</p>
</div>
{/* Comments - kind 1111 */}
<div
className={cn(
'cursor-pointer grid gap-1.5 rounded-lg border px-4 py-3',
temporaryShowKind1111 ? 'border-primary/60 bg-primary/5' : 'clickable'
gitGroupEnabled ? 'border-primary/60 bg-primary/5' : 'clickable'
)}
onClick={() => setTemporaryShowKind1111((prev) => !prev)}
onClick={() => {
setTemporaryShowKinds(applyFeedGitGroupToggle(temporaryShowKinds, !gitGroupEnabled))
}}
>
<p className="leading-none font-medium">{t('Comments')}</p>
<p className="text-muted-foreground text-xs">kind {KIND_1111}</p>
<p className="leading-none font-medium">{t('Git')}</p>
<p className="text-muted-foreground text-xs">
{t('Feed filter git group kinds', { kinds: FEED_GIT_GROUP_KINDS.join(', ') })}
</p>
</div>
{KIND_FILTER_OPTIONS.map(({ kindGroup, label }) => {
/** `some` not `every`: saved kinds may include e.g. only 30311 while the row lists 30311–30313; `every` made the box look off while 30311 still matched the feed. */
@ -259,7 +289,14 @@ export default function KindFilter({ @@ -259,7 +289,14 @@ export default function KindFilter({
variant="secondary"
onClick={() => {
setTemporaryShowKinds(
PROFILE_FEED_KINDS.filter((k) => k !== KIND_1 && k !== KIND_1111)
Array.from(
new Set([
...PROFILE_FEED_KINDS.filter((k) => k !== KIND_1 && k !== KIND_1111),
...FEED_POSTS_GROUP_KINDS,
...FEED_REPLIES_GROUP_KINDS,
...FEED_GIT_GROUP_KINDS
])
)
)
setTemporaryShowKind1OPs(true)
setTemporaryShowKind1Replies(true)

26
src/components/NoteList/index.tsx

@ -30,7 +30,8 @@ import { isLocalNetworkUrl, normalizeUrl } from '@/lib/url' @@ -30,7 +30,8 @@ import { isLocalNetworkUrl, normalizeUrl } from '@/lib/url'
import { eventPassesNoteListKindPicker } from '@/lib/feed-kind-filter'
import { collectLocalEventsForTextSearch } from '@/lib/local-nip50-search-merge'
import { eventMatchesNip50LocalFullTextQuery } from '@/lib/nip50-local-text-match'
import { shouldIncludeZapReceiptAtReplyThreshold } from '@/lib/event-metadata'
import { useFeedAttestedSuperchatIds } from '@/hooks/useFeedAttestedSuperchatIds'
import { shouldIncludePaymentInFeed } from '@/lib/superchat'
import { isTouchDevice } from '@/lib/utils'
import { useContentPolicyOptional } from '@/providers/ContentPolicyProvider'
import { useDeletedEvent } from '@/providers/DeletedEventProvider'
@ -38,7 +39,6 @@ import { useMuteList } from '@/contexts/mute-list-context' @@ -38,7 +39,6 @@ import { useMuteList } from '@/contexts/mute-list-context'
import { muteSetHas } from '@/lib/mute-set'
import { useNostr } from '@/providers/NostrProvider'
import { useUserTrust } from '@/contexts/user-trust-context'
import { useZap } from '@/providers/ZapProvider'
import client from '@/services/client.service'
import noteStatsService from '@/services/note-stats.service'
import indexedDb from '@/services/indexed-db.service'
@ -859,7 +859,6 @@ const NoteList = forwardRef( @@ -859,7 +859,6 @@ const NoteList = forwardRef(
contentPolicy?.isOffline ??
(!navigator.onLine || (navigator as Navigator & { connection?: { type?: string } }).connection?.type === 'none')
const { isEventDeleted } = useDeletedEvent()
const { zapReplyThreshold } = useZap()
const { favoriteRelays, blockedRelays } = useFavoriteRelays()
const [events, setEvents] = useState<Event[]>([])
const eventsRef = useRef<Event[]>([])
@ -996,6 +995,19 @@ const NoteList = forwardRef( @@ -996,6 +995,19 @@ const NoteList = forwardRef(
// Memoize subRequests serialization to avoid expensive JSON.stringify on every render
const subRequestsKey = useMemo(() => legacyFeedSubscriptionKey(subRequests), [subRequests])
const feedRelayUrls = useMemo(() => {
const urls = new Set<string>()
for (const req of subRequests) {
for (const url of req.urls ?? []) {
const trimmed = url.trim()
if (trimmed) urls.add(trimmed)
}
}
return [...urls]
}, [subRequestsKey])
const feedAttestedSuperchatIds = useFeedAttestedSuperchatIds(feedRelayUrls)
const followingFeedDeltaSubRequestsKey = useMemo(
() =>
legacyFeedSubscriptionKey(followingFeedDeltaSubRequests ?? []),
@ -1310,8 +1322,8 @@ const NoteList = forwardRef( @@ -1310,8 +1322,8 @@ const NoteList = forwardRef(
// Filter out expired events
if (shouldFilterEvent(evt)) return true
// Filter out zap receipts below the zap-reply threshold (same rule as thread replies)
if (evt.kind === ExtendedKind.ZAP_RECEIPT && !shouldIncludeZapReceiptAtReplyThreshold(evt, zapReplyThreshold)) {
// Attested superchats only (9741), same as threads / profile walls.
if (!shouldIncludePaymentInFeed(evt, feedAttestedSuperchatIds)) {
return true
}
@ -1338,7 +1350,7 @@ const NoteList = forwardRef( @@ -1338,7 +1350,7 @@ const NoteList = forwardRef(
mutePubkeySet,
pinnedEventIds,
isEventDeleted,
zapReplyThreshold,
feedAttestedSuperchatIds,
extraShouldHideEvent,
homeFeedActiveSeenOnAllowlist,
homeFeedListMode
@ -3327,7 +3339,6 @@ const NoteList = forwardRef( @@ -3327,7 +3339,6 @@ const NoteList = forwardRef(
if (!isReply && !showKind1OPsRef.current) return
}
if (event.kind === ExtendedKind.COMMENT && !showKind1111Ref.current) return
if (event.kind === ExtendedKind.GIT_RELEASE && !showKind1OPsRef.current) return
}
}
}
@ -3640,7 +3651,6 @@ const NoteList = forwardRef( @@ -3640,7 +3651,6 @@ const NoteList = forwardRef(
if (!isReply && !showKind1OPsRef.current) return
}
if (event.kind === ExtendedKind.COMMENT && !showKind1111Ref.current) return
if (event.kind === ExtendedKind.GIT_RELEASE && !showKind1OPsRef.current) return
}
}
}

29
src/components/ReplyNoteList/index.tsx

@ -29,7 +29,6 @@ import { useSmartNoteNavigation } from '@/PageManager' @@ -29,7 +29,6 @@ import { useSmartNoteNavigation } from '@/PageManager'
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
import { useMuteList } from '@/contexts/mute-list-context'
import { useNostr } from '@/providers/NostrProvider'
import { useZap } from '@/providers/ZapProvider'
import { useReplyIngress } from '@/hooks/useReplyIngress'
import { useUserTrust } from '@/contexts/user-trust-context'
import { useCurrentRelays } from '@/providers/CurrentRelaysProvider'
@ -115,7 +114,6 @@ function ReplyNoteList({ @@ -115,7 +114,6 @@ function ReplyNoteList({
const { mutePubkeySet } = useMuteList()
const { hideContentMentioningMutedUsers } = useContentPolicy()
const { pubkey: userPubkey } = useNostr()
const { zapReplyThreshold } = useZap()
const { blockedRelays, favoriteRelays } = useFavoriteRelays()
const { relayUrls: browsingRelayUrls } = useCurrentRelays()
const relayAuthoritativeRead =
@ -241,11 +239,7 @@ function ReplyNoteList({ @@ -241,11 +239,7 @@ function ReplyNoteList({
threadWalkFromRepliesMap.set(evt.id.toLowerCase(), evt)
}
const { superchats, rest: nonZaps } = partitionAttestedSuperchats(
replyEvents,
attestedPaymentIds,
zapReplyThreshold
)
const { superchats, rest: nonZaps } = partitionAttestedSuperchats(replyEvents, attestedPaymentIds)
const zaps = superchats
const replyScoreById =
sort === 'top' || sort === 'controversial' || sort === 'most-zapped'
@ -336,7 +330,6 @@ function ReplyNoteList({ @@ -336,7 +330,6 @@ function ReplyNoteList({
mutePubkeySet,
hideContentMentioningMutedUsers,
sort,
zapReplyThreshold,
attestedPaymentIds,
isDiscussionRoot,
event.kind
@ -361,11 +354,7 @@ function ReplyNoteList({ @@ -361,11 +354,7 @@ function ReplyNoteList({
const mergedFeed = useMemo(() => {
/** Quotes + time-sorted feeds must not interleave zap receipts chronologically */
const zapsThenTimeSorted = (merged: NEvent[], direction: 'asc' | 'desc') => {
const { superchats, rest: nonZaps } = partitionAttestedSuperchats(
merged,
attestedPaymentIds,
zapReplyThreshold
)
const { superchats, rest: nonZaps } = partitionAttestedSuperchats(merged, attestedPaymentIds)
const sortedNon = [...nonZaps].sort((a, b) =>
direction === 'asc' ? a.created_at - b.created_at : b.created_at - a.created_at
)
@ -376,11 +365,7 @@ function ReplyNoteList({ @@ -376,11 +365,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 { superchats, rest: nonZaps } = partitionAttestedSuperchats(
replies,
attestedPaymentIds,
zapReplyThreshold
)
const { superchats, rest: nonZaps } = partitionAttestedSuperchats(replies, attestedPaymentIds)
const middle = nonZaps.filter((e) => !isEaThreadTailBacklinkCandidate(e, rootInfo))
const tailFromReplies = nonZaps.filter((e) => isEaThreadTailBacklinkCandidate(e, rootInfo))
const tailSeen = new Set<string>()
@ -397,11 +382,7 @@ function ReplyNoteList({ @@ -397,11 +382,7 @@ function ReplyNoteList({
// Web article / URL thread (NIP-22): same zaps → middle → tail layout as E/A
if (rootInfo?.type === 'I') {
const { superchats, rest: nonZaps } = partitionAttestedSuperchats(
replies,
attestedPaymentIds,
zapReplyThreshold
)
const { superchats, rest: nonZaps } = partitionAttestedSuperchats(replies, attestedPaymentIds)
const middle = nonZaps.filter((e) => !isWebThreadTailKind(e.kind))
const tailFromReplies = nonZaps.filter((e) => isWebThreadTailKind(e.kind))
const tailSeen = new Set<string>()
@ -423,7 +404,7 @@ function ReplyNoteList({ @@ -423,7 +404,7 @@ function ReplyNoteList({
return [...replies]
}
return zapsThenTimeSorted(merged, 'desc')
}, [replies, showQuotes, sort, replyIdSet, rootInfo, event.kind, attestedPaymentIds, zapReplyThreshold])
}, [replies, showQuotes, sort, replyIdSet, rootInfo, event.kind, attestedPaymentIds])
const parentNoteFeed = useNoteFeedProfileContext()
const threadProfileLoadedRef = useRef<Set<string>>(new Set())

1
src/constants.ts

@ -336,7 +336,6 @@ export const StorageKey = { @@ -336,7 +336,6 @@ export const StorageKey = {
DEFAULT_ZAP_COMMENT: 'defaultZapComment',
QUICK_ZAP: 'quickZap',
INCLUDE_PUBLIC_ZAP_RECEIPT: 'includePublicZapReceipt',
ZAP_REPLY_THRESHOLD: 'zapReplyThreshold',
/** Per-pubkey ms timestamps: last full network hydrate (see ACCOUNT_SESSION_NETWORK_HYDRATE_MIN_INTERVAL_MS). */
ACCOUNT_NETWORK_HYDRATE_AT_MAP: 'accountNetworkHydrateAtMap',
AUTOPLAY: 'autoplay',

73
src/hooks/useFeedAttestedSuperchatIds.ts

@ -0,0 +1,73 @@ @@ -0,0 +1,73 @@
import { ExtendedKind } from '@/constants'
import {
collectPaymentAttestationsFromSession,
mergeAttestedPaymentIdSets
} from '@/lib/payment-attestation-cache'
import { buildGlobalAttestedSuperchatIdSet } from '@/lib/superchat'
import client from '@/services/client.service'
import type { Event as NostrEvent } from 'nostr-tools'
import { useCallback, useEffect, useMemo, useState } from 'react'
function idsFromAttestations(attestations: NostrEvent[]): Set<string> {
return buildGlobalAttestedSuperchatIdSet(attestations)
}
/** Attested superchat target ids (9735 / 9740 / 9736 / 1814) for feed filtering. */
export function useFeedAttestedSuperchatIds(relayUrls: string[]): Set<string> {
const [attestedIds, setAttestedIds] = useState<Set<string>>(() =>
idsFromAttestations(collectPaymentAttestationsFromSession())
)
const mergeAttestations = useCallback((incoming: NostrEvent[]) => {
if (incoming.length === 0) return
const next = idsFromAttestations(incoming)
setAttestedIds((prev) => {
const merged = mergeAttestedPaymentIdSets(prev, next)
return merged.size === prev.size ? prev : merged
})
}, [])
const relayUrlsKey = useMemo(
() =>
[...relayUrls]
.map((u) => u.trim())
.filter(Boolean)
.sort()
.join('|'),
[relayUrls]
)
useEffect(() => {
mergeAttestations(collectPaymentAttestationsFromSession())
}, [mergeAttestations])
useEffect(() => {
const handleNewEvent = (data: Event) => {
const evt = (data as CustomEvent<NostrEvent>).detail
if (!evt || evt.kind !== ExtendedKind.PAYMENT_ATTESTATION) return
mergeAttestations([evt])
}
client.addEventListener('newEvent', handleNewEvent)
return () => client.removeEventListener('newEvent', handleNewEvent)
}, [mergeAttestations])
useEffect(() => {
if (!relayUrlsKey) return
const urls = relayUrlsKey.split('|').filter(Boolean)
if (urls.length === 0) return
let cancelled = false
void client
.fetchEvents(urls, { kinds: [ExtendedKind.PAYMENT_ATTESTATION], limit: 500 }, { cache: true })
.then((events) => {
if (!cancelled) mergeAttestations(events)
})
.catch(() => {
/* optional */
})
return () => {
cancelled = true
}
}, [relayUrlsKey, mergeAttestations])
return attestedIds
}

11
src/i18n/locales/de.ts

@ -1212,6 +1212,10 @@ export default { @@ -1212,6 +1212,10 @@ export default {
'Posts (OPs)': 'Beiträge (OPs)',
'Kind 1 replies': 'Kind-1-Antworten',
Comments: 'Kommentare',
'Feed filter posts group kinds': 'Kind {{kinds}}',
'Feed filter replies group kinds': 'Kind {{kinds}}',
'Feed filter git group kinds': 'Kind {{kinds}}',
Git: 'Git',
'Replies & comments': 'Antworten & Kommentare',
Articles: 'Artikel',
Highlights: 'Highlights',
@ -1533,9 +1537,12 @@ export default { @@ -1533,9 +1537,12 @@ export default {
'Invalid zap receipt': 'Invalid zap receipt',
'Zapped note': 'Zapped note',
'Zapped profile': 'Zapped profile',
'Zap reply threshold': 'Zap reply threshold',
'Zap reply threshold': 'Zap-Feed-Schwelle',
'Zap feed threshold': 'Zap-Feed-Schwelle',
'Zaps above this amount will appear in feeds':
'Lightning-Zaps, Zahlungsbenachrichtigungen und Monero-Tips ab diesem Betrag (in Sats, umgerechnet wenn Kurse verfügbar) erscheinen in Home- und Relay-Feeds, wenn diese Kinds aktiv sind. Threads zeigen nur bestätigte Superchats.',
'Zaps above this amount will appear as replies in threads':
'Zaps above this amount will appear as replies in threads',
'Lightning-Zaps, Zahlungsbenachrichtigungen und Monero-Tips ab diesem Betrag (in Sats, umgerechnet wenn Kurse verfügbar) erscheinen in Home- und Relay-Feeds, wenn diese Kinds aktiv sind. Threads zeigen nur bestätigte Superchats.',
'Mark as read': 'Als gelesen markieren',
Report: 'Melden',
'Successfully report': 'Erfolgreich gemeldet',

11
src/i18n/locales/en.ts

@ -1191,6 +1191,10 @@ export default { @@ -1191,6 +1191,10 @@ export default {
Posts: 'Posts',
'Posts (OPs)': 'Posts (OPs)',
'Kind 1 replies': 'Kind 1 replies',
'Feed filter posts group kinds': 'kind {{kinds}}',
'Feed filter replies group kinds': 'kind {{kinds}}',
'Feed filter git group kinds': 'kind {{kinds}}',
Git: 'Git',
Comments: 'Comments',
'Replies & comments': 'Replies & comments',
Articles: 'Articles',
@ -1506,9 +1510,12 @@ export default { @@ -1506,9 +1510,12 @@ export default {
'Invalid zap receipt': 'Invalid zap receipt',
'Zapped note': 'Zapped note',
'Zapped profile': 'Zapped profile',
'Zap reply threshold': 'Zap reply threshold',
'Zap reply threshold': 'Zap feed threshold',
'Zap feed threshold': 'Zap feed threshold',
'Zaps above this amount will appear in feeds':
'Lightning zaps, payment notifications, and Monero tips at or above this amount (in sats, or converted when rates are available) appear in home and relay feeds when those kinds are enabled. Threads only show attested superchats.',
'Zaps above this amount will appear as replies in threads':
'Only zap receipts (kind 9735) with at least this many sats are shown in home and relay feeds (with “Zaps” enabled in the kind filter) and listed under notes as zap replies.',
'Lightning zaps, payment notifications, and Monero tips at or above this amount (in sats, or converted when rates are available) appear in home and relay feeds when those kinds are enabled. Threads only show attested superchats.',
'Mark as read': 'Mark as read',
Report: 'Report',
'Successfully report': 'Successfully reported',

6
src/lib/btc-usd-rate.ts

@ -2,6 +2,12 @@ const CACHE_MS = 5 * 60 * 1000 @@ -2,6 +2,12 @@ const CACHE_MS = 5 * 60 * 1000
let cache: { usd: number; at: number } | null = null
/** Cached BTC/USD if {@link fetchBtcUsdRate} has run recently (sync feed filters). */
export function getCachedBtcUsdRate(): number | null {
if (cache && Date.now() - cache.at < CACHE_MS) return cache.usd
return null
}
/** Latest BTC/USD spot price (cached ~5 min). */
export async function fetchBtcUsdRate(): Promise<number | null> {
if (cache && Date.now() - cache.at < CACHE_MS) {

13
src/lib/event-metadata.ts

@ -618,19 +618,6 @@ export function getZapInfoFromEvent(receiptEvent: Event) { @@ -618,19 +618,6 @@ export function getZapInfoFromEvent(receiptEvent: Event) {
}
}
/**
* Kind 9735: include in timelines and reply lists only when amount (sats) is known and at least `thresholdSats`.
* Matches {@link NoteList} zap filtering.
*/
export function shouldIncludeZapReceiptAtReplyThreshold(receipt: Event, thresholdSats: number): boolean {
if (receipt.kind !== kinds.Zap) return true
const zapInfo = getZapInfoFromEvent(receipt)
if (!zapInfo || zapInfo.amount === undefined || zapInfo.amount === 0 || zapInfo.amount < thresholdSats) {
return false
}
return true
}
// Helper function to convert d-tag to title case
export function dTagToTitleCase(dTag: string): string {
return dTag

91
src/lib/feed-kind-filter.test.ts

@ -0,0 +1,91 @@ @@ -0,0 +1,91 @@
import { ExtendedKind } from '@/constants'
import {
applyFeedGitGroupToggle,
applyFeedPostsGroupToggle,
applyFeedRepliesGroupToggle,
eventPassesNoteListKindPicker,
FEED_GIT_GROUP_KINDS,
FEED_REPLIES_GROUP_KINDS,
isFeedGitGroupEnabled,
isFeedPostsGroupEnabled,
isFeedRepliesGroupEnabled
} from '@/lib/feed-kind-filter'
import { describe, expect, it } from 'vitest'
import { kinds, type Event } from 'nostr-tools'
function fakeEvent(partial: Partial<Event> & Pick<Event, 'kind' | 'tags'>): Event {
return {
id: '0'.repeat(64),
pubkey: 'b'.repeat(64),
created_at: 1,
content: '',
sig: 'sig',
...partial
}
}
describe('feed kind groups', () => {
it('posts group toggles kind 1 OPs, highlights, discussions, photos, and voice posts together', () => {
const off = applyFeedPostsGroupToggle([], false)
expect(off.showKind1OPs).toBe(false)
expect(isFeedPostsGroupEnabled(off.showKind1OPs, off.showKinds)).toBe(false)
const on = applyFeedPostsGroupToggle(off.showKinds, true)
expect(on.showKind1OPs).toBe(true)
expect(on.showKinds).toContain(kinds.Highlights)
expect(on.showKinds).toContain(ExtendedKind.DISCUSSION)
expect(on.showKinds).toContain(ExtendedKind.PICTURE)
expect(on.showKinds).toContain(ExtendedKind.VOICE)
expect(isFeedPostsGroupEnabled(on.showKind1OPs, on.showKinds)).toBe(true)
})
it('git group toggles all git kinds together', () => {
const off = applyFeedGitGroupToggle([], false)
expect(isFeedGitGroupEnabled(off)).toBe(false)
const on = applyFeedGitGroupToggle(off, true)
for (const k of FEED_GIT_GROUP_KINDS) {
expect(on).toContain(k)
}
expect(isFeedGitGroupEnabled(on)).toBe(true)
})
it('replies group toggles kind 1 replies, comments, voice comments, and superchat kinds together', () => {
const off = applyFeedRepliesGroupToggle([], false)
expect(off.showKind1Replies).toBe(false)
expect(off.showKind1111).toBe(false)
expect(isFeedRepliesGroupEnabled(off.showKind1Replies, off.showKind1111, off.showKinds)).toBe(
false
)
const on = applyFeedRepliesGroupToggle(off.showKinds, true)
expect(on.showKind1Replies).toBe(true)
expect(on.showKind1111).toBe(true)
expect(on.showKinds).toContain(ExtendedKind.VOICE_COMMENT)
for (const k of FEED_REPLIES_GROUP_KINDS) {
expect(on.showKinds).toContain(k)
}
expect(
isFeedRepliesGroupEnabled(on.showKind1Replies, on.showKind1111, on.showKinds)
).toBe(true)
})
it('hides payment notifications when replies group flags are off', () => {
const payment = fakeEvent({
kind: ExtendedKind.PAYMENT_NOTIFICATION,
tags: [['p', 'a'.repeat(64)], ['amount', '21000']]
})
const showKinds = [...FEED_REPLIES_GROUP_KINDS]
expect(eventPassesNoteListKindPicker(payment, showKinds, true, false, false)).toBe(false)
expect(eventPassesNoteListKindPicker(payment, showKinds, true, true, true)).toBe(true)
expect(
eventPassesNoteListKindPicker(
fakeEvent({ id: 'z'.repeat(64), kind: ExtendedKind.ZAP_RECEIPT, tags: [] }),
showKinds,
true,
true,
true
)
).toBe(true)
})
})

88
src/lib/feed-kind-filter.ts

@ -3,6 +3,86 @@ import { isReplyNoteEvent } from '@/lib/event' @@ -3,6 +3,86 @@ import { isReplyNoteEvent } from '@/lib/event'
import type { Event } from 'nostr-tools'
import { kinds } from 'nostr-tools'
/** Kind 1 OPs, highlights, discussions, photos, voice posts — feed filter “Posts” group. */
export const FEED_POSTS_GROUP_KINDS: readonly number[] = [
kinds.Highlights,
ExtendedKind.DISCUSSION,
ExtendedKind.PICTURE,
ExtendedKind.VOICE
]
/** Kind 1 replies, comments, voice comments, superchats — feed filter “Replies” group. */
export const FEED_REPLIES_GROUP_KINDS: readonly number[] = [
ExtendedKind.VOICE_COMMENT,
ExtendedKind.ZAP_RECEIPT,
ExtendedKind.PAYMENT_NOTIFICATION,
ExtendedKind.MONERO_TIP_DISCLOSURE,
ExtendedKind.MONERO_TIP_RECEIPT
]
export const FEED_GIT_GROUP_KINDS: readonly number[] = [
ExtendedKind.GIT_REPO_ANNOUNCEMENT,
ExtendedKind.GIT_ISSUE,
ExtendedKind.GIT_RELEASE
]
const FEED_POSTS_GROUP_KIND_SET = new Set(FEED_POSTS_GROUP_KINDS)
const FEED_REPLIES_GROUP_KIND_SET = new Set(FEED_REPLIES_GROUP_KINDS)
const FEED_GIT_GROUP_KIND_SET = new Set(FEED_GIT_GROUP_KINDS)
export function isFeedPostsGroupEnabled(showKind1OPs: boolean, showKinds: readonly number[]): boolean {
return showKind1OPs && FEED_POSTS_GROUP_KINDS.every((k) => showKinds.includes(k))
}
export function isFeedRepliesGroupEnabled(
showKind1Replies: boolean,
showKind1111: boolean,
showKinds: readonly number[]
): boolean {
return (
showKind1Replies &&
showKind1111 &&
FEED_REPLIES_GROUP_KINDS.every((k) => showKinds.includes(k))
)
}
export function isFeedGitGroupEnabled(showKinds: readonly number[]): boolean {
return FEED_GIT_GROUP_KINDS.every((k) => showKinds.includes(k))
}
export function applyFeedPostsGroupToggle(
showKinds: number[],
enabled: boolean
): { showKinds: number[]; showKind1OPs: boolean } {
const rest = showKinds.filter((k) => !FEED_POSTS_GROUP_KINDS.includes(k))
if (!enabled) return { showKind1OPs: false, showKinds: rest }
return {
showKind1OPs: true,
showKinds: Array.from(new Set([...rest, ...FEED_POSTS_GROUP_KINDS]))
}
}
export function applyFeedRepliesGroupToggle(
showKinds: number[],
enabled: boolean
): { showKinds: number[]; showKind1Replies: boolean; showKind1111: boolean } {
const rest = showKinds.filter((k) => !FEED_REPLIES_GROUP_KINDS.includes(k))
if (!enabled) {
return { showKind1Replies: false, showKind1111: false, showKinds: rest }
}
return {
showKind1Replies: true,
showKind1111: true,
showKinds: Array.from(new Set([...rest, ...FEED_REPLIES_GROUP_KINDS]))
}
}
export function applyFeedGitGroupToggle(showKinds: number[], enabled: boolean): number[] {
const rest = showKinds.filter((k) => !FEED_GIT_GROUP_KINDS.includes(k))
if (!enabled) return rest
return Array.from(new Set([...rest, ...FEED_GIT_GROUP_KINDS]))
}
/**
* Same rules as visible-row filtering when the home kind picker applies
* (not {@link shouldHideEvent} / mute / trust layers).
@ -15,12 +95,18 @@ export function eventPassesNoteListKindPicker( @@ -15,12 +95,18 @@ export function eventPassesNoteListKindPicker(
showKind1111: boolean
): boolean {
if (!effectiveShowKinds.includes(event.kind)) return false
if (FEED_POSTS_GROUP_KIND_SET.has(event.kind) && !showKind1OPs) return false
if (FEED_REPLIES_GROUP_KIND_SET.has(event.kind) && (!showKind1Replies || !showKind1111)) {
return false
}
if (FEED_GIT_GROUP_KIND_SET.has(event.kind) && !isFeedGitGroupEnabled(effectiveShowKinds)) {
return false
}
if (event.kind === kinds.ShortTextNote) {
const isReply = isReplyNoteEvent(event)
if (isReply && !showKind1Replies) return false
if (!isReply && !showKind1OPs) return false
}
if (event.kind === ExtendedKind.COMMENT && !showKind1111) return false
if (event.kind === ExtendedKind.GIT_RELEASE && !showKind1OPs) return false
return true
}

8
src/lib/payment-attestation-cache.ts

@ -11,6 +11,14 @@ const authorHydrateByPubkey = new Map<string, Promise<void>>() @@ -11,6 +11,14 @@ const authorHydrateByPubkey = new Map<string, Promise<void>>()
const LOCAL_ATTESTED_KEY_PREFIX = 'jumble:attested-payment-ids:'
/** Kind 9741 events already in the session LRU (for feed attestation index). */
export function collectPaymentAttestationsFromSession(limit = 2000): NostrEvent[] {
return client.eventService.getSessionEventsMatchingFilters(
[{ kinds: [ExtendedKind.PAYMENT_ATTESTATION], limit }],
limit
)
}
export function paymentAttestationCacheKey(targetEventId: string, recipientPubkey: string): string {
return `${targetEventId.trim().toLowerCase()}:${recipientPubkey.trim().toLowerCase()}`
}

61
src/lib/superchat.test.ts

@ -12,7 +12,9 @@ import { @@ -12,7 +12,9 @@ import {
isProfileWallPaymentNotification,
isProfileWallZapReceipt,
isNestedThreadReplyParentKind,
partitionAttestedSuperchats
buildGlobalAttestedSuperchatIdSet,
partitionAttestedSuperchats,
shouldIncludePaymentInFeed
} from '@/lib/superchat'
import { parsePaytoTagType } from '@/lib/payto'
import { kinds, type Event } from 'nostr-tools'
@ -202,15 +204,14 @@ describe('partitionAttestedSuperchats', () => { @@ -202,15 +204,14 @@ describe('partitionAttestedSuperchats', () => {
const { superchats, rest } = partitionAttestedSuperchats(
[zapAttested, zapUnattested, payment, comment],
attested,
1
attested
)
expect(superchats.map((e) => e.id)).toEqual([payment.id, zapAttested.id])
expect(rest).toEqual([comment])
})
it('includes attested zaps below the reply threshold at the top', () => {
it('includes attested micro zaps in threads (feed threshold does not apply)', () => {
const attested = new Set([ZAP_ID])
const microZap = fakeEvent({
id: ZAP_ID,
@ -234,12 +235,62 @@ describe('partitionAttestedSuperchats', () => { @@ -234,12 +235,62 @@ describe('partitionAttestedSuperchats', () => {
kind: ExtendedKind.COMMENT,
tags: [['e', '2'.repeat(64)]]
})
const { superchats, rest } = partitionAttestedSuperchats([microZap, comment], attested, 21)
const { superchats, rest } = partitionAttestedSuperchats([microZap, comment], attested)
expect(superchats.map((e) => e.id)).toEqual([ZAP_ID])
expect(rest).toEqual([comment])
})
})
describe('buildGlobalAttestedSuperchatIdSet', () => {
it('collects attested target ids from valid kind 9741 events', () => {
const paymentId = PAYMENT_ID
const attestation = fakeEvent({
id: 'a'.repeat(64),
kind: ExtendedKind.PAYMENT_ATTESTATION,
pubkey: RECIPIENT,
tags: [
['e', paymentId],
['k', String(ExtendedKind.PAYMENT_NOTIFICATION)]
]
})
expect(buildGlobalAttestedSuperchatIdSet([attestation]).has(paymentId)).toBe(true)
})
})
describe('shouldIncludePaymentInFeed', () => {
it('requires attestation for superchat kinds only', () => {
const zap = fakeEvent({
id: ZAP_ID,
kind: kinds.Zap,
tags: [
['P', SENDER],
['p', RECIPIENT],
['bolt11', 'lnbc210n1p0fake'],
[
'description',
JSON.stringify({
pubkey: SENDER,
content: 'Zap!',
tags: [['p', RECIPIENT], ['amount', '21000']]
})
]
]
})
const payment = fakeEvent({
id: PAYMENT_ID,
kind: ExtendedKind.PAYMENT_NOTIFICATION,
tags: [['p', RECIPIENT], ['amount', '100000']]
})
const note = fakeEvent({ id: '1'.repeat(64), kind: kinds.ShortTextNote, content: 'hi', tags: [] })
const attested = new Set([ZAP_ID, PAYMENT_ID])
expect(shouldIncludePaymentInFeed(zap, attested)).toBe(true)
expect(shouldIncludePaymentInFeed(payment, attested)).toBe(true)
expect(shouldIncludePaymentInFeed(zap, new Set())).toBe(false)
expect(shouldIncludePaymentInFeed(note, attested)).toBe(true)
})
})
describe('getPaymentNotificationInfo', () => {
it('uses only the first p, e, and a tags', () => {
const evt = fakeEvent({

39
src/lib/superchat.ts

@ -16,6 +16,19 @@ import { Event, kinds } from 'nostr-tools' @@ -16,6 +16,19 @@ import { Event, kinds } from 'nostr-tools'
export const PAYMENT_ATTESTATION_TARGET_KINDS = new Set(['9735', '9740', '9736', '1814'])
/** Payment kinds shown in feeds only when attested (kind 9741); excludes lightning zap receipts. */
export const FEED_SUPERCHAT_KINDS: readonly number[] = [
ExtendedKind.PAYMENT_NOTIFICATION,
ExtendedKind.MONERO_TIP_DISCLOSURE,
ExtendedKind.MONERO_TIP_RECEIPT
]
const FEED_SUPERCHAT_KIND_SET = new Set(FEED_SUPERCHAT_KINDS)
export function isFeedSuperchatKind(kind: number): boolean {
return FEED_SUPERCHAT_KIND_SET.has(kind)
}
export type PaymentNotificationInfo = {
senderPubkey: string
recipientPubkey: string
@ -267,8 +280,7 @@ export function collectAttestedSuperchatsFromRepliesMap( @@ -267,8 +280,7 @@ export function collectAttestedSuperchatsFromRepliesMap(
export function partitionAttestedSuperchats(
items: Event[],
attestedIds: Set<string>,
_zapReplyThreshold: number
attestedIds: Set<string>
): { superchats: Event[]; rest: Event[] } {
const superchats: Event[] = []
const rest: Event[] = []
@ -298,6 +310,29 @@ export function partitionAttestedSuperchats( @@ -298,6 +310,29 @@ export function partitionAttestedSuperchats(
return { superchats: sortSuperchatsByAmountDesc(superchats), rest }
}
/** Target payment ids from any valid kind 9741 (feeds are not scoped to one recipient). */
export function buildGlobalAttestedSuperchatIdSet(attestations: Event[]): Set<string> {
const out = new Set<string>()
for (const attestation of attestations) {
if (!isValidPaymentAttestation(attestation, attestation.pubkey)) continue
const targetId = getPaymentAttestationTargetId(attestation)
if (targetId) out.add(targetId.toLowerCase())
}
return out
}
/**
* Feeds: kind 9735 / 9740 / 9736 / 1814 only when attested (9741).
* Same attestation rule as threads and profile walls.
*/
export function shouldIncludePaymentInFeed(
event: Event,
attestedIds: ReadonlySet<string>
): boolean {
if (!isSuperchatKind(event.kind)) return true
return isAttestedSuperchat(event, attestedIds)
}
export function replyFeedSuperchatsFirst(sortedNonSuperchatReplies: Event[], superchats: Event[]) {
return [...superchats, ...sortedNonSuperchatReplies]
}

6
src/lib/xmr-usd-rate.ts

@ -2,6 +2,12 @@ const CACHE_MS = 5 * 60 * 1000 @@ -2,6 +2,12 @@ const CACHE_MS = 5 * 60 * 1000
let cache: { usd: number; at: number } | null = null
/** Cached XMR/USD if {@link fetchXmrUsdRate} has run recently (sync feed filters). */
export function getCachedXmrUsdRate(): number | null {
if (cache && Date.now() - cache.at < CACHE_MS) return cache.usd
return null
}
/** Latest XMR/USD spot price (cached ~5 min). */
export async function fetchXmrUsdRate(): Promise<number | null> {
if (cache && Date.now() - cache.at < CACHE_MS) {

50
src/pages/secondary/WalletPage/ZapReplyThresholdInput.tsx

@ -1,50 +0,0 @@ @@ -1,50 +0,0 @@
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { useZap } from '@/providers/ZapProvider'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
export default function ZapReplyThresholdInput() {
const { t } = useTranslation()
const { zapReplyThreshold, updateZapReplyThreshold } = useZap()
const [zapReplyThresholdInput, setZapReplyThresholdInput] = useState(zapReplyThreshold)
useEffect(() => {
setZapReplyThresholdInput(zapReplyThreshold)
}, [zapReplyThreshold])
return (
<div className="w-full space-y-1">
<Label htmlFor="zap-reply-threshold-input">
<div className="text-base font-medium">{t('Zap reply threshold')}</div>
<div className="text-muted-foreground text-sm">
{t('Zaps above this amount will appear as replies in threads')}
</div>
</Label>
<div className="flex items-center gap-2">
<Input
id="zap-reply-threshold-input"
className="w-20"
value={zapReplyThresholdInput}
onChange={(e) => {
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)
}}
/>
<span className="text-sm text-muted-foreground shrink-0">{t('sats')}</span>
</div>
</div>
)
}

2
src/pages/secondary/WalletPage/index.tsx

@ -5,7 +5,6 @@ import { usePrimaryNoteView } from '@/contexts/primary-note-view-context' @@ -5,7 +5,6 @@ import { usePrimaryNoteView } from '@/contexts/primary-note-view-context'
import { forwardRef, useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import LightningAddressInput from './LightningAddressInput'
import ZapReplyThresholdInput from './ZapReplyThresholdInput'
import WalletZapSendingSettings from './WalletZapSendingSettings'
const WalletPage = forwardRef(({ index, hideTitlebar = false }: { index?: number; hideTitlebar?: boolean }, ref) => {
@ -33,7 +32,6 @@ const WalletPage = forwardRef(({ index, hideTitlebar = false }: { index?: number @@ -33,7 +32,6 @@ const WalletPage = forwardRef(({ index, hideTitlebar = false }: { index?: number
<div key={contentKey} className="px-4 pt-3 space-y-4">
{LIGHTNING_WALLET_PAY_ENABLED ? <WalletZapSendingSettings /> : null}
<LightningAddressInput />
<ZapReplyThresholdInput />
</div>
</SecondaryPageLayout>
)

10
src/providers/ZapProvider.tsx

@ -18,8 +18,6 @@ type TZapContext = { @@ -18,8 +18,6 @@ type TZapContext = {
updateDefaultComment: (comment: string) => void
quickZap: boolean
updateQuickZap: (quickZap: boolean) => void
zapReplyThreshold: number
updateZapReplyThreshold: (sats: number) => void
includePublicZapReceipt: boolean
updateIncludePublicZapReceipt: (include: boolean) => void
}
@ -39,7 +37,6 @@ export function ZapProvider({ children }: { children: React.ReactNode }) { @@ -39,7 +37,6 @@ export function ZapProvider({ children }: { children: React.ReactNode }) {
const [defaultZapSats, setDefaultZapSats] = useState<number>(storage.getDefaultZapSats())
const [defaultZapComment, setDefaultZapComment] = useState<string>(storage.getDefaultZapComment())
const [quickZap, setQuickZap] = useState<boolean>(storage.getQuickZap())
const [zapReplyThreshold, setZapReplyThreshold] = useState<number>(storage.getZapReplyThreshold())
const [includePublicZapReceipt, setIncludePublicZapReceipt] = useState<boolean>(
storage.getIncludePublicZapReceipt()
)
@ -95,11 +92,6 @@ export function ZapProvider({ children }: { children: React.ReactNode }) { @@ -95,11 +92,6 @@ export function ZapProvider({ children }: { children: React.ReactNode }) {
setQuickZap(quickZap)
}
const updateZapReplyThreshold = (sats: number) => {
storage.setZapReplyThreshold(sats)
setZapReplyThreshold(sats)
}
const updateIncludePublicZapReceipt = (include: boolean) => {
setIncludePublicZapReceipt(include)
void storage.setIncludePublicZapReceiptAsync(include)
@ -117,8 +109,6 @@ export function ZapProvider({ children }: { children: React.ReactNode }) { @@ -117,8 +109,6 @@ export function ZapProvider({ children }: { children: React.ReactNode }) {
updateDefaultComment,
quickZap,
updateQuickZap,
zapReplyThreshold,
updateZapReplyThreshold,
includePublicZapReceipt,
updateIncludePublicZapReceipt
}}

24
src/services/local-storage.service.ts

@ -54,7 +54,6 @@ const SETTINGS_KEYS = [ @@ -54,7 +54,6 @@ const SETTINGS_KEYS = [
StorageKey.DEFAULT_ZAP_COMMENT,
StorageKey.QUICK_ZAP,
StorageKey.INCLUDE_PUBLIC_ZAP_RECEIPT,
StorageKey.ZAP_REPLY_THRESHOLD,
StorageKey.AUTOPLAY,
StorageKey.HIDE_UNTRUSTED_INTERACTIONS,
StorageKey.HIDE_UNTRUSTED_NOTIFICATIONS,
@ -102,7 +101,6 @@ class LocalStorageService { @@ -102,7 +101,6 @@ class LocalStorageService {
private defaultZapComment: string = 'Zap!'
private quickZap: boolean = false
private includePublicZapReceipt: boolean = true
private zapReplyThreshold: number = 1
private mediaUploadService: string = DEFAULT_NIP_96_SERVICE
private autoplay: boolean = true
private hideUntrustedInteractions: boolean = false
@ -205,14 +203,6 @@ class LocalStorageService { @@ -205,14 +203,6 @@ class LocalStorageService {
this.includePublicZapReceipt = includeReceiptStr !== 'false'
}
const zapReplyThresholdStr = window.localStorage.getItem(StorageKey.ZAP_REPLY_THRESHOLD)
if (zapReplyThresholdStr) {
const num = parseInt(zapReplyThresholdStr)
if (!isNaN(num)) {
this.zapReplyThreshold = num
}
}
// deprecated
this.mediaUploadService =
window.localStorage.getItem(StorageKey.MEDIA_UPLOAD_SERVICE) ?? DEFAULT_NIP_96_SERVICE
@ -609,11 +599,6 @@ class LocalStorageService { @@ -609,11 +599,6 @@ class LocalStorageService {
if (quickZapStr != null) this.quickZap = quickZapStr === 'true'
const includeReceiptStr = get(StorageKey.INCLUDE_PUBLIC_ZAP_RECEIPT)
if (includeReceiptStr != null) this.includePublicZapReceipt = includeReceiptStr !== 'false'
const zapReplyStr = get(StorageKey.ZAP_REPLY_THRESHOLD)
if (zapReplyStr != null) {
const num = parseInt(zapReplyStr)
if (!isNaN(num)) this.zapReplyThreshold = num
}
this.autoplay = get(StorageKey.AUTOPLAY) !== 'false'
const hideInteractions = get(StorageKey.HIDE_UNTRUSTED_INTERACTIONS)
if (hideInteractions != null) this.hideUntrustedInteractions = hideInteractions === 'true'
@ -841,15 +826,6 @@ class LocalStorageService { @@ -841,15 +826,6 @@ class LocalStorageService {
await this.persistSettingToIndexedDb(StorageKey.INCLUDE_PUBLIC_ZAP_RECEIPT, include.toString())
}
getZapReplyThreshold() {
return this.zapReplyThreshold
}
setZapReplyThreshold(sats: number) {
this.zapReplyThreshold = sats
this.persistSetting(StorageKey.ZAP_REPLY_THRESHOLD, sats.toString())
}
getAutoplay() {
return this.autoplay
}

Loading…
Cancel
Save