From a6bfd2cb689de28684d933f0d4ecc8ce5ba4639e Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 5 May 2026 17:24:57 -0500 Subject: [PATCH] Add Birdex chorus button MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New BirdexChorusButton plays every species' Wikipedia recording at once — a dawn chorus for the whole life list. Hides itself when no species has usable audio, and the feed-card variant swallows clicks so toggling playback doesn't navigate away from the NoteCard. --- src/components/BirdDetectionContent.tsx | 166 +++++++------- src/components/BirdexChorusButton.tsx | 279 ++++++++++++++++++++++++ src/components/BirdexContent.tsx | 62 ++++-- 3 files changed, 406 insertions(+), 101 deletions(-) create mode 100644 src/components/BirdexChorusButton.tsx diff --git a/src/components/BirdDetectionContent.tsx b/src/components/BirdDetectionContent.tsx index f3684427..6946471e 100644 --- a/src/components/BirdDetectionContent.tsx +++ b/src/components/BirdDetectionContent.tsx @@ -1,8 +1,9 @@ import { useMemo } from 'react'; -import { Bird, ExternalLink, MessageCircle } from 'lucide-react'; +import { Bird } from 'lucide-react'; import { Link } from 'react-router-dom'; import type { NostrEvent } from '@nostrify/nostrify'; +import { BirdSongPlayer } from '@/components/BirdSongPlayer'; import { Skeleton } from '@/components/ui/skeleton'; import { useWikidataEntity } from '@/hooks/useWikidataEntity'; import { useWikipediaSummary } from '@/hooks/useWikipediaSummary'; @@ -87,12 +88,11 @@ export function BirdDetectionContent({ event, className }: BirdDetectionContentP const commonName = summary?.title ?? altSpecies?.common ?? 'Unknown species'; const extract = summary?.extract; const thumbnail = sanitizeUrl(summary?.thumbnail?.source); - const articleUrl = sanitizeUrl(summary?.articleUrl); - // "Discuss" routes the user to Ditto's external-content page for this - // species' Wikidata URL. Other users' kind 2473 detections and NIP-22 - // comments both attach to the same `i`-tag identifier, so the discussion - // thread aggregates naturally across clients. + // The whole card routes to Ditto's external-content page for this + // species' Wikidata URL. Other users' kind 2473 detections and + // NIP-22 comments both attach to the same `i`-tag identifier, so + // the discussion thread aggregates naturally across clients. const discussPath = wikidata ? `/i/${encodeURIComponent(wikidata.url)}` : undefined; // When the user's own freeform note exists we show it above the @@ -114,93 +114,89 @@ export function BirdDetectionContent({ event, className }: BirdDetectionContentP return (
-
- {/* Thumbnail panel */} -
- {isLoading ? ( - - ) : thumbnail ? ( - {commonName} - ) : ( -
- e.stopPropagation()} + className="block overflow-hidden rounded-2xl border border-border bg-card shadow-sm transition-shadow hover:shadow-md focus:outline-none focus-visible:ring-2 focus-visible:ring-ring" + > +
+ {/* Thumbnail panel */} +
+ {isLoading ? ( + + ) : thumbnail ? ( + {commonName} + ) : ( +
+ +
+ )} +
+
+ {timeStr}
- )} -
-
- {timeStr}
-
- {/* Text panel */} -
-
-
- -

- {commonName} -

+ {/* Text panel */} +
+
+
+

+ {commonName} +

+ {scientificName && ( +

+ {scientificName} +

+ )} +
+ + {isLoading ? ( +
+ + + +
+ ) : extract ? ( +

+ {extract} +

+ ) : ( +

+ Heard at {new Date(event.created_at * 1000).toLocaleString()}. +

+ )}
- {scientificName && ( -

- {scientificName} -

+ + {/* Reference recording from Wikipedia/Commons, when available. + * `BirdSongPlayer` returns null when the article has no + * usable audio, so the right-hand column collapses + * cleanly and the text reflows across the full card. + * The click handler stops propagation so toggling + * playback doesn't also navigate to `/i/...`. */} + {wikipediaTitle && ( +
{ + e.stopPropagation(); + e.preventDefault(); + }} + className="shrink-0" + > + +
)}
- - {isLoading ? ( -
- - - -
- ) : extract ? ( -

- {extract} -

- ) : ( -

- Heard at {new Date(event.created_at * 1000).toLocaleString()}. -

- )} - - {(articleUrl || discussPath) && ( -
- {discussPath && ( - e.stopPropagation()} - className="inline-flex items-center gap-1 text-[11px] font-medium text-muted-foreground hover:text-foreground" - > - - Discuss - - )} - {articleUrl && ( - e.stopPropagation()} - className="inline-flex items-center gap-1 text-[11px] font-medium text-muted-foreground hover:text-foreground" - > - - Wikipedia - - )} -
- )}
-
+ {note && (

diff --git a/src/components/BirdexChorusButton.tsx b/src/components/BirdexChorusButton.tsx new file mode 100644 index 00000000..a04d32fe --- /dev/null +++ b/src/components/BirdexChorusButton.tsx @@ -0,0 +1,279 @@ +import { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; +import { Play } from 'lucide-react'; + +import { Skeleton } from '@/components/ui/skeleton'; +import { useBirdSong } from '@/hooks/useBirdSong'; +import { useWikidataEntity } from '@/hooks/useWikidataEntity'; +import { cn } from '@/lib/utils'; + +/** + * Chorus play/pause button for a Birdex (kind 12473) life list. + * + * A single control that fires every species' reference recording from + * Wikipedia/Commons *at the same time*, producing an overlapping + * dawn-chorus effect. Each recording loops independently so the + * chorus sustains until the user hits pause. + * + * Architecture: the parent owns a single `isPlaying` flag. It renders + * one `BirdexChorusVoice` per species, each of which: + * 1. Resolves its Wikidata ID → Wikipedia title → Commons audio URL + * via the same hooks the tile thumbnails already call (so the + * title-resolution round-trips are cache hits). + * 2. Owns a hidden `

-
- -

- Birdex - - {entries.length} species - -

+
+
+ +

+ Birdex + + {entries.length} species + +

+
+ + {/* Single control that plays every species' Wikipedia + * recording simultaneously — a dawn chorus for the whole + * life list. Hides itself when no species has usable audio. */} + ({ entityId }))} + className="shrink-0" + />
@@ -95,14 +106,33 @@ export function BirdexContent({ event, expanded, className }: BirdexContentProps return (
-
- - - Birdex - - - · {entries.length} species - +
+
+ + + Birdex + + + · {entries.length} species + +
+ + {/* Same chorus button as the expanded view — plays the + * entire life list, not just the preview slice, because the + * button represents the whole event. Wrapped in a click + * swallower: the feed variant sits inside a clickable + * NoteCard, and toggling playback must not navigate away. */} +
{ + e.stopPropagation(); + e.preventDefault(); + }} + className="shrink-0" + > + ({ entityId }))} + /> +