Browse Source

remove zaps

imwald
Silberengel 4 weeks ago
parent
commit
ee8ce71158
  1. 161
      src/components/NoteStats/ZapButton.tsx
  2. 4
      src/components/PaytoLink/index.tsx
  3. 36
      src/components/Profile/index.tsx
  4. 55
      src/components/ZapDialog/index.tsx
  5. 3
      src/constants.ts
  6. 15
      src/hooks/useProfileAuthorFeedSubRequests.ts
  7. 13
      src/hooks/useProfileReportsEvents.tsx
  8. 49
      src/hooks/useProfileWall.tsx
  9. 74
      src/pages/secondary/WalletPage/WalletZapSendingSettings.tsx
  10. 69
      src/pages/secondary/WalletPage/index.tsx
  11. 3
      src/providers/ZapProvider.tsx

161
src/components/NoteStats/ZapButton.tsx

@ -1,3 +1,4 @@
import { ZAP_SENDING_ENABLED } from '@/constants'
import { Skeleton } from '@/components/ui/skeleton' import { Skeleton } from '@/components/ui/skeleton'
import { useNoteStatsById } from '@/hooks/useNoteStatsById' import { useNoteStatsById } from '@/hooks/useNoteStatsById'
import { import {
@ -33,7 +34,161 @@ type ZapButtonProps = {
noteStats?: Partial<TNoteStats> noteStats?: Partial<TNoteStats>
} }
function formatAmount(amount: number) {
if (amount < 1000) return amount
if (amount < 1000000) return `${Math.round(amount / 100) / 10}k`
return `${Math.round(amount / 100000) / 10}M`
}
/** Zap tally + payment-methods dialog when {@link ZAP_SENDING_ENABLED} is false. */
function ZapPaymentMethodsButton({ event, hideCount = false, noteStats }: ZapButtonProps) {
const { t } = useTranslation()
const { pubkey } = useNostr()
const [openPaymentDialog, setOpenPaymentDialog] = useState(false)
const statsLoaded = noteStats?.updatedAt != null
const { zapAmount, hasZapped } = useMemo(() => {
return {
zapAmount: noteStats?.zaps?.reduce((acc, zap) => acc + zap.amount, 0),
hasZapped: pubkey ? noteStats?.zaps?.some((zap) => zap.pubkey === pubkey) : false
}
}, [noteStats, pubkey])
const showZapAmount = !hideCount && (statsLoaded || (zapAmount ?? 0) > 0)
const authorPubkey = event.pubkey.toLowerCase()
const isSelf = !!pubkey && pubkey.toLowerCase() === authorPubkey
const feedProfiles = useNoteFeedProfileContext()
const feedProfile = feedProfiles?.profiles.get(authorPubkey)
const feedProfileRef = useRef(feedProfile)
feedProfileRef.current = feedProfile
const [disable, setDisable] = useState(true)
const [tipPaymentData, setTipPaymentData] = useState<RecipientZapPaymentData | null>(null)
const applyTipAvailability = useCallback(
(
profile: TProfile | null,
profileEvent: Event | null | undefined,
paymentInfo: ReturnType<typeof getPaymentInfoFromEvent> | null
) => {
const canTip = recipientHasAnyPaymentOptions(
paymentInfo,
profile,
profileEvent ?? null
)
setDisable(!canTip)
setTipPaymentData((prev) =>
mergeRecipientZapPaymentData(
buildRecipientZapPaymentData(paymentInfo, profile, profileEvent ?? null),
prev
)
)
},
[]
)
useEffect(() => {
if (isSelf) return
if (!feedProfile || feedProfile.batchPlaceholder) return
applyTipAvailability(feedProfile, null, null)
}, [isSelf, feedProfile, feedProfiles?.version, applyTipAvailability])
useEffect(() => {
if (isSelf) {
setDisable(true)
setTipPaymentData(null)
return
}
setDisable(true)
setTipPaymentData(null)
let cancelled = false
void Promise.allSettled([
replaceableEventService.fetchReplaceableEvent(authorPubkey, kinds.Metadata),
client.fetchPaymentInfoEvent(authorPubkey),
replaceableEventService.getProfileFromIndexedDB(authorPubkey)
]).then(([profileRes, paymentRes, idbRes]) => {
if (cancelled) return
const profileEvent =
profileRes.status === 'fulfilled' ? profileRes.value : undefined
const paymentEvent =
paymentRes.status === 'fulfilled' ? paymentRes.value : undefined
const idbProfile = idbRes.status === 'fulfilled' ? idbRes.value : undefined
const cachedFeed = feedProfileRef.current
const profile =
(profileEvent ? getProfileFromEvent(profileEvent) : null) ??
(cachedFeed && !cachedFeed.batchPlaceholder ? cachedFeed : null) ??
idbProfile ??
null
const paymentInfo = paymentEvent ? getPaymentInfoFromEvent(paymentEvent) : null
applyTipAvailability(profile, profileEvent ?? null, paymentInfo)
})
return () => {
cancelled = true
}
}, [authorPubkey, isSelf, applyTipAvailability])
const handleOpenPaymentMethods = (e: React.MouseEvent) => {
e.stopPropagation()
e.preventDefault()
if (disable) return
setOpenPaymentDialog(true)
}
return (
<>
<button
type="button"
className={cn(
'group flex items-center gap-1 select-none px-3 h-full',
disable ? 'cursor-not-allowed' : 'cursor-pointer'
)}
title={disable ? t('Zaps') : t('Payment methods')}
aria-label={disable ? t('Zaps') : t('Payment methods')}
disabled={disable}
onClick={handleOpenPaymentMethods}
>
<Zap
className={cn(
hasZapped && 'fill-yellow-400',
disable
? 'text-muted-foreground/40'
: cn(
'text-muted-foreground group-hover:text-yellow-400',
hasZapped && 'text-yellow-400'
)
)}
/>
{showZapAmount && (
<div
className={cn(
'text-sm tabular-nums',
hasZapped ? 'text-yellow-400' : 'text-muted-foreground'
)}
>
{formatAmount(zapAmount ?? 0)}
</div>
)}
</button>
<ZapDialog
open={openPaymentDialog}
setOpen={setOpenPaymentDialog}
pubkey={event.pubkey}
event={event}
prefetchedPayment={tipPaymentData}
/>
</>
)
}
export function ZapButtonWithStats({ event, hideCount = false, noteStats }: ZapButtonProps) { export function ZapButtonWithStats({ event, hideCount = false, noteStats }: ZapButtonProps) {
if (!ZAP_SENDING_ENABLED) {
return <ZapPaymentMethodsButton event={event} hideCount={hideCount} noteStats={noteStats} />
}
const { t } = useTranslation() const { t } = useTranslation()
const { checkLogin, pubkey } = useNostr() const { checkLogin, pubkey } = useNostr()
const { defaultZapSats, defaultZapComment, quickZap, includePublicZapReceipt } = useZap() const { defaultZapSats, defaultZapComment, quickZap, includePublicZapReceipt } = useZap()
@ -305,9 +460,3 @@ export default function ZapButton({ event, hideCount = false }: ZapButtonProps)
const noteStats = useNoteStatsById(event.id) const noteStats = useNoteStatsById(event.id)
return <ZapButtonWithStats event={event} hideCount={hideCount} noteStats={noteStats} /> return <ZapButtonWithStats event={event} hideCount={hideCount} noteStats={noteStats} />
} }
function formatAmount(amount: number) {
if (amount < 1000) return amount
if (amount < 1000000) return `${Math.round(amount / 100) / 10}k`
return `${Math.round(amount / 100000) / 10}M`
}

4
src/components/PaytoLink/index.tsx

@ -1,3 +1,4 @@
import { ZAP_SENDING_ENABLED } from '@/constants'
import { useState } from 'react' import { useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { toast } from 'sonner' import { toast } from 'sonner'
@ -65,7 +66,8 @@ export default function PaytoLink({
const info = getPaytoTypeInfo(type) const info = getPaytoTypeInfo(type)
const known = isKnownPaytoType(type) const known = isKnownPaytoType(type)
const isLightning = isLightningPaytoType(type) const isLightning = isLightningPaytoType(type)
const canZap = isZappableLightningPaytoType(type) && !!pubkey && !!onOpenZap const canZap =
ZAP_SENDING_ENABLED && isZappableLightningPaytoType(type) && !!pubkey && !!onOpenZap
const handleClick = (e: React.MouseEvent) => { const handleClick = (e: React.MouseEvent) => {
e.preventDefault() e.preventDefault()

36
src/components/Profile/index.tsx

@ -68,7 +68,6 @@ import ProfileReportsDialog from './ProfileReportsDialog'
import SmartFollowings from './SmartFollowings' import SmartFollowings from './SmartFollowings'
import SmartMuteLink from './SmartMuteLink' import SmartMuteLink from './SmartMuteLink'
import SmartRelays from './SmartRelays' import SmartRelays from './SmartRelays'
import ZapDialog from '@/components/ZapDialog'
import PostEditor from '@/components/PostEditor' import PostEditor from '@/components/PostEditor'
import { import {
ScheduleVideoCallDialog, ScheduleVideoCallDialog,
@ -81,6 +80,7 @@ import { FAST_READ_RELAY_URLS, FAST_WRITE_RELAY_URLS } from '@/constants'
import { nip66Service } from '@/services/nip66.service' import { nip66Service } from '@/services/nip66.service'
import PaymentMethodsSection from '@/components/PaymentMethodsSection' import PaymentMethodsSection from '@/components/PaymentMethodsSection'
import { buildRecipientZapPaymentData } from '@/hooks/useRecipientAlternativePayments' import { buildRecipientZapPaymentData } from '@/hooks/useRecipientAlternativePayments'
import ZapDialog from '@/components/ZapDialog'
import { import {
groupPaymentMethodsByDisplayType, groupPaymentMethodsByDisplayType,
mergePaymentMethods, mergePaymentMethods,
@ -114,8 +114,7 @@ export default function Profile({
const { pubkey: accountPubkey, profileEvent: accountProfileEvent, publish, checkLogin } = useNostr() const { pubkey: accountPubkey, profileEvent: accountProfileEvent, publish, checkLogin } = useNostr()
const [paymentInfo, setPaymentInfo] = useState<ReturnType<typeof getPaymentInfoFromEvent> | null>(null) const [paymentInfo, setPaymentInfo] = useState<ReturnType<typeof getPaymentInfoFromEvent> | null>(null)
const [profileEvent, setProfileEvent] = useState<NostrEvent | undefined>(undefined) const [profileEvent, setProfileEvent] = useState<NostrEvent | undefined>(undefined)
const [openZapDialog, setOpenZapDialog] = useState(false) const [openPaymentDialog, setOpenPaymentDialog] = useState(false)
const [zapLightningDefault, setZapLightningDefault] = useState<string | null>(null)
const [openPublicMessageTo, setOpenPublicMessageTo] = useState<string | null>(null) const [openPublicMessageTo, setOpenPublicMessageTo] = useState<string | null>(null)
const [openCallInviteTo, setOpenCallInviteTo] = useState<{ pubkey: string; url: string } | null>(null) const [openCallInviteTo, setOpenCallInviteTo] = useState<{ pubkey: string; url: string } | null>(null)
const [openScheduleOwnCall, setOpenScheduleOwnCall] = useState(false) const [openScheduleOwnCall, setOpenScheduleOwnCall] = useState(false)
@ -147,12 +146,12 @@ export default function Profile({
[mergedPaymentMethods] [mergedPaymentMethods]
) )
const hasTipDialog = useMemo( const hasPaymentMethods = useMemo(
() => recipientHasAnyPaymentOptions(paymentInfo, profile ?? null, effectiveProfileEvent), () => recipientHasAnyPaymentOptions(paymentInfo, profile ?? null, effectiveProfileEvent),
[paymentInfo, profile, effectiveProfileEvent] [paymentInfo, profile, effectiveProfileEvent]
) )
const prefetchedZapPayment = useMemo( const prefetchedPaymentData = useMemo(
() => () =>
profile?.pubkey profile?.pubkey
? buildRecipientZapPaymentData(paymentInfo, profile ?? null, effectiveProfileEvent ?? null) ? buildRecipientZapPaymentData(paymentInfo, profile ?? null, effectiveProfileEvent ?? null)
@ -506,15 +505,11 @@ export default function Profile({
)} )}
{!isSelf ? ( {!isSelf ? (
<> <>
{hasTipDialog && ( {hasPaymentMethods && (
<ProfileZapButton <ProfileZapButton
pubkey={pubkey} pubkey={pubkey}
openZapDialog={openZapDialog} openZapDialog={openPaymentDialog}
setOpenZapDialog={(open) => { setOpenZapDialog={setOpenPaymentDialog}
if (open) setZapLightningDefault(null)
setOpenZapDialog(open)
if (!open) setZapLightningDefault(null)
}}
/> />
)} )}
<FollowButton pubkey={pubkey} /> <FollowButton pubkey={pubkey} />
@ -585,24 +580,17 @@ export default function Profile({
<PaymentMethodsSection <PaymentMethodsSection
groups={paymentMethodsByType} groups={paymentMethodsByType}
recipientPubkey={pubkey} recipientPubkey={pubkey}
onOpenZap={(lightningAuthority) => {
setZapLightningDefault(lightningAuthority)
setOpenZapDialog(true)
}}
className="mt-2 mb-4 p-3 pb-4 border rounded-lg bg-muted/50 min-w-0" className="mt-2 mb-4 p-3 pb-4 border rounded-lg bg-muted/50 min-w-0"
/> />
)} )}
{!isSelf && hasPaymentMethods && (
<ZapDialog <ZapDialog
open={openZapDialog} open={openPaymentDialog}
setOpen={(next) => { setOpen={setOpenPaymentDialog}
const willOpen = typeof next === 'function' ? next(openZapDialog) : next
setOpenZapDialog(willOpen)
if (!willOpen) setZapLightningDefault(null)
}}
pubkey={pubkey} pubkey={pubkey}
defaultLightningAddress={zapLightningDefault} prefetchedPayment={prefetchedPaymentData}
prefetchedPayment={prefetchedZapPayment}
/> />
)}
<div className="flex flex-wrap gap-4 items-center gap-x-4 gap-y-2 mt-2 text-sm min-w-0"> <div className="flex flex-wrap gap-4 items-center gap-x-4 gap-y-2 mt-2 text-sm min-w-0">
<SmartFollowings pubkey={pubkey} /> <SmartFollowings pubkey={pubkey} />
<SmartRelays pubkey={pubkey} /> <SmartRelays pubkey={pubkey} />

55
src/components/ZapDialog/index.tsx

@ -1,3 +1,4 @@
import { ZAP_SENDING_ENABLED } from '@/constants'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { import {
Dialog, Dialog,
@ -32,7 +33,10 @@ import { useTranslation } from 'react-i18next'
import { toast } from 'sonner' import { toast } from 'sonner'
import { import {
buildOrderedZapLightningAddresses, buildOrderedZapLightningAddresses,
groupPaymentMethodsByDisplayType,
mergePaymentMethods,
prepareZapDialogAlternativePayments, prepareZapDialogAlternativePayments,
sortMergedPaymentMethods,
ZAP_HIDE_BITCOIN_ALTS_MAX_SATS ZAP_HIDE_BITCOIN_ALTS_MAX_SATS
} from '@/lib/merge-payment-methods' } from '@/lib/merge-payment-methods'
import PaymentMethodsSection from '@/components/PaymentMethodsSection' import PaymentMethodsSection from '@/components/PaymentMethodsSection'
@ -102,12 +106,20 @@ export default function ZapDialog({
] ]
) )
const canLightningZap = lightningAddressOptions.length > 0 const canLightningZap = lightningAddressOptions.length > 0
const dialogTitlePrefix = canLightningZap ? t('Zap to') : t('Pay to') const paymentsOnly = !ZAP_SENDING_ENABLED
const dialogDescription = canLightningZap const dialogTitlePrefix = paymentsOnly
? t('Payment methods')
: canLightningZap
? t('Zap to')
: t('Pay to')
const dialogDescription = paymentsOnly
? t('Payment methods')
: canLightningZap
? t('Send a Lightning payment to this user') ? t('Send a Lightning payment to this user')
: t('Send a payment to this user') : t('Send a payment to this user')
const maybeOfferTipNoticeOnClose = () => { const maybeOfferTipNoticeOnClose = () => {
if (paymentsOnly) return
if (skipTipNoticeOnCloseRef.current) return if (skipTipNoticeOnCloseRef.current) return
if (selfPubkey && pubkey === selfPubkey) return if (selfPubkey && pubkey === selfPubkey) return
setTipNoticeOpen(true) setTipNoticeOpen(true)
@ -186,11 +198,13 @@ export default function ZapDialog({
}} }}
/> />
</DrawerContent> </DrawerContent>
{!paymentsOnly && (
<TipPublicMessagePrompt <TipPublicMessagePrompt
open={tipNoticeOpen} open={tipNoticeOpen}
onOpenChange={setTipNoticeOpen} onOpenChange={setTipNoticeOpen}
recipientPubkey={pubkey} recipientPubkey={pubkey}
/> />
)}
</Drawer> </Drawer>
) )
} }
@ -223,11 +237,13 @@ export default function ZapDialog({
/> />
</DialogContent> </DialogContent>
</Dialog> </Dialog>
{!paymentsOnly && (
<TipPublicMessagePrompt <TipPublicMessagePrompt
open={tipNoticeOpen} open={tipNoticeOpen}
onOpenChange={setTipNoticeOpen} onOpenChange={setTipNoticeOpen}
recipientPubkey={pubkey} recipientPubkey={pubkey}
/> />
)}
</> </>
) )
} }
@ -258,8 +274,43 @@ function ZapDialogContent({
}) { }) {
const { t, i18n } = useTranslation() const { t, i18n } = useTranslation()
const { pubkey } = useNostr() const { pubkey } = useNostr()
const paymentsOnly = !ZAP_SENDING_ENABLED
const { defaultZapSats, defaultZapComment, includePublicZapReceipt, updateIncludePublicZapReceipt } = const { defaultZapSats, defaultZapComment, includePublicZapReceipt, updateIncludePublicZapReceipt } =
useZap() useZap()
const allPaymentGroups = useMemo(() => {
if (!paymentsOnly) return []
const merged = sortMergedPaymentMethods(
mergePaymentMethods(
recipientPayment.paymentInfo,
recipientPayment.profile,
recipientPayment.profileEvent
)
)
return groupPaymentMethodsByDisplayType(merged)
}, [paymentsOnly, recipientPayment])
if (paymentsOnly) {
return (
<div
className="px-4 pb-4"
style={{ paddingBottom: 'max(1rem, env(safe-area-inset-bottom))' }}
>
{allPaymentGroups.length > 0 ? (
<PaymentMethodsSection
groups={allPaymentGroups}
recipientPubkey={recipient}
title={t('Payment methods')}
className="rounded-lg border border-border bg-muted/40 p-3 min-w-0"
/>
) : (
<p className="py-8 text-center text-sm text-muted-foreground">
{t('No payment methods available for this profile')}
</p>
)}
</div>
)
}
const [sats, setSats] = useState(() => clampZapSats(defaultAmount ?? defaultZapSats)) const [sats, setSats] = useState(() => clampZapSats(defaultAmount ?? defaultZapSats))
const [comment, setComment] = useState(defaultComment ?? defaultZapComment) const [comment, setComment] = useState(defaultComment ?? defaultZapComment)
const [zapping, setZapping] = useState(false) const [zapping, setZapping] = useState(false)

3
src/constants.ts

@ -603,6 +603,9 @@ export const ExtendedKind = {
EVENTS_I_MUTED_NOTIFICATIONS_LIST: 19132 EVENTS_I_MUTED_NOTIFICATIONS_LIST: 19132
} }
/** NIP-57 send flow (wallet connect, zap dialog, note zap button). Kind 9735 receipts and payment targets stay visible when false. */
export const ZAP_SENDING_ENABLED = false
/** Kind 0 + NIP-A3 payment: publish to profile mirrors, full outbox (NIP-65 + HTTP + cache), and IndexedDB. */ /** Kind 0 + NIP-A3 payment: publish to profile mirrors, full outbox (NIP-65 + HTTP + cache), and IndexedDB. */
export function isAuthorProfileMetadataPublishKind(kind: number): boolean { export function isAuthorProfileMetadataPublishKind(kind: number): boolean {
return kind === kinds.Metadata || kind === ExtendedKind.PAYMENT_INFO return kind === kinds.Metadata || kind === ExtendedKind.PAYMENT_INFO

15
src/hooks/useProfileAuthorFeedSubRequests.ts

@ -2,7 +2,7 @@ import { buildProfileAuthorSubRequestsFromUrlGroups } from '@/lib/profile-author
import { isSocialKindBlockedKind } from '@/constants' import { isSocialKindBlockedKind } from '@/constants'
import { useGlobalRelayBootstrapDefaults } from '@/hooks/use-global-relay-bootstrap-defaults' import { useGlobalRelayBootstrapDefaults } from '@/hooks/use-global-relay-bootstrap-defaults'
import { buildProfilePageReadRelayUrls } from '@/lib/favorites-feed-relays' import { buildProfilePageReadRelayUrls } from '@/lib/favorites-feed-relays'
import { hexPubkeysEqual, normalizeHexPubkey } from '@/lib/pubkey' import { hexPubkeysEqual, isValidPubkey, normalizeHexPubkey, userIdToPubkey } from '@/lib/pubkey'
import { normalizeAnyRelayUrl } from '@/lib/url' import { normalizeAnyRelayUrl } from '@/lib/url'
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import { useNostrOptional } from '@/providers/nostr-context' import { useNostrOptional } from '@/providers/nostr-context'
@ -65,11 +65,8 @@ export function useProfileAuthorFeedSubRequests({
const kindsKey = useMemo(() => [...kinds].join(','), [kinds]) const kindsKey = useMemo(() => [...kinds].join(','), [kinds])
const authorHex = useMemo(() => { const authorHex = useMemo(() => {
try { const pk = userIdToPubkey(pubkey)
return normalizeHexPubkey(pubkey) return isValidPubkey(pk) ? pk : ''
} catch {
return pubkey.trim()
}
}, [pubkey]) }, [pubkey])
const [refreshToken, setRefreshToken] = useState(0) const [refreshToken, setRefreshToken] = useState(0)
@ -103,6 +100,10 @@ export function useProfileAuthorFeedSubRequests({
} }
} }
// Bootstrap immediately (favorites + fast-read) so /users/… feeds are not stuck on "Nothing to load"
// while fetchRelayList runs (often 10–30s under relay contention).
applyRelayList(emptyAuthor)
void client void client
.peekRelayListFromStorage(pubkey) .peekRelayListFromStorage(pubkey)
.then((cached) => { .then((cached) => {
@ -125,7 +126,7 @@ export function useProfileAuthorFeedSubRequests({
}, [pubkey, relayListsKey, kindsKey, kinds, refreshToken, includeAuthorLocalRelays, useGlobalRelayBootstrap]) }, [pubkey, relayListsKey, kindsKey, kinds, refreshToken, includeAuthorLocalRelays, useGlobalRelayBootstrap])
const subRequests = useMemo(() => { const subRequests = useMemo(() => {
if (!relayUrls?.length) return [] as TFeedSubRequest[] if (!relayUrls?.length || !authorHex) return [] as TFeedSubRequest[]
return buildProfileAuthorSubRequestsFromUrlGroups([relayUrls], authorHex, [...kinds], limit) return buildProfileAuthorSubRequestsFromUrlGroups([relayUrls], authorHex, [...kinds], limit)
}, [relayUrls, authorHex, kinds, limit]) }, [relayUrls, authorHex, kinds, limit])

13
src/hooks/useProfileReportsEvents.tsx

@ -1,10 +1,11 @@
import { ExtendedKind } from '@/constants' import { ExtendedKind, FAST_READ_RELAY_URLS, PROFILE_RELAY_URLS } from '@/constants'
import { useGlobalRelayBootstrapDefaults } from '@/hooks/use-global-relay-bootstrap-defaults' import { useGlobalRelayBootstrapDefaults } from '@/hooks/use-global-relay-bootstrap-defaults'
import type { ProfileTimelineRelayUrlsBuilder } from '@/hooks/useProfileTimeline' import type { ProfileTimelineRelayUrlsBuilder } from '@/hooks/useProfileTimeline'
import { buildProfilePageReadRelayUrls, mergeRelayUrlLayers } from '@/lib/favorites-feed-relays' import { buildProfilePageReadRelayUrls, mergeRelayUrlLayers } from '@/lib/favorites-feed-relays'
import { isNip56ReportEvent } from '@/lib/event' import { isNip56ReportEvent } from '@/lib/event'
import { isReportAuthoredBy, reportTargetsPubkey } from '@/lib/nip56-reports' import { isReportAuthoredBy, reportTargetsPubkey } from '@/lib/nip56-reports'
import { normalizeHexPubkey } from '@/lib/pubkey' import { normalizeHexPubkey } from '@/lib/pubkey'
import { dedupeNormalizeRelayUrlsOrdered } from '@/lib/relay-url-priority'
import { normalizeAnyRelayUrl, subtractNormalizedRelayUrls } from '@/lib/url' import { normalizeAnyRelayUrl, subtractNormalizedRelayUrls } from '@/lib/url'
import { useDeletedEvent } from '@/providers/DeletedEventProvider' import { useDeletedEvent } from '@/providers/DeletedEventProvider'
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
@ -207,7 +208,15 @@ export function useProfileReportsEvents({
} }
const emptyAuthor = { read: [] as string[], write: [] as string[], httpRead: [] as string[], httpWrite: [] as string[] } const emptyAuthor = { read: [] as string[], write: [] as string[], httpRead: [] as string[], httpWrite: [] as string[] }
const provisionalUrls = resolveFeedUrls(emptyAuthor, includeAuthorLocalRelays) const authorPeek = await client.peekRelayListFromStorage(pubkey).catch(() => emptyAuthor)
if (cancelled) return
let provisionalUrls = resolveFeedUrls(authorPeek, includeAuthorLocalRelays)
if (provisionalUrls.length === 0) {
provisionalUrls = dedupeNormalizeRelayUrlsOrdered([
...PROFILE_RELAY_URLS,
...FAST_READ_RELAY_URLS
]).slice(0, 24)
}
if (provisionalUrls.length === 0) return if (provisionalUrls.length === 0) return
const filter = buildFilter(pkNorm, mode, limit) const filter = buildFilter(pkNorm, mode, limit)

49
src/hooks/useProfileWall.tsx

@ -18,12 +18,11 @@ import {
type ResolvedProfileBadge type ResolvedProfileBadge
} from '@/lib/nip58-profile-badges' } from '@/lib/nip58-profile-badges'
import { isDirectProfileWallComment } from '@/lib/profile-wall-comments' import { isDirectProfileWallComment } from '@/lib/profile-wall-comments'
import { normalizeHexPubkey } from '@/lib/pubkey' import { isValidPubkey, userIdToPubkey } from '@/lib/pubkey'
import { normalizeAnyRelayUrl } from '@/lib/url' import { normalizeAnyRelayUrl } from '@/lib/url'
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import { useDeletedEvent } from '@/providers/DeletedEventProvider' import { useDeletedEvent } from '@/providers/DeletedEventProvider'
import client, { replaceableEventService } from '@/services/client.service' import client, { replaceableEventService } from '@/services/client.service'
import { ReplaceableEventService } from '@/services/client-replaceable-events.service'
import indexedDb from '@/services/indexed-db.service' import indexedDb from '@/services/indexed-db.service'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Event, kinds, type Filter } from 'nostr-tools' import { Event, kinds, type Filter } from 'nostr-tools'
@ -111,6 +110,8 @@ export function useProfileWall(pubkey: string, profileEventId: string | undefine
useEffect(() => { useEffect(() => {
let cancelled = false let cancelled = false
let idleHandle: number | undefined
let idleTimeout: ReturnType<typeof setTimeout> | undefined
const run = async () => { const run = async () => {
const mem = wallCacheByKey.get(cacheKey) const mem = wallCacheByKey.get(cacheKey)
@ -123,15 +124,14 @@ export function useProfileWall(pubkey: string, profileEventId: string | undefine
setIsLoading(true) setIsLoading(true)
let pkNorm = pubkey const pkNorm = userIdToPubkey(pubkey) || pubkey
try { if (!isValidPubkey(pkNorm)) {
pkNorm = normalizeHexPubkey(pubkey) if (!cancelled) setIsLoading(false)
} catch { return
/* use raw */
} }
const emptyAuthor = { read: [] as string[], write: [] as string[], httpRead: [] as string[], httpWrite: [] as string[] } const emptyAuthor = { read: [] as string[], write: [] as string[], httpRead: [] as string[], httpWrite: [] as string[] }
const authorRl = await client.fetchRelayList(pubkey).catch(() => emptyAuthor) const authorRl = await client.peekRelayListFromStorage(pubkey).catch(() => emptyAuthor)
if (cancelled) return if (cancelled) return
const relayUrls = buildProfilePageReadRelayUrls( const relayUrls = buildProfilePageReadRelayUrls(
@ -212,9 +212,23 @@ export function useProfileWall(pubkey: string, profileEventId: string | undefine
setIsLoading(false) setIsLoading(false)
} }
void run() const scheduleRun = () => {
if (typeof requestIdleCallback === 'function') {
idleHandle = requestIdleCallback(() => void run(), { timeout: 4_000 })
} else {
idleTimeout = setTimeout(() => void run(), 400)
}
}
scheduleRun()
return () => { return () => {
cancelled = true cancelled = true
if (idleHandle !== undefined && typeof cancelIdleCallback === 'function') {
cancelIdleCallback(idleHandle)
}
if (idleTimeout !== undefined) {
clearTimeout(idleTimeout)
}
} }
}, [pubkey, profileEventId, cacheKey, refreshToken, relayListsKey]) }, [pubkey, profileEventId, cacheKey, refreshToken, relayListsKey])
@ -224,22 +238,5 @@ export function useProfileWall(pubkey: string, profileEventId: string | undefine
setRefreshToken((t) => t + 1) setRefreshToken((t) => t + 1)
}, [cacheKey]) }, [cacheKey])
useEffect(() => {
const onAuthorReplaceablesRefreshed: EventListener = (domEvt) => {
const pk = (domEvt as CustomEvent<{ pubkey?: string }>).detail?.pubkey?.toLowerCase()
if (!pk || pk !== normalizeHexPubkey(pubkey)) return
refresh()
}
window.addEventListener(
ReplaceableEventService.AUTHOR_REPLACEABLES_REFRESHED_EVENT,
onAuthorReplaceablesRefreshed
)
return () =>
window.removeEventListener(
ReplaceableEventService.AUTHOR_REPLACEABLES_REFRESHED_EVENT,
onAuthorReplaceablesRefreshed
)
}, [pubkey, refresh])
return { badges, comments, isLoading, refresh } return { badges, comments, isLoading, refresh }
} }

74
src/pages/secondary/WalletPage/WalletZapSendingSettings.tsx

@ -0,0 +1,74 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger
} from '@/components/ui/alert-dialog'
import { Button } from '@/components/ui/button'
import { useZap } from '@/providers/ZapProvider'
import { disconnect, launchModal } from '@getalby/bitcoin-connect-react'
import { useTranslation } from 'react-i18next'
import DefaultZapAmountInput from './DefaultZapAmountInput'
import DefaultZapCommentInput from './DefaultZapCommentInput'
import QuickZapSwitch from './QuickZapSwitch'
import IncludePublicZapReceiptSwitch from './IncludePublicZapReceiptSwitch'
export default function WalletZapSendingSettings() {
const { t } = useTranslation()
const { isWalletConnected, walletInfo } = useZap()
if (isWalletConnected) {
return (
<>
<div>
{walletInfo?.node.alias && (
<div className="mb-2">
{t('Connected to')} <strong>{walletInfo.node.alias}</strong>
</div>
)}
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="destructive">{t('Disconnect Wallet')}</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{t('Are you absolutely sure?')}</AlertDialogTitle>
<AlertDialogDescription>
{t('You will not be able to send zaps to others.')}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>{t('Cancel')}</AlertDialogCancel>
<AlertDialogAction variant="destructive" onClick={() => disconnect()}>
{t('Disconnect')}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
<DefaultZapAmountInput />
<DefaultZapCommentInput />
<QuickZapSwitch />
<IncludePublicZapReceiptSwitch />
</>
)
}
return (
<div>
<Button
className="bg-foreground hover:bg-foreground/90"
onClick={() => {
launchModal()
}}
>
{t('Connect Wallet')}
</Button>
</div>
)
}

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

@ -1,35 +1,18 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger
} from '@/components/ui/alert-dialog'
import { RefreshButton } from '@/components/RefreshButton' import { RefreshButton } from '@/components/RefreshButton'
import { Button } from '@/components/ui/button' import { ZAP_SENDING_ENABLED } from '@/constants'
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout' import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
import { usePrimaryNoteView } from '@/contexts/primary-note-view-context' import { usePrimaryNoteView } from '@/contexts/primary-note-view-context'
import { useZap } from '@/providers/ZapProvider'
import { disconnect, launchModal } from '@getalby/bitcoin-connect-react'
import { forwardRef, useCallback, useEffect, useState } from 'react' import { forwardRef, useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import DefaultZapAmountInput from './DefaultZapAmountInput'
import DefaultZapCommentInput from './DefaultZapCommentInput'
import LightningAddressInput from './LightningAddressInput' import LightningAddressInput from './LightningAddressInput'
import QuickZapSwitch from './QuickZapSwitch'
import IncludePublicZapReceiptSwitch from './IncludePublicZapReceiptSwitch'
import ZapReplyThresholdInput from './ZapReplyThresholdInput' import ZapReplyThresholdInput from './ZapReplyThresholdInput'
import WalletZapSendingSettings from './WalletZapSendingSettings'
const WalletPage = forwardRef(({ index, hideTitlebar = false }: { index?: number; hideTitlebar?: boolean }, ref) => { const WalletPage = forwardRef(({ index, hideTitlebar = false }: { index?: number; hideTitlebar?: boolean }, ref) => {
const { t } = useTranslation() const { t } = useTranslation()
const { registerPrimaryPanelRefresh } = usePrimaryNoteView() const { registerPrimaryPanelRefresh } = usePrimaryNoteView()
const [contentKey, setContentKey] = useState(0) const [contentKey, setContentKey] = useState(0)
const bump = useCallback(() => setContentKey((k) => k + 1), []) const bump = useCallback(() => setContentKey((k) => k + 1), [])
const { isWalletConnected, walletInfo } = useZap()
useEffect(() => { useEffect(() => {
if (!hideTitlebar) { if (!hideTitlebar) {
@ -48,54 +31,8 @@ const WalletPage = forwardRef(({ index, hideTitlebar = false }: { index?: number
controls={hideTitlebar ? undefined : <RefreshButton onClick={bump} />} controls={hideTitlebar ? undefined : <RefreshButton onClick={bump} />}
> >
<div key={contentKey} className="px-4 pt-3 space-y-4"> <div key={contentKey} className="px-4 pt-3 space-y-4">
{isWalletConnected ? ( {ZAP_SENDING_ENABLED ? <WalletZapSendingSettings /> : null}
<>
<div>
{walletInfo?.node.alias && (
<div className="mb-2">
{t('Connected to')} <strong>{walletInfo.node.alias}</strong>
</div>
)}
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="destructive">{t('Disconnect Wallet')}</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{t('Are you absolutely sure?')}</AlertDialogTitle>
<AlertDialogDescription>
{t('You will not be able to send zaps to others.')}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>{t('Cancel')}</AlertDialogCancel>
<AlertDialogAction variant="destructive" onClick={() => disconnect()}>
{t('Disconnect')}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
<DefaultZapAmountInput />
<DefaultZapCommentInput />
<QuickZapSwitch />
<IncludePublicZapReceiptSwitch />
<LightningAddressInput /> <LightningAddressInput />
</>
) : (
<div>
<Button
className="bg-foreground hover:bg-foreground/90"
onClick={() => {
launchModal()
}}
>
{t('Connect Wallet')}
</Button>
</div>
)}
{/* Zap Reply Threshold - always visible as it's just a display setting */}
<ZapReplyThresholdInput /> <ZapReplyThresholdInput />
</div> </div>
</SecondaryPageLayout> </SecondaryPageLayout>

3
src/providers/ZapProvider.tsx

@ -1,3 +1,4 @@
import { ZAP_SENDING_ENABLED } from '@/constants'
import lightningService from '@/services/lightning.service' import lightningService from '@/services/lightning.service'
import storage from '@/services/local-storage.service' import storage from '@/services/local-storage.service'
import { onConnected, onDisconnected } from '@getalby/bitcoin-connect-react' import { onConnected, onDisconnected } from '@getalby/bitcoin-connect-react'
@ -43,6 +44,8 @@ export function ZapProvider({ children }: { children: React.ReactNode }) {
const [walletInfo, setWalletInfo] = useState<GetInfoResponse | null>(null) const [walletInfo, setWalletInfo] = useState<GetInfoResponse | null>(null)
useEffect(() => { useEffect(() => {
if (!ZAP_SENDING_ENABLED) return
const unSubOnConnected = onConnected((provider) => { const unSubOnConnected = onConnected((provider) => {
setIsWalletConnected(true) setIsWalletConnected(true)
setWalletInfo(null) setWalletInfo(null)

Loading…
Cancel
Save