From f811245f9053f297cd171209e013d2aa064141e5 Mon Sep 17 00:00:00 2001 From: Chad Curtis Date: Thu, 21 May 2026 13:22:57 -0500 Subject: [PATCH 01/60] Make settings utilitarian and disable whimsical content types by default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the spellbook-themed settings index: drop the "Codex of Configuration" heading, the gradient ornaments with ✦/◆ dividers, the sigil that appeared after two minutes of inactivity, and the IntroImage illustration tiles on every section row and sub-page intro block. The index is now a flat divider-separated list of labels and one-line descriptions, with breathing room on both sides. Delete the Magic settings page, its CursorFireEffect overlay, the animate-sigil-glow / animate-pulse-slow keyframes, the magicMouse AppConfig flag (schema, default, test fixture), and the /settings/magic route. Delete the now-unreferenced IntroImage component and the ten *-intro.png assets it masked. Disable content types that don't fit an activist tool by default: vines, treasures (geocaches + found logs), colors, decks, webxdc, birdstar (detections / birdex / constellations), emoji packs, custom emojis, user statuses, music, podcasts, and development. They remain available in settings — just off out of the box. Highlights is bumped on by default to pair with Articles. Posts, comments, reposts, articles, highlights, events, polls, communities, people lists, badges, photos, videos, and voice messages stay on. --- public/advanced-intro.png | Bin 22673 -> 0 bytes public/community-intro.png | Bin 10584 -> 0 bytes public/feed-intro.png | Bin 32782 -> 0 bytes public/lists-intro.png | Bin 22618 -> 0 bytes public/magic-intro.png | Bin 40382 -> 0 bytes public/mute-intro.png | Bin 11059 -> 0 bytes public/notification-intro.png | Bin 17163 -> 0 bytes public/profile-intro.png | Bin 16082 -> 0 bytes public/relay-intro.png | Bin 15816 -> 0 bytes public/theme-intro.png | Bin 14580 -> 0 bytes src/App.tsx | 61 ++++--- src/AppRouter.tsx | 2 - src/components/ContentSettings.tsx | 27 ++- src/components/CursorFireEffect.tsx | 257 --------------------------- src/components/EditProfileForm.tsx | 14 +- src/components/IntroImage.tsx | 25 --- src/components/MainLayout.tsx | 6 - src/contexts/AppContext.ts | 2 - src/index.css | 20 --- src/lib/schemas.ts | 1 - src/pages/AdvancedSettingsPage.tsx | 14 +- src/pages/AppearanceSettingsPage.tsx | 14 +- src/pages/ContentPage.tsx | 16 +- src/pages/MagicSettingsPage.tsx | 85 --------- src/pages/NetworkSettingsPage.tsx | 14 +- src/pages/ProfileSettings.tsx | 14 +- src/pages/SettingsPage.tsx | 171 ++++-------------- src/pages/UserListsPage.tsx | 14 +- src/test/TestApp.tsx | 1 - 29 files changed, 110 insertions(+), 648 deletions(-) delete mode 100644 public/advanced-intro.png delete mode 100644 public/community-intro.png delete mode 100644 public/feed-intro.png delete mode 100644 public/lists-intro.png delete mode 100644 public/magic-intro.png delete mode 100644 public/mute-intro.png delete mode 100644 public/notification-intro.png delete mode 100644 public/profile-intro.png delete mode 100644 public/relay-intro.png delete mode 100644 public/theme-intro.png delete mode 100644 src/components/CursorFireEffect.tsx delete mode 100644 src/components/IntroImage.tsx delete mode 100644 src/pages/MagicSettingsPage.tsx diff --git a/public/advanced-intro.png b/public/advanced-intro.png deleted file mode 100644 index e4aeb62ae2a6d89a7e7726c21298de0be3217ea8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 22673 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 diff --git a/public/feed-intro.png b/public/feed-intro.png deleted file mode 100644 index 474df32c9012eb031925bc124752747c7e54f44f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32782 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 diff --git a/public/lists-intro.png b/public/lists-intro.png deleted file mode 100644 index 77bab59666ca99f995610c5f02b92f22ad1e6fda..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 22618 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 b{;6)qxIe|OGYX@$^977=--FiZKaTz%j%Ns9 diff --git a/public/magic-intro.png b/public/magic-intro.png deleted file mode 100644 index 72ef5931418afbedfe943c3f3984cbb0aba4f873..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 40382 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 diff --git a/public/mute-intro.png b/public/mute-intro.png deleted file mode 100644 index 5fb777113ce377bdaa21a78309e8bfdebcd47b4b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11059 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
{/* Intro section for Other Stuff */} -
- -
-

Other Stuff

-

- Nostr isn't just text posts — people publish all kinds of things. Pick what shows up in your sidebar and feed. -

-
+
+

Other Stuff

+

+ Nostr isn't just text posts — people publish all kinds of things. Pick what shows up in your sidebar and feed. +

{/* Column headers */} @@ -407,14 +403,11 @@ function FeedTabsSection() { return (
{/* Intro section for Feed Tabs */} -
- -
-

Feed Navigation

-

- Manage which feed tabs appear in your navigation and follow communities by domain. -

-
+
+

Feed Navigation

+

+ Manage which feed tabs appear in your navigation and follow communities by domain. +

{/* Feed Tab Toggles */} diff --git a/src/components/CursorFireEffect.tsx b/src/components/CursorFireEffect.tsx deleted file mode 100644 index 2c4b6819..00000000 --- a/src/components/CursorFireEffect.tsx +++ /dev/null @@ -1,257 +0,0 @@ -import { useEffect, useRef } from 'react'; - -interface Particle { - x: number; - y: number; - vx: number; - vy: number; - life: number; - size: number; -} - -interface Ring { - x: number; - y: number; - radius: number; - maxRadius: number; - life: number; // 1 → 0 -} - -function parseHslString(hsl: string): { h: number; s: number; l: number } { - const parts = hsl.trim().split(/\s+/); - return { - h: parseFloat(parts[0] ?? '30'), - s: parseFloat(parts[1] ?? '100'), - l: parseFloat(parts[2] ?? '55'), - }; -} - -export function CursorFireEffect() { - const canvasRef = useRef(null); - const particles = useRef([]); - const rings = useRef([]); - const cursor = useRef<{ x: number; y: number } | null>(null); - const active = useRef(false); - const raf = useRef(0); - const pulse = useRef(0); - const frame = useRef(0); - - useEffect(() => { - const canvas = canvasRef.current; - if (!canvas) return; - const ctx = canvas.getContext('2d'); - if (!ctx) return; - - function resize() { - if (!canvas) return; - canvas.width = window.innerWidth; - canvas.height = window.innerHeight; - } - resize(); - window.addEventListener('resize', resize); - - function onMouseMove(e: MouseEvent) { - cursor.current = { x: e.clientX, y: e.clientY }; - active.current = true; - } - function onTouchMove(e: TouchEvent) { - const t = e.touches[0]; - if (t) { cursor.current = { x: t.clientX, y: t.clientY }; active.current = true; } - } - function onLeave() { active.current = false; } - - function onClick(e: MouseEvent) { - spawnClickBurst(e.clientX, e.clientY); - } - function onTouchStart(e: TouchEvent) { - const t = e.touches[0]; - if (t) spawnClickBurst(t.clientX, t.clientY); - } - - window.addEventListener('mousemove', onMouseMove); - window.addEventListener('touchmove', onTouchMove, { passive: true }); - window.addEventListener('mouseleave', onLeave); - window.addEventListener('touchend', onLeave); - window.addEventListener('click', onClick); - window.addEventListener('touchstart', onTouchStart, { passive: true }); - - function getPrimary() { - const raw = getComputedStyle(document.documentElement).getPropertyValue('--primary').trim(); - return raw ? parseHslString(raw) : { h: 270, s: 80, l: 60 }; - } - - function spawnWispParticles(x: number, y: number) { - const count = Math.floor(Math.random() * 2) + 2; - for (let i = 0; i < count; i++) { - const angle = -Math.PI / 2 + (Math.random() - 0.5) * 0.3; - const speed = Math.random() * 0.6 + 0.3; - particles.current.push({ - x: x + (Math.random() - 0.5) * 6, - y, - vx: Math.cos(angle) * speed * 0.2, - vy: Math.sin(angle) * speed, - life: 1, - size: Math.random() * 28 + 20, - }); - } - } - - function spawnClickBurst(x: number, y: number) { - // Expanding shockwave ring - rings.current.push({ x, y, radius: 0, maxRadius: 120, life: 1 }); - - // Secondary smaller ring - rings.current.push({ x, y, radius: 0, maxRadius: 60, life: 1 }); - - // Radial burst of particles in all directions - const count = 18; - for (let i = 0; i < count; i++) { - const angle = (i / count) * Math.PI * 2; - const speed = Math.random() * 3.5 + 1.5; - particles.current.push({ - x, - y, - vx: Math.cos(angle) * speed, - vy: Math.sin(angle) * speed, - life: 1, - size: Math.random() * 20 + 12, - }); - } - - // Extra upward plume - for (let i = 0; i < 8; i++) { - const angle = -Math.PI / 2 + (Math.random() - 0.5) * 0.8; - const speed = Math.random() * 4 + 2; - particles.current.push({ - x: x + (Math.random() - 0.5) * 10, - y, - vx: Math.cos(angle) * speed * 0.3, - vy: Math.sin(angle) * speed, - life: 1, - size: Math.random() * 30 + 18, - }); - } - } - - function draw() { - if (!canvas || !ctx) return; - ctx.clearRect(0, 0, canvas.width, canvas.height); - - const { h, s, l } = getPrimary(); - - // Spawn wisp particles every 4th frame - frame.current++; - if (active.current && cursor.current && frame.current % 4 === 0) { - spawnWispParticles(cursor.current.x, cursor.current.y); - } - - ctx.globalCompositeOperation = 'screen'; - - // Draw expanding rings - const aliveRings: Ring[] = []; - for (const r of rings.current) { - r.life -= 0.022; - if (r.life <= 0) continue; - r.radius += (r.maxRadius - r.radius) * 0.08; - - const t = r.life; - const lineAlpha = Math.pow(t, 1.5) * 0.8; - const glowAlpha = Math.pow(t, 2) * 0.4; - const lineWidth = t * 3; - - // Outer glow halo - ctx.beginPath(); - ctx.arc(r.x, r.y, r.radius, 0, Math.PI * 2); - ctx.strokeStyle = `hsla(${h}, ${s}%, ${Math.min(l + 20, 85)}%, ${glowAlpha})`; - ctx.lineWidth = lineWidth + 8; - ctx.stroke(); - - // Sharp ring - ctx.beginPath(); - ctx.arc(r.x, r.y, r.radius, 0, Math.PI * 2); - ctx.strokeStyle = `hsla(${h - 10}, ${s}%, 90%, ${lineAlpha})`; - ctx.lineWidth = lineWidth; - ctx.stroke(); - - aliveRings.push(r); - } - rings.current = aliveRings; - - // Draw flame particles - const alive: Particle[] = []; - for (const p of particles.current) { - p.life -= 0.005 + Math.random() * 0.002; - if (p.life <= 0) continue; - - p.x += p.vx; - p.y += p.vy; - p.vy -= 0.018; - p.vx *= 0.98; - p.size *= 0.985; - - const t = p.life; - const ph = h + (1 - t) * 25; - const pl = Math.min(l + t * 40, 90); - const alpha = Math.pow(t, 1.5) * 0.18; - const radius = p.size * (0.4 + t * 0.6); - - const g = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, radius); - g.addColorStop(0, `hsla(${ph - 5}, ${s}%, ${pl}%, ${alpha})`); - g.addColorStop(0.35, `hsla(${ph}, ${s}%, ${Math.max(l, 40)}%, ${alpha * 0.6})`); - g.addColorStop(0.7, `hsla(${ph + 15}, ${s}%, ${Math.max(l - 15, 20)}%, ${alpha * 0.2})`); - g.addColorStop(1, `hsla(${ph + 25}, ${s}%, ${Math.max(l - 25, 5)}%, 0)`); - - ctx.beginPath(); - ctx.arc(p.x, p.y, radius, 0, Math.PI * 2); - ctx.fillStyle = g; - ctx.fill(); - - alive.push(p); - } - particles.current = alive; - - // Orb: slow pulsing core glow at cursor - if (active.current && cursor.current) { - const { x, y } = cursor.current; - pulse.current += 0.025; - const pv = (Math.sin(pulse.current) + 1) / 2; - const r = 20 + pv * 12; - const a = 0.5 + pv * 0.3; - - const orb = ctx.createRadialGradient(x, y, 0, x, y, r); - orb.addColorStop(0, `hsla(${h - 10}, ${Math.max(s - 10, 0)}%, 95%, ${a})`); - orb.addColorStop(0.4, `hsla(${h}, ${s}%, ${Math.min(l + 15, 85)}%, ${a * 0.5})`); - orb.addColorStop(1, `hsla(${h + 15}, ${s}%, ${l}%, 0)`); - - ctx.beginPath(); - ctx.arc(x, y, r, 0, Math.PI * 2); - ctx.fillStyle = orb; - ctx.fill(); - } - - ctx.globalCompositeOperation = 'source-over'; - raf.current = requestAnimationFrame(draw); - } - - raf.current = requestAnimationFrame(draw); - - return () => { - cancelAnimationFrame(raf.current); - window.removeEventListener('resize', resize); - window.removeEventListener('mousemove', onMouseMove); - window.removeEventListener('touchmove', onTouchMove); - window.removeEventListener('mouseleave', onLeave); - window.removeEventListener('touchend', onLeave); - window.removeEventListener('click', onClick); - window.removeEventListener('touchstart', onTouchStart); - }; - }, []); - - return ( -
{/* Intro */} -
- -
-

Your Identity

-

- Customize your profile with a name, bio, images, and verification. This is how others will see you on Nostr. -

-
+
+

Your Identity

+

+ Customize your profile with a name, bio, images, and verification. This is how others will see you on Nostr. +

{/* Crop dialog */} diff --git a/src/components/IntroImage.tsx b/src/components/IntroImage.tsx deleted file mode 100644 index 977c6e32..00000000 --- a/src/components/IntroImage.tsx +++ /dev/null @@ -1,25 +0,0 @@ -interface IntroImageProps { - src: string; - /** Tailwind size class, e.g. "w-40" (default) or "w-10" */ - size?: string; - className?: string; -} - -export function IntroImage({ src, size = 'w-40', className }: IntroImageProps) { - return ( -
- ); -} diff --git a/src/components/MainLayout.tsx b/src/components/MainLayout.tsx index cc1789b6..e98ee6ee 100644 --- a/src/components/MainLayout.tsx +++ b/src/components/MainLayout.tsx @@ -4,10 +4,8 @@ import { LeftSidebar } from '@/components/LeftSidebar'; import { MobileTopBar } from '@/components/MobileTopBar'; import { MobileDrawer } from '@/components/MobileDrawer'; import { FloatingComposeButton } from '@/components/FloatingComposeButton'; -import { CursorFireEffect } from '@/components/CursorFireEffect'; import { Skeleton } from '@/components/ui/skeleton'; import { CenterColumnContext, DrawerContext, LayoutStore, LayoutStoreContext, NavHiddenContext, useLayoutSnapshot } from '@/contexts/LayoutContext'; -import { useAppContext } from '@/hooks/useAppContext'; import { useScrollDirection } from '@/hooks/useScrollDirection'; import { cn } from '@/lib/utils'; @@ -55,15 +53,11 @@ function MainLayoutInner() { const openDrawer = useCallback(() => setDrawerOpen(true), []); const centerColumnRef = useRef(null); const [centerColumnEl, setCenterColumnEl] = useState(null); - const { config } = useAppContext(); const { hidden: navHidden } = useScrollDirection(scrollContainer); return ( - {/* Magic Mouse fire particle overlay */} - {config.magicMouse && } - {/* Mobile top bar - only on small screens, hidden when page requests immersive mode */} {!hideTopBar && setDrawerOpen(true)} hasSubHeader={hasSubHeader} />} diff --git a/src/contexts/AppContext.ts b/src/contexts/AppContext.ts index b0210cac..8038c957 100644 --- a/src/contexts/AppContext.ts +++ b/src/contexts/AppContext.ts @@ -208,8 +208,6 @@ export interface AppConfig { clientName?: string; /** NIP-19 `naddr1…` identifying this client's kind 31990 handler event. Decoded at publish time to produce the `31990::` addr and relay hint for the "client" tag per NIP-89. */ client?: string; - /** Enable Magic Mouse mode: cursor/finger emanates magical fire in the primary color */ - magicMouse: boolean; /** Current theme */ theme: Theme; /** Custom theme config (colors, fonts, background). Only used when theme === "custom". */ diff --git a/src/index.css b/src/index.css index 9f6f012d..fccd7ad2 100644 --- a/src/index.css +++ b/src/index.css @@ -242,26 +242,6 @@ animation: ping-3 4s ease-out forwards; } -/* Slow continuous pulse for nagging buttons */ -@keyframes pulse-slow { - 0%, 100% { transform: scale(1); opacity: 0.5; } - 50% { transform: scale(1.8); opacity: 0; } -} - -.animate-pulse-slow { - animation: pulse-slow 2.5s ease-in-out infinite; -} - -/* Eerie sigil glow pulse */ -@keyframes sigil-glow { - 0%, 100% { opacity: 0.12; filter: drop-shadow(0 0 4px hsl(var(--primary) / 0.3)); } - 50% { opacity: 0.28; filter: drop-shadow(0 0 12px hsl(var(--primary) / 0.6)) drop-shadow(0 0 24px hsl(var(--primary) / 0.25)); } -} - -.animate-sigil-glow { - animation: sigil-glow 4s ease-in-out infinite; -} - /* Eyeball blink animation - eyelid sweeps down then back up */ @keyframes eyelid-blink { 0% { transform: translateY(-100%); } diff --git a/src/lib/schemas.ts b/src/lib/schemas.ts index b4732123..c94ae921 100644 --- a/src/lib/schemas.ts +++ b/src/lib/schemas.ts @@ -221,7 +221,6 @@ export const AppConfigSchema = z.object({ clientName: z.string().optional(), /** NIP-19 naddr1 string for the kind 31990 handler event. */ client: z.string().startsWith('naddr1').optional(), - magicMouse: z.boolean().optional(), theme: ThemeSchema, customTheme: ThemeConfigCompatSchema.optional(), themes: ThemesConfigSchema.optional(), diff --git a/src/pages/AdvancedSettingsPage.tsx b/src/pages/AdvancedSettingsPage.tsx index db8f9930..a219ed3d 100644 --- a/src/pages/AdvancedSettingsPage.tsx +++ b/src/pages/AdvancedSettingsPage.tsx @@ -1,6 +1,5 @@ import { useSeoMeta } from '@unhead/react'; import { PageHeader } from '@/components/PageHeader'; -import { IntroImage } from '@/components/IntroImage'; import { AdvancedSettings } from '@/components/AdvancedSettings'; import { WalletSettings } from '@/components/WalletSettings'; import { useCurrentUser } from '@/hooks/useCurrentUser'; @@ -38,14 +37,11 @@ export function AdvancedSettingsPage() {
{/* Intro */} -
- -
-

Power User Settings

-

- Wallet connections, system configuration, and other advanced options. -

-
+
+

Power User Settings

+

+ Wallet connections, system configuration, and other advanced options. +

{/* Wallet collapsible — only when logged in */} diff --git a/src/pages/AppearanceSettingsPage.tsx b/src/pages/AppearanceSettingsPage.tsx index 1facc1c4..e96bf04d 100644 --- a/src/pages/AppearanceSettingsPage.tsx +++ b/src/pages/AppearanceSettingsPage.tsx @@ -1,7 +1,6 @@ import { useSeoMeta } from '@unhead/react'; import { Monitor, Moon, Sun } from 'lucide-react'; import { PageHeader } from '@/components/PageHeader'; -import { IntroImage } from '@/components/IntroImage'; import { useAppContext } from '@/hooks/useAppContext'; import { useTheme } from '@/hooks/useTheme'; import { cn } from '@/lib/utils'; @@ -64,14 +63,11 @@ export function AppearanceSettingsPage() {
{/* Intro */} -
- -
-

Color Mode

-

- Pick your preferred color mode. System will automatically match your device's light or dark setting. -

-
+
+

Color Mode

+

+ Pick your preferred color mode. System will automatically match your device's light or dark setting. +

{/* Theme options */} diff --git a/src/pages/ContentPage.tsx b/src/pages/ContentPage.tsx index 9084086a..2036ff95 100644 --- a/src/pages/ContentPage.tsx +++ b/src/pages/ContentPage.tsx @@ -4,7 +4,6 @@ import { RotateCcw } from 'lucide-react'; import { MuteSettingsInternals, SensitiveContentSection } from '@/components/ContentSettings'; import { MuteListRecoveryDialog } from '@/components/MuteListRecoveryDialog'; import { PageHeader } from '@/components/PageHeader'; -import { IntroImage } from '@/components/IntroImage'; import { HelpTip } from '@/components/HelpTip'; import { Button } from '@/components/ui/button'; import { useAppContext } from '@/hooks/useAppContext'; @@ -36,15 +35,12 @@ export function ContentPage() { } /> - {/* Lead image — Muted Content */} -
- -
-

Content Control

-

- Hide posts from specific users, hashtags, words, or entire threads. All mutes are encrypted and private. -

-
+ {/* Intro */} +
+

Content Control

+

+ Hide posts from specific users, hashtags, words, or entire threads. All mutes are encrypted and private. +

diff --git a/src/pages/MagicSettingsPage.tsx b/src/pages/MagicSettingsPage.tsx deleted file mode 100644 index 78edfa2d..00000000 --- a/src/pages/MagicSettingsPage.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import { useSeoMeta } from '@unhead/react'; -import { PageHeader } from '@/components/PageHeader'; -import { IntroImage } from '@/components/IntroImage'; -import { Switch } from '@/components/ui/switch'; -import { Label } from '@/components/ui/label'; -import { useAppContext } from '@/hooks/useAppContext'; - -export function MagicSettingsPage() { - const { config, updateConfig } = useAppContext(); - - useSeoMeta({ - title: `Magic | Settings | ${config.appName}`, - description: 'Magical cursor effects and enchanted interface settings', - }); - - function toggleMagicMouse(checked: boolean) { - updateConfig((c) => ({ ...c, magicMouse: checked })); - } - - return ( -
- {/* Header with back link */} - -

Magic

-

- Harness the mystical energies of your device. Imbue your cursor with elemental fire. -

-
- } - /> - -
- {/* Intro */} -
- -
-

Arcane Configuration

-

- Harness the mystical energies of your device. Imbue your cursor with elemental fire and make every interaction feel enchanted. -

-
-
- - {/* Ornament */} -
-
- -
-
- - {/* Magic Mouse toggle */} -
-
- -

- Your cursor (or finger on touch devices) will emanate magical fire in the glow of your primary color. Move with purpose — every path you trace becomes a trail of flame. -

-
- -
- - {/* Bottom ornament */} -
-
- -
-
-
- - ); -} diff --git a/src/pages/NetworkSettingsPage.tsx b/src/pages/NetworkSettingsPage.tsx index f05d828c..11b0578c 100644 --- a/src/pages/NetworkSettingsPage.tsx +++ b/src/pages/NetworkSettingsPage.tsx @@ -3,7 +3,6 @@ import { Navigate } from 'react-router-dom'; import { PageHeader } from '@/components/PageHeader'; import { RelayListManager } from '@/components/RelayListManager'; import { BlossomSettings } from '@/components/BlossomSettings'; -import { IntroImage } from '@/components/IntroImage'; import { HelpTip } from '@/components/HelpTip'; import { Label } from '@/components/ui/label'; import { useCurrentUser } from '@/hooks/useCurrentUser'; @@ -41,14 +40,11 @@ export function NetworkSettingsPage() {
{/* Intro */} -
- -
-

Network Connections

-

- Manage your relay connections. Relays are servers that store and distribute Nostr events across the network. -

-
+
+

Network Connections

+

+ Manage your relay connections. Relays are servers that store and distribute Nostr events across the network. +

{/* Relays */} diff --git a/src/pages/ProfileSettings.tsx b/src/pages/ProfileSettings.tsx index 6ea83513..a716c17a 100644 --- a/src/pages/ProfileSettings.tsx +++ b/src/pages/ProfileSettings.tsx @@ -20,7 +20,6 @@ import { useQueryClient } from '@tanstack/react-query'; import { ProfileCard } from '@/components/ProfileCard'; import { ProfileRightSidebar } from '@/components/ProfileRightSidebar'; import { PageHeader } from '@/components/PageHeader'; -import { IntroImage } from '@/components/IntroImage'; import { HelpTip } from '@/components/HelpTip'; import { ImageCropDialog } from '@/components/ImageCropDialog'; import { SortableList, SortableItem } from '@/components/SortableList'; @@ -701,14 +700,11 @@ export function ProfileSettings() {
{/* Intro */} -
- -
-

Your Identity

-

- Tap any field on the card to edit. Click your avatar or banner to upload and crop a new image. -

-
+
+

Your Identity

+

+ Tap any field on the card to edit. Click your avatar or banner to upload and crop a new image. +

{/* Interactive profile card */} diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx index ad1b4816..cdda580d 100644 --- a/src/pages/SettingsPage.tsx +++ b/src/pages/SettingsPage.tsx @@ -1,13 +1,11 @@ import { useSeoMeta } from '@unhead/react'; -import { lazy, Suspense, useState, useEffect, useRef } from 'react'; +import { lazy, Suspense, useState } from 'react'; import { ChevronRight, Settings } from 'lucide-react'; import { Link, useNavigate } from 'react-router-dom'; import { PageHeader } from '@/components/PageHeader'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useAppContext } from '@/hooks/useAppContext'; -import { IntroImage } from '@/components/IntroImage'; import { useLayoutOptions } from '@/contexts/LayoutContext'; -import { toast } from '@/hooks/useToast'; import { isAdmin } from '@/lib/admins'; const RequestToVanishDialog = lazy(() => import('@/components/RequestToVanishDialog').then(m => ({ default: m.RequestToVanishDialog }))); @@ -16,7 +14,6 @@ interface SettingsSection { id: string; label: string; description: string; - illustration?: string; path: string; requiresAuth?: boolean; /** When true, only shown to platform admins (see `isAdmin` in `@/lib/admins`). */ @@ -27,67 +24,52 @@ const settingsSections: SettingsSection[] = [ { id: 'profile', label: 'Profile', - description: 'Edit your display name, bio, and avatar', - illustration: '/profile-intro.png', + description: 'Display name, bio, avatar, and verification.', path: '/settings/profile', requiresAuth: true, }, { id: 'appearance', label: 'Appearance', - description: 'Switch between system, light, and dark mode', - illustration: '/theme-intro.png', + description: 'System, light, or dark mode.', path: '/settings/appearance', }, { id: 'feed', label: 'Home Feed', - description: 'Choose what types of posts appear in your home feed', - illustration: '/community-intro.png', + description: 'Choose which post types appear in your home feed.', path: '/settings/feed', }, { id: 'content', label: 'Content', - description: 'Muted users, hashtags, and sensitive content settings', - illustration: '/mute-intro.png', + description: 'Muted users, hashtags, and sensitive-content handling.', path: '/settings/content', }, { id: 'network', label: 'Network', - description: 'Relays and file upload servers', - illustration: '/relay-intro.png', + description: 'Relays and file upload servers.', path: '/settings/network', requiresAuth: true, }, { id: 'notifications', label: 'Notifications', - description: 'Configure push notification preferences', - illustration: '/notification-intro.png', + description: 'Push notification preferences.', path: '/settings/notifications', requiresAuth: true, }, { id: 'advanced', label: 'Advanced', - description: 'Wallet, system, and power user settings', - illustration: '/advanced-intro.png', + description: 'Wallet, system, and power-user options.', path: '/settings/advanced', }, - { - id: 'magic', - label: 'Magic', - description: 'Enchanted cursor effects and mystical interface powers', - illustration: '/magic-intro.png', - path: '/settings/magic', - }, { id: 'organizers', label: 'Organizers', - description: 'Appoint country organizers who can pin posts to country feeds', - illustration: '/community-intro.png', + description: 'Appoint country organizers who can pin posts to country feeds.', path: '/organizers', requiresAuth: true, requiresAdmin: true, @@ -96,20 +78,10 @@ const settingsSections: SettingsSection[] = [ export function SettingsPage() { const { user } = useCurrentUser(); - const { config, updateConfig } = useAppContext(); + const { config } = useAppContext(); const navigate = useNavigate(); - const [sigilFlash, setSigilFlash] = useState(false); - const [sigilVisible, setSigilVisible] = useState(false); const [deleteAccountOpen, setDeleteAccountOpen] = useState(false); - const inactivityTimer = useRef | null>(null); - useEffect(() => { - if (config.magicMouse) return; - inactivityTimer.current = setTimeout(() => setSigilVisible(true), 2 * 60 * 1000); - return () => { - if (inactivityTimer.current) clearTimeout(inactivityTimer.current); - }; - }, [config.magicMouse]); useLayoutOptions({}); useSeoMeta({ @@ -117,86 +89,48 @@ export function SettingsPage() { description: `Manage your ${config.appName} settings`, }); - // Magic section only appears once unlocked; admin-only sections gate by isAdmin. const visibleSections = settingsSections.filter((section) => { if (section.requiresAuth && !user) return false; if (section.requiresAdmin && !isAdmin(user?.pubkey)) return false; - if (section.id === 'magic' && !config.magicMouse) return false; return true; }); - function unlockMagic() { - if (config.magicMouse) { - navigate('/settings/magic'); - return; - } - setSigilFlash(true); - setTimeout(() => setSigilFlash(false), 1000); - updateConfig((c) => ({ ...c, magicMouse: true })); - toast({ - title: '✨ Magical potential unlocked', - description: 'You have awakened the arcane. Your cursor now burns with enchanted fire.', - }); - } - return ( -
- {/* Page header */} +
} backTo="/" /> - {/* Codex heading + exposition */} -
-

- Shape your identity, tune your feed, and manage how you connect to the Nostr network.
Everything you need to make this place feel like yours. -

-

Codex of Configuration

-
- - {/* Tome ornament */} -
-
- -
-
- - {/* Settings menu */} -
- {visibleSections.map((section, i) => { - return ( -
-
+
    + {visibleSections.map((section) => ( +
  • +
- {i < visibleSections.length - 1 && ( -
- )} -
- ); - })} -
+
); } diff --git a/src/pages/UserListsPage.tsx b/src/pages/UserListsPage.tsx index 49014e1a..a4d1510e 100644 --- a/src/pages/UserListsPage.tsx +++ b/src/pages/UserListsPage.tsx @@ -24,7 +24,6 @@ import { import { Dialog, DialogContent, DialogDescription, DialogTitle, DialogTrigger, } from '@/components/ui/dialog'; -import { IntroImage } from '@/components/IntroImage'; import { PageHeader } from '@/components/PageHeader'; import { useAppContext } from '@/hooks/useAppContext'; import { useCurrentUser } from '@/hooks/useCurrentUser'; @@ -234,14 +233,11 @@ export function UserListsPage() {
{/* Intro block */} -
- -
-

Lists

-

- Group people into named lists. Use any list as the source for a custom feed, or filter searches by list members. -

-
+
+

Lists

+

+ Group people into named lists. Use any list as the source for a custom feed, or filter searches by list members. +

{/* Create new list */} diff --git a/src/test/TestApp.tsx b/src/test/TestApp.tsx index 839e25d8..1739c9ed 100644 --- a/src/test/TestApp.tsx +++ b/src/test/TestApp.tsx @@ -107,7 +107,6 @@ export function TestApp({ children }: TestAppProps) { faviconUrl: "https://ditto.pub/api/favicon/{hostname}", linkPreviewUrl: "https://ditto.pub/api/link-preview/{url}", corsProxy: "https://proxy.shakespeare.diy/?url={href}", - magicMouse: false, contentWarningPolicy: "blur", sentryDsn: "", sentryEnabled: false, From 4aa358d6852c127f105047867724b6b15fbdbfe5 Mon Sep 17 00:00:00 2001 From: Chad Curtis Date: Thu, 21 May 2026 13:36:20 -0500 Subject: [PATCH 02/60] Limit content-type toggles in settings to Agora-curated kinds The settings UI iterates EXTRA_KINDS and renders a toggle row per kind, which exposed every Nostr content type the app understands (vines, treasures, colors, decks, webxdc, birdstar, emoji packs, music, podcasts, development, etc.) regardless of whether they fit Agora's activist-utopian framing. The result was a wall of toggles with no meaningful default. Add an `agora` boolean to ExtraKindDef and mark only the curated set: posts, comments, reposts, generic-reposts, reactions, zaps, articles, highlights, photos, videos (with sub-toggles), voice messages, events, polls, organizations (NIP-72 communities), and badges. Filter the "Basic Home Feed Options" and "Show More Content Types" sections to `def.agora === true`. Move badges from the "Whimsy" section into "Social" so the Whimsy and Development groups vanish entirely after filtering. Enable zaps in the home feed by default (they're core engagement, not noise) and drop "Disabled by default" from the zaps description. Other pages (KindFeedPage deep-links, ExternalContentHeader quoted events, etc.) still see the full EXTRA_KINDS registry, so external content from non-curated kinds still renders correctly when linked. --- src/App.tsx | 2 +- src/components/ContentSettings.tsx | 4 ++-- src/lib/extraKinds.ts | 21 +++++++++++++++++++-- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index f9d29374..d018dc00 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -56,7 +56,7 @@ const hardcodedConfig: AppConfig = { feedIncludeReposts: true, feedIncludeGenericReposts: true, feedIncludeReactions: false, - feedIncludeZaps: false, + feedIncludeZaps: true, feedIncludeArticles: true, showArticles: true, showHighlights: true, diff --git a/src/components/ContentSettings.tsx b/src/components/ContentSettings.tsx index d946b1a7..d993c4ff 100644 --- a/src/components/ContentSettings.tsx +++ b/src/components/ContentSettings.tsx @@ -206,7 +206,7 @@ function ContentTypeRow({ def }: { def: ExtraKindDef }) { function NotesFeedSettings() { return ( <> - {FEED_KINDS.map((def) => ( + {FEED_KINDS.filter((def) => def.agora).map((def) => ( ))} @@ -217,7 +217,7 @@ function FeedSettingsFormInternals() { return ( <> {SECTION_ORDER.map((section) => { - const sectionKinds = EXTRA_KINDS.filter((def) => def.section === section); + const sectionKinds = EXTRA_KINDS.filter((def) => def.section === section && def.agora); if (sectionKinds.length === 0) return null; return (
diff --git a/src/lib/extraKinds.ts b/src/lib/extraKinds.ts index a45d95a5..a07855e5 100644 --- a/src/lib/extraKinds.ts +++ b/src/lib/extraKinds.ts @@ -74,6 +74,8 @@ export interface ExtraKindDef { blurb?: string; /** External sites where users can create or participate in this kind of content. */ sites?: ExtraKindSite[]; + /** If true, this kind is part of the Agora-curated content set and surfaces in the settings UI. */ + agora?: boolean; } /** All supported extra content kinds, ordered by section (feed → media → social → whimsy). */ @@ -88,6 +90,7 @@ export const EXTRA_KINDS: ExtraKindDef[] = [ addressable: false, section: 'feed', feedOnly: true, + agora: true, }, { kind: 1111, @@ -98,6 +101,7 @@ export const EXTRA_KINDS: ExtraKindDef[] = [ addressable: false, section: 'feed', feedOnly: true, + agora: true, }, { kind: 6, @@ -108,6 +112,7 @@ export const EXTRA_KINDS: ExtraKindDef[] = [ addressable: false, section: 'feed', feedOnly: true, + agora: true, }, { kind: 16, @@ -118,6 +123,7 @@ export const EXTRA_KINDS: ExtraKindDef[] = [ addressable: false, section: 'feed', feedOnly: true, + agora: true, }, { kind: 7, @@ -128,6 +134,7 @@ export const EXTRA_KINDS: ExtraKindDef[] = [ addressable: false, section: 'feed', feedOnly: true, + agora: true, }, { kind: 9735, @@ -137,10 +144,11 @@ export const EXTRA_KINDS: ExtraKindDef[] = [ // toggle so users don't have to think about which rail was used. extraFeedKinds: [8333], label: 'Zaps', - description: 'People zapping posts (Lightning and on-chain Bitcoin). Disabled by default.', + description: 'People zapping posts (Lightning and on-chain Bitcoin).', addressable: false, section: 'feed', feedOnly: true, + agora: true, }, { kind: 30023, @@ -153,6 +161,7 @@ export const EXTRA_KINDS: ExtraKindDef[] = [ addressable: true, section: 'feed', blurb: 'Blog posts, essays, and guides. Write and publish long-form articles.', + agora: true, }, // Media { @@ -167,6 +176,7 @@ export const EXTRA_KINDS: ExtraKindDef[] = [ section: 'media', blurb: 'Instagram-style photo posts. Share images with captions and tags.', sites: [{ url: 'https://nostr.build', name: 'nostr.build' }], + agora: true, }, { kind: 21, @@ -179,6 +189,7 @@ export const EXTRA_KINDS: ExtraKindDef[] = [ section: 'media', blurb: 'Watch and discover videos and live streams in a YouTube/Twitch-style interface.', sites: [{ url: 'https://zap.stream', name: 'zap.stream' }, { url: 'https://vidstr.shakespeare.wtf', name: 'Vidstr' }], + agora: true, subKinds: [ { kind: 21, @@ -208,6 +219,7 @@ export const EXTRA_KINDS: ExtraKindDef[] = [ section: 'media', feedOnly: true, blurb: 'Record and share short voice messages, up to 60 seconds long.', + agora: true, }, { kind: 34236, @@ -306,6 +318,7 @@ export const EXTRA_KINDS: ExtraKindDef[] = [ section: 'social', blurb: 'Events and meetups on Nostr. RSVP and see who else is going. Create and manage events on Plektos.', sites: [{ url: 'https://plektos.app', name: 'Plektos' }], + agora: true, }, { kind: 1063, @@ -332,6 +345,7 @@ export const EXTRA_KINDS: ExtraKindDef[] = [ section: 'social', blurb: 'Ask a question, let people vote. Create polls from a polling app.', sites: [{ url: 'https://pollerama.fun' }], + agora: true, }, { kind: 39089, @@ -361,6 +375,7 @@ export const EXTRA_KINDS: ExtraKindDef[] = [ addressable: true, section: 'social', blurb: 'Organizations on Nostr with an explicit founder, listed moderators, and event-level moderation.', + agora: true, }, { kind: 62, @@ -420,8 +435,9 @@ export const EXTRA_KINDS: ExtraKindDef[] = [ description: 'Badges and awards (NIP-58)', route: 'badges', addressable: true, - section: 'whimsy', + section: 'social', blurb: 'Discover badges created on Nostr. Badge issuers award them for recognition, participation, or appreciation.', + agora: true, subKinds: [ { kind: 30009, @@ -490,6 +506,7 @@ export const EXTRA_KINDS: ExtraKindDef[] = [ addressable: false, section: 'social', blurb: "Highlights are excerpts people find valuable — a paragraph from an article, a passage from a blog post, or a quote from anywhere on the web. Browse what people are reading and what's resonating.", + agora: true, }, // Birdstar (feed-only — external app, no Ditto page) { From 5920523b57694b279013238a3f866d7be6cfcf29 Mon Sep 17 00:00:00 2001 From: Chad Curtis Date: Thu, 21 May 2026 13:48:19 -0500 Subject: [PATCH 03/60] Flatten Home Feed settings; merge mutes inline; drop jargon MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reduce cognitive load on the Content settings page by collapsing the two-section toggle layout, group sub-headers, sub-kind rows, kind badges, and column headers into a single flat list of 14 toggles ordered by importance: Posts, Replies, Reposts, Articles, Highlights, Photos, Videos, Voice Messages, Events, Polls, Organizations, Badges, Reactions, Zaps. Each row is now a plain label + one-line description + switch. No content-kind icons, no [1234] kind-number badges, no Media / Social / Whimsy sub-headers, no Normal/Short video or Badge Definitions / Profile Badges / Badge Awards sub-rows (the parent toggle now governs all sub-kinds together). Combine kind 6 (Reposted Notes) and kind 16 (Reposted Other Content) into a single "Reposts" toggle via extraFeedKinds: [16]. The old feedIncludeGenericReposts flag stays in the schema for backwards compat but no longer surfaces in UI. Rename "Comments" -> "Replies" — Nostr's NIP-22 threading is most naturally called replies. Strip NIP / kind-number references from all curated descriptions (NIP-22, NIP-52, NIP-58, NIP-68, NIP-71, NIP-72, NIP-84, NIP-A0, "kind 30009", etc.). Plain English only. Merge the standalone /settings/content page (mutes + sensitive content) into /settings/feed as inline sections under the toggle list, since both are about "what you see in the feed." Delete ContentPage.tsx and its route; remove the Content entry from the settings index. Drop the giant ShieldAlert icon from the sensitive content intro. Rename "Home Feed Tabs" -> "Saved Feeds" in the page section heading. --- src/AppRouter.tsx | 2 - src/components/ContentSettings.tsx | 259 ++++++++--------------------- src/lib/extraKinds.ts | 44 ++--- src/pages/ContentPage.tsx | 91 ---------- src/pages/ContentSettingsPage.tsx | 8 +- src/pages/SettingsPage.tsx | 8 +- 6 files changed, 93 insertions(+), 319 deletions(-) delete mode 100644 src/pages/ContentPage.tsx diff --git a/src/AppRouter.tsx b/src/AppRouter.tsx index f6a45f6a..28835a17 100644 --- a/src/AppRouter.tsx +++ b/src/AppRouter.tsx @@ -47,7 +47,6 @@ const ChangelogPage = lazy(() => import("./pages/ChangelogPage").then(m => ({ de const CommunitiesPage = lazy(() => import("./pages/CommunitiesPage").then(m => ({ default: m.CommunitiesPage }))); const CreateCommunityPage = lazy(() => import("./pages/CreateCommunityPage").then(m => ({ default: m.CreateCommunityPage }))); const CreateEventPage = lazy(() => import("./pages/CreateEventPage").then(m => ({ default: m.CreateEventPage }))); -const ContentPage = lazy(() => import("./pages/ContentPage").then(m => ({ default: m.ContentPage }))); const ContentSettingsPage = lazy(() => import("./pages/ContentSettingsPage").then(m => ({ default: m.ContentSettingsPage }))); const CSAEPolicyPage = lazy(() => import("./pages/CSAEPolicyPage").then(m => ({ default: m.CSAEPolicyPage }))); const DiscoverPage = lazy(() => import("./pages/DiscoverPage").then(m => ({ default: m.DiscoverPage }))); @@ -187,7 +186,6 @@ export function AppRouter() { } /> } /> } /> - } /> } /> - {/* Intro */} -
-

What You See

-

- Customize your feed, choose what content appears, and control what you want to hide. -

-
- - {/* Homepage Section */} +
- {/* Feed Tabs Section */} -
-
-

Home Feed Tabs

-
-
-
- -
-
+
+ +
- {/* Notes Section */} -
-
-

Basic Home Feed Options

-
-
-
-
-

- Core content types that appear in your feed. -

-
+
+ +
- {/* Column headers */} -
- Feed -
- - -
-
- - {/* Other Stuff Section */} -
-
-

Show More Content Types in Home Feed

-
-
-
- {/* Intro section for Other Stuff */} -
-

Other Stuff

-

- Nostr isn't just text posts — people publish all kinds of things. Pick what shows up in your sidebar and feed. -

-
- - {/* Column headers */} -
- Feed -
- - {/* Content type rows - reuse the internals from FeedSettingsForm */} - -
-
+
+ +
+
+ +
); } - - -function KindBadge({ kind }: { kind: number }) { +function Section({ title, children }: { title: string; children: ReactNode }) { return ( - - [{kind}] - +
+

{title}

+
{children}
+
); } -function SubKindRow({ sub }: { sub: SubKindDef }) { - const { feedSettings, updateFeedSettings } = useFeedSettings(); - const { updateSettings } = useEncryptedSettings(); - const { user } = useCurrentUser(); - - const handleToggle = async (key: string, value: boolean) => { - updateFeedSettings({ [key]: value }); - if (user) { - await updateSettings.mutateAsync({ feedSettings: { ...feedSettings, [key]: value } }); - } - }; +function FlatContentList() { + // Flat, ordered list of curated kinds. No section grouping, no sub-rows, no kind badges. + const orderedIds = [ + 'posts', 'replies', 'reposts', 'articles', 'highlights', + 'photos', 'videos', 'voice', + 'events', 'polls', 'communities', 'badges', + 'reactions', 'zaps', + ]; + const byId = new Map(EXTRA_KINDS.map((def) => [def.id, def])); + // Replies is id 'comments' in the registry; alias here for readability. + byId.set('replies', byId.get('comments')!); + const rows = orderedIds.map((id) => byId.get(id)).filter((d): d is ExtraKindDef => !!d && !!d.agora); return ( -
-
- {sub.label} -

- {' '}{sub.description} -

-
-
-
- handleToggle(sub.feedKey, checked)} - className="scale-90" - /> -
-
-
+
    + {rows.map((def) => ( +
  • + +
  • + ))} +
); } + + function ContentTypeRow({ def }: { def: ExtraKindDef }) { const { feedSettings, updateFeedSettings } = useFeedSettings(); const { updateSettings } = useEncryptedSettings(); const { user } = useCurrentUser(); - const IconComponent = CONTENT_KIND_ICONS[def.id] ?? Palette; - const icon = ; - const hasSubKinds = !!def.subKinds; - const handleToggle = async (key: string, value: boolean) => { - updateFeedSettings({ [key]: value }); + // Toggle key: prefer the feed inclusion key; fall back to the sidebar visibility key + // for kinds that have no direct feed key of their own (e.g. parent kinds with sub-kinds). + const toggleKey: keyof FeedSettings | undefined = def.feedKey ?? def.showKey; + if (!toggleKey) return null; + + const checked = feedSettings[toggleKey] !== false; + + const handleToggle = async (value: boolean) => { + const next: Partial = { [toggleKey]: value }; + // Parent kinds with sub-kinds: toggle all sub-kind feed keys together so the + // single parent switch governs everything below it. + if (def.subKinds) { + for (const sub of def.subKinds) { + next[sub.feedKey] = value; + } + } + updateFeedSettings(next); if (user) { - await updateSettings.mutateAsync({ feedSettings: { ...feedSettings, [key]: value } }); + await updateSettings.mutateAsync({ feedSettings: { ...feedSettings, ...next } }); } }; return ( -
-
-
- {icon} -
- {def.label} -

- {' '}{def.description} -

-
-
-
-
- {!hasSubKinds && def.feedKey ? ( - handleToggle(def.feedKey!, checked)} - /> - ) : !hasSubKinds && def.feedOnly && def.showKey ? ( - handleToggle(def.showKey!, checked)} - /> - ) : null} -
-
+
+
+ {def.label} +

{def.description}

- - {hasSubKinds && def.subKinds && def.subKinds.map((sub) => ( - - ))} +
); } -function NotesFeedSettings() { - return ( - <> - {FEED_KINDS.filter((def) => def.agora).map((def) => ( - - ))} - - ); -} - -function FeedSettingsFormInternals() { - return ( - <> - {SECTION_ORDER.map((section) => { - const sectionKinds = EXTRA_KINDS.filter((def) => def.section === section && def.agora); - if (sectionKinds.length === 0) return null; - return ( -
-
- - {SECTION_LABELS[section]} - -
- {sectionKinds.map((def) => ( - - ))} -
- ); - })} - - ); -} - // Feed Tabs Section Component function FeedTabsSection() { const { toast } = useToast(); @@ -915,16 +806,10 @@ export function SensitiveContentSection() { return (
{/* Intro */} -
-
- -
-
-

Content Warnings

-

- Some posts are tagged with content warnings (NIP-36) by their authors. This can include NSFW material, spoilers, or other sensitive content. -

-
+
+

+ Some posts are tagged by their authors as sensitive — NSFW, graphic, or otherwise needing a content warning. Choose how to handle them. +

{/* Policy options — consistent row style with other settings */} diff --git a/src/lib/extraKinds.ts b/src/lib/extraKinds.ts index a07855e5..bfcda530 100644 --- a/src/lib/extraKinds.ts +++ b/src/lib/extraKinds.ts @@ -92,34 +92,24 @@ export const EXTRA_KINDS: ExtraKindDef[] = [ feedOnly: true, agora: true, }, - { - kind: 1111, - id: 'comments', - feedKey: 'feedIncludeComments', - label: 'Comments', - description: 'NIP-22 comments on posts and external content', - addressable: false, - section: 'feed', - feedOnly: true, - agora: true, - }, { kind: 6, id: 'reposts', feedKey: 'feedIncludeReposts', - label: 'Reposted Notes', - description: 'Shared posts from others', + extraFeedKinds: [16], + label: 'Reposts', + description: 'Posts and media reshared by others', addressable: false, section: 'feed', feedOnly: true, agora: true, }, { - kind: 16, - id: 'generic-reposts', - feedKey: 'feedIncludeGenericReposts', - label: 'Reposted Other Content', - description: 'Shared non-text-note posts from others', + kind: 1111, + id: 'comments', + feedKey: 'feedIncludeComments', + label: 'Replies', + description: 'Threaded replies to posts', addressable: false, section: 'feed', feedOnly: true, @@ -130,7 +120,7 @@ export const EXTRA_KINDS: ExtraKindDef[] = [ id: 'reactions', feedKey: 'feedIncludeReactions', label: 'Reactions', - description: 'People reacting to posts (likes and emoji reactions). Disabled by default.', + description: 'Likes and emoji reactions on posts', addressable: false, section: 'feed', feedOnly: true, @@ -144,7 +134,7 @@ export const EXTRA_KINDS: ExtraKindDef[] = [ // toggle so users don't have to think about which rail was used. extraFeedKinds: [8333], label: 'Zaps', - description: 'People zapping posts (Lightning and on-chain Bitcoin).', + description: 'Bitcoin tips sent to posts', addressable: false, section: 'feed', feedOnly: true, @@ -170,7 +160,7 @@ export const EXTRA_KINDS: ExtraKindDef[] = [ showKey: 'showPhotos', feedKey: 'feedIncludePhotos', label: 'Photos', - description: 'Picture-first posts (NIP-68)', + description: 'Picture-first posts', route: 'photos', addressable: false, section: 'media', @@ -183,7 +173,7 @@ export const EXTRA_KINDS: ExtraKindDef[] = [ id: 'videos', showKey: 'showVideos', label: 'Videos', - description: 'Video posts (NIP-71 kinds 21 & 22) and live streams', + description: 'Recorded videos and live streams', route: 'videos', addressable: false, section: 'media', @@ -214,7 +204,7 @@ export const EXTRA_KINDS: ExtraKindDef[] = [ id: 'voice', feedKey: 'feedIncludeVoiceMessages', label: 'Voice Messages', - description: 'Short audio voice messages (NIP-A0)', + description: 'Short audio recordings', addressable: false, section: 'media', feedOnly: true, @@ -312,7 +302,7 @@ export const EXTRA_KINDS: ExtraKindDef[] = [ feedKey: 'feedIncludeEvents', extraFeedKinds: [31922], label: 'Events', - description: 'Calendar events and meetups (NIP-52)', + description: 'Calendar events and meetups', route: 'events', addressable: true, section: 'social', @@ -370,7 +360,7 @@ export const EXTRA_KINDS: ExtraKindDef[] = [ feedKey: 'feedIncludeCommunities', extraFeedKinds: [9041], label: 'Organizations', - description: 'Agora organizations with founder + moderator trust model (NIP-72)', + description: 'Coalitions and groups with founders and moderators', route: 'communities', addressable: true, section: 'social', @@ -432,7 +422,7 @@ export const EXTRA_KINDS: ExtraKindDef[] = [ id: 'badges', showKey: 'showBadges', label: 'Badges', - description: 'Badges and awards (NIP-58)', + description: 'Awards and recognition issued to people', route: 'badges', addressable: true, section: 'social', @@ -501,7 +491,7 @@ export const EXTRA_KINDS: ExtraKindDef[] = [ showKey: 'showHighlights', feedKey: 'feedIncludeHighlights', label: 'Highlights', - description: 'Noteworthy excerpts from articles, posts, and the web (NIP-84)', + description: 'Noteworthy excerpts from articles, posts, and the web', route: 'highlights', addressable: false, section: 'social', diff --git a/src/pages/ContentPage.tsx b/src/pages/ContentPage.tsx deleted file mode 100644 index 2036ff95..00000000 --- a/src/pages/ContentPage.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import { useState } from 'react'; -import { useSeoMeta } from '@unhead/react'; -import { RotateCcw } from 'lucide-react'; -import { MuteSettingsInternals, SensitiveContentSection } from '@/components/ContentSettings'; -import { MuteListRecoveryDialog } from '@/components/MuteListRecoveryDialog'; -import { PageHeader } from '@/components/PageHeader'; -import { HelpTip } from '@/components/HelpTip'; -import { Button } from '@/components/ui/button'; -import { useAppContext } from '@/hooks/useAppContext'; -import { useCurrentUser } from '@/hooks/useCurrentUser'; - -export function ContentPage() { - const { config } = useAppContext(); - const { user } = useCurrentUser(); - const [recoveryOpen, setRecoveryOpen] = useState(false); - - useSeoMeta({ - title: `Content | Settings | ${config.appName}`, - description: 'Muted users, hashtags, and sensitive content settings', - }); - - return ( -
- {/* Header with back link */} - -

Content

-

- Control what you see. Mute users, hashtags, or words, and choose how content warnings are handled. Mutes are encrypted and private. -

-
- } - /> - - {/* Intro */} -
-

Content Control

-

- Hide posts from specific users, hashtags, words, or entire threads. All mutes are encrypted and private. -

-
- -
- - {/* Muted Content Section */} -
-
-

Muted Content

- {user && ( - - )} -
-
-
- -
-
- - {user && ( - - )} - - {/* Sensitive Content Section */} -
-
-

Sensitive Content

-
-
-
- -
-
- -
-
- ); -} diff --git a/src/pages/ContentSettingsPage.tsx b/src/pages/ContentSettingsPage.tsx index 196075bf..589bf9c5 100644 --- a/src/pages/ContentSettingsPage.tsx +++ b/src/pages/ContentSettingsPage.tsx @@ -1,7 +1,6 @@ import { useSeoMeta } from '@unhead/react'; import { ContentSettings } from '@/components/ContentSettings'; import { PageHeader } from '@/components/PageHeader'; -import { HelpTip } from '@/components/HelpTip'; import { useAppContext } from '@/hooks/useAppContext'; export function ContentSettingsPage() { @@ -13,16 +12,15 @@ export function ContentSettingsPage() { }); return ( -
- {/* Header with back link */} +
-

Home Feed

+

Home Feed

- Nostr supports many content types beyond text posts. Customize which appear in your home feed. + Choose what appears in your feed, manage saved searches, and hide content you don't want to see.

} diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx index cdda580d..34cfaff9 100644 --- a/src/pages/SettingsPage.tsx +++ b/src/pages/SettingsPage.tsx @@ -37,15 +37,9 @@ const settingsSections: SettingsSection[] = [ { id: 'feed', label: 'Home Feed', - description: 'Choose which post types appear in your home feed.', + description: 'Choose which post types appear in your home feed, and manage muted users, hashtags, and sensitive content.', path: '/settings/feed', }, - { - id: 'content', - label: 'Content', - description: 'Muted users, hashtags, and sensitive-content handling.', - path: '/settings/content', - }, { id: 'network', label: 'Network', From bfc8d1ab070270debdcf7eb863d8ccd44ce4cf1b Mon Sep 17 00:00:00 2001 From: Chad Curtis Date: Thu, 21 May 2026 14:57:38 -0500 Subject: [PATCH 04/60] Full-bleed campaign hero with overlaid title, creator, summary --- src/pages/CampaignDetailPage.tsx | 330 +++++++++++++++++-------------- 1 file changed, 184 insertions(+), 146 deletions(-) diff --git a/src/pages/CampaignDetailPage.tsx b/src/pages/CampaignDetailPage.tsx index ac9233c1..7486da39 100644 --- a/src/pages/CampaignDetailPage.tsx +++ b/src/pages/CampaignDetailPage.tsx @@ -68,7 +68,6 @@ import { formatNumber } from '@/lib/formatNumber'; import { genUserName } from '@/lib/genUserName'; import { sanitizeUrl } from '@/lib/sanitizeUrl'; import { timeAgo } from '@/lib/timeAgo'; -import { cn } from '@/lib/utils'; import NotFound from './NotFound'; interface CampaignDetailPageProps { @@ -129,7 +128,6 @@ function CampaignDetailContent({ campaign }: { campaign: ParsedCampaign }) { const [replyOpen, setReplyOpen] = useState(false); const [moreMenuOpen, setMoreMenuOpen] = useState(false); const [interactionsOpen, setInteractionsOpen] = useState(false); - const [storyExpanded, setStoryExpanded] = useState(false); const [interactionsTab, setInteractionsTab] = useState('reposts'); const [archiveConfirmOpen, setArchiveConfirmOpen] = useState(false); @@ -323,12 +321,15 @@ function CampaignDetailContent({ campaign }: { campaign: ParsedCampaign }) { return (
- {/* Cover hero stretches edge-to-edge on every breakpoint. */} + {/* Full-bleed cover hero. Title, creator, meta, summary, and the + back/admin buttons all live ON the image — the banner is the + page's emotional entry point. */} 0} - expanded={storyExpanded} - onToggle={() => setStoryExpanded((v) => !v)} /> {/* Engagement: stats counters, action bar, threaded replies - + donation receipts interleaved. */} + + donation receipts interleaved. Rendered flush with the + story (no card chrome) so the page reads as one + continuous flow — story → counters → action chips → + comments — rather than a stack of disconnected boxes. + A subtle top border separates it from the story above. */}
-
+
{hasStats && (
{engagementStats?.reposts ? ( @@ -406,7 +409,6 @@ function CampaignDetailContent({ campaign }: { campaign: ParsedCampaign }) { hideZap onReply={() => setReplyOpen(true)} onMore={() => setMoreMenuOpen(true)} - className={hasStats ? 'pt-3 border-t border-border/60' : undefined} />
@@ -525,7 +527,11 @@ function CampaignDetailContent({ campaign }: { campaign: ParsedCampaign }) { } // ───────────────────────────────────────────────────────────────────── -// Hero +// Hero — full-bleed cover with title, creator, meta, summary, and the +// back / admin controls all overlaid on the image. The banner is the +// page's emotional entry point: the photo carries the campaign's story +// at a glance, and the overlay text makes the pitch readable without +// taking the reader off the image. // ───────────────────────────────────────────────────────────────────── interface CampaignHeroProps { @@ -533,6 +539,7 @@ interface CampaignHeroProps { cover: string | undefined; creatorName: string; creatorProfileUrl: string; + creatorPicture: string | undefined; deadline: { label: string; isPast: boolean } | null; countryLabel: string | undefined; tagLabel: string | undefined; @@ -549,6 +556,7 @@ function CampaignHero({ cover, creatorName, creatorProfileUrl, + creatorPicture, deadline, countryLabel, tagLabel, @@ -559,69 +567,102 @@ function CampaignHero({ onArchive, onReopen, }: CampaignHeroProps) { + const initials = creatorName.slice(0, 2).toUpperCase(); + return ( -
-
- {cover ? ( - - ) : ( -
- + // True full-bleed: no max-width wrapper, no horizontal padding, no + // rounded corners — the image touches every edge on every + // breakpoint. Height is generous on mobile so the banner fills the + // viewport for an immersive first impression instead of being a + // strip; on larger screens we cap it so the page content below + // stays visible. +
+ {cover ? ( + + ) : ( +
+ +
+ )} + + {/* Single, smooth dark gradient anchored at the bottom — no + middle band, no top wash. The image stays vibrant at the + top while the overlay text gets a clean reading surface + underneath. */} +
+ {/* Subtle top scrim purely to keep the back/admin buttons + legible against bright skies, beaches, etc. */} +
+ + {/* Top controls — back left, admin right. Use chip-style + backdrops so they read on any image without an opaque pill. */} +
+ + + {isCreator && ( +
+ + {campaign.archived ? ( + + ) : ( + + )}
)} -
+
-
- - {isCreator && ( -
- - {campaign.archived ? ( - - ) : ( - - )} -
- )} -
- -
+ {/* Overlay content — sits at the bottom of the image, contained + to the 6xl column on desktop so it lines up with the body + content below. Generous bottom padding (incl. safe-area) + keeps the title comfortably above the home-indicator on + notched phones. Drop-shadow on text gives extra contrast on + busy photos without darkening the gradient further. */} +
+
{campaign.archived && ( )} -
-

- {campaign.title} -

- e.stopPropagation()} - className="text-xs sm:text-sm text-white/85 hover:text-white motion-safe:transition-colors" - > - by {creatorName} - -
-
- {tagLabel && ( - - - {tagLabel} - - )} - {countryLabel && ( - - - {countryLabel} - - )} - {deadline && ( - - - {deadline.label} - - )} - - - {campaign.recipients.length}{' '} - {campaign.recipients.length === 1 ? 'recipient' : 'recipients'} - -
+ + {tagLabel && ( +
+ + {tagLabel} +
+ )} + +

+ {campaign.title} +

+ {campaign.summary && ( -

+

{campaign.summary}

)} + + e.stopPropagation()} + className="inline-flex items-center gap-2.5 pt-1 text-sm sm:text-base text-white/90 hover:text-white motion-safe:transition-colors group [text-shadow:none]" + > + + {creatorPicture && } + + {initials} + + + + by{' '} + + {creatorName} + + + + + {(countryLabel || deadline || campaign.recipients.length > 0) && ( +
+ {countryLabel && ( + + + {countryLabel} + + )} + {deadline && ( + + + {deadline.label} + + )} + + + {campaign.recipients.length}{' '} + {campaign.recipients.length === 1 ? 'recipient' : 'recipients'} + +
+ )}
-
+
); } @@ -686,54 +743,38 @@ function CampaignHero({ function CampaignStory({ storyEvent, hasContent, - expanded, - onToggle, }: { storyEvent: NostrEvent; hasContent: boolean; - expanded: boolean; - onToggle: () => void; + // expanded/onToggle retained on the call site for backwards-compat + // but no longer used — the story shows in full. A fundraiser pitch + // is the entire point of the page; hiding most of it behind a + // fade-out gradient buries the message. + expanded?: boolean; + onToggle?: () => void; }) { if (!hasContent) { return ( -
+

The organizer hasn't written a story for this campaign yet.

-
+
); } return ( -
-
+

-
- -
- {!expanded && ( -
- )} -
- -

+ The story + +
+ +
+ ); } @@ -1060,13 +1101,10 @@ function CampaignReplySkeleton() { function CampaignDetailSkeleton() { return (
-
- -
+
-
- +
From 840769af210e20178e973e87e2f294e02e678abd Mon Sep 17 00:00:00 2001 From: Chad Curtis Date: Thu, 21 May 2026 14:59:56 -0500 Subject: [PATCH 05/60] Move campaign tag to bottom meta row; drop tacky Tag icon --- src/pages/CampaignDetailPage.tsx | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/pages/CampaignDetailPage.tsx b/src/pages/CampaignDetailPage.tsx index 7486da39..3a47326e 100644 --- a/src/pages/CampaignDetailPage.tsx +++ b/src/pages/CampaignDetailPage.tsx @@ -5,7 +5,6 @@ import { useQueryClient } from '@tanstack/react-query'; import type { NostrEvent } from '@nostrify/nostrify'; import { CalendarClock, - Tag, Archive, ArchiveRestore, ChevronLeft, @@ -673,13 +672,6 @@ function CampaignHero({ )} - {tagLabel && ( -
- - {tagLabel} -
- )} -

{campaign.title}

@@ -709,8 +701,13 @@ function CampaignHero({ - {(countryLabel || deadline || campaign.recipients.length > 0) && ( + {(tagLabel || countryLabel || deadline || campaign.recipients.length > 0) && (
+ {tagLabel && ( + + {tagLabel} + + )} {countryLabel && ( From 7777271df1c770d8baf852204649185c8c334e15 Mon Sep 17 00:00:00 2001 From: Chad Curtis Date: Thu, 21 May 2026 15:02:04 -0500 Subject: [PATCH 06/60] Taller campaign hero with deeper, taller bottom gradient --- src/pages/CampaignDetailPage.tsx | 39 ++++++++++++++++---------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/src/pages/CampaignDetailPage.tsx b/src/pages/CampaignDetailPage.tsx index 3a47326e..32e919a6 100644 --- a/src/pages/CampaignDetailPage.tsx +++ b/src/pages/CampaignDetailPage.tsx @@ -575,7 +575,7 @@ function CampaignHero({ // viewport for an immersive first impression instead of being a // strip; on larger screens we cap it so the page content below // stays visible. -
+
{cover ? ( )} - {/* Single, smooth dark gradient anchored at the bottom — no - middle band, no top wash. The image stays vibrant at the - top while the overlay text gets a clean reading surface - underneath. */} + {/* Tall, deep bottom gradient covering ~80% of the hero so the + overlay text sits on a near-opaque base no matter how busy + the photo is. Image stays vibrant only in the very top of + the banner. */}
{/* Subtle top scrim purely to keep the back/admin buttons legible against bright skies, beaches, etc. */} @@ -660,24 +660,30 @@ function CampaignHero({ keeps the title comfortably above the home-indicator on notched phones. Drop-shadow on text gives extra contrast on busy photos without darkening the gradient further. */} -
-
+
+
{campaign.archived && ( Archived )} + {tagLabel && ( +
+ {tagLabel} +
+ )} +

{campaign.title}

{campaign.summary && ( -

+

{campaign.summary}

)} @@ -685,7 +691,7 @@ function CampaignHero({ e.stopPropagation()} - className="inline-flex items-center gap-2.5 pt-1 text-sm sm:text-base text-white/90 hover:text-white motion-safe:transition-colors group [text-shadow:none]" + className="mt-5 inline-flex items-center gap-2.5 text-sm sm:text-base text-white/90 hover:text-white motion-safe:transition-colors group [text-shadow:none]" > {creatorPicture && } @@ -693,7 +699,7 @@ function CampaignHero({ {initials} - + by{' '} {creatorName} @@ -701,13 +707,8 @@ function CampaignHero({ - {(tagLabel || countryLabel || deadline || campaign.recipients.length > 0) && ( -
- {tagLabel && ( - - {tagLabel} - - )} + {(countryLabel || deadline || campaign.recipients.length > 0) && ( +
{countryLabel && ( From 421d4f366e5f2a330843e395271fc4f239bedd2b Mon Sep 17 00:00:00 2001 From: Chad Curtis Date: Thu, 21 May 2026 15:09:48 -0500 Subject: [PATCH 07/60] Tighter campaign hero on desktop; align back button to content width; drop boxes --- src/components/BeneficiaryDonateDialog.tsx | 10 +- src/components/ThreadedReplyList.tsx | 8 +- src/pages/CampaignDetailPage.tsx | 103 +++++++++++---------- 3 files changed, 69 insertions(+), 52 deletions(-) diff --git a/src/components/BeneficiaryDonateDialog.tsx b/src/components/BeneficiaryDonateDialog.tsx index 77dcc680..d09192a1 100644 --- a/src/components/BeneficiaryDonateDialog.tsx +++ b/src/components/BeneficiaryDonateDialog.tsx @@ -111,7 +111,9 @@ export function BeneficiaryDonatePanel({
- {/* Copyable address */} + {/* Copyable address. Truncated to a single line so it doesn't + wrap into a multi-row block on mobile — tap the row to copy + the full address. */}
); diff --git a/src/pages/ActionDetailPage.tsx b/src/pages/ActionDetailPage.tsx index 98327f5e..30305600 100644 --- a/src/pages/ActionDetailPage.tsx +++ b/src/pages/ActionDetailPage.tsx @@ -17,7 +17,6 @@ import { useAction, type Action } from '@/hooks/useActions'; import { useAuthor } from '@/hooks/useAuthor'; import { useBitcoinWallet } from '@/hooks/useBitcoinWallet'; import { useComments } from '@/hooks/useComments'; -import { useEventStats } from '@/hooks/useTrending'; import { useProfileUrl } from '@/hooks/useProfileUrl'; import { useSubmissionZapTotals } from '@/hooks/useSubmissionZapTotals'; import { useToast } from '@/hooks/useToast'; @@ -33,13 +32,10 @@ import { Button } from '@/components/ui/button'; import { Card, CardContent } from '@/components/ui/card'; import { Progress } from '@/components/ui/progress'; import { Skeleton } from '@/components/ui/skeleton'; +import { DetailCommentComposer } from '@/components/DetailCommentComposer'; import { PostActionBar } from '@/components/PostActionBar'; import { ReplyComposeModal } from '@/components/ReplyComposeModal'; import { NoteMoreMenu } from '@/components/NoteMoreMenu'; -import { - InteractionsModal, - type InteractionTab, -} from '@/components/InteractionsModal'; import { ThreadedReplyList, type ReplyNode } from '@/components/ThreadedReplyList'; import NotFound from '@/pages/NotFound'; @@ -81,13 +77,10 @@ function PledgeDetailContent({ action }: { action: Action }) { const author = useAuthor(action.pubkey); const navigate = useNavigate(); const { toast } = useToast(); - const { data: engagementStats } = useEventStats(action.event.id, action.event); const { data: commentsData, isLoading: commentsLoading } = useComments(action.event, 500); const [replyOpen, setReplyOpen] = useState(false); const [moreMenuOpen, setMoreMenuOpen] = useState(false); - const [interactionsOpen, setInteractionsOpen] = useState(false); - const [interactionsTab, setInteractionsTab] = useState('reposts'); const topLevel = useMemo( () => commentsData?.topLevelComments ?? [], @@ -136,11 +129,6 @@ function PledgeDetailContent({ action }: { action: Action }) { const deadline = action.deadline ? formatDeadline(action.deadline) : null; const cover = sanitizeUrl(action.image); const progressValue = action.bounty > 0 ? Math.min(100, Math.round((fundedSats / action.bounty) * 100)) : 0; - const hasStats = - !!engagementStats?.replies || - !!engagementStats?.reposts || - !!engagementStats?.quotes || - !!engagementStats?.reactions; const naddr = nip19.naddrEncode({ kind: 36639, @@ -156,11 +144,6 @@ function PledgeDetailContent({ action }: { action: Action }) { [action.event], ); - const openInteractions = (tab: InteractionTab) => { - setInteractionsTab(tab); - setInteractionsOpen(true); - }; - const handleShare = async () => { const url = `${window.location.origin}/${naddr}`; try { @@ -187,6 +170,17 @@ function PledgeDetailContent({ action }: { action: Action }) { onBack={() => navigate(-1)} /> +
+
+ setReplyOpen(true)} + onMore={() => setMoreMenuOpen(true)} + /> +
+
+
@@ -205,39 +199,7 @@ function PledgeDetailContent({ action }: { action: Action }) { 0} />
-
- {hasStats && ( -
- {engagementStats?.reposts ? ( - - ) : null} - {engagementStats?.quotes ? ( - - ) : null} - {engagementStats?.reactions ? ( - - ) : null} -
- )} - - setReplyOpen(true)} - onMore={() => setMoreMenuOpen(true)} - /> -
- -
+

Submissions

{topLevel.length > 0 ? ( @@ -247,6 +209,12 @@ function PledgeDetailContent({ action }: { action: Action }) { ) : null}
+ + {commentsLoading && replyTree.length === 0 ? (
{Array.from({ length: 3 }).map((_, i) => )} @@ -289,12 +257,6 @@ function PledgeDetailContent({ action }: { action: Action }) { -
); } From ad2e9a2ee96de1bd4f3c18538782bdd23a958511 Mon Sep 17 00:00:00 2001 From: lemon Date: Thu, 21 May 2026 11:03:03 -0700 Subject: [PATCH 15/60] Align event detail comments layout --- src/pages/PostDetailPage.tsx | 60 +++++++++++++++++++++++++++--------- 1 file changed, 45 insertions(+), 15 deletions(-) diff --git a/src/pages/PostDetailPage.tsx b/src/pages/PostDetailPage.tsx index a245106e..335a0aa3 100644 --- a/src/pages/PostDetailPage.tsx +++ b/src/pages/PostDetailPage.tsx @@ -37,6 +37,7 @@ const CustomNipCard = lazy(() => import("@/components/CustomNipCard").then(m => import { FileMetadataContent } from "@/components/FileMetadataContent"; import { FollowPackContent } from "@/components/FollowPackContent"; import { GoalCard } from "@/components/GoalCard"; +import { DetailCommentComposer } from "@/components/DetailCommentComposer"; import { FollowPackDetailContent } from "@/components/FollowPackDetailContent"; import { FoundLogContent } from "@/components/FoundLogContent"; import { GeocacheContent } from "@/components/GeocacheContent"; @@ -436,8 +437,8 @@ function ProfileBadgesDetailView({ event }: { event: NostrEvent }) { }, [commentsData, muteItems]); return ( -
- +
+
@@ -1397,13 +1398,13 @@ function PostDetailContent({ event }: { event: NostrEvent }) { return ( -
+
{/* Focused post card — ancestor previews, ancestor thread, and the focused event itself share one rounded surface so the page reads as "thread context → this post" instead of an edge-to-edge Twitter timeline. Replies sit in their own FeedCard below. */} - + {/* Content preview for kind 1111 comments: external content, profile, or community */} {externalIdentifier && ( @@ -2075,22 +2076,51 @@ function PostDetailContent({ event }: { event: NostrEvent }) { {/* Replies */} -
+
+
+

+ {isKind1 ? 'Replies' : 'Comments'} +

+ {replyTree.length > 0 ? ( + + {formatNumber(replyTree.length)}{' '} + {replyTree.length === 1 + ? isKind1 ? 'reply' : 'comment' + : isKind1 ? 'replies' : 'comments'} + + ) : null} +
+ + + {repliesLoading ? ( - +
{Array.from({ length: 3 }).map((_, i) => ( ))} - - ) : replyTree.length > 0 ? ( - - - - ) : !parentEventId ? ( -
- No replies yet. Be the first to reply!
- ) : null} + ) : replyTree.length > 0 ? ( +
+ +
+ ) : ( + + )}
From b32ae751a2027ceaa0bf0649f704e861263c26a5 Mon Sep 17 00:00:00 2001 From: lemon Date: Thu, 21 May 2026 11:09:49 -0700 Subject: [PATCH 16/60] Use megaphone for boost action --- src/components/RepostMenu.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/RepostMenu.tsx b/src/components/RepostMenu.tsx index 53e75bdb..35931fab 100644 --- a/src/components/RepostMenu.tsx +++ b/src/components/RepostMenu.tsx @@ -1,4 +1,4 @@ -import { Quote, Rocket, Undo2 } from 'lucide-react'; +import { Megaphone, Quote, Undo2 } from 'lucide-react'; import { RepostIcon } from '@/components/icons/RepostIcon'; import { useState } from 'react'; import type { NostrEvent } from '@nostrify/nostrify'; @@ -195,7 +195,7 @@ export function RepostMenu({ event, children }: RepostMenuProps) { }} className="flex items-center gap-3 w-full px-4 py-3 text-[15px] text-foreground hover:bg-secondary/60 transition-colors" > - + Boost
From 62517cc062d526fcc15e913a42d6b00c0ee4a306 Mon Sep 17 00:00:00 2001 From: lemon Date: Thu, 21 May 2026 13:26:56 -0700 Subject: [PATCH 17/60] Join detail hero action bars --- src/components/CommunityDetailPage.tsx | 4 ++-- src/pages/ActionDetailPage.tsx | 6 +++--- src/pages/CampaignDetailPage.tsx | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/components/CommunityDetailPage.tsx b/src/components/CommunityDetailPage.tsx index c503fc84..53eca1c7 100644 --- a/src/components/CommunityDetailPage.tsx +++ b/src/components/CommunityDetailPage.tsx @@ -833,7 +833,7 @@ export function CommunityDetailPage({ event }: { event: NostrEvent }) {
{/* ── Hero ─────────────────────────────────────────────────────── */} -
+
{cover ? ( ) : ( @@ -962,7 +962,7 @@ export function CommunityDetailPage({ event }: { event: NostrEvent }) {
-
+
navigate(-1)} /> -
-
+
+
-
+
-
-
+
+
-
+
{cover ? ( ) : ( From 0cf561450253710aef32fe2345cd0a55c46a2402 Mon Sep 17 00:00:00 2001 From: lemon Date: Thu, 21 May 2026 13:30:34 -0700 Subject: [PATCH 18/60] Move campaign pins below hero --- src/pages/CampaignDetailPage.tsx | 73 +++++++++++++++++--------------- 1 file changed, 38 insertions(+), 35 deletions(-) diff --git a/src/pages/CampaignDetailPage.tsx b/src/pages/CampaignDetailPage.tsx index 358cbaf6..3cece562 100644 --- a/src/pages/CampaignDetailPage.tsx +++ b/src/pages/CampaignDetailPage.tsx @@ -107,19 +107,6 @@ function collectReplyEvents(nodes: ReplyNode[], out = new Map): ReplyNode[] { - return nodes.flatMap((node): ReplyNode[] => { - if (pinnedIds.has(node.event.id)) return []; - return [{ - ...node, - children: removePinnedReplyNodes(node.children, pinnedIds), - hiddenChildren: node.hiddenChildren - ? removePinnedReplyNodes(node.hiddenChildren, pinnedIds) - : undefined, - }]; - }); -} - export function CampaignDetailPage({ pubkey, identifier, relays }: CampaignDetailPageProps) { // Drop the default 600px column cap and the default right widget sidebar // — this page renders its own GoFundMe-style 2-column layout (article on @@ -230,20 +217,14 @@ function CampaignDetailContent({ campaign }: { campaign: ParsedCampaign }) { ); }, [commentsData, donationReceipts]); - const pinnedIdSet = useMemo(() => new Set(pinnedIds), [pinnedIds]); const feedEventsById = useMemo(() => collectReplyEvents(replyTree), [replyTree]); - const activityTree = useMemo((): ReplyNode[] => { - const pinnedEventNodes = pinnedIds + const pinnedNodes = useMemo((): ReplyNode[] => { + return pinnedIds .map((id) => feedEventsById.get(id) ?? pinnedEvents.find((event) => event.id === id)) .filter((event): event is NostrEvent => !!event) .map((event): ReplyNode => ({ event, children: [] })); - - return [ - ...pinnedEventNodes, - ...removePinnedReplyNodes(replyTree, pinnedIdSet), - ]; - }, [feedEventsById, pinnedEvents, pinnedIdSet, pinnedIds, replyTree]); + }, [feedEventsById, pinnedEvents, pinnedIds]); const cover = sanitizeUrl(campaign.image); const creatorMetadata = author.data?.metadata; @@ -381,6 +362,26 @@ function CampaignDetailContent({ campaign }: { campaign: ParsedCampaign }) {
+ {pinnedNodes.length > 0 && ( +
+
+ ( + handleTogglePin(event)} + /> + )} + /> +
+
+ )} + {/* Two-column body. On mobile the right column collapses inline immediately below the hero so the donate CTA stays above the fold. On lg+ the right column sticks to the viewport edge of @@ -426,10 +427,10 @@ function CampaignDetailContent({ campaign }: { campaign: ParsedCampaign }) { ))}
- ) : activityTree.length > 0 ? ( + ) : replyTree.length > 0 ? (
( { - const wasPinned = isPinned(event.id); - togglePin.mutate(event.id, { - onSuccess: () => { - toast({ title: wasPinned ? 'Unpinned from campaign' : 'Pinned to campaign' }); - }, - onError: () => { - toast({ title: 'Failed to update campaign pins', variant: 'destructive' }); - }, - }); - }} + onTogglePin={() => handleTogglePin(event)} /> )} /> @@ -535,6 +526,18 @@ function CampaignDetailContent({ campaign }: { campaign: ParsedCampaign }) { ); + + function handleTogglePin(event: NostrEvent) { + const wasPinned = isPinned(event.id); + togglePin.mutate(event.id, { + onSuccess: () => { + toast({ title: wasPinned ? 'Unpinned from campaign' : 'Pinned to campaign' }); + }, + onError: () => { + toast({ title: 'Failed to update campaign pins', variant: 'destructive' }); + }, + }); + } } function CampaignActivityItemHeader({ From cf10654ea6677c022ddbe50adcc6c442b86cc7bf Mon Sep 17 00:00:00 2001 From: lemon Date: Thu, 21 May 2026 13:34:00 -0700 Subject: [PATCH 19/60] Align campaign activity widths --- src/pages/CampaignDetailPage.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/CampaignDetailPage.tsx b/src/pages/CampaignDetailPage.tsx index 3cece562..5e7d3d5c 100644 --- a/src/pages/CampaignDetailPage.tsx +++ b/src/pages/CampaignDetailPage.tsx @@ -364,7 +364,7 @@ function CampaignDetailContent({ campaign }: { campaign: ParsedCampaign }) { {pinnedNodes.length > 0 && (
-
+
( @@ -417,7 +417,7 @@ function CampaignDetailContent({ campaign }: { campaign: ParsedCampaign }) { queryClient.invalidateQueries({ queryKey: ['nostr', 'comments'] })} /> @@ -428,7 +428,7 @@ function CampaignDetailContent({ campaign }: { campaign: ParsedCampaign }) { ))}
) : replyTree.length > 0 ? ( -
+
( From e41e8396d76d51cc263ac969e21b879bd7dde0a6 Mon Sep 17 00:00:00 2001 From: lemon Date: Thu, 21 May 2026 13:38:28 -0700 Subject: [PATCH 20/60] Align detail comment widths --- src/components/CommunityDetailPage.tsx | 2 +- src/components/DetailCommentComposer.tsx | 3 +-- src/pages/ActionDetailPage.tsx | 2 +- src/pages/CampaignDetailPage.tsx | 2 +- src/pages/PostDetailPage.tsx | 4 ++-- 5 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/components/CommunityDetailPage.tsx b/src/components/CommunityDetailPage.tsx index 53eca1c7..430c3b0f 100644 --- a/src/components/CommunityDetailPage.tsx +++ b/src/components/CommunityDetailPage.tsx @@ -1039,7 +1039,7 @@ export function CommunityDetailPage({ event }: { event: NostrEvent }) { ))}
) : replyTree.length > 0 ? ( -
+
) : ( diff --git a/src/components/DetailCommentComposer.tsx b/src/components/DetailCommentComposer.tsx index 1e8e319a..45654081 100644 --- a/src/components/DetailCommentComposer.tsx +++ b/src/components/DetailCommentComposer.tsx @@ -1,7 +1,6 @@ import type { NostrEvent } from '@nostrify/nostrify'; import { ComposeBox } from '@/components/ComposeBox'; -import { cn } from '@/lib/utils'; interface DetailCommentComposerProps { event: NostrEvent; @@ -17,7 +16,7 @@ export function DetailCommentComposer({ className, }: DetailCommentComposerProps) { return ( -
+
)}
) : replyTree.length > 0 ? ( -
+
) : ( diff --git a/src/pages/CampaignDetailPage.tsx b/src/pages/CampaignDetailPage.tsx index 5e7d3d5c..9298120f 100644 --- a/src/pages/CampaignDetailPage.tsx +++ b/src/pages/CampaignDetailPage.tsx @@ -417,7 +417,7 @@ function CampaignDetailContent({ campaign }: { campaign: ParsedCampaign }) { queryClient.invalidateQueries({ queryKey: ['nostr', 'comments'] })} /> diff --git a/src/pages/PostDetailPage.tsx b/src/pages/PostDetailPage.tsx index 335a0aa3..f8b693a9 100644 --- a/src/pages/PostDetailPage.tsx +++ b/src/pages/PostDetailPage.tsx @@ -2098,13 +2098,13 @@ function PostDetailContent({ event }: { event: NostrEvent }) { /> {repliesLoading ? ( -
+
{Array.from({ length: 3 }).map((_, i) => ( ))}
) : replyTree.length > 0 ? ( -
+
) : ( From 4dd913d3ca34e5c912533e45a891b6553c1aba7a Mon Sep 17 00:00:00 2001 From: lemon Date: Thu, 21 May 2026 13:43:04 -0700 Subject: [PATCH 21/60] Redesign calendar event details --- src/components/CalendarEventDetailPage.tsx | 499 ++++++++++++--------- 1 file changed, 289 insertions(+), 210 deletions(-) diff --git a/src/components/CalendarEventDetailPage.tsx b/src/components/CalendarEventDetailPage.tsx index 9512a197..0ca04340 100644 --- a/src/components/CalendarEventDetailPage.tsx +++ b/src/components/CalendarEventDetailPage.tsx @@ -1,8 +1,8 @@ import { useMemo, useCallback, useState } from 'react'; import { Link, useNavigate } from 'react-router-dom'; import { - ArrowLeft, CalendarDays, + ChevronLeft, MapPin, Clock, Users, @@ -18,7 +18,8 @@ import type { NostrEvent, NostrMetadata } from '@nostrify/nostrify'; import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; -import { Separator } from '@/components/ui/separator'; +import { Card, CardContent } from '@/components/ui/card'; +import { DetailCommentComposer } from '@/components/DetailCommentComposer'; import { NoteContent } from '@/components/NoteContent'; import { NoteMoreMenu } from '@/components/NoteMoreMenu'; import { PostActionBar } from '@/components/PostActionBar'; @@ -36,6 +37,7 @@ import { usePublishRSVP } from '@/hooks/usePublishRSVP'; import { useProfileUrl } from '@/hooks/useProfileUrl'; import { useToast } from '@/hooks/useToast'; import { genUserName } from '@/lib/genUserName'; +import { openUrl } from '@/lib/downloadFile'; import { sanitizeUrl } from '@/lib/sanitizeUrl'; import { cn } from '@/lib/utils'; @@ -126,6 +128,26 @@ function formatDetailDate(event: NostrEvent): string { return startStr; } +function formatCalendarHeroDate(event: NostrEvent): string | null { + const startRaw = getTag(event.tags, 'start'); + if (!startRaw) return null; + + if (event.kind === 31922) { + const [year, month, day] = startRaw.split('-').map(Number); + const date = new Date(year, month - 1, day); + if (isNaN(date.getTime())) return startRaw; + return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); + } + + const timestamp = Number(startRaw); + if (!Number.isFinite(timestamp)) return startRaw; + const startTzid = getTag(event.tags, 'start_tzid'); + return new Date(timestamp * 1000).toLocaleDateString('en-US', { + month: 'short', day: 'numeric', year: 'numeric', + ...(startTzid ? { timeZone: startTzid } : {}), + }); +} + const ROLE_ORDER = ['host', 'speaker', 'moderator', 'participant']; function roleSort(a: string, b: string): number { const ai = ROLE_ORDER.indexOf(a.toLowerCase()); @@ -162,6 +184,15 @@ function PersonRow({ pubkey, label, size = 'md' }: { pubkey: string; label?: str ); } +function EventDetailRow({ icon, children }: { icon: React.ReactNode; children: React.ReactNode }) { + return ( +
+
{icon}
+
{children}
+
+ ); +} + // --- Main Component --- export function CalendarEventDetailPage({ event }: { event: NostrEvent }) { @@ -170,7 +201,7 @@ export function CalendarEventDetailPage({ event }: { event: NostrEvent }) { const { toast } = useToast(); const title = getTag(event.tags, 'title') ?? 'Untitled Event'; - const image = getTag(event.tags, 'image'); + const image = sanitizeUrl(getTag(event.tags, 'image')); const locationRaw = getTag(event.tags, 'location'); const location = locationRaw ? parseLocation(locationRaw) : undefined; const summary = getTag(event.tags, 'summary'); @@ -179,6 +210,7 @@ export function CalendarEventDetailPage({ event }: { event: NostrEvent }) { const eventCoord = useMemo(() => getEventCoord(event), [event]); const dateStr = useMemo(() => formatDetailDate(event), [event]); + const heroDate = useMemo(() => formatCalendarHeroDate(event), [event]); // Participants grouped by role const participantsByRole = useMemo(() => { @@ -240,202 +272,274 @@ export function CalendarEventDetailPage({ event }: { event: NostrEvent }) { }, [eventCoord, event.pubkey, myRsvp.status, publishRSVP, toast]); const showRSVP = !!user; + const attendeeCount = rsvps.accepted.length + rsvps.tentative.length; return ( -
- {/* ── Standard top bar ── */} -
- -

Event Details

- {canEdit && ( - - )} -
- - {/* ── Cover image ── */} - {image ? ( -
- {title} -
- ) : ( -
- -
- )} - - {/* ── Content ── */} -
- {/* Title */} -

{title}

- {/* Organizer row */} -
-
- -
-
- - {/* Date & Location — sidebar-style pills */} -
-
- - {dateStr} -
- {location && ( -
- - {location} +
+
+
+ {image ? ( + + ) : ( +
+
)} -
+
- {/* Hashtags */} - {hashtags.length > 0 && ( -
- {hashtags.map((tag) => ( - - - #{tag} - - - ))} +
+ + {canEdit && ( + + )}
- )} - {/* Description */} - {(event.content || summary) && ( - <> - -
-

About

- {event.content ? ( - - ) : ( -

{summary}

+
+
+

+ {title} +

+ calendar event +
+
+ {heroDate && ( + + + {heroDate} + + )} + {location && ( + + + {location} + + )} + {attendeeCount > 0 && ( + + + {attendeeCount} interested + + )} +
+ {summary && ( +

+ {summary} +

+ )} +
+
+
+ +
+
+ setReplyOpen(true)} + onMore={() => setMoreMenuOpen(true)} + /> +
+
+ +
+
+
+
+
+
Hosted by
+ +
+ + {hashtags.length > 0 && ( +
+ {hashtags.map((tag) => ( + + + #{tag} + + + ))} +
+ )} + + {(event.content || summary) && ( +
+ {event.content ? ( + + ) : ( +

{summary}

+ )} +
)}
- - )} - {/* External links */} - {links.length > 0 && ( -
- {links.map((url) => ( - - - {url.replace(/^https?:\/\//, '')} - - - ))} +
+
+

Comments

+ {replyTree.length > 0 ? ( + + {replyTree.length.toLocaleString()} {replyTree.length === 1 ? 'comment' : 'comments'} + + ) : null} +
+ + + + {commentsLoading ? ( +
+ {Array.from({ length: 3 }).map((_, i) => ( +
+
+ +
+ + + +
+
+
+ ))} +
+ ) : replyTree.length > 0 ? ( +
+ +
+ ) : ( + + )} +
- )} - {/* Participants */} - {participantsByRole.length > 0 && ( - <> - -
-

- Participants -

-
- {participantsByRole.map(([role, pubkeys]) => - pubkeys.map((pk) => ), - )} -
-
- - )} - - {/* Attendees */} - {rsvps.total > 0 && ( - <> - -
-

- Attendees -

-
- {([ - ['Going', rsvps.accepted, 'border-green-500/50 bg-green-500/5 text-green-600'], - ['Interested', rsvps.tentative, 'border-amber-500/50 bg-amber-500/5 text-amber-600'], - ["Can't Go", rsvps.declined, 'border-muted-foreground/30 bg-muted/30 text-muted-foreground'], - ] as const).map(([label, pks, cls]) => pks.length > 0 && ( -
- {label} ({pks.length}) - +
- - )} - {/* RSVP section */} - {showRSVP && ( - <> - -
-

- RSVP -

-
- - - -
-
- - )} + {showRSVP && ( +
+
RSVP
+
+ + + +
+
+ )} - setReplyOpen(true)} - onMore={() => setMoreMenuOpen(true)} - className="-mx-5 px-5" - /> + {rsvps.total > 0 && ( +
+
Attendees
+
+ {([ + ['Going', rsvps.accepted, 'border-green-500/50 bg-green-500/5 text-green-600'], + ['Interested', rsvps.tentative, 'border-amber-500/50 bg-amber-500/5 text-amber-600'], + ["Can't Go", rsvps.declined, 'border-muted-foreground/30 bg-muted/30 text-muted-foreground'], + ] as const).map(([label, pks, cls]) => pks.length > 0 && ( +
+ {label} ({pks.length}) + +
+ ))} +
+
+ )} + + {links.length > 0 && ( +
+
Links
+
+ {links.map((url) => ( + + ))} +
+
+ )} + + + + {participantsByRole.length > 0 && ( + + +
Participants
+
+ {participantsByRole.map(([role, pubkeys]) => + pubkeys.map((pk) => ), + )} +
+
+
+ )} +
+ +
@@ -446,32 +550,7 @@ export function CalendarEventDetailPage({ event }: { event: NostrEvent }) { event={event} /> )} - -
- {commentsLoading ? ( -
- {Array.from({ length: 3 }).map((_, i) => ( -
- -
- - - -
-
- ))} -
- ) : replyTree.length > 0 ? ( -
- -
- ) : ( -
- No comments yet. Be the first to comment! -
- )} -
-
+
); } From 97ec528b5049c3acd16e61f008d782571a4eb304 Mon Sep 17 00:00:00 2001 From: lemon Date: Thu, 21 May 2026 13:46:34 -0700 Subject: [PATCH 22/60] Normalize organization activity cards --- src/components/CampaignCard.tsx | 12 ++++++-- src/components/CommunityDetailPage.tsx | 39 +++++++++++++------------- 2 files changed, 29 insertions(+), 22 deletions(-) diff --git a/src/components/CampaignCard.tsx b/src/components/CampaignCard.tsx index e3feb639..b43e6c66 100644 --- a/src/components/CampaignCard.tsx +++ b/src/components/CampaignCard.tsx @@ -1,4 +1,5 @@ import { useMemo } from 'react'; +import type { ReactNode } from 'react'; import { Link } from 'react-router-dom'; import { CalendarClock, HandHeart, MapPin, Target, Users, Archive } from 'lucide-react'; @@ -68,13 +69,15 @@ interface CampaignCardProps { /** Visual variant: `compact` for grid items, `featured` for hero placement. */ variant?: 'compact' | 'featured'; className?: string; + /** Optional footer affordance rendered opposite the author line. */ + footerBadge?: ReactNode; } /** * Renders a single campaign as a clickable card. The whole card is a * `` to the campaign's naddr-based detail route. */ -export function CampaignCard({ campaign, variant = 'compact', className }: CampaignCardProps) { +export function CampaignCard({ campaign, variant = 'compact', className, footerBadge }: CampaignCardProps) { const author = useAuthor(campaign.pubkey); const { data: stats } = useCampaignDonations(campaign.aTag); const { data: btcPrice } = useBtcPrice(); @@ -224,8 +227,11 @@ export function CampaignCard({ campaign, variant = 'compact', className }: Campa )}
-
- by {creatorName} +
+
+ by {creatorName} +
+ {footerBadge &&
{footerBadge}
}
diff --git a/src/components/CommunityDetailPage.tsx b/src/components/CommunityDetailPage.tsx index 430c3b0f..f0522745 100644 --- a/src/components/CommunityDetailPage.tsx +++ b/src/components/CommunityDetailPage.tsx @@ -217,7 +217,7 @@ function parseShelfLocation(raw: string): string { function ActivityTypePill({ icon, label }: { icon: React.ReactNode; label: string }) { return ( -
+
{icon} {label}
@@ -243,11 +243,8 @@ function PledgeShelfCard({ pledge }: { pledge: Action }) { return ( -
- } label="Pledge" /> -
-
- by {displayName} +
+
+ by {displayName} +
+ } label="Pledge" />
@@ -336,11 +336,8 @@ function CalendarEventShelfCard({ event }: { event: NostrEvent }) { return ( -
- } label="Event" /> -
{coverImage ? ( @@ -405,8 +402,11 @@ function CalendarEventShelfCard({ event }: { event: NostrEvent }) { ) : null}
-
- by {displayName} +
+
+ by {displayName} +
+ } label="Event" />
@@ -438,7 +438,7 @@ function OfficialShelf({ title, count, isLoading, isEmpty, children }: OfficialS )}
-
+
{children}
@@ -529,11 +529,12 @@ function OfficialActivityShelves({ {mixedActivity.map((item) => { if (item.type === 'campaign') { return ( -
-
- } label="Campaign" /> -
- +
+ } label="Campaign" />} + />
); } From ffb9c93ee658ed19c6099779d8121b96b758d53f Mon Sep 17 00:00:00 2001 From: lemon Date: Thu, 21 May 2026 13:52:00 -0700 Subject: [PATCH 23/60] Add pins across detail comments --- NIP.md | 18 ++--- src/components/CalendarEventDetailPage.tsx | 76 +++++++++++++++++- src/components/CommunityDetailPage.tsx | 76 +++++++++++++++++- src/components/PinnedCommentHeader.tsx | 53 +++++++++++++ ...nedEvents.ts => usePinnedEventComments.ts} | 43 ++++++----- src/pages/ActionDetailPage.tsx | 77 ++++++++++++++++++- src/pages/CampaignDetailPage.tsx | 63 +++++---------- 7 files changed, 329 insertions(+), 77 deletions(-) create mode 100644 src/components/PinnedCommentHeader.tsx rename src/hooks/{useCampaignPinnedEvents.ts => usePinnedEventComments.ts} (64%) diff --git a/NIP.md b/NIP.md index 5862fc40..177f952d 100644 --- a/NIP.md +++ b/NIP.md @@ -322,20 +322,20 @@ This mirrors the community batch-zap pattern documented in the kind 8333 section Clients MUST verify each kind 8333 event on-chain before counting it toward the campaign total, per the verification rules in the kind 8333 section. -**Fetch pinned campaign activity:** +**Fetch pinned event comments:** -Campaign creators MAY pin important activity feed events (comments, updates, or donation receipts) with a NIP-78 app-specific data event (`kind: 30078`) authored by the campaign creator. The `d` tag is scoped to the campaign coordinate: +Event owners MAY pin important comments or activity feed events with a NIP-78 app-specific data event (`kind: 30078`) authored by the root event owner. The `d` tag is scoped to the root event coordinate. Agora uses this for campaigns (`30223`), pledges (`36639`), organizations (`34550`), and calendar events (`31922` / `31923`). ```json { "kind": 30078, - "pubkey": "", + "pubkey": "", "content": "{\"pinnedEvents\":[\"\",\"\"]}", "tags": [ - ["d", "agora-campaign-pins:30223::"], - ["a", "30223::"], - ["k", "30223"], - ["alt", "Pinned campaign activity"] + ["d", "agora-pinned-comments:::"], + ["a", "::"], + ["k", ""], + ["alt", "Pinned event comments"] ] } ``` @@ -343,10 +343,10 @@ Campaign creators MAY pin important activity feed events (comments, updates, or Clients SHOULD query the pin list with: ```json -{ "kinds": [30078], "authors": [""], "#d": ["agora-campaign-pins:30223::"], "limit": 1 } +{ "kinds": [30078], "authors": [""], "#d": ["agora-pinned-comments:::"], "limit": 1 } ``` -The `pinnedEvents` array is ordered newest pin first. Pinning an already-pinned event removes it. Clients SHOULD ignore pin lists not authored by the campaign creator. +The `pinnedEvents` array is ordered newest pin first. Pinning an already-pinned event removes it. Clients SHOULD ignore pin lists not authored by the root event owner. ### Client Behavior diff --git a/src/components/CalendarEventDetailPage.tsx b/src/components/CalendarEventDetailPage.tsx index 0ca04340..1c672740 100644 --- a/src/components/CalendarEventDetailPage.tsx +++ b/src/components/CalendarEventDetailPage.tsx @@ -23,6 +23,7 @@ import { DetailCommentComposer } from '@/components/DetailCommentComposer'; import { NoteContent } from '@/components/NoteContent'; import { NoteMoreMenu } from '@/components/NoteMoreMenu'; import { PostActionBar } from '@/components/PostActionBar'; +import { PinnedCommentHeader } from '@/components/PinnedCommentHeader'; import { ReplyComposeModal } from '@/components/ReplyComposeModal'; import { CreateCommunityEventDialog } from '@/components/CreateCommunityEventDialog'; import { RSVPAvatars } from '@/components/RSVPAvatars'; @@ -34,6 +35,7 @@ import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useEventRSVPs } from '@/hooks/useEventRSVPs'; import { useMyRSVP } from '@/hooks/useMyRSVP'; import { usePublishRSVP } from '@/hooks/usePublishRSVP'; +import { usePinnedEventComments } from '@/hooks/usePinnedEventComments'; import { useProfileUrl } from '@/hooks/useProfileUrl'; import { useToast } from '@/hooks/useToast'; import { genUserName } from '@/lib/genUserName'; @@ -232,6 +234,12 @@ export function CalendarEventDetailPage({ event }: { event: NostrEvent }) { const myRsvp = useMyRSVP(eventCoord); const publishRSVP = usePublishRSVP(); const { data: commentsData, isLoading: commentsLoading } = useComments(event, 500); + const { + pinnedEvents, + isPinned, + canManagePins, + togglePin, + } = usePinnedEventComments(eventCoord, event.pubkey); const [replyOpen, setReplyOpen] = useState(false); const [moreMenuOpen, setMoreMenuOpen] = useState(false); const [editOpen, setEditOpen] = useState(false); @@ -257,6 +265,11 @@ export function CalendarEventDetailPage({ event }: { event: NostrEvent }) { .map((comment) => buildNode(comment)); }, [commentsData]); + const pinnedNodes = useMemo( + () => pinnedEvents.map((event): ReplyNode => ({ event, children: [] })), + [pinnedEvents], + ); + const handleRSVP = useCallback(async (status: 'accepted' | 'declined' | 'tentative') => { if (status === myRsvp.status) return; try { @@ -356,6 +369,24 @@ export function CalendarEventDetailPage({ event }: { event: NostrEvent }) {
+ {pinnedNodes.length > 0 && ( +
+
+ ( + handleTogglePin(event)} + /> + )} + /> +
+
+ )} +
@@ -417,7 +448,17 @@ export function CalendarEventDetailPage({ event }: { event: NostrEvent }) {
) : replyTree.length > 0 ? (
- + ( + handleTogglePin(event)} + /> + )} + />
) : (
+ {pinnedNodes.length > 0 && ( +
+
+ ( + handleTogglePin(event)} + /> + )} + /> +
+
+ )} + {/* ── Body — single column, pledge-detail-style ─────────────────── */}
{/* Donate (when there's a member set) and Share buttons. Sits @@ -1041,7 +1072,17 @@ export function CommunityDetailPage({ event }: { event: NostrEvent }) {
) : replyTree.length > 0 ? (
- + ( + handleTogglePin(event)} + /> + )} + />
) : ( + )} +
+ ); +} diff --git a/src/hooks/useCampaignPinnedEvents.ts b/src/hooks/usePinnedEventComments.ts similarity index 64% rename from src/hooks/useCampaignPinnedEvents.ts rename to src/hooks/usePinnedEventComments.ts index 5cd67c03..50a29985 100644 --- a/src/hooks/useCampaignPinnedEvents.ts +++ b/src/hooks/usePinnedEventComments.ts @@ -6,11 +6,11 @@ import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useNostrPublish } from '@/hooks/useNostrPublish'; import { fetchFreshEvent } from '@/lib/fetchFreshEvent'; -const CAMPAIGN_PIN_LIST_KIND = 30078; -const CAMPAIGN_PIN_D_TAG_PREFIX = 'agora-campaign-pins:'; +const PIN_LIST_KIND = 30078; +const PIN_D_TAG_PREFIX = 'agora-pinned-comments:'; -function campaignPinDTag(campaignATag: string): string { - return `${CAMPAIGN_PIN_D_TAG_PREFIX}${campaignATag}`; +function pinDTag(rootATag: string): string { + return `${PIN_D_TAG_PREFIX}${rootATag}`; } function parsePinnedIds(event: NostrEvent | null | undefined): string[] { @@ -25,30 +25,32 @@ function parsePinnedIds(event: NostrEvent | null | undefined): string[] { } } -export function useCampaignPinnedEvents(campaignATag: string, campaignAuthorPubkey: string) { +export function usePinnedEventComments(rootATag: string | undefined, ownerPubkey: string | undefined) { const { nostr } = useNostr(); const { user } = useCurrentUser(); const queryClient = useQueryClient(); const { mutateAsync: publishEvent } = useNostrPublish(); - const dTag = campaignPinDTag(campaignATag); - const canManagePins = user?.pubkey === campaignAuthorPubkey; + const dTag = rootATag ? pinDTag(rootATag) : undefined; + const canManagePins = !!user && !!ownerPubkey && user.pubkey === ownerPubkey; const pinnedListQuery = useQuery({ - queryKey: ['campaign-pinned-events-list', campaignATag, campaignAuthorPubkey], + queryKey: ['pinned-event-comments-list', rootATag, ownerPubkey], queryFn: async ({ signal }) => { + if (!rootATag || !ownerPubkey || !dTag) return null; const events = await nostr.query( - [{ kinds: [CAMPAIGN_PIN_LIST_KIND], authors: [campaignAuthorPubkey], '#d': [dTag], limit: 1 }], + [{ kinds: [PIN_LIST_KIND], authors: [ownerPubkey], '#d': [dTag], limit: 1 }], { signal: AbortSignal.any([signal, AbortSignal.timeout(5000)]) }, ); return events[0] ?? null; }, + enabled: !!rootATag && !!ownerPubkey && !!dTag, staleTime: 30_000, }); const pinnedIds = parsePinnedIds(pinnedListQuery.data); const pinnedEventsQuery = useQuery({ - queryKey: ['campaign-pinned-events', campaignATag, pinnedIds], + queryKey: ['pinned-event-comments', rootATag, pinnedIds], queryFn: async ({ signal }) => { if (pinnedIds.length === 0) return []; const events = await nostr.query( @@ -57,18 +59,19 @@ export function useCampaignPinnedEvents(campaignATag: string, campaignAuthorPubk ); return events.sort((a, b) => pinnedIds.indexOf(a.id) - pinnedIds.indexOf(b.id)); }, - enabled: pinnedIds.length > 0, + enabled: !!rootATag && pinnedIds.length > 0, staleTime: 30_000, }); const togglePin = useMutation({ mutationFn: async (eventId: string) => { if (!user) throw new Error('User is not logged in'); - if (user.pubkey !== campaignAuthorPubkey) throw new Error('Only the campaign author can pin updates.'); + if (!rootATag || !ownerPubkey || !dTag) throw new Error('Missing pin context.'); + if (user.pubkey !== ownerPubkey) throw new Error('Only the event owner can pin comments.'); const prev = await fetchFreshEvent(nostr, { - kinds: [CAMPAIGN_PIN_LIST_KIND], - authors: [campaignAuthorPubkey], + kinds: [PIN_LIST_KIND], + authors: [ownerPubkey], '#d': [dTag], }); @@ -78,20 +81,20 @@ export function useCampaignPinnedEvents(campaignATag: string, campaignAuthorPubk : [eventId, ...current.filter((id) => id !== eventId)]; await publishEvent({ - kind: CAMPAIGN_PIN_LIST_KIND, + kind: PIN_LIST_KIND, content: JSON.stringify({ pinnedEvents: next }), tags: [ ['d', dTag], - ['a', campaignATag], - ['k', campaignATag.split(':')[0] ?? '30223'], - ['alt', 'Pinned campaign activity'], + ['a', rootATag], + ['k', rootATag.split(':')[0] ?? ''], + ['alt', 'Pinned event comments'], ], prev: prev ?? undefined, }); }, onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['campaign-pinned-events-list', campaignATag, campaignAuthorPubkey] }); - queryClient.invalidateQueries({ queryKey: ['campaign-pinned-events', campaignATag] }); + queryClient.invalidateQueries({ queryKey: ['pinned-event-comments-list', rootATag, ownerPubkey] }); + queryClient.invalidateQueries({ queryKey: ['pinned-event-comments', rootATag] }); }, }); diff --git a/src/pages/ActionDetailPage.tsx b/src/pages/ActionDetailPage.tsx index 7971298f..15abae84 100644 --- a/src/pages/ActionDetailPage.tsx +++ b/src/pages/ActionDetailPage.tsx @@ -34,9 +34,11 @@ import { Progress } from '@/components/ui/progress'; import { Skeleton } from '@/components/ui/skeleton'; import { DetailCommentComposer } from '@/components/DetailCommentComposer'; import { PostActionBar } from '@/components/PostActionBar'; +import { PinnedCommentHeader } from '@/components/PinnedCommentHeader'; import { ReplyComposeModal } from '@/components/ReplyComposeModal'; import { NoteMoreMenu } from '@/components/NoteMoreMenu'; import { ThreadedReplyList, type ReplyNode } from '@/components/ThreadedReplyList'; +import { usePinnedEventComments } from '@/hooks/usePinnedEventComments'; import NotFound from '@/pages/NotFound'; function formatDeadline(unixSeconds: number): { label: string; isPast: boolean } { @@ -78,6 +80,13 @@ function PledgeDetailContent({ action }: { action: Action }) { const navigate = useNavigate(); const { toast } = useToast(); const { data: commentsData, isLoading: commentsLoading } = useComments(action.event, 500); + const rootATag = `36639:${action.pubkey}:${action.id}`; + const { + pinnedEvents, + isPinned, + canManagePins, + togglePin, + } = usePinnedEventComments(rootATag, action.pubkey); const [replyOpen, setReplyOpen] = useState(false); const [moreMenuOpen, setMoreMenuOpen] = useState(false); @@ -123,6 +132,11 @@ function PledgeDetailContent({ action }: { action: Action }) { .map((c) => buildNode(c)); }, [commentsData, topLevel, zapTotals]); + const pinnedNodes = useMemo( + () => pinnedEvents.map((event): ReplyNode => ({ event, children: [] })), + [pinnedEvents], + ); + const metadata: NostrMetadata | undefined = author.data?.metadata; const creatorName = getDisplayName(metadata, action.pubkey); const creatorProfileUrl = useProfileUrl(action.pubkey, metadata); @@ -181,6 +195,24 @@ function PledgeDetailContent({ action }: { action: Action }) {
+ {pinnedNodes.length > 0 && ( +
+
+ ( + handleTogglePin(event)} + /> + )} + /> +
+
+ )} +
@@ -221,7 +253,17 @@ function PledgeDetailContent({ action }: { action: Action }) {
) : replyTree.length > 0 ? (
- + ( + handleTogglePin(event)} + /> + )} + />
) : ( + + {isCampaignAuthor && ( + + + Campaigner + )} -
+ ); } From b7d33577f1ad1c7ba1f249a298ba6ab5f8891398 Mon Sep 17 00:00:00 2001 From: lemon Date: Thu, 21 May 2026 13:54:20 -0700 Subject: [PATCH 24/60] Restore event RSVP detail card --- src/components/CalendarEventDetailPage.tsx | 228 +++++++++++---------- 1 file changed, 124 insertions(+), 104 deletions(-) diff --git a/src/components/CalendarEventDetailPage.tsx b/src/components/CalendarEventDetailPage.tsx index 1c672740..75e78bb0 100644 --- a/src/components/CalendarEventDetailPage.tsx +++ b/src/components/CalendarEventDetailPage.tsx @@ -286,6 +286,122 @@ export function CalendarEventDetailPage({ event }: { event: NostrEvent }) { const showRSVP = !!user; const attendeeCount = rsvps.accepted.length + rsvps.tentative.length; + const rsvpStatusLabel = myRsvp.status === 'accepted' + ? 'You are going' + : myRsvp.status === 'tentative' + ? 'You are interested' + : myRsvp.status === 'declined' + ? "You can't go" + : 'Choose your RSVP'; + + const eventDetailsCard = ( + + +
+ }> + {dateStr} + + {location && ( + }> + {location} + + )} +
+ + {showRSVP && ( +
+
+
RSVP
+ {rsvpStatusLabel} +
+
+ + + +
+
+ )} + + {rsvps.total > 0 && ( +
+
Attendees
+
+ {([ + ['Going', rsvps.accepted, 'border-green-500/50 bg-green-500/5 text-green-600'], + ['Interested', rsvps.tentative, 'border-amber-500/50 bg-amber-500/5 text-amber-600'], + ["Can't Go", rsvps.declined, 'border-muted-foreground/30 bg-muted/30 text-muted-foreground'], + ] as const).map(([label, pks, cls]) => pks.length > 0 && ( +
+ {label} ({pks.length}) + +
+ ))} +
+
+ )} + + {links.length > 0 && ( +
+
Links
+
+ {links.map((url) => ( + + ))} +
+
+ )} +
+
+ ); + + const participantsCard = participantsByRole.length > 0 ? ( + + +
Participants
+
+ {participantsByRole.map(([role, pubkeys]) => + pubkeys.map((pk) => ), + )} +
+
+
+ ) : null; return (
@@ -388,6 +504,11 @@ export function CalendarEventDetailPage({ event }: { event: NostrEvent }) { )}
+
+ {eventDetailsCard} + {participantsCard} +
+
@@ -473,111 +594,10 @@ export function CalendarEventDetailPage({ event }: { event: NostrEvent }) {
-
From 48744aa13d751f9903ca92d551fb2ae7c3f2daa2 Mon Sep 17 00:00:00 2001 From: lemon Date: Thu, 21 May 2026 14:05:15 -0700 Subject: [PATCH 25/60] Refine calendar event details --- src/components/CalendarEventDetailPage.tsx | 35 ++++++++++++++++------ 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/src/components/CalendarEventDetailPage.tsx b/src/components/CalendarEventDetailPage.tsx index 75e78bb0..370b54b0 100644 --- a/src/components/CalendarEventDetailPage.tsx +++ b/src/components/CalendarEventDetailPage.tsx @@ -285,7 +285,8 @@ export function CalendarEventDetailPage({ event }: { event: NostrEvent }) { }, [eventCoord, event.pubkey, myRsvp.status, publishRSVP, toast]); const showRSVP = !!user; - const attendeeCount = rsvps.accepted.length + rsvps.tentative.length; + const attendingCount = rsvps.accepted.length; + const interestedCount = rsvps.tentative.length; const rsvpStatusLabel = myRsvp.status === 'accepted' ? 'You are going' : myRsvp.status === 'tentative' @@ -297,6 +298,22 @@ export function CalendarEventDetailPage({ event }: { event: NostrEvent }) { const eventDetailsCard = ( +
+
Hosted by
+ +
+ + {(event.content || summary) && ( +
+
Description
+ {event.content ? ( + + ) : ( +

{summary}

+ )} +
+ )} +
}> {dateStr} @@ -443,7 +460,6 @@ export function CalendarEventDetailPage({ event }: { event: NostrEvent }) {

{title}

- calendar event
{heroDate && ( @@ -458,10 +474,16 @@ export function CalendarEventDetailPage({ event }: { event: NostrEvent }) { {location} )} - {attendeeCount > 0 && ( + {attendingCount > 0 && ( - {attendeeCount} interested + {attendingCount} attending + + )} + {interestedCount > 0 && ( + + + {interestedCount} interested )}
@@ -512,11 +534,6 @@ export function CalendarEventDetailPage({ event }: { event: NostrEvent }) {
-
-
Hosted by
- -
- {hashtags.length > 0 && (
{hashtags.map((tag) => ( From 81f1fd5d1fa44bd971d69a8c5c38ef5000014011 Mon Sep 17 00:00:00 2001 From: Chad Curtis Date: Thu, 21 May 2026 16:39:14 -0500 Subject: [PATCH 26/60] Use foreground token in soft Bitcoin disclaimer for light-mode contrast --- src/components/BitcoinPublicDisclaimer.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/BitcoinPublicDisclaimer.tsx b/src/components/BitcoinPublicDisclaimer.tsx index b354072b..cfe9bab1 100644 --- a/src/components/BitcoinPublicDisclaimer.tsx +++ b/src/components/BitcoinPublicDisclaimer.tsx @@ -67,7 +67,12 @@ export function BitcoinPublicDisclaimer({ role={isSoft ? 'note' : 'alert'} className={cn( isSoft - ? 'border-amber-300/60 bg-amber-50 text-amber-900 dark:border-amber-500/30 dark:bg-amber-500/10 dark:text-amber-100' + // Use the project's foreground token (not raw amber-900) so + // the text always contrasts against the page in both light + // and dark themes. The faint amber tint keeps the + // "informational notice" cue without leaning on hard-coded + // amber text that disappears on the wrong backdrop. + ? 'border-amber-500/30 bg-amber-500/10 text-foreground' : 'border-destructive/50 bg-destructive/5 text-destructive dark:border-destructive', )} > From 2b3a2e7dafcc633047c2b3607aab605a3498d59f Mon Sep 17 00:00:00 2001 From: Chad Curtis Date: Thu, 21 May 2026 17:38:44 -0500 Subject: [PATCH 27/60] Remove NIP-05 username from note card header --- src/components/NoteCard.tsx | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/components/NoteCard.tsx b/src/components/NoteCard.tsx index 08da9d8f..f2f6c2b9 100644 --- a/src/components/NoteCard.tsx +++ b/src/components/NoteCard.tsx @@ -58,7 +58,6 @@ import { ChestIcon } from "@/components/icons/ChestIcon"; import { RepostIcon } from "@/components/icons/RepostIcon"; import { LiveStreamPlayer } from "@/components/LiveStreamPlayer"; import { MagicDeckContent } from "@/components/MagicDeckContent"; -import { Nip05Badge } from "@/components/Nip05Badge"; import { NoteContent } from "@/components/NoteContent"; import { NoteMoreMenu } from "@/components/NoteMoreMenu"; import { PatchCard } from "@/components/PatchCard"; @@ -86,7 +85,6 @@ import { ZapDialog } from "@/components/ZapDialog"; import { useAppContext } from "@/hooks/useAppContext"; import { useAuthor } from "@/hooks/useAuthor"; import { useCurrentUser } from "@/hooks/useCurrentUser"; -import { useNip05Verify } from "@/hooks/useNip05Verify"; import { useOpenPost } from "@/hooks/useOpenPost"; import { useProfileUrl } from "@/hooks/useProfileUrl"; import { toast } from "@/hooks/useToast"; @@ -398,11 +396,6 @@ export const NoteCard = memo(function NoteCard({ const metadata = author.data?.metadata; const actionMetadata = actionEvent ? actionAuthor.data?.metadata : metadata; const displayName = getDisplayName(metadata, event.pubkey); - const nip05 = metadata?.nip05; - const { data: nip05Verified, isPending: nip05Pending } = useNip05Verify( - nip05, - event.pubkey, - ); const profileUrl = useProfileUrl(event.pubkey, metadata); const encodedId = useMemo(() => encodeEventId(actionTarget), [actionTarget]); const { data: stats } = useEventStats(actionTarget.id, actionTarget); @@ -776,12 +769,6 @@ export const NoteCard = memo(function NoteCard({ )}
- {nip05 && nip05Pending && } - {nip05 && nip05Pending && ·} - {nip05 && nip05Verified && ( - - )} - {nip05 && nip05Verified && ·} {timeAgo(event.created_at)} From b53cb20d6167a2ea75f4966b0f738733066e3fca Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 21 May 2026 17:39:15 -0500 Subject: [PATCH 28/60] Replace Discover with Support nav link Remove the Discover page and route entirely. In the top nav, swap the Discover entry for a Support link pointing at /campaigns/all (the all campaigns directory). Also drops the now-orphaned DiscoverHero component and useDiscoverFeed hook, and updates the NIP.md campaign moderation note that referenced the deleted /discover route. --- NIP.md | 2 +- src/AppRouter.tsx | 2 - src/components/TopNav.tsx | 2 +- src/components/discovery/DiscoverHero.tsx | 355 ---------------------- src/hooks/useDiscoverFeed.ts | 148 --------- src/pages/DiscoverPage.tsx | 23 -- 6 files changed, 2 insertions(+), 530 deletions(-) delete mode 100644 src/components/discovery/DiscoverHero.tsx delete mode 100644 src/hooks/useDiscoverFeed.ts delete mode 100644 src/pages/DiscoverPage.tsx diff --git a/NIP.md b/NIP.md index 177f952d..e2f34faf 100644 --- a/NIP.md +++ b/NIP.md @@ -357,7 +357,7 @@ The `pinnedEvents` array is ordered newest pin first. Pinning an already-pinned ### Campaign Moderation Labels -Agora curates which kind 30223 campaigns appear on the homepage (`/`) and on Discover (`/discover`) via moderator-signed NIP-32 label events (kind 1985) in a dedicated label namespace. The campaign event itself is never modified — surfacing is purely a client-side rollup of label events. +Agora curates which kind 30223 campaigns appear on the homepage (`/`) and on the Support directory (`/campaigns/all`) via moderator-signed NIP-32 label events (kind 1985) in a dedicated label namespace. The campaign event itself is never modified — surfacing is purely a client-side rollup of label events. #### Namespace diff --git a/src/AppRouter.tsx b/src/AppRouter.tsx index 28835a17..f8cdc8fd 100644 --- a/src/AppRouter.tsx +++ b/src/AppRouter.tsx @@ -49,7 +49,6 @@ const CreateCommunityPage = lazy(() => import("./pages/CreateCommunityPage").the const CreateEventPage = lazy(() => import("./pages/CreateEventPage").then(m => ({ default: m.CreateEventPage }))); const ContentSettingsPage = lazy(() => import("./pages/ContentSettingsPage").then(m => ({ default: m.ContentSettingsPage }))); const CSAEPolicyPage = lazy(() => import("./pages/CSAEPolicyPage").then(m => ({ default: m.CSAEPolicyPage }))); -const DiscoverPage = lazy(() => import("./pages/DiscoverPage").then(m => ({ default: m.DiscoverPage }))); const DomainFeedPage = lazy(() => import("./pages/DomainFeedPage").then(m => ({ default: m.DomainFeedPage }))); const EventsFeedPage = lazy(() => import("./pages/EventsFeedPage").then(m => ({ default: m.EventsFeedPage }))); const ExternalContentPage = lazy(() => import("./pages/ExternalContentPage").then(m => ({ default: m.ExternalContentPage }))); @@ -169,7 +168,6 @@ export function AppRouter() { {/* All routes share the persistent FundraiserLayout (top nav + footer) */} }> } /> - } /> } /> } /> } /> diff --git a/src/components/TopNav.tsx b/src/components/TopNav.tsx index 9ffee0e6..7720cffa 100644 --- a/src/components/TopNav.tsx +++ b/src/components/TopNav.tsx @@ -33,7 +33,7 @@ interface NavItem { } const NAV_ITEMS: NavItem[] = [ - { label: 'Discover', to: '/discover', icon: HandHeart }, + { label: 'Support', to: '/campaigns/all', icon: HandHeart }, { label: 'Organize', to: '/communities', icon: Users }, { label: 'Pledge', to: '/pledges', icon: Megaphone }, ]; diff --git a/src/components/discovery/DiscoverHero.tsx b/src/components/discovery/DiscoverHero.tsx deleted file mode 100644 index d00219a7..00000000 --- a/src/components/discovery/DiscoverHero.tsx +++ /dev/null @@ -1,355 +0,0 @@ -import { useEffect, useMemo, useState } from 'react'; -import { Link } from 'react-router-dom'; -import { Globe2, HandHeart, PlusCircle, Users } from 'lucide-react'; - -import { HeroGlobe, type GlobeMarkerKind } from '@/components/HeroGlobe'; -import { HeroAtmosphere } from '@/components/HeroAtmosphere'; -import { Button } from '@/components/ui/button'; -import { Skeleton } from '@/components/ui/skeleton'; -import { useCampaigns } from '@/hooks/useCampaigns'; -import { useGlobalActivity } from '@/hooks/useGlobalActivity'; -import { useGlobalDonations } from '@/hooks/useGlobalDonations'; -import { useDiscoverCommunities } from '@/hooks/useDiscoverCommunities'; -import { HOPE_PALETTE } from '@/lib/hopePalette'; -import { searchCountry } from '@/lib/countries'; -import { getCoordinates } from '@/lib/coordinates'; -import { formatSatsShort } from '@/lib/formatCampaignAmount'; -import { cn } from '@/lib/utils'; - -interface DiscoverHeroProps { - className?: string; -} - -interface GlobeMarker { - key: string; - lat: number; - lng: number; - label: string; - kind: GlobeMarkerKind; -} - -interface TickerStat { - /** Stable React key. */ - id: string; - /** Big number / value text. */ - value: string; - /** Trailing label that describes what the number is. */ - label: string; - /** Decorative leading icon. */ - icon: React.ReactNode; -} - -/** Country code → lat/lng. Used to seed country-pulse markers. */ -function lookupCountryCoords(code: string) { - const coords = getCoordinates(code); - return coords ? { lat: coords.latitude, lng: coords.longitude } : null; -} - -/** - * Discover-page hero. The same hand-drawn `HeroGlobe` that anchors the - * fundraising home page (`/`), but reframed: the globe is the - * protagonist, three marker types sit on it at once — campaigns - * (hearts), communities (rings), and country-pulse (warm dots) — and a - * rotating stat ticker headlines what the network has done. - * - * Visual chrome: - * - Slow hue drift through `HOPE_PALETTE` every ~8s (the page literally - * pulses with hope). - * - `HeroAtmosphere` carries the warm scrim + radial glow + sunrise rim, - * same component the campaigns hero uses for crossfade. - * - No background photo — Discover isn't selling any one campaign, so - * the sphere reads against a soft secondary wash instead. - */ -export function DiscoverHero({ className }: DiscoverHeroProps) { - // ─── Data ────────────────────────────────────────────────────────────── - const { data: campaigns } = useCampaigns({ limit: 60 }); - const { data: communities } = useDiscoverCommunities({ limit: 60 }); - const { data: activityByCountry } = useGlobalActivity(); - const { data: donations, isLoading: donationsLoading } = useGlobalDonations(); - - // ─── Globe markers ───────────────────────────────────────────────────── - // Layer three pin types. We dedupe primarily by country so the globe - // never piles dozens of markers on top of each other — the goal is a - // sparse, hopeful constellation, not a heatmap. Hearts win over rings - // win over dots when the same country shows up in multiple sources. - const markers = useMemo(() => { - const out: GlobeMarker[] = []; - const claimedCountries = new Set(); - - // 1. Campaigns → hearts. Newest first; cap at 18 so they don't crowd. - let heartCount = 0; - for (const c of campaigns ?? []) { - if (heartCount >= 18) break; - if (!c.location) continue; - const match = searchCountry(c.location); - if (!match) continue; - if (claimedCountries.has(match.country.code)) continue; - const coords = getCoordinates(match.country.code); - if (!coords) continue; - claimedCountries.add(match.country.code); - out.push({ - key: `campaign:${c.aTag}`, - lat: coords.latitude, - lng: coords.longitude, - label: c.title, - kind: 'campaign', - }); - heartCount++; - } - - // 2. Country-pulse dots — the trusted-stats country activity, sized - // implicitly by the marker glyph. Cap at 28 so the back of the globe - // doesn't bristle when it rotates into view. - let pulseCount = 0; - if (activityByCountry) { - const sortedCodes = [...activityByCountry.entries()] - .sort((a, b) => b[1] - a[1]) - .map(([code]) => code); - for (const code of sortedCodes) { - if (pulseCount >= 28) break; - if (claimedCountries.has(code)) continue; - const coords = lookupCountryCoords(code); - if (!coords) continue; - claimedCountries.add(code); - out.push({ - key: `pulse:${code}`, - lat: coords.lat, - lng: coords.lng, - label: `Active in ${code}`, - kind: 'country-pulse', - }); - pulseCount++; - } - } - - // 3. Community rings — only when we can geolocate one of the - // moderators. Communities don't carry a location tag of their own, - // so we use a small heuristic: spread the first N communities across - // continents by scattering them on a stable hash. Keeps the layer - // present without inventing coordinates we can't justify. - // - // To keep ourselves honest we cap this at 6 rings and never overwrite - // a country that already has a campaign heart or pulse dot. If we - // genuinely can't place any, we skip the layer. - const scatter: Array<{ lat: number; lng: number }> = [ - { lat: 40.7, lng: -74.0 }, // Americas - { lat: -23.5, lng: -46.6 }, // S. America - { lat: 51.5, lng: -0.1 }, // Europe - { lat: -1.3, lng: 36.8 }, // Africa - { lat: 35.7, lng: 139.7 }, // E. Asia - { lat: -33.9, lng: 151.2 }, // Oceania - ]; - let ringCount = 0; - for (const community of communities ?? []) { - if (ringCount >= scatter.length) break; - const slot = scatter[ringCount]; - out.push({ - key: `community:${community.aTag}`, - lat: slot.lat, - lng: slot.lng, - label: community.name, - kind: 'community', - }); - ringCount++; - } - - return out; - }, [campaigns, communities, activityByCountry]); - - // ─── Hue drift ───────────────────────────────────────────────────────── - // Cycle through the hopeful palette on a slow ~9s interval. We seed - // HeroAtmosphere with a stable string per cycle so its crossfade logic - // kicks in correctly between hues. - const [hueIndex, setHueIndex] = useState(0); - useEffect(() => { - const id = window.setInterval(() => { - setHueIndex((i) => (i + 1) % HOPE_PALETTE.length); - }, 9_000); - return () => window.clearInterval(id); - }, []); - const activeHue = HOPE_PALETTE[hueIndex]; - const atmosphereSeed = `discover-hue-${activeHue.name}`; - - // ─── Stat ticker ─────────────────────────────────────────────────────── - // Three rotating, immutable network-wide stats. We compute them - // defensively — when the underlying query is still loading we surface - // a small skeleton inside the ticker row instead of "0" so the page - // doesn't lie about the network's scale. - const stats = useMemo(() => { - const items: TickerStat[] = []; - - if (donations && donations.totalSats > 0) { - items.push({ - id: 'sats', - value: formatSatsShort(donations.totalSats), - label: `raised on-chain across ${donations.campaignCount.toLocaleString()} ${ - donations.campaignCount === 1 ? 'campaign' : 'campaigns' - }`, - icon: , - }); - } - if (communities && communities.length > 0) { - items.push({ - id: 'communities', - value: communities.length.toLocaleString(), - label: `${communities.length === 1 ? 'organization' : 'organizations'} gathering on Nostr`, - icon: , - }); - } - if (activityByCountry && activityByCountry.size > 0) { - items.push({ - id: 'countries', - value: activityByCountry.size.toLocaleString(), - label: `${activityByCountry.size === 1 ? 'country' : 'countries'} posting today`, - icon: , - }); - } - return items; - }, [donations, communities, activityByCountry]); - - // Auto-advance the ticker. Holds at the first slot until at least one - // stat is known so the visitor doesn't see an empty pill. - const [tickerIndex, setTickerIndex] = useState(0); - useEffect(() => { - if (stats.length <= 1) return; - const id = window.setInterval(() => { - setTickerIndex((i) => (i + 1) % stats.length); - }, 4_000); - return () => window.clearInterval(id); - }, [stats.length]); - - const currentStat = stats[tickerIndex % Math.max(stats.length, 1)]; - - return ( -
- {/* Atmosphere — same scrim + radial glow + sunrise rim used on - `/`. Seeded by the active hue so the whole hero blooms together - when the palette advances. */} - - - {/* Globe — centered, dominant. Slight upward bias so the headline - beneath has breathing room. */} -
-
- -
-
- - {/* Readability scrim. Sits above the globe + atmosphere but below - the foreground content so the headline / paragraph stay legible - regardless of which hue the palette is currently cycling - through. Top-down so the eye-line lands on the darkest pixels; - we taper to transparent before the ticker pill so the CTAs and - stat row underneath keep their warm wash. */} -
- ); -} diff --git a/src/hooks/useDiscoverFeed.ts b/src/hooks/useDiscoverFeed.ts deleted file mode 100644 index f3abbb32..00000000 --- a/src/hooks/useDiscoverFeed.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { useNostr } from '@nostrify/react'; -import { useInfiniteQuery } from '@tanstack/react-query'; -import type { NostrEvent, NostrFilter } from '@nostrify/nostrify'; - -import { CAMPAIGN_KIND } from '@/lib/campaign'; -import { shouldHideFeedEvent } from '@/lib/feedUtils'; - -const DISCOVER_PAGE_SIZE = 30; - -/** - * Kinds surfaced in the mixed Discover feed: - * - * - **30223** — new campaign creations (addressable). When a campaign is - * minted or revised it bubbles back to the top, the same way a new - * Substack post would. - * - **1111** — NIP-22 comments. We pull two slices: comments scoped to - * countries (`#K = iso3166`) and comments scoped to communities - * (`#K = 34550`). Together these are "posts from the world" + "voices - * inside the communities". - * - **36639** — Agora pledges (challenges / civic calls). Always - * included because they're the most action-oriented funding signal. - * - * We deliberately *exclude* free-form kind 1 notes here — the Discover - * page is the place to see content that's tagged to a real-world thread - * (country, community, campaign), not the global text-note firehose. The - * old plain feed still lives at `/feed`. - */ - -/** Tag scopes we accept on kind 1111 comments. */ -const COMMENT_K_SCOPES = ['iso3166', 'geo', '34550']; - -/** Aliases we accept on kind 36639 pledge `t` tags. */ -const ACTION_T_ALIASES = ['agora-action', 'pathos-challenge', 'agora-challenge']; - -/** - * Apply Discover-specific filtering after relay fetch. Drops events that - * `shouldHideFeedEvent` flags (mutes, content filters happen later) and - * any 1111 comment that lacks a recognised scope tag, since relays may - * over-return when we union filters. - */ -function filterDiscoverEvents(events: NostrEvent[]): NostrEvent[] { - return events - .filter((event) => { - if (shouldHideFeedEvent(event)) return false; - if (event.kind === 1111) { - const kTags = event.tags - .filter(([n]) => n === 'k' || n === 'K') - .map(([, v]) => v); - return kTags.some((v) => COMMENT_K_SCOPES.includes(v)); - } - return true; - }) - .sort((a, b) => b.created_at - a.created_at); -} - -/** - * Public infinite feed for the Discover page. Streams together new - * campaigns, world-tagged comments, community comments, and Agora - * pledges, paginated by `created_at` cursor. - * - * Each page issues exactly one relay request (the union of all relevant - * filters) to stay inside per-page rate budgets — the same pattern - * `useWorldFeed` uses. - * - * Returns the standard `useInfiniteQuery` surface plus a flattened - * `events` list for convenient consumption. - */ -export function useDiscoverFeed(enabled = true) { - const { nostr } = useNostr(); - - const query = useInfiniteQuery({ - queryKey: ['discover-feed'], - queryFn: async ({ pageParam, signal: querySignal }) => { - const signal = AbortSignal.any([querySignal, AbortSignal.timeout(8_000)]); - const until = pageParam as number | undefined; - - const filters: NostrFilter[] = [ - // New / revised campaigns — addressable, so we lean on a small - // limit and let the relay's natural newest-first ordering surface - // recent edits. No `#k` scoping needed. - { - kinds: [CAMPAIGN_KIND], - limit: Math.floor(DISCOVER_PAGE_SIZE / 3), - ...(until && { until }), - }, - // Community + country-scoped comments. - { - kinds: [1111], - '#K': COMMENT_K_SCOPES, - limit: DISCOVER_PAGE_SIZE, - ...(until && { until }), - }, - // Agora pledges. - { - kinds: [36639], - '#t': ACTION_T_ALIASES, - limit: Math.floor(DISCOVER_PAGE_SIZE / 3), - ...(until && { until }), - }, - ]; - - const raw = await nostr.query(filters, { signal }); - const filtered = filterDiscoverEvents(raw); - const page = filtered.slice(0, DISCOVER_PAGE_SIZE); - - const oldestTimestamp = page.length > 0 - ? page[page.length - 1].created_at - : null; - - return { - events: page, - oldestTimestamp, - totalFetched: filtered.length, - }; - }, - initialPageParam: undefined as number | undefined, - getNextPageParam: (lastPage) => { - if (lastPage.totalFetched < DISCOVER_PAGE_SIZE || !lastPage.oldestTimestamp) { - return undefined; - } - return lastPage.oldestTimestamp - 1; - }, - enabled, - staleTime: 30_000, - placeholderData: (prev) => prev, - }); - - // Flatten + dedupe. Each addressable event may legitimately appear - // across pages if a newer revision lands; we keep the newest version. - const seen = new Set(); - const events: NostrEvent[] = []; - for (const page of query.data?.pages ?? []) { - for (const event of page.events) { - if (seen.has(event.id)) continue; - seen.add(event.id); - events.push(event); - } - } - - return { - events, - isLoading: query.isPending, - isFetchingNextPage: query.isFetchingNextPage, - hasNextPage: query.hasNextPage, - fetchNextPage: query.fetchNextPage, - pageCount: query.data?.pages.length, - }; -} diff --git a/src/pages/DiscoverPage.tsx b/src/pages/DiscoverPage.tsx deleted file mode 100644 index dfd8e6e8..00000000 --- a/src/pages/DiscoverPage.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { useSeoMeta } from '@unhead/react'; - -import { Feed } from '@/components/Feed'; -import { useLayoutOptions } from '@/contexts/LayoutContext'; -import { useAppContext } from '@/hooks/useAppContext'; -import { useCurrentUser } from '@/hooks/useCurrentUser'; - -/** `/discover` — the Agora activity feed. */ -export function DiscoverPage() { - const { config } = useAppContext(); - const { user } = useCurrentUser(); - - useLayoutOptions({ showFAB: true, fabKind: 1, hasSubHeader: !!user }); - - useSeoMeta({ - title: `Discover | ${config.appName}`, - description: 'Campaigns, pledges, donations, and conversations happening on Agora.', - }); - - return ; -} - -export default DiscoverPage; From 51d50e3b339242a082dd606c53b6f460c54a4b82 Mon Sep 17 00:00:00 2001 From: Chad Curtis Date: Thu, 21 May 2026 17:40:10 -0500 Subject: [PATCH 29/60] Remove "React" label from reaction button --- src/components/ReactionButton.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/ReactionButton.tsx b/src/components/ReactionButton.tsx index b8e490de..a0c12427 100644 --- a/src/components/ReactionButton.tsx +++ b/src/components/ReactionButton.tsx @@ -214,8 +214,6 @@ export function ReactionButton({ {formatNumber(reactionCount)} - ) : variant === 'chip' ? ( - React ) : null} From d66eaf6aa456f241f1517fd3a58720d1d9ca7a9e Mon Sep 17 00:00:00 2001 From: Chad Curtis Date: Thu, 21 May 2026 18:15:47 -0500 Subject: [PATCH 30/60] Redefine Campaign kind: 30223 -> 33863 with wallet endpoint Replaces the kind 30223 Campaign spec with a clean kind 33863 design. Hard cutoff: no migration, no dual-read, no legacy support. Key changes: - Kind number 33863 (FUND on T9 keypad). - Single `w` tag carries one bech32(m) wallet endpoint. Prefix selects the mode: `bc1q`/`bc1p` for public on-chain, `sp1` for silent payments (BIP-352). Other prefixes are rejected. - Recipient `p` tags removed. Campaigns are self-authored; the event author is the beneficiary and owns the wallet in `w`. - No split weights, no dust calculations, no BIP-340/341 Taproot derivation from Nostr pubkeys. - `t` topic tags and `agora.category` labels removed. Discovery is via search, country (`#i`), and moderator curation. - `image` -> `banner`, with required NIP-92 `imeta` for dim, blurhash, MIME, and SHA-256. - `goal` is a single integer USD value, no unit, no currency code. - `status` tag removed. To close a campaign, publish a NIP-09 kind 5 deletion referencing the campaign's `a` coordinate. - `location` legacy tag removed. - Kind 8333 receipts for campaigns carry no `p` tags; verification matches tx outputs against the campaign's `w` address. SP campaigns publish no receipts and hide all aggregate UI by design. --- NIP.md | 223 ++++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 142 insertions(+), 81 deletions(-) diff --git a/NIP.md b/NIP.md index e2f34faf..5b5ba9cc 100644 --- a/NIP.md +++ b/NIP.md @@ -12,7 +12,7 @@ | Kind | Name | Description | |-------|----------------------------|----------------------------------------------------------------| -| 30223 | Campaign | Fundraising campaign with a list of on-chain Bitcoin recipients | +| 33863 | Campaign | Self-authored fundraising campaign with a single Bitcoin wallet endpoint (`bc1...` or `sp1...`) | | 30385 | Community Stats Snapshot | Pre-computed per-country / global community leaderboards | | 36639 | Pledge | Donor pledge for concrete submissions, stored as sats | @@ -22,7 +22,7 @@ |--------------------------|-----------------------------------------|-----------------------------------------------------------------| | Flat Communities | 34550, 30009, 8, 1111, 1984 | One-level badge membership with explicit moderators (NIP-72 ext) | | Community Chat | 34550, 1311 | Realtime member chat scoped to a NIP-72 community | -| Campaign Moderation | 30223, 1985, 39089 | Homepage curation (approved / hidden / featured axes) via moderator-signed labels in the `agora.moderation` namespace, gated by a follow-pack moderator roster | +| Campaign Moderation | 33863, 1985, 39089 | Homepage curation (approved / hidden / featured axes) via moderator-signed labels in the `agora.moderation` namespace, gated by a follow-pack moderator roster | ### Community Chat @@ -89,26 +89,45 @@ Single-recipient zap (the common case — tipping a post or profile): } ``` -Multi-recipient zap (one transaction paying multiple recipients — campaign donations, community splits): +Multi-recipient zap (one transaction paying multiple recipients — community splits): ```json { "kind": 8333, "pubkey": "", - "content": "Great campaign!", + "content": "Great community!", "tags": [ ["i", "bitcoin:tx:"], ["p", ""], ["p", ""], ["p", ""], ["amount", ""], - ["a", "30223::"], - ["K", "30223"], + ["a", "34550::"], + ["K", "34550"], ["alt", "Donation: 75000 sats across 3 recipients"] ] } ``` +Campaign donation (one transaction paying a single campaign wallet — see Kind 33863 below): + +```json +{ + "kind": 8333, + "pubkey": "", + "content": "Keep up the good work.", + "tags": [ + ["i", "bitcoin:tx:"], + ["amount", ""], + ["a", "33863::"], + ["K", "33863"], + ["alt", "Donation to Save the Last Bookstore: 25000 sats"] + ] +} +``` + +Campaign donation receipts MUST NOT include `p` tags — campaigns no longer have Nostr-identity recipients, only a `w` wallet endpoint. Verification matches tx outputs against the campaign's declared `w` address rather than derived Taproot addresses (see *Verification* and Kind 33863 below). + ### Content The `content` field is a human-readable comment from the sender (may be empty). It is NOT a zap request JSON (unlike NIP-57 kind 9735). @@ -165,13 +184,25 @@ For addressable events, use `"#a": ["::"]` instead. For pro **Verification (REQUIRED before trusting amounts):** -Clients MUST verify a kind 8333 event on-chain before counting it toward a zap total or displaying its amount. The `amount` tag is self-reported by the sender and would otherwise be trivially spoofable. To verify: +Clients MUST verify a kind 8333 event on-chain before counting it toward a zap total or displaying its amount. The `amount` tag is self-reported by the sender and would otherwise be trivially spoofable. Verification has two modes depending on the event shape: + +*Identity-recipient mode* (the event has `p` tags — profile zaps, event zaps, community splits): 1. Extract the txid from the `i` tag. 2. Fetch the transaction from a Bitcoin data source (e.g. a mempool.space-compatible Esplora API). 3. For each `p` tag, derive the recipient's expected Taproot address. 4. Sum the values of all outputs in the transaction that pay any of the derived recipient addresses. This is the **verified amount**. Change outputs paying back to the **sender's** derived Taproot address MUST NOT be counted toward the verified amount — only outputs to listed recipients. -5. If the verified amount is 0 (no listed recipient received anything in the tx), the event SHOULD be discarded. + +*Campaign-wallet mode* (the event has an `a` tag pointing at a kind 33863 campaign and no `p` tags): + +1. Extract the txid from the `i` tag and the campaign coordinate from the `a` tag. +2. Fetch the campaign event and read its `w` tag to get the campaign's declared bech32(m) wallet address. Reject the receipt if `w` is missing, malformed, or starts with `sp1` (silent-payment campaigns do not publish receipts; see Kind 33863). +3. Fetch the transaction from a Bitcoin data source. +4. Sum the values of all outputs in the transaction that pay the campaign's `w` address. This is the **verified amount**. + +In both modes: + +5. If the verified amount is 0, the event SHOULD be discarded. 6. If the sender's `amount` tag exceeds the verified amount, clients MAY discard the event or MAY display the smaller verified amount (capping). Clients MUST NOT display or count the claimed amount when it exceeds the verified amount. 7. Unconfirmed transactions MAY be displayed as pending; clients MAY require confirmation before counting them toward public totals. Because unconfirmed transactions can be evicted (RBF, double-spend), clients SHOULD either exclude them from aggregate zap totals or clearly label them as pending. @@ -199,132 +230,161 @@ The two zap kinds are complementary. Clients SHOULD sum verified amounts from bo --- -## Kind 30223: Campaign +## Kind 33863: Campaign ### Summary -Addressable event representing a **fundraising campaign**. A campaign carries the marketing-style metadata you would expect on GoFundMe, Kickstarter, or GiveSendGo (title, summary, cover image, story, category, goal, optional deadline, and recommended country), and — most importantly — a list of recipient pubkeys (`p` tags) that share the proceeds of any donation. +Addressable event representing a **self-authored fundraising campaign**. A campaign carries marketing-style metadata (title, summary, banner image, markdown story, optional goal, optional deadline, optional country) and exactly one Bitcoin wallet endpoint declared in a `w` tag. The wallet endpoint is either a public on-chain bech32(m) address (`bc1q…`, `bc1p…`) or a silent-payment code (`sp1…`, per BIP-352). The mode is inferred from the prefix — the client renders the corresponding QR code and adjusts the donation-progress UI accordingly. -Donations are sent as a **single Bitcoin on-chain transaction** with one output per recipient. The donor's wallet derives each recipient's Taproot address from their pubkey via BIP-340/BIP-341 (the same scheme used by kind 8333 onchain zaps), so the campaign event itself does not need to carry Bitcoin addresses. After broadcasting the funding tx, the donor's client publishes one kind 8333 event referencing the `txid`, listing every campaign recipient under its own `p` tag, and tagging the campaign via `a` / `K`. The donation then shows up in the campaign's totals and in each recipient's profile zap history (the `#p` filter matches every listed recipient). +The author of the event is also the beneficiary. Campaigns are never authored on behalf of someone else; the event creator owns the wallet declared in `w` and receives the donations. To stop accepting donations, the creator publishes a NIP-09 kind 5 deletion request referencing the campaign's `a` coordinate. -The kind is addressable so the creator can edit the story, image, goal, deadline, and recipient list over the life of the campaign without minting new identifiers. The `d` tag is the campaign's slug. +The kind is addressable so the creator can edit the story, banner, goal, deadline, and wallet over the life of the campaign without minting new identifiers. The `d` tag is the campaign's slug. ### Event Structure ```json { - "kind": 30223, + "kind": 33863, "pubkey": "", "content": "", "tags": [ - ["d", "save-the-bookstore"], + ["d", "save-the-last-bookstore"], + ["title", "Save the Last Bookstore"], ["summary", "Help our 40-year-old neighborhood bookstore make rent through winter."], - ["image", "https://example.com/cover.jpg"], - ["t", "human-rights"], - ["t", "legal-defense"], - ["goal", "10000000"], + ["banner", "https://blossom.example/abc123.jpg"], + ["imeta", + "url https://blossom.example/abc123.jpg", + "m image/jpeg", + "x abc123def456...", + "dim 1600x900", + "blurhash LKO2?U%2Tw=w]~RBVZRi};RPxuwH", + "alt Storefront of the Last Bookstore at dusk" + ], + ["alt", "Fundraising campaign: Save the Last Bookstore"], + + ["w", "bc1p7w2k3xq9...xyz"], + + ["goal", "25000"], ["deadline", "1735689600"], - ["i", "iso3166:VE"], - ["k", "iso3166"], - ["p", "", "wss://relay.example", "2"], - ["p", "", "wss://relay.example", "1"], - ["p", ""], - ["alt", "Fundraising campaign: Save the Last Bookstore"] + + ["i", "iso3166-1:US"], + ["k", "iso3166-1"] ] } ``` +A silent-payment campaign is identical except the `w` tag carries an `sp1…` code: + +```json +["w", "sp1qq...verylongsilentpaymentcode..."] +``` + ### Content The `content` field is the **campaign story**, formatted as Markdown. Clients SHOULD render it with the same Markdown renderer they use for NIP-23 long-form content. Empty content is permitted (e.g. for a campaign that lives entirely in its summary). ### Tags -| Tag | Required | Description | -|------------|----------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `d` | Yes | Campaign slug, unique per author. Forms the addressable coordinate `30223::`. | -| `title` | Yes | Display title of the campaign (plain text, max ~200 chars). | -| `summary` | Recommended | Short one-paragraph tagline shown in feed cards and previews. | -| `image` | Recommended | HTTPS URL of the cover image (jpg/png/webp). Clients MUST sanitize and verify the URL before rendering. | -| `t` | Recommended | Topic tag for discovery and filtering (e.g. `human-rights`, `legal-defense`, `independent-media`). Multiple `t` tags MAY be used. Clients SHOULD normalize user-entered tag labels by removing a leading `#`, lowercasing, and replacing whitespace with hyphens. | -| `goal` | Recommended | Fundraising goal in **satoshis** (decimal integer). Omit if the campaign has no fixed goal. | -| `deadline` | Optional | Unix timestamp (seconds) at which the campaign closes. After the deadline, clients SHOULD show the campaign as ended but MAY still accept donations. | -| `i` | Recommended | NIP-73 country identifier for sorting and discovery. SHOULD be `iso3166:` with an uppercase ISO 3166-1 alpha-2 country code (e.g. `iso3166:VE`). | -| `k` | Recommended if `i` is present | NIP-73 external content kind. For country identifiers this SHOULD be `iso3166`. | -| `location` | Legacy | Human-readable location string used by older campaign events. New events SHOULD prefer `i` + `k` country tags. Clients MAY display this as a fallback only. | -| `status` | Optional | Lifecycle status. The only defined value is `archived`, which marks the campaign closed without deleting it. Other values SHOULD be ignored. See *Closing & archiving* below. | -| `p` | Yes (≥1) | Recipient pubkey. The 2nd element is the hex pubkey; the 3rd (optional) is a relay hint; the 4th (optional) is a positive decimal **weight** for split allocation. | -| `alt` | Recommended | NIP-31 human-readable fallback. | +| Tag | Required | Description | +|-----------|----------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `d` | Yes | Campaign slug, unique per author. Forms the addressable coordinate `33863::`. | +| `title` | Yes | Display title of the campaign (plain text, max ~200 chars). | +| `w` | Yes | Bitcoin wallet endpoint. The 2nd element is a single bech32(m) string: a mainnet on-chain address starting with `bc1q` (P2WPKH/P2WSH) or `bc1p` (P2TR), **or** a silent-payment code starting with `sp1` per BIP-352. Exactly one `w` tag per campaign. | +| `summary` | Recommended | Short one-paragraph tagline shown in feed cards and previews. | +| `banner` | Recommended | HTTPS URL of the wide banner image. Clients MUST sanitize the URL (see `sanitizeUrl()` in `nostr-security`) before rendering, and SHOULD pair the URL with a NIP-92 `imeta` tag for dimensions, blurhash, MIME type, and SHA-256. | +| `imeta` | Recommended | NIP-92 media metadata for the banner. The first `url ` pair MUST match the `banner` URL; clients SHOULD ignore an `imeta` whose URL does not match. | +| `goal` | Optional | Fundraising goal in **integer US Dollars** (no unit suffix, no decimals). Clients MAY display an estimated sat-equivalent at view time using a live exchange rate. | +| `deadline`| Optional | Unix timestamp (seconds) at which the campaign closes for new donations. After the deadline, clients SHOULD show the campaign as ended but MAY still accept donations. | +| `i` | Recommended | NIP-73 country identifier. SHOULD be `iso3166-1:` with an uppercase ISO 3166-1 alpha-2 country code (e.g. `iso3166-1:VE`). | +| `k` | Recommended if `i` is present | NIP-73 external content kind. For country identifiers this SHOULD be `iso3166-1`. | +| `alt` | Recommended | NIP-31 human-readable fallback. | -### Recipient Split Rules +### Wallet Modes -When a donor sends an amount `T` in satoshis to a campaign: +The prefix of the `w` value selects one of two donation modes. Clients MUST detect the mode from the prefix; the event carries no other mode discriminator. -1. Read all `p` tags from the campaign event. -2. Parse the weight of each `p` tag from the 4th element. If absent, malformed, or non-positive, the weight defaults to **1**. -3. Compute each recipient's share as `floor(T * weight_i / sum_of_weights)` satoshis. -4. Any remainder from rounding (at most N−1 sats) MAY be appended to the largest share or kept by the donor as change — clients SHOULD prefer appending the remainder to the largest share so the full amount reaches the campaign. -5. If any computed share is below the Bitcoin dust limit (546 sats for P2TR), the donor's client MUST refuse the donation and surface a minimum-amount error. +| Prefix | Mode | Description | +|---------------------|-----------|------------------------------------------------------------------------------------------------------------------------------------------| +| `bc1q…` / `bc1p…` | On-chain | Public mainnet bech32(m) address. Donations are traceable; clients show a progress bar, total raised, and donation list. | +| `sp1…` | Silent payment | BIP-352 silent-payment code. Donations are **unlinkable by design**. Clients MUST hide all aggregate totals and progress UI (see below). | -Equal splits are the default: omit the weight on every `p` tag, and all recipients receive `floor(T / N)` sats each. +Other prefixes (`tb1…`, `bcrt1…`, `tsp1…`, lightning invoices, etc.) MUST be rejected at parse time; the campaign does not render. -### Donation Flow +Clients SHOULD validate the bech32(m) checksum of the `w` value, not just its prefix. -1. Donor opens the campaign and chooses an amount in sats (preset or custom). -2. Donor's client computes per-recipient amounts using the split rules above. -3. Donor's client builds a **single PSBT** with one output per recipient (paying each recipient's derived Taproot address) plus a change output back to the donor. -4. Donor signs the PSBT with their Nostr key (Taproot key-path spend) and broadcasts the resulting transaction. -5. Donor's client publishes **one kind 8333 event for the whole transaction**, listing every recipient under its own `p` tag. The event MUST include: +### Client Behavior by Mode + +| UI element | On-chain (`bc1`) | Silent payment (`sp1`) | +|-----------------------------|-----------------------------------------------------------------|-------------------------------------------------------| +| QR code | bech32(m) address QR (or BIP-21 `bitcoin:` URI) | SP code QR (BIP-352 / BIP-21 SP extension) | +| "Raised X" / progress bar | Shown, computed from verified kind 8333 receipts | **Hidden.** Replaced with a "Private campaign — totals are not public" notice. | +| Donor / recent-donation list| Shown | **Hidden.** | +| Goal display | Shown as USD target with optional sat-equivalent estimate | Shown as USD target; no progress computation | +| Donation receipt published | Donor's client publishes a kind 8333 receipt (see below) | **No receipt published.** Publishing one would defeat SP unlinkability and is forbidden. | + +For silent-payment campaigns, clients MUST NOT attempt to scan the chain, MUST NOT publish receipts, and MUST NOT display any aggregate that could leak donation activity. The only signal the public sees is the campaign event itself. + +### Donation Flow — On-chain (`bc1`) + +1. Donor opens the campaign and chooses an amount. +2. Donor's client constructs and broadcasts a Bitcoin transaction paying the campaign's `w` address. +3. After broadcast, the donor's client publishes a single kind 8333 receipt: ```json [ ["i", "bitcoin:tx:"], - ["p", ""], - ["p", ""], - ["p", ""], - ["amount", ""], - ["a", "30223::"], - ["K", "30223"], + ["amount", ""], + ["a", "33863::"], + ["K", "33863"], ["alt", "Donation to : sats"] ] ``` - The `amount` tag is the sum of the outputs paying the listed recipients (i.e. the full donation, excluding the donor's change). Per-recipient amounts are not encoded in the event; clients that need them recompute them from the on-chain transaction by matching each recipient's derived Taproot address against the tx outputs. + The receipt MUST NOT carry `p` tags — campaigns are not Nostr-identity recipients. The `amount` tag is the sum of tx outputs paying the campaign's `w` address (excluding the donor's change output). -This mirrors the community batch-zap pattern documented in the kind 8333 section above, with the campaign's addressable coordinate replacing the community coordinate. +4. The receipt is published **after** the tx is broadcast; the txid is already final at that point. A receipt-publish failure does not roll back the donation — the on-chain transaction stands. + +### Donation Flow — Silent Payment (`sp1`) + +1. Donor opens the campaign and chooses an amount. +2. Donor's client uses the campaign's SP code to derive a fresh, one-time Taproot output script per BIP-352. +3. Donor broadcasts a Bitcoin transaction paying that derived output. +4. **No Nostr event is published.** The campaign owner discovers the donation by scanning the chain locally with their SP private key. + +Silent-payment unlinkability is the entire point of this mode. Clients MUST NOT publish receipts, MUST NOT advertise the donation in any other Nostr event (replies, mentions, etc.) on the donor's behalf, and MUST NOT correlate the donor's pubkey with the campaign in any persisted client telemetry. ### Querying **List campaigns (newest first):** ```json -{ "kinds": [30223], "limit": 50 } -``` - -**Filter by category:** - -```json -{ "kinds": [30223], "#t": ["medical"], "limit": 50 } +{ "kinds": [33863], "limit": 50 } ``` **Fetch a specific campaign:** ```json -{ "kinds": [30223], "authors": [""], "#d": [""], "limit": 1 } +{ "kinds": [33863], "authors": [""], "#d": [""], "limit": 1 } ``` -**Aggregate donations for a campaign:** +**Aggregate donations for an on-chain campaign:** ```json -{ "kinds": [8333], "#a": ["30223::"], "limit": 500 } +{ "kinds": [8333], "#a": ["33863::"], "limit": 500 } ``` -Clients MUST verify each kind 8333 event on-chain before counting it toward the campaign total, per the verification rules in the kind 8333 section. +Clients MUST verify each kind 8333 event on-chain before counting it toward the campaign total, per the *Campaign-wallet mode* verification rules in the kind 8333 section. + +**Filter by country:** + +```json +{ "kinds": [33863], "#i": ["iso3166-1:VE"], "limit": 50 } +``` **Fetch pinned event comments:** -Event owners MAY pin important comments or activity feed events with a NIP-78 app-specific data event (`kind: 30078`) authored by the root event owner. The `d` tag is scoped to the root event coordinate. Agora uses this for campaigns (`30223`), pledges (`36639`), organizations (`34550`), and calendar events (`31922` / `31923`). +Event owners MAY pin important comments or activity feed events with a NIP-78 app-specific data event (`kind: 30078`) authored by the root event owner. The `d` tag is scoped to the root event coordinate. Agora uses this for campaigns (`33863`), pledges (`36639`), organizations (`34550`), and calendar events (`31922` / `31923`). ```json { @@ -350,14 +410,15 @@ The `pinnedEvents` array is ordered newest pin first. Pinning an already-pinned ### Client Behavior -- **Recipient validity:** clients SHOULD reject `p` tag entries whose pubkey is not 64 hex characters and SHOULD ignore weights that are not positive finite decimals. -- **Dust protection:** when a donor enters an amount that would assign any recipient less than the dust limit, the client MUST block the donation and either suggest the minimum viable total or prompt the donor to remove recipients. -- **Editability:** the creator MAY republish the same `(kind, pubkey, d)` triple to update the campaign. Clients SHOULD keep `published_at` from the first publish on subsequent edits (NIP-23 convention). -- **Closing & archiving:** the creator MAY soft-close a campaign by republishing it with a `["status", "archived"]` tag. Clients SHOULD hide archived campaigns from discovery feeds and disable the donate flow, but MUST keep them reachable by direct link so existing donors can still find them and donation history is preserved. The creator can reopen the campaign by republishing without the status tag (or with any other status value). For a hard delete, the creator MAY publish a NIP-09 kind 5 deletion request referencing the campaign's `a` coordinate; clients SHOULD continue to render past donations against the campaign even after deletion. +- **Wallet validity:** clients MUST reject events whose `w` tag is missing, present more than once, or whose value does not pass bech32(m) checksum validation for one of the supported prefixes. Invalid campaigns do not render. +- **Editability:** the creator MAY republish the same `(33863, pubkey, d)` triple to update any field, including the `w` wallet endpoint. Clients SHOULD keep `published_at` from the first publish on subsequent edits (NIP-23 convention). +- **Closing a campaign:** there is no `status` tag. To stop accepting donations, the creator publishes a NIP-09 kind 5 deletion request referencing the campaign's `a` coordinate. Clients SHOULD honor the deletion by removing the campaign from discovery feeds. Historical kind 8333 receipts MAY still be rendered against the (now-deleted) campaign coordinate so donors can find their past donations. +- **No category, no topics:** kind 33863 events MUST NOT carry `t` tags or NIP-32 category labels in any `agora.*` namespace. Campaigns are individual stories; discovery happens via search (NIP-50 against title/summary/content), country (`#i`), and moderator curation (below). +- **Migration:** kind 33863 has no relationship to any earlier campaign kind. Clients MUST NOT read, merge, or migrate events of any other kind into the kind 33863 namespace. ### Campaign Moderation Labels -Agora curates which kind 30223 campaigns appear on the homepage (`/`) and on the Support directory (`/campaigns/all`) via moderator-signed NIP-32 label events (kind 1985) in a dedicated label namespace. The campaign event itself is never modified — surfacing is purely a client-side rollup of label events. +Agora curates which kind 33863 campaigns appear on the homepage (`/`) and on the Support directory (`/campaigns/all`) via moderator-signed NIP-32 label events (kind 1985) in a dedicated label namespace. The campaign event itself is never modified — surfacing is purely a client-side rollup of label events. #### Namespace @@ -397,7 +458,7 @@ Surfacing rules (hide always wins): "tags": [ ["L", "agora.moderation"], ["l", "approved", "agora.moderation"], - ["a", "30223::"], + ["a", "33863::"], ["alt", "Campaign moderation: approved"] ] } @@ -409,7 +470,7 @@ Required tags: - `L` set to `agora.moderation`. - `l` with the label value as the 2nd element and `agora.moderation` as the 3rd. -- `a` referencing the campaign coordinate `30223::`. +- `a` referencing the campaign coordinate `33863::`. - `alt` (NIP-31) — clients without label support will display this string. #### Trust Model @@ -422,7 +483,7 @@ pubkey: 932614571afcbad4d17a191ee281e39eebbb41b93fac8fd87829622aeb112f4d d-tag: k4p5w0n22suf ``` -The pack `p` tags are the authoritative moderator list. Anyone may publish a kind 1985 event in the `agora.moderation` namespace, but events from non-pack authors are silently ignored at the relay-filter layer (`authors:` is pinned to the pack `p` tags). This means: +The pack `p` tags are the authoritative moderator list. Clients MUST pin `authors:` on their label REQ to the pack `p` tags; events from non-pack authors MUST be ignored. This means: - Self-approval is impossible unless the pack author has added you. - A moderator removed from the pack immediately loses moderation authority — campaigns kept alive only by their labels return to "pending" until another moderator approves them. From 935c121bab99cfbc16e705330a465bf5b100e0ee Mon Sep 17 00:00:00 2001 From: Chad Curtis Date: Thu, 21 May 2026 18:48:28 -0500 Subject: [PATCH 31/60] Implement Campaign kind 33863 Replaces every kind 30223 surface with kind 33863 -- the self-authored fundraising campaign with a single `w` Bitcoin wallet endpoint. Hard cutoff: no migration, no dual-read, no legacy support. Schema (src/lib/campaign.ts): - `CAMPAIGN_KIND` constant bumped 30223 -> 33863. - New `CampaignWallet` type with `onchain` (`bc1q`/`bc1p`) and `sp` (`sp1`) modes, prefix-disambiguated. Bitcoin-mainnet only; testnet/regtest/lightning prefixes are rejected at parse time. - New `parseCampaignWallet()` validates bech32 via bitcoinjs-lib for on-chain addresses and shape-checks silent-payment codes. - `ParsedCampaign` drops `recipients`, `category`, `tags`, `location`, `archived`, `image` (-> `banner`), `goalSats` (-> `goalUsd`). Adds `wallet` and `bannerImeta` (parsed NIP-92). - `CAMPAIGN_CATEGORIES`, `CampaignCategory`, `LEGACY_CAMPAIGN_CATEGORY_ALIASES`, `getCampaignPrimaryTagLabel`, `splitDonation`, `minDonationForSplit`, `DonationSplit`, `CampaignRecipient` removed. Publishing (useDonateCampaign): - Single-output PSBT paying `campaign.wallet.value`. - Kind 8333 receipt has NO `p` tags -- campaigns are not Nostr-identity recipients. `i`, `amount`, `a`, `K`, `alt` only. - SP campaigns are refused with a clear error directing donors to external BIP-352-capable wallets via the on-page QR/copy panel. Verification (useOnchainZaps.verifyOnchainZap): - Two modes: identity-recipient (existing `p`-tag derivation) and campaign-wallet (match outputs against `campaign.w`). The branch is selected by whether the receipt has an `a` tag pointing at a kind 33863 campaign. SP-targeted receipts are rejected. Querying (useCampaigns, useAllCampaigns, useCampaignDonations): - Drop `category`, `recipientPubkeys`, `includeArchived` options. - `useCampaignDonations` now takes a `ParsedCampaign` and verifies every receipt on-chain against the campaign's `w` address before counting it. SP campaigns short-circuit to zeros. - Search drops `location` and `t`-tag branches; title/summary/story only. UI: - `CampaignCard`, `HeroCampaignSpotlight`, `CampaignsPage`: drop recipient counts, archived/category badges, `location`; use `banner`. Silent-payment campaigns render a "Private -- totals not public" notice instead of progress. - `CampaignDetailPage`: archive flow replaced with NIP-09 kind 5 deletion. Drops the multi-beneficiary recipient column. Donate column shows the in-app PSBT donate button (on-chain) plus the always- available external-wallet QR/copy panel. SP campaigns show the panel only -- no in-app donate. - `CreateCampaignPage`: drops Beneficiaries section, tag input, and USD-to-sats conversion. Adds a Wallet field with mode-aware validation hint. Goal is integer USD. Banner upload captures NIP-94 tags and converts to NIP-92 `imeta` at publish. - `DonateDialog`: collapses ~1200 LOC of split logic into a single- output flow. Form -> Confirm -> Success. Logged-out and signer- unsupported users are pointed at the external-wallet panel. - New `CampaignWalletDonatePanel` component (replaces the pubkey-derived `BeneficiaryDonateDialog`). QR + copy + open-in-wallet for any `bc1`/`sp1` endpoint, with mode-appropriate privacy notice. Removed: - `useArchiveCampaign` hook (closure via NIP-09 deletion only). - `ClaimPage` and its `/claim` route (claim-for-someone-else flow no longer applies -- campaigns are self-authored). - `BeneficiaryDonateDialog.tsx` (replaced by `CampaignWalletDonatePanel.tsx`). - Community-donate synthesis hack in `CommunityDetailPage` (no more fabricating a `ParsedCampaign` from community moderators). NIP.md was updated separately to specify Kind 33863. --- src/AppRouter.tsx | 4 +- src/components/BeneficiaryDonateDialog.tsx | 212 ---- src/components/CampaignCard.tsx | 88 +- src/components/CampaignWalletDonatePanel.tsx | 151 +++ src/components/CommunityDetailPage.tsx | 56 +- src/components/CoverImageField.tsx | 25 +- src/components/DonateDialog.tsx | 1201 ++++++------------ src/components/HeroCampaignSpotlight.tsx | 33 +- src/hooks/useAllCampaigns.ts | 10 +- src/hooks/useArchiveCampaign.ts | 82 -- src/hooks/useCampaignDonations.ts | 158 ++- src/hooks/useCampaigns.ts | 53 +- src/hooks/useDonateCampaign.ts | 110 +- src/hooks/useOnchainZaps.ts | 157 ++- src/lib/campaign.ts | 385 +++--- src/lib/communityUtils.ts | 2 +- src/lib/formatCampaignAmount.ts | 20 + src/lib/kindLabels.ts | 3 +- src/lib/zapHelpers.ts | 2 +- src/pages/CampaignDetailPage.tsx | 390 ++---- src/pages/CampaignsPage.tsx | 18 +- src/pages/ClaimPage.tsx | 275 ---- src/pages/CommunitiesPage.tsx | 2 +- src/pages/CreateCampaignPage.tsx | 601 +++------ 24 files changed, 1387 insertions(+), 2651 deletions(-) delete mode 100644 src/components/BeneficiaryDonateDialog.tsx create mode 100644 src/components/CampaignWalletDonatePanel.tsx delete mode 100644 src/hooks/useArchiveCampaign.ts delete mode 100644 src/pages/ClaimPage.tsx diff --git a/src/AppRouter.tsx b/src/AppRouter.tsx index f8cdc8fd..03d851b5 100644 --- a/src/AppRouter.tsx +++ b/src/AppRouter.tsx @@ -25,7 +25,7 @@ const ReplyComposeModal = lazy(() => import("@/components/ReplyComposeModal").th const EmojiPackDialog = lazy(() => import("@/components/EmojiPackDialog").then(m => ({ default: m.EmojiPackDialog }))); // Campaigns: home + create. (Campaign detail is dispatched from NIP19Page -// when an naddr resolves to kind 30223.) The campaigns list IS the homepage; +// when an naddr resolves to kind 33863.) The campaigns list IS the homepage; // the configurable HomePage delegation from the Twitter-era app is gone. const CampaignsPage = lazy(() => import("./pages/CampaignsPage").then(m => ({ default: m.CampaignsPage }))); const CreateCampaignPage = lazy(() => import("./pages/CreateCampaignPage").then(m => ({ default: m.CreateCampaignPage }))); @@ -89,7 +89,6 @@ const WikipediaPage = lazy(() => import("./pages/WikipediaPage").then(m => ({ de const WorldPage = lazy(() => import("./pages/WorldPage").then(m => ({ default: m.WorldPage }))); const FollowPage = lazy(() => import("./pages/FollowPage").then(m => ({ default: m.FollowPage }))); const ReceivePage = lazy(() => import("./pages/ReceivePage").then(m => ({ default: m.ReceivePage }))); -const ClaimPage = lazy(() => import("./pages/ClaimPage").then(m => ({ default: m.ClaimPage }))); const RemoteLoginSuccessPage = lazy(() => import("./pages/RemoteLoginSuccessPage").then(m => ({ default: m.RemoteLoginSuccessPage }))); const pollsDef = getExtraKindDef("polls")!; @@ -163,7 +162,6 @@ export function AppRouter() { {/* Auto-follow deep link: fullscreen immersive (no sidebars/nav) */} } /> } /> - } /> {/* All routes share the persistent FundraiserLayout (top nav + footer) */} }> diff --git a/src/components/BeneficiaryDonateDialog.tsx b/src/components/BeneficiaryDonateDialog.tsx deleted file mode 100644 index e04e5272..00000000 --- a/src/components/BeneficiaryDonateDialog.tsx +++ /dev/null @@ -1,212 +0,0 @@ -import { useMemo, useState } from 'react'; -import { Link } from 'react-router-dom'; -import { AlertTriangle, Check, Copy, ExternalLink } from 'lucide-react'; - -import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; -import { BitcoinPublicDisclaimer } from '@/components/BitcoinPublicDisclaimer'; -import { Button } from '@/components/ui/button'; -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog'; -import { QRCodeCanvas } from '@/components/ui/qrcode'; -import { useAuthor } from '@/hooks/useAuthor'; -import { useProfileUrl } from '@/hooks/useProfileUrl'; -import { useToast } from '@/hooks/useToast'; -import { nostrPubkeyToBitcoinAddress } from '@/lib/bitcoin'; -import { genUserName } from '@/lib/genUserName'; -import { sanitizeUrl } from '@/lib/sanitizeUrl'; - -interface BeneficiaryDonatePanelProps { - /** Hex pubkey of the beneficiary. */ - pubkey: string; -} - -/** - * Inline panel rendering a beneficiary's Taproot address as a scannable - * BIP-21 QR code, a copyable string, and an "Open in wallet" button. - * - * Used both by `BeneficiaryDonateDialog` (modal context) and embedded - * directly into the campaign page when there's a single beneficiary. - * - * Always shows the beneficiary's profile preview (avatar + name) as a - * link to their Nostr profile — even when the surrounding page also - * identifies a campaign organizer, the beneficiary is a distinct party - * (the organizer may be running the campaign on someone else's behalf). - * - * Intentionally minimal: no amount input, no PSBT/in-app wallet flow — - * that's `DonateDialog`'s job. - */ -export function BeneficiaryDonatePanel({ - pubkey, -}: BeneficiaryDonatePanelProps) { - const { toast } = useToast(); - const [copied, setCopied] = useState(false); - - const author = useAuthor(pubkey); - const metadata = author.data?.metadata; - const displayName = - metadata?.display_name || metadata?.name || genUserName(pubkey); - const picture = sanitizeUrl(metadata?.picture); - const profileUrl = useProfileUrl(pubkey, metadata); - - const address = useMemo( - () => nostrPubkeyToBitcoinAddress(pubkey), - [pubkey], - ); - // BIP-21 URI: most wallets recognize the `bitcoin:` scheme when scanning. - // No amount field — donor picks one in their wallet. - const bip21 = address ? `bitcoin:${address}` : ''; - - const copyAddress = async () => { - if (!address) return; - try { - await navigator.clipboard.writeText(address); - setCopied(true); - setTimeout(() => setCopied(false), 1500); - toast({ title: 'Address copied' }); - } catch { - toast({ - title: 'Copy failed', - description: 'Select and copy the address manually.', - variant: 'destructive', - }); - } - }; - - if (!address) { - return ( -
- - We couldn't derive a Bitcoin address for this beneficiary. -
- ); - } - - return ( -
- {/* Beneficiary header — bigger avatar + a clear "Sending to" - eyebrow above the name so the reader knows exactly who's - receiving the donation. Avatar links to the profile. */} - - - {picture && } - - {displayName.slice(0, 2).toUpperCase()} - - -
-
- Supporting -
-
- {displayName} -
-
- - - {/* QR — large, centered on a clean white tile with the Agora - logo embedded in an orange circular badge in the center. - Error-correction level H tolerates the centered occlusion - (~30% of modules can be missing and the code still scans). - No nested panel around it — the QR is its own visual anchor. */} -
-
- -
-
- -
-
-
-
- - {/* Copyable address — single line, tap to copy. No wrapping - container; sits flush with the rest of the column. */} - - - {/* Privacy notice — informational only. Bitcoin is a public - ledger, so the donation can be traced back to the donor's - wallet. */} - - - {/* Open in wallet — relies on the `bitcoin:` URI handler. */} - -
- ); -} - -interface BeneficiaryDonateDialogProps { - /** Hex pubkey of the beneficiary. */ - pubkey: string; - open: boolean; - onOpenChange: (open: boolean) => void; -} - -/** - * Modal wrapper around `BeneficiaryDonatePanel` for places that still want - * the dialog UX (e.g. multi-beneficiary campaigns, where each row's - * "Donate" button opens this dialog). - */ -export function BeneficiaryDonateDialog({ - pubkey, - open, - onOpenChange, -}: BeneficiaryDonateDialogProps) { - const author = useAuthor(pubkey); - const metadata = author.data?.metadata; - const displayName = - metadata?.display_name || metadata?.name || genUserName(pubkey); - - return ( - - - - Donate to {displayName} - - Scan the QR code or copy the Bitcoin address below to donate. - - - - - - - ); -} diff --git a/src/components/CampaignCard.tsx b/src/components/CampaignCard.tsx index b43e6c66..ae6ece7f 100644 --- a/src/components/CampaignCard.tsx +++ b/src/components/CampaignCard.tsx @@ -1,7 +1,7 @@ import { useMemo } from 'react'; import type { ReactNode } from 'react'; import { Link } from 'react-router-dom'; -import { CalendarClock, HandHeart, MapPin, Target, Users, Archive } from 'lucide-react'; +import { CalendarClock, EyeOff, HandHeart, MapPin, ShieldCheck, Target } from 'lucide-react'; import { Badge } from '@/components/ui/badge'; import { Card } from '@/components/ui/card'; @@ -16,9 +16,8 @@ import { type ParsedCampaign, encodeCampaignNaddr, getCampaignCountryLabel, - getCampaignPrimaryTagLabel, } from '@/lib/campaign'; -import { formatCampaignAmount } from '@/lib/formatCampaignAmount'; +import { formatCampaignAmount, formatUsdGoal, satsToUsd } from '@/lib/formatCampaignAmount'; import { genUserName } from '@/lib/genUserName'; import { sanitizeUrl } from '@/lib/sanitizeUrl'; import { cn } from '@/lib/utils'; @@ -34,20 +33,32 @@ function formatDeadline(unixSeconds: number): { label: string; isPast: boolean } return { label: `${months} mo left`, isPast: false }; } -/** Short helper rendered both inline (cards) and in the detail page. */ +/** + * Short helper rendered both inline (cards) and in the detail page. + * + * Per NIP.md Kind 33863, the campaign **goal** is integer USD and the + * **raised** total is the sum of verified sats. We render both in the + * goal's unit (USD) for consistency, converting the sats total at view + * time using the live BTC price. While the price is loading the raised + * amount falls back to sats. + */ export function CampaignProgress({ raisedSats, - goalSats, + goalUsd, btcPrice, className, }: { raisedSats: number; - goalSats?: number; + goalUsd?: number; btcPrice?: number; className?: string; }) { - const hasGoal = !!goalSats && goalSats > 0; - const pct = hasGoal ? Math.min(100, Math.round((raisedSats / goalSats!) * 100)) : 0; + const hasGoal = !!goalUsd && goalUsd > 0; + const raisedUsd = satsToUsd(raisedSats, btcPrice); + const pct = hasGoal && raisedUsd !== undefined + ? Math.min(100, Math.round((raisedUsd / goalUsd!) * 100)) + : 0; + return (
{hasGoal && } @@ -57,13 +68,38 @@ export function CampaignProgress({ {!hasGoal && raised} {hasGoal && ( - of {formatCampaignAmount(goalSats!, btcPrice)} goal + of {formatUsdGoal(goalUsd!)} goal )}
); } +/** + * Replaces {@link CampaignProgress} for silent-payment campaigns, where + * on-chain totals are unobservable by design. Shows the goal as a target + * (if set) but no progress bar or raised amount. + */ +export function CampaignPrivateNotice({ + goalUsd, + className, +}: { + goalUsd?: number; + className?: string; +}) { + return ( +
+
+ + Private campaign — totals are not public +
+ {goalUsd && goalUsd > 0 && ( +
Target: {formatUsdGoal(goalUsd)}
+ )} +
+ ); +} + interface CampaignCardProps { campaign: ParsedCampaign; /** Visual variant: `compact` for grid items, `featured` for hero placement. */ @@ -79,12 +115,12 @@ interface CampaignCardProps { */ export function CampaignCard({ campaign, variant = 'compact', className, footerBadge }: CampaignCardProps) { const author = useAuthor(campaign.pubkey); - const { data: stats } = useCampaignDonations(campaign.aTag); + const { data: stats } = useCampaignDonations(campaign); const { data: btcPrice } = useBtcPrice(); const { data: moderation } = useCampaignModeration(); const naddr = useMemo(() => encodeCampaignNaddr(campaign), [campaign]); - const cover = sanitizeUrl(campaign.image); + const cover = sanitizeUrl(campaign.banner); const creatorName = author.data?.metadata?.display_name || author.data?.metadata?.name || @@ -92,7 +128,7 @@ export function CampaignCard({ campaign, variant = 'compact', className, footerB const deadline = campaign.deadline ? formatDeadline(campaign.deadline) : null; const raisedSats = stats?.totalSats ?? 0; const countryLabel = getCampaignCountryLabel(campaign); - const tagLabel = getCampaignPrimaryTagLabel(campaign); + const isSilentPayment = campaign.wallet.mode === 'sp'; const isFeaturedVariant = variant === 'featured'; const isApproved = moderation.approvedCoords.has(campaign.aTag); @@ -132,29 +168,22 @@ export function CampaignCard({ campaign, variant = 'compact', className, footerB
)} - {tagLabel && ( + {isSilentPayment && ( - {tagLabel} + + Private )}
- {campaign.archived && ( - - - Archived - - )} {isHidden && ( + Hidden )} @@ -193,16 +222,15 @@ export function CampaignCard({ campaign, variant = 'compact', className, footerB
- + {isSilentPayment ? ( + + ) : ( + + )} {/* Meta row */}
- - - {campaign.recipients.length}{' '} - {campaign.recipients.length === 1 ? 'recipient' : 'recipients'} - - {stats && stats.donorCount > 0 && ( + {!isSilentPayment && stats && stats.donorCount > 0 && ( {stats.donorCount} {stats.donorCount === 1 ? 'donor' : 'donors'} diff --git a/src/components/CampaignWalletDonatePanel.tsx b/src/components/CampaignWalletDonatePanel.tsx new file mode 100644 index 00000000..222a6775 --- /dev/null +++ b/src/components/CampaignWalletDonatePanel.tsx @@ -0,0 +1,151 @@ +import { useState } from 'react'; +import { AlertTriangle, Check, Copy, ExternalLink, ShieldCheck } from 'lucide-react'; + +import { BitcoinPublicDisclaimer } from '@/components/BitcoinPublicDisclaimer'; +import { Button } from '@/components/ui/button'; +import { QRCodeCanvas } from '@/components/ui/qrcode'; +import { useToast } from '@/hooks/useToast'; +import type { CampaignWallet } from '@/lib/campaign'; + +interface CampaignWalletDonatePanelProps { + /** Parsed wallet endpoint declared by the campaign's `w` tag. */ + wallet: CampaignWallet; + /** Optional campaign title used in toast/copy messages. */ + campaignTitle?: string; +} + +/** + * Inline panel rendering the campaign's wallet endpoint as a scannable + * QR code, a copyable string, and an "Open in wallet" button. + * + * Behavior forks on the wallet's mode: + * + * - **on-chain** (`bc1q…` / `bc1p…`) — BIP-21 QR with the address; a + * public-ledger disclaimer reminds donors that the donation is + * traceable. + * - **sp** (`sp1…`) — raw silent-payment code QR; an "unlinkable by + * design" notice replaces the traceability disclaimer. + * + * Intentionally minimal: no amount input, no PSBT/in-app wallet flow — + * that's `DonateDialog`'s job. This panel is the always-available + * "scan and pay from any wallet" affordance. + */ +export function CampaignWalletDonatePanel({ + wallet, + campaignTitle, +}: CampaignWalletDonatePanelProps) { + const { toast } = useToast(); + const [copied, setCopied] = useState(false); + + // Build the QR payload. For on-chain we use BIP-21 so any wallet that + // recognizes the `bitcoin:` scheme can pre-fill the address; for SP we + // use the BIP-21 `bitcoin:?sp=` extension. Donors pick the amount in + // their wallet either way. + const qrPayload = wallet.mode === 'onchain' + ? `bitcoin:${wallet.value}` + : `bitcoin:?sp=${wallet.value}`; + + const copyValue = async () => { + try { + await navigator.clipboard.writeText(wallet.value); + setCopied(true); + setTimeout(() => setCopied(false), 1500); + toast({ title: wallet.mode === 'sp' ? 'Silent-payment code copied' : 'Address copied' }); + } catch { + toast({ + title: 'Copy failed', + description: 'Select and copy the value manually.', + variant: 'destructive', + }); + } + }; + + return ( +
+ {/* QR — large, centered on a clean white tile with the Agora logo + embedded in an orange circular badge in the center. + Error-correction level H tolerates the centered occlusion + (~30% of modules can be missing and the code still scans). */} +
+
+ +
+
+ +
+
+
+
+ + {/* Copyable value — single line, tap to copy. No wrapping + container; sits flush with the rest of the column. */} + + + {wallet.mode === 'onchain' ? ( + + ) : ( +
+ + + Silent-payment campaigns are unlinkable by design. Your donation + cannot be tied to the campaign by anyone other than the + organizer{campaignTitle ? ` (${campaignTitle})` : ''}. + +
+ )} + + {/* Open in wallet — relies on the `bitcoin:` URI handler. SP codes + inside `bitcoin:?sp=` are still understood by BIP-352-aware + wallets. Older wallets that don't know about SP will ignore + the parameter and either refuse the link or show an error — at + which point the donor falls back to copy/paste anyway. */} + +
+ ); +} + +/** + * Fallback rendered when the wallet failed to parse. The detail page + * should normally never reach this — `parseCampaign` rejects events + * without a valid `w` tag — but a defensive surface is cheap and helps + * debugging. + */ +export function CampaignWalletMissing() { + return ( +
+ + This campaign is missing a valid wallet endpoint. +
+ ); +} diff --git a/src/components/CommunityDetailPage.tsx b/src/components/CommunityDetailPage.tsx index 3ede5332..a00ad7f1 100644 --- a/src/components/CommunityDetailPage.tsx +++ b/src/components/CommunityDetailPage.tsx @@ -34,7 +34,6 @@ import { Card } from '@/components/ui/card'; import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'; import { Skeleton } from '@/components/ui/skeleton'; -import { DonateDialog } from '@/components/DonateDialog'; import { NoteContent } from '@/components/NoteContent'; import { FollowToggleButton } from '@/components/FollowButton'; import { NoteMoreMenu } from '@/components/NoteMoreMenu'; @@ -464,11 +463,9 @@ function OfficialActivityShelves({ eventsLoading: boolean; now: number; }) { - // Drop archived campaigns; mixed activity is sorted newest publish first. - const liveCampaigns = useMemo( - () => campaigns.filter((c) => !c.archived), - [campaigns], - ); + // All loaded campaigns. Closure is via NIP-09 deletion (relay-level), + // so anything that reached us is current. + const liveCampaigns = campaigns; // Drop expired pledges; mixed activity is sorted newest publish first. const livePledges = useMemo(() => { @@ -614,11 +611,8 @@ export function CommunityDetailPage({ event }: { event: NostrEvent }) { const navigate = useNavigate(); const { toast } = useToast(); const { user } = useCurrentUser(); - const { btcPrice } = useBitcoinWallet(); - const [membersDialogOpen, setMembersDialogOpen] = useState(false); const [descriptionDialogOpen, setDescriptionDialogOpen] = useState(false); - const [donateOpen, setDonateOpen] = useState(false); const [replyOpen, setReplyOpen] = useState(false); const [moreMenuOpen, setMoreMenuOpen] = useState(false); @@ -670,29 +664,6 @@ export function CommunityDetailPage({ event }: { event: NostrEvent }) { // (content bans, reports) used by the comment thread below. const { moderation, rankMap, isLoading: membersLoading } = useCommunityMembers(community); - const communityDonationTarget = useMemo(() => { - if (!community) return null; - const recipients = [ - { pubkey: community.founderPubkey, weight: 1 }, - ...community.moderatorPubkeys.map((pubkey) => ({ pubkey, weight: 1 })), - ]; - return { - event, - pubkey: event.pubkey, - identifier: community.dTag, - aTag: community.aTag, - title: community.name, - summary: community.description, - story: community.description, - image: community.image, - category: 'community', - tags: ['community'], - recipients, - createdAt: event.created_at, - archived: false, - }; - }, [community, event]); - // Only the founder can edit organization metadata. Moderators can // moderate content via the community context but don't get the // "Edit community" action. @@ -1008,17 +979,7 @@ export function CommunityDetailPage({ event }: { event: NostrEvent }) {
{/* Donate (when there's a member set) and Share buttons. Sits just below the hero like the pledge page's action row. */} -
- {communityDonationTarget && ( - - )} +
+ + ); @@ -269,42 +214,51 @@ export function DonateDialog({ campaign, open, onOpenChange, btcPrice }: DonateD return ( - - {step === 'success' && result ? ( - - ) : step === 'confirm' ? ( + + {step === 'form' && ( + { + setAmountUsd(usd); + setCustomUsd(''); + }} + onCustomChange={setCustomUsd} + onCommentChange={setComment} + onFeeSpeedChange={setFeeSpeed} + onContinue={() => setStep('confirm')} + onClose={handleClose} + /> + )} + + {step === 'confirm' && ( setStep('form')} - onConfirm={() => donateMutation.mutate()} + onSubmit={() => donateMutation.mutate()} /> - ) : ( - { - setAmountUsd(amt); - setCustomUsd(''); - }} - onCustomChange={(v) => setCustomUsd(v)} - onCommentChange={setComment} - onFeeSpeedChange={setFeeSpeed} - onNext={() => setStep('confirm')} - onCancel={handleClose} + onClose={handleClose} /> )} @@ -312,455 +266,325 @@ export function DonateDialog({ campaign, open, onOpenChange, btcPrice }: DonateD ); } -// ─── Form view ─────────────────────────────────────────────────────────────── +// ───────────────────────────────────────────────────────────────────── +// Form step +// ───────────────────────────────────────────────────────────────────── + +interface FormViewProps { + campaign: ParsedCampaign; + amountUsd: number; + customUsd: string; + comment: string; + feeSpeed: DonationFeeSpeed; + effectiveAmount: number; + effectiveUsd: number; + belowDust: boolean; + btcPrice: number | undefined; + isPending: boolean; + onAmountChange: (usd: number) => void; + onCustomChange: (value: string) => void; + onCommentChange: (value: string) => void; + onFeeSpeedChange: (speed: DonationFeeSpeed) => void; + onContinue: () => void; + onClose: () => void; +} function FormView({ campaign, - presetAmountUsd, + amountUsd, customUsd, - effectiveUsd, - effectiveAmount, - minDonation, - belowMin, - tooSmallSplit, comment, feeSpeed, + effectiveAmount, + effectiveUsd, + belowDust, btcPrice, - onPresetClick, + isPending, + onAmountChange, onCustomChange, onCommentChange, onFeeSpeedChange, - onNext, - onCancel, -}: { - campaign: ParsedCampaign; - presetAmountUsd: number; - customUsd: string; - effectiveUsd: number; - effectiveAmount: number; - minDonation: number; - belowMin: boolean; - tooSmallSplit: boolean; - comment: string; - feeSpeed: DonationFeeSpeed; - btcPrice?: number; - onPresetClick: (amt: number) => void; - onCustomChange: (v: string) => void; - onCommentChange: (v: string) => void; - onFeeSpeedChange: (v: DonationFeeSpeed) => void; - onNext: () => void; - onCancel: () => void; -}) { - const recipientCount = campaign.recipients.length; - const { user } = useCurrentUser(); - const { config } = useAppContext(); - const { esploraBaseUrl } = config; - const senderAddress = user ? nostrPubkeyToBitcoinAddress(user.pubkey) : ''; - const hasPrice = !!btcPrice && Number.isFinite(btcPrice) && btcPrice > 0; - const validAmount = hasPrice && Number.isFinite(effectiveUsd) && effectiveUsd > 0 && effectiveAmount > 0; - const canContinue = validAmount && !belowMin && !tooSmallSplit; - - const { data: utxos } = useQuery({ - queryKey: ['bitcoin-utxos', esploraBaseUrl, senderAddress], - queryFn: () => fetchUTXOs(senderAddress, esploraBaseUrl), - enabled: !!senderAddress && validAmount, - staleTime: 30_000, - }); - - const { data: feeRates } = useQuery({ - queryKey: ['bitcoin-fee-rates', esploraBaseUrl], - queryFn: () => getFeeRates(esploraBaseUrl), - enabled: validAmount, - staleTime: 30_000, - }); - - const totalBalance = useMemo(() => utxos?.reduce((sum, utxo) => sum + utxo.value, 0) ?? 0, [utxos]); - const feeEstimates = useMemo(() => { - if (!utxos?.length || !feeRates || !validAmount) return {}; - - let outputCount = recipientCount; - try { - outputCount = splitDonation(campaign.recipients, effectiveAmount, user?.pubkey).length; - } catch { - return {}; - } - - return (Object.keys(FEE_SPEED_LABELS) as DonationFeeSpeed[]).reduce>>((acc, speed) => { - acc[speed] = estimateDonationFee({ - inputCount: utxos.length, - outputCount, - totalBalance, - amountSats: effectiveAmount, - feeRate: feeRateForSpeed(feeRates, speed), - }); - return acc; - }, {}); - }, [utxos, feeRates, validAmount, recipientCount, campaign.recipients, effectiveAmount, user?.pubkey, totalBalance]); + onContinue, +}: FormViewProps) { + const usingCustom = customUsd.trim().length > 0; + const canContinue = effectiveAmount > 0 && !belowDust; return ( <> - - - Donate to {campaign.title} - + Donate to {campaign.title} - Your donation is sent as one Bitcoin transaction split across {recipientCount}{' '} - {recipientCount === 1 ? 'recipient' : 'recipients'}. + Send Bitcoin to the campaign's wallet from your in-app balance. -
- {/* Preset grid */} -
- -
- {PRESET_AMOUNTS.map(({ amountUsd: amount, icon: Icon, label }) => { - const selected = !customUsd && amount === presetAmountUsd; +
+ {/* Preset amounts */} +
+ +
+ {PRESET_AMOUNTS.map(({ amountUsd: usd, icon: Icon, label }) => { + const selected = !usingCustom && amountUsd === usd; return ( ); })}
- {/* Custom override */} -
- - onCustomChange(e.target.value)} - /> -
- - {validAmount - ? `${satsToUSD(effectiveAmount, btcPrice)} · ${formatSats(effectiveAmount)} sats` - : btcPrice - ? `Minimum: ${satsToUSD(minDonation, btcPrice)} (${minDonation.toLocaleString()} sats)` - : 'Waiting for BTC/USD price'} - - - {recipientCount > 1 && validAmount - ? `≈ ${formatSats(Math.floor(effectiveAmount / recipientCount))} sats per recipient` - : ''} + {/* Custom amount */} +
+ +
+ + $ + onCustomChange(e.target.value)} + className="pl-7" + />
+ {effectiveAmount > 0 && ( +
+ ≈ {formatSats(effectiveAmount)} sats + {btcPrice && effectiveUsd > 0 && ( + <> · ${effectiveUsd.toLocaleString()} at current price + )} +
+ )}
- {/* Optional comment */} -
- + {/* Comment */} +
+

-%`~X|cDFERq@U&Qh7w`ArtS0#zqdPQuCQxY(Ef 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 bWB;h$t@-=*wvWKmEf_pq{an^LB{Ts5Ar&?H diff --git a/public/profile-intro.png b/public/profile-intro.png deleted file mode 100644 index c9e05539d2e43f60916b0e447942e6a5eb1d7706..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16082 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 diff --git a/public/relay-intro.png b/public/relay-intro.png deleted file mode 100644 index 554dcc99ae2e62244664474fde3be7df519c7e8e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15816 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 R_g@?pNJ>mzv_=>Z^gk?p0<{1D diff --git a/public/theme-intro.png b/public/theme-intro.png deleted file mode 100644 index 5c76c0effac5cc41c56fe0b803208e6a6a99fd90..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14580 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 MteQ-dltsk<0HieNApigX diff --git a/src/App.tsx b/src/App.tsx index 1e1df42c..f9d29374 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -43,7 +43,6 @@ const hardcodedConfig: AppConfig = { shareOrigin: import.meta.env.VITE_SHARE_ORIGIN || undefined, homePage: "campaigns", client: "naddr1qvzqqqru7cpzq7q6z5ns2hm5c8msyv83qwzxpxe52j8c4d4q5m92wsp9sflelkh9qqzkg6t5w3hswjl4yp", - magicMouse: false, theme: "system", useAppRelays: true, useUserRelays: false, @@ -61,44 +60,44 @@ const hardcodedConfig: AppConfig = { feedIncludeArticles: true, showArticles: true, showHighlights: true, - feedIncludeHighlights: false, + feedIncludeHighlights: true, showEvents: true, feedIncludeEvents: true, - showVines: true, + showVines: false, showPolls: true, - showTreasures: true, - showTreasureGeocaches: true, - showTreasureFoundLogs: true, - showColors: true, + showTreasures: false, + showTreasureGeocaches: false, + showTreasureFoundLogs: false, + showColors: false, showPeopleLists: true, - feedIncludeVines: true, + feedIncludeVines: false, feedIncludePolls: true, - feedIncludeTreasureGeocaches: true, - feedIncludeTreasureFoundLogs: true, - feedIncludeColors: true, + feedIncludeTreasureGeocaches: false, + feedIncludeTreasureFoundLogs: false, + feedIncludeColors: false, feedIncludePeopleLists: true, - showDecks: true, - feedIncludeDecks: true, - showWebxdc: true, - feedIncludeWebxdc: true, + showDecks: false, + feedIncludeDecks: false, + showWebxdc: false, + feedIncludeWebxdc: false, showPhotos: true, feedIncludePhotos: true, showVideos: true, feedIncludeNormalVideos: true, feedIncludeShortVideos: true, feedIncludeVoiceMessages: true, - showEmojiPacks: true, - feedIncludeEmojiPacks: true, - showCustomEmojis: true, - showUserStatuses: true, - showMusic: true, - feedIncludeMusicTracks: true, - feedIncludeMusicPlaylists: true, - showPodcasts: true, - feedIncludePodcastEpisodes: true, - feedIncludePodcastTrailers: true, - showDevelopment: true, - feedIncludeDevelopment: true, + showEmojiPacks: false, + feedIncludeEmojiPacks: false, + showCustomEmojis: false, + showUserStatuses: false, + showMusic: false, + feedIncludeMusicTracks: false, + feedIncludeMusicPlaylists: false, + showPodcasts: false, + feedIncludePodcastEpisodes: false, + feedIncludePodcastTrailers: false, + showDevelopment: false, + feedIncludeDevelopment: false, showCommunities: true, feedIncludeCommunities: true, showBadges: true, @@ -109,10 +108,10 @@ const hardcodedConfig: AppConfig = { feedIncludeProfileBadges: true, feedIncludeBadgeAwards: true, feedIncludeVanish: true, - showBirdstar: true, - feedIncludeBirdDetections: true, - feedIncludeBirdex: true, - feedIncludeConstellations: true, + showBirdstar: false, + feedIncludeBirdDetections: false, + feedIncludeBirdex: false, + feedIncludeConstellations: false, followsFeedShowReplies: true, }, sidebarOrder: [ diff --git a/src/AppRouter.tsx b/src/AppRouter.tsx index 04536666..f6a45f6a 100644 --- a/src/AppRouter.tsx +++ b/src/AppRouter.tsx @@ -63,7 +63,6 @@ const KindFeedPage = lazy(() => import("./pages/KindFeedPage").then(m => ({ defa const LetterComposePage = lazy(() => import("./pages/LetterComposePage").then(m => ({ default: m.LetterComposePage }))); const LetterPreferencesPage = lazy(() => import("./pages/LetterPreferencesPage").then(m => ({ default: m.LetterPreferencesPage }))); const LettersPage = lazy(() => import("./pages/LettersPage").then(m => ({ default: m.LettersPage }))); -const MagicSettingsPage = lazy(() => import("./pages/MagicSettingsPage").then(m => ({ default: m.MagicSettingsPage }))); const MusicPage = lazy(() => import("./pages/MusicPage").then(m => ({ default: m.MusicPage }))); const NetworkSettingsPage = lazy(() => import("./pages/NetworkSettingsPage").then(m => ({ default: m.NetworkSettingsPage }))); const NIP19Page = lazy(() => import("./pages/NIP19Page").then(m => ({ default: m.NIP19Page }))); @@ -198,7 +197,6 @@ export function AppRouter() { path="/settings/advanced" element={} /> - } /> } /> } /> } /> diff --git a/src/components/ContentSettings.tsx b/src/components/ContentSettings.tsx index 899a4d33..d946b1a7 100644 --- a/src/components/ContentSettings.tsx +++ b/src/components/ContentSettings.tsx @@ -1,5 +1,4 @@ import { useState } from 'react'; -import { IntroImage } from '@/components/IntroImage'; import { Users, Download, Loader2, X, Pencil, Home, Globe, MapPin, Palette, Trash2, Plus, UserX, Hash, MessageSquareOff, ExternalLink, ShieldAlert, @@ -86,14 +85,11 @@ export function ContentSettings() {