740fc1c63c
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).
103 lines
3.4 KiB
TypeScript
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>
|
|
);
|
|
}
|