Files
eranos/src/components/FollowQRDialog.tsx
T
Alex Gleason 740fc1c63c Merge ditto/main into agora
Pulls in 387 commits from ditto/main while preserving Agora-specific
features. Where the two codebases diverged on the same concept, kept
the Agora side per project direction.

Kept Agora-specific:
- SparkWallet stack (over Ditto's nostr-derived Bitcoin wallet)
- Communities (NIP-72 + chat + members), Messages, Organizers,
  Actions, Verified, Appearance settings
- DMProviderWrapper, country/organizer moderation in NoteMoreMenu
- 'Agora' branding, pub.agora.app bundle ID, version 2.8.0
- Built-in theme system (src/themes.ts) only

Rejected from Ditto:
- All Blobbi virtual pet code (80+ files, route, provider, sidebar,
  kind labels, feed setting, NIP.md entries, CSS animations)
- Custom theme events (kinds 36767/16767) — ThemesPage, ThemeContent,
  active profile themes, theme snapshot recovery
- On-chain zaps (kind 8333) and the entire Bitcoin wallet implementation
  (useBitcoinWallet, bitcoin-signers, BitcoinContentHeader,
  bitcoinjs-lib / @bitcoinerlab/secp256k1 / ecpair / tiny-secp256k1)
- ZapSuccessScreen (depended on dropped bitcoin lib)

Pulled in from Ditto:
- .agents/skills/* (12 new specialized skills, slim AGENTS.md)
- @nostrify bumps to 0.52 / 0.6 / 0.37
- New routes/pages: Music, Podcasts, Videos, Vines, Wikipedia, Books,
  Bluesky, Archive, AIChat, Trends, Webxdc, Highlights, Decks, Emojis,
  Development, Treasures, Colors, Packs
- Birdstar feed integration (kinds 2473, 12473, 30621)
- Wikipedia/Wikidata/Scryfall lookup in ExternalContentPage
- release-notes CI job + extract-release-notes.mjs script
- nsite:// URI handling in feed/sidebar
- iOS fastlane setup
- src/lib/avatarShape.ts + Avatar shape prop (kept for new Music/People
  components that depend on it)

Preserved Agora's ABSOLUTE 'NEVER COMMIT' rule at the top of AGENTS.md
and dropped Ditto's contradicting 'Commit at the end of every task'
section.

Validation: npm run test passes (tsc, eslint, 40/40 vitest, vite build).
2026-05-13 18:35:03 -05:00

103 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 { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog';
import { useCurrentUser } from '@/hooks/useCurrentUser';
import { useAuthor } from '@/hooks/useAuthor';
import { useShareOrigin } from '@/hooks/useShareOrigin';
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 shareOrigin = useShareOrigin();
const [qrDataUrl, setQrDataUrl] = useState<string>('');
const [copied, setCopied] = useState(false);
const metadata = author.data?.metadata;
const displayName = user ? metadata?.name || metadata?.display_name || genUserName(user.pubkey) : '';
const npub = user ? nip19.npubEncode(user.pubkey) : '';
const followUrl = npub ? `${shareOrigin}/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 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>
);
}