cleanup + icons

This commit is contained in:
Chad Curtis 2025-07-13 05:59:56 +00:00
parent ec1a9777de
commit 55bf545646
2 changed files with 59 additions and 61 deletions

View File

@ -1,5 +1,5 @@
import { useState, useEffect, useRef } from 'react'; import { useState, useEffect, useRef } from 'react';
import { Zap, Copy } from 'lucide-react'; import { Zap, Copy, Sparkle, Sparkles, Star, Rocket } from 'lucide-react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { import {
Dialog, Dialog,
@ -18,6 +18,7 @@ import { useAuthor } from '@/hooks/useAuthor';
import { useToast } from '@/hooks/useToast'; import { useToast } from '@/hooks/useToast';
import { useZaps } from '@/hooks/useZaps'; import { useZaps } from '@/hooks/useZaps';
import type { WebLNProvider } from 'webln'; import type { WebLNProvider } from 'webln';
import { requestProvider } from 'webln';
import QRCode from 'qrcode'; import QRCode from 'qrcode';
import type { Event } from 'nostr-tools'; import type { Event } from 'nostr-tools';
@ -27,11 +28,17 @@ interface ZapDialogProps {
className?: string; className?: string;
} }
const presetAmounts = [1, 50, 100, 250, 1000]; const presetAmounts = [
{ amount: 1, icon: Sparkle },
{ amount: 50, icon: Sparkles },
{ amount: 100, icon: Zap },
{ amount: 250, icon: Star },
{ amount: 1000, icon: Rocket },
];
export function ZapDialog({ target, children, className }: ZapDialogProps) { export function ZapDialog({ target, children, className }: ZapDialogProps) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [webln, _setWebln] = useState<WebLNProvider | null>(null); const [webln, setWebln] = useState<WebLNProvider | null>(null);
const { user } = useCurrentUser(); const { user } = useCurrentUser();
const { data: author } = useAuthor(target.pubkey); const { data: author } = useAuthor(target.pubkey);
const { toast } = useToast(); const { toast } = useToast();
@ -47,6 +54,23 @@ export function ZapDialog({ target, children, className }: ZapDialogProps) {
} }
}, [target]); }, [target]);
// Detect WebLN when dialog opens
useEffect(() => {
const detectWebLN = async () => {
if (open && !webln) {
try {
const provider = await requestProvider();
setWebln(provider);
} catch (error) {
console.warn('WebLN requestProvider failed:', error);
setWebln(null);
}
}
};
detectWebLN();
}, [open, webln]);
useEffect(() => { useEffect(() => {
if (invoice && qrCodeRef.current) { if (invoice && qrCodeRef.current) {
QRCode.toCanvas(qrCodeRef.current, invoice, { width: 256 }); QRCode.toCanvas(qrCodeRef.current, invoice, { width: 256 });
@ -81,8 +105,8 @@ export function ZapDialog({ target, children, className }: ZapDialogProps) {
return ( return (
<Dialog open={open} onOpenChange={setOpen}> <Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button size="sm" className={className}> <Button variant="ghost" size="sm" className={`text-muted-foreground hover:text-yellow-600 ${className || ''}`}>
<Zap className={`h-4 w-4 ${children ? 'mr-2' : ''}`} /> <Zap className={`h-4 w-4 ${children ? 'mr-1' : ''}`} />
{children} {children}
</Button> </Button>
</DialogTrigger> </DialogTrigger>
@ -123,12 +147,13 @@ export function ZapDialog({ target, children, className }: ZapDialogProps) {
}} }}
className="grid grid-cols-5 gap-2" className="grid grid-cols-5 gap-2"
> >
{presetAmounts.map((presetAmount) => ( {presetAmounts.map(({ amount: presetAmount, icon: Icon }) => (
<ToggleGroupItem <ToggleGroupItem
key={presetAmount} key={presetAmount}
value={String(presetAmount)} value={String(presetAmount)}
className="flex flex-col h-auto" className="flex flex-col h-auto"
> >
<Icon className="h-5 w-5 mb-1.5" />
{presetAmount} {presetAmount}
</ToggleGroupItem> </ToggleGroupItem>
))} ))}

View File

@ -1,11 +1,12 @@
import { useState } from 'react'; import { useState } from 'react';
import { useNostrPublish } from '@/hooks/useNostrPublish';
import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useCurrentUser } from '@/hooks/useCurrentUser';
import { useAuthor } from '@/hooks/useAuthor'; import { useAuthor } from '@/hooks/useAuthor';
import { useAppContext } from '@/hooks/useAppContext'; import { useAppContext } from '@/hooks/useAppContext';
import { useToast } from '@/hooks/useToast'; import { useToast } from '@/hooks/useToast';
import { nip57, nip19, Event } from 'nostr-tools'; import { nip57, nip19 } from 'nostr-tools';
import type { Event } from 'nostr-tools';
import type { WebLNProvider } from 'webln'; import type { WebLNProvider } from 'webln';
import { LNURL } from '@nostrify/nostrify/ln';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { useNostr } from '@nostrify/react'; import { useNostr } from '@nostrify/react';
@ -16,7 +17,6 @@ export function useZaps(target: Event, webln: WebLNProvider | null, onZapSuccess
const { toast } = useToast(); const { toast } = useToast();
const { user } = useCurrentUser(); const { user } = useCurrentUser();
const { config } = useAppContext(); const { config } = useAppContext();
const { mutate: publishEvent } = useNostrPublish();
const author = useAuthor(target?.pubkey); const author = useAuthor(target?.pubkey);
const [isZapping, setIsZapping] = useState(false); const [isZapping, setIsZapping] = useState(false);
const [invoice, setInvoice] = useState<string | null>(null); const [invoice, setInvoice] = useState<string | null>(null);
@ -86,18 +86,8 @@ export function useZaps(target: Event, webln: WebLNProvider | null, onZapSuccess
} }
try { try {
const lud16 = author.data?.metadata?.lud16;
if (!lud16) {
toast({
title: 'Lightning address not found',
description: 'The author does not have a lightning address configured.',
variant: 'destructive',
});
setIsZapping(false);
return;
}
if (!author.data) { if (!author.data || !author.data?.metadata) {
toast({ toast({
title: 'Author not found', title: 'Author not found',
description: 'Could not find the author of this item.', description: 'Could not find the author of this item.',
@ -107,67 +97,50 @@ export function useZaps(target: Event, webln: WebLNProvider | null, onZapSuccess
return; return;
} }
const zapEndpoint = await nip57.getZapEndpoint(author.data.event as Event); const { lud06, lud16 } = author.data.metadata;
if (!zapEndpoint) { if (!lud16 && !lud06) {
toast({ toast({
title: 'Zap endpoint not found', title: 'Lightning address not found',
description: 'Could not find a zap endpoint for the author.', description: 'The author does not have a lightning address (lud16 or lud06) configured.',
variant: 'destructive', variant: 'destructive',
}); });
setIsZapping(false); setIsZapping(false);
return; return;
} }
const lnurl = lud06 ? LNURL.fromString(lud06) : LNURL.fromLightningAddress(lud16!);
const zapAmount = amount * 1000; // convert to millisats const zapAmount = amount * 1000; // convert to millisats
const relays = [config.relayUrl]; const zapRequest = await user.signer.signEvent(nip57.makeZapRequest({
const zapRequest = nip57.makeZapRequest({
profile: target.pubkey, profile: target.pubkey,
event: target.id, event: target,
amount: zapAmount, amount: zapAmount,
relays, relays: [config.relayUrl],
comment: comment, comment: comment,
}));
const { pr: newInvoice } = await lnurl.getInvoice({
amount: zapAmount,
nostr: zapRequest,
}); });
if (naddr) { if (webln) {
const decoded = nip19.decode(naddr).data as nip19.AddressPointer; await webln.sendPayment(newInvoice);
zapRequest.tags.push(["a", `${decoded.kind}:${decoded.pubkey}:${decoded.identifier}`]); toast({
zapRequest.tags = zapRequest.tags.filter(t => t[0] !== 'e'); title: 'Zap successful!',
description: `You sent ${amount} sats to the author.`,
});
onZapSuccess?.();
} else {
setInvoice(newInvoice);
} }
publishEvent(zapRequest, {
onSuccess: async (event) => {
try {
const res = await fetch(`${zapEndpoint}?amount=${zapAmount}&nostr=${encodeURI(JSON.stringify(event))}`);
const { pr: newInvoice } = await res.json();
if (webln) {
await webln.sendPayment(newInvoice);
toast({
title: 'Zap successful!',
description: `You sent ${amount} sats to the author.`,
});
onZapSuccess?.();
} else {
setInvoice(newInvoice);
}
} catch (err) {
console.error('Zap error:', err);
toast({
title: 'Zap failed',
description: (err as Error).message,
variant: 'destructive',
});
} finally {
setIsZapping(false);
}
},
});
} catch (err) { } catch (err) {
console.error('Zap error:', err);
toast({ toast({
title: 'Zap failed', title: 'Zap failed',
description: (err as Error).message, description: (err as Error).message,
variant: 'destructive', variant: 'destructive',
}); });
} finally {
setIsZapping(false); setIsZapping(false);
} }
}; };