Files
eranos/src/components/FollowQRDialog.tsx
T
Chad Curtis 76c6846e91 Render BOLT11 lightning invoices in note content
Detect lnbc/lntb/lnbcrt/lntbs invoices (with optional lightning: prefix)
in note text and render them as interactive cards with a theme-aware QR
code, decoded amount, copy button, and Open in Wallet action.

- Add lightning-invoice token type to NoteContent tokenizer
- Create LightningInvoiceCard with tap-to-expand square QR, cqw-scaled
  amount text, and responsive layout
- Extract shared theme-aware QR color logic into src/lib/qrColors.ts
  (deduplicate from FollowQRDialog)
2026-04-09 08:02:26 -05:00

102 lines
3.4 KiB
TypeScript

import { useEffect, useState } from 'react';
import { nip19 } from 'nostr-tools';
import QRCode from 'qrcode';
import { Copy, Check } from 'lucide-react';
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar';
import { getAvatarShape } from '@/lib/avatarShape';
import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog';
import { useCurrentUser } from '@/hooks/useCurrentUser';
import { useAuthor } from '@/hooks/useAuthor';
import { genUserName } from '@/lib/genUserName';
import { getThemedQRColors } from '@/lib/qrColors';
interface FollowQRDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
}
export function FollowQRDialog({ open, onOpenChange }: FollowQRDialogProps) {
const { user } = useCurrentUser();
const author = useAuthor(user?.pubkey ?? '');
const [qrDataUrl, setQrDataUrl] = useState<string>('');
const [copied, setCopied] = useState(false);
const metadata = author.data?.metadata;
const displayName = user ? metadata?.name || genUserName(user.pubkey) : '';
const npub = user ? nip19.npubEncode(user.pubkey) : '';
const followUrl = npub ? `${window.location.origin}/follow/${npub}` : '';
useEffect(() => {
if (!followUrl || !open) return;
const { dark, light } = getThemedQRColors();
QRCode.toDataURL(followUrl, {
width: 400,
margin: 2,
color: { dark, light },
errorCorrectionLevel: 'M',
})
.then(setQrDataUrl)
.catch(console.error);
}, [followUrl, open]);
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(followUrl);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch {
// ignore
}
};
if (!user) return null;
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-sm p-6 flex flex-col items-center gap-5 rounded-2xl">
<DialogTitle className="sr-only">Share follow link</DialogTitle>
{/* Avatar + name */}
<div className="flex flex-col items-center gap-2">
<Avatar shape={getAvatarShape(metadata)} className="size-16 ring-2 ring-secondary">
<AvatarImage src={metadata?.picture} alt={displayName} />
<AvatarFallback className="text-xl font-semibold">
{displayName.charAt(0).toUpperCase()}
</AvatarFallback>
</Avatar>
<p className="text-sm text-muted-foreground text-center">
Scan to follow <span className="text-foreground font-medium">{displayName}</span>
</p>
</div>
{/* QR code */}
{qrDataUrl ? (
<img
src={qrDataUrl}
alt="Follow QR code"
className="w-full rounded-xl border border-border"
style={{ imageRendering: 'pixelated' }}
/>
) : (
<div className="w-full aspect-square rounded-xl border border-border bg-muted animate-pulse" />
)}
{/* Copy link */}
<button
onClick={handleCopy}
className="flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors"
>
{copied
? <Check className="size-3.5 text-primary flex-shrink-0" />
: <Copy className="size-3.5 flex-shrink-0" />}
<span className="truncate max-w-64">{followUrl}</span>
</button>
</DialogContent>
</Dialog>
);
}