User relays are no longer used until the user explicitly opts in via
Settings > Network. Adds a useUserRelays toggle alongside the existing
useAppRelays toggle in RelayListManager, defaulting to false. Fresh
installs and new accounts will only query app-default relays until the
user enables their personal NIP-65 list.
The user's relay list (kind 10002) is still synced from Nostr and
managed in the UI when logged in — the toggle only controls whether it
is included in the effective relay set used by NostrProvider's pool
and useNativeNotifications. The setting is persisted to AppConfig and
synced cross-device via NIP-78 encrypted settings.
getEffectiveRelays now takes both flags and short-circuits accordingly,
producing an empty list when both are off (instead of the previous
behavior of always returning user relays).
- Seed ['event', id] query cache from the community activity feed so
embedded previews resolve without a second fetch.
- Add placeholderData and a 30-minute gcTime to the community activity
feed so navigation and background refetches don't flash empty.
- Surface useGoalProgress's isPartial flag in GoalCard with a '~'
prefix and tooltip so users know when a tally hit the safety cap.
Drop LNURL signer resolution and NIP-57 receipt validation from goal
progress tallying. This removes a network request per beneficiary for a
trust level that is still spoofable and that no other zap display in the
app enforces. Revert this commit to restore strict validation.
Include communitiesLoading in the hook's isLoading so the skeleton
shows while the dependent communities query is still resolving,
instead of briefly rendering the empty state.
Goals use lowercase 'a' tags (not uppercase 'A' like NIP-22 comments)
to link to communities. The activity feed's moderation filter, members-
only filter, and CommunityModerationContext provider lookup all only
checked uppercase 'A', so goals bypassed moderation and had no '...'
menu. Now all three check both tag casings.
Content-banned goals and goals from member-banned authors are now
filtered out of the fundraising tab via applyCommunityModerationToEvents,
matching the behavior of the comments tab.
Replace standalone GoalCard with NoteCard in the community fundraising
tab so goals get the same '...' menu with remove/ban actions that
comments have. Strip GoalCard down to just the compact inline renderer
(no variant prop, no skeleton, no card-only imports). Simplify
useCommunityGoals to return plain events instead of parsed wrappers.
- Unify GoalCard and GoalContent into a single component with variant prop
- Extract useGoalDisplay hook for shared display logic (author, progress,
community link, deadline, image)
- Add useNow(60s) interval so deadline labels refresh automatically
- Add generic parseATagCoordinate utility to nostrEvents.ts
- Replace DOM-mutating image onError with React state
- Remove dead isGoalFunded export and redundant created_at in publish
- Delete GoalContent.tsx (-144 net lines)
- PostDetailPage: render GoalContent for kind 9041 instead of plain text
- CommunityDetailPage: add floating action button on comments (compose) and fundraising (new goal) tabs, remove inline New Goal button
- CreateGoalDialog: support controlled open/onOpenChange props for external triggers
Implement zap goals (kind 9041) linked to communities via a-tag.
Includes goal creation dialog, progress tracking from zap receipts,
recipient profile/lightning address display, community link, and
members-only filtering. Goals appear in community detail Fundraising
tab, activity feed, and main feed via NoteCard.
- Move vomit trigger from delayed dizzy timer to immediate on drag
release when conditions are met (cycleHadNausea + peakIntensity >= 0.7)
- Add vomitedThisCycle ref to prevent duplicate emissions per cycle
- Change landY from viewport bottom to short distance below Blobbi
(renderedPosition.y + size*0.9, clamped to floor limit)
- Replace puddle <div role=button> with semantic <button type=button>
- Use translate(-50%, -100%) so puddle bottom sits at the landing point
- Update click handler type from PointerEvent to MouseEvent
When Blobbi is shaken hard enough while nauseous (hunger >= 90 and
peak intensity >= 0.7), the dizzy phase now escalates to a brief
'vomiting' phase. A green drop falls from Blobbi's mouth to the ground
and becomes a persistent puddle that stays fixed in place until the
user clicks/taps it to clean up. Max 3 puddles; oldest removed on
overflow.
New files:
- VomitSplat.tsx: falling drop + landed puddle (SVG, click-to-remove)
Modified:
- useShakeReaction.ts: new 'vomiting' phase, peakIntensity tracking,
VomitEvent signal, vomitTimer with proper cleanup
- BlobbiCompanionLayer.tsx: splat state management, coordinate
calculation, renders VomitSplat siblings in the overlay
- index.css: vomit-fall keyframe animation
Move the useId()-based instance ID generation into a shared hook so the
logic and comment live in one place instead of being duplicated across
BlobbiAdultSvgRenderer and BlobbiBabySvgRenderer.
When a Blobbi appeared in multiple spots simultaneously (hero + drawer
grid, hero + floating companion, feed card + companion), all SVG
instances shared the same clip-path IDs (e.g. blobbi-blink-clip-{d}-left).
The browser resolves clip-path: url(#id) to the first matching element
in document order, so the hero's eyes would use the drawer thumbnail's
non-animated clip-rects — eyes stayed open while CSS eyelid animations
still ran.
Use React's useId() to generate a unique instanceId per component
instance instead of reusing blobbi.id, ensuring clip-path and gradient
IDs are unique across all concurrent renders of the same Blobbi.
Regression-of: 384936f1
The right sidebar previously required xl (1280px) to appear, so horizontal
iPad (1024px) viewports saw only the left sidebar + main column. Use the
existing lg breakpoint (1024px) to control right-sidebar visibility, and
let the sidebars scale fluidly with the viewport by setting them to w-1/4
max-w-[300px] instead of fixed pixel widths. The center column (flex-1)
absorbs whatever space remains, so the layout fills the viewport smoothly
from 1024px up through the 1200px wrapper cap instead of leaving dead
space at intermediate widths. Below the lg breakpoint, the left sidebar
keeps its fixed 300px width.
The wrapper div around BlobbiStageVisual in BlobbiRoomHero had an
unconditional pointer-events-none class, which blocked click/tap
events from reaching EggGraphic's handleEggClick handler. Now the
class is only applied when the companion is NOT in egg stage, so
the egg vibration interaction works on /blobbi just like in the
blobbi page view.
Tags like #70-706 and #bitcoin-conference were split at the hyphen,
since the regex only matched letters, numbers, and underscores. Allow
internal hyphens (but not leading or trailing ones, so #nostr- still
captures just #nostr) and share the pattern across NoteContent,
BioContent, and the compose boxes so posted t-tags match what renders.
Country, book, and Bitcoin tx/address pages were querying with a stray
`#` prefix on the NIP-73 identifier, so the filter never matched any
real comments. Pass the raw identifier like PostDetailPage does.
Regression-of: 363e39d7
Wikipedia/Wikimedia Commons hosts editorially-curated, mostly Xeno-
Canto-sourced recordings on bird species articles. Surface them on
Ditto's /i/ page whenever the article exposes one: an emerald play
button sits inline with the article title, looping the song on
click and swapping its play triangle for an animated equaliser to
indicate active playback. When a recording is present, the footer
row gains a second link crediting the recordist and license and
pointing at the Commons file-description page for verification.
Adapted from Birdstar's BirdInfoDialog / useBirdSound / useWikipedia
Sound hooks; the iNaturalist fallback from the original is dropped
per request — Ditto only uses Wikipedia/Commons.
Birdstar kind 2473 events now carry the species' scientific binomial
name in an authoritative 'n' tag, added to the NIP so clients can
label a detection without round-tripping Wikidata. Ditto was still
scraping the name out of the 'alt' tag's parenthetical, which loses
the label when the publisher emits a bare alt like "Bird detection".
Prefer the 'n' tag and fall back to 'alt' parsing only for older
events authored before the tag was part of the NIP. Also show the
scientific name as a persistent italic sub-label on Birdex tiles,
matching how detection cards stack the two labels.
Regression-of: b2634d2f
A Birdex is a replaceable per-author index of every bird species the
author has ever confirmed via kind 2473, stored as positional i/n tag
pairs in chronological first-seen order. In feeds, show a compact
tile strip of the most recently-added species with a "+N" capstone
when the list overflows — mirroring how kind 3 follow lists preview
members as an avatar stack plus "+N more". On the post-detail page,
render every species as a responsive grid so visitors can browse the
author's whole life list.
Each tile resolves the species' Wikidata entity through English
Wikipedia to pull a thumbnail and common-name label, reusing the
same fetch path as kind 2473 detection cards. The Wikidata URL is
sanitized before being routed, and the paired n tag provides a
scientific-name fallback while the remote lookup is in flight.
Previously getDisplayName() and ~50 inline sites only consulted
metadata.name, ignoring display_name. A handful of other sites used the
opposite priority (display_name || name), so the same user could render
under different names across the UI.
Standardize on `name || display_name || genUserName(pubkey)` in the
helper and at every call site, and widen two local inline metadata types
in RightSidebar and SearchPage that did not declare display_name.
When the Bitcoin tab only had a dead-end error panel, ZapDialog auto-
switched to Lightning whenever capability was unsupported and the author
had a Lightning address. Now that the Bitcoin tab renders a QR fallback
the user can actually use, that forced redirect prevented them from
clicking into it: every render while activeTab === 'onchain' flipped it
back to 'lightning'.
Drop the mid-session auto-flip effect. The initial tab choice still
biases toward Lightning (via the useState initializer and the open-reset
effect), but manual navigation into Bitcoin is respected.
Regression-of: bf540fb5
When the user's signer can't sign PSBTs (extension without signPsbt, or a
bunker that rejected sign_psbt), the onchain zap tab previously showed a
dead-end 'Bitcoin zaps aren't available' panel. Replace it with an
amount selector + BIP-21 QR code the user can scan from any external
Bitcoin wallet, plus copy buttons for the address and payment link.
Because Ditto never sees the resulting transaction, no kind 8333 is
published in this flow. A warning explains that the zap won't show up
as theirs on Nostr even though the recipient still gets the Bitcoin.
Also gate the mempool.space UTXO and fee-rate queries on the capability
being non-unsupported to avoid pointless network calls in this branch.
The previous `private readonly secretKeyBytes` was compile-time-only:
at runtime the field was a plain enumerable property, readable as
`user.signer.secretKeyBytes`. This regressed the runtime privacy
boundary that the parent `NSecSigner` deliberately enforces with its
ES `#private` field.
Switch to `#secretKeyBytes` so the bytes are unreachable via property
enumeration or reflection on the instance, matching the parent class's
protection.