Previously, a successful send from the Zap dialog auto-closed and surfaced a
toast. That undersold what just happened — the user sent Bitcoin. Now both
rails (on-chain + Lightning) flip the dialog over to a dedicated success
screen with an animated check, amount, recipient card, rail indicator, and
(for on-chain) a "View transaction" link to mempool.space. The dialog
auto-dismisses after six seconds if the user walks away.
The dev editor read tags and content from the TanStack Query cache
(companion.allTags / companion.event.content) and published without
prev, risking overwrite of concurrent changes (e.g. social
consolidation advancing the checkpoint on another device).
Apply the standard read-modify-write pattern: fetchFreshEvent before
merge, use prev.tags/content as the base, and pass prev to
publishEvent so published_at is preserved.
Users with multiple Blobbis can now change which one is displayed in the
widget without navigating to the full Blobbi page. A new ArrowLeftRight
icon appears below the companion (Footprints) button and opens a popover
with all available Blobbis for quick selection.
Move effectiveSince computation from useMemo into queryFn so
Date.now() is evaluated fresh on each TanStack Query refetch.
Previously, long-lived pages froze the window floor at mount time,
causing interactions from friends to go undetected after hours.
Also document the intentional boost→feed reaction animation reuse.
- Gate social actions by projected stat thresholds (< 70) so visitors
can only help stats in visual distress
- Add energy category with Energy Drink and Power Nap Pillow items
- Apply 6-hour recency window to interaction queries (limit 30)
- Fix BlobbiActionsProvider tree placement so BlobbiPage shares context
with the companion layer
- Preserve event content in dev editor (don't overwrite checkpoint JSON)
- Show Needs Now summary in activity tab with priority badges
- Remove unused need-driven consolidation infrastructure
Regression-of: 9aecefff
When the user has no NIP-65 write relays configured, the nostrconnect://
URI was built with a single relay (wss://relay.ditto.pub). Fall back to
the full APP_RELAYS write list instead so the remote signer has more
connection options during the handshake.
The Send and Pay-with-WebLN buttons no longer carry a lightning-bolt icon; the on-chain tab's primary button is text-only too, so this brings the flow in line. Sats are no longer shown anywhere on the Lightning screens — the USD amount is the whole story as far as the user is concerned, and the sats figure is just an implementation detail the LNURL flow handles.
Presets drop from $1/$5/$10/$25/$100 to $0.10/$0.50/$1/$2/$5 and the default amount moves from $5 to $0.50. Lightning zaps are tip-jar-shaped, not dinner-shaped — the on-chain tab can stay where it is because a fixed-fee on-chain send doesn't make sense below a few dollars.
Both tabs now lead with a big clickable USD amount and the same //// preset row, with the sats figure relegated to a small secondary line. Dropped the Lightning-only comment textarea and emoji picker — the on-chain flow never had them and keeping two different preambles made the tabs feel like different products. Preset buttons are shorter (h-8) in both tabs.
The invoice screen now shows USD primary / sats secondary in the header and renders the QR through QRCodeCanvas instead of a data-URL img, matching the on-chain fallback's QR styling. Large-amount two-tap confirmation (>= $100) now applies to Lightning too.
When the requested amount plus fee exceeds the user's balance, the
big dollar readout and send button both go destructive-red, the
button text becomes 'Not enough Bitcoin' and disables, and the
'Balance: $X' footnote in the fee row is hidden. The zero-balance
case ("you don't have any Bitcoin yet") still shows the balance line
so users understand why send is disabled.
The dialog collapses to: amount, presets, Send button, fee. Removes
the sats-per-amount subtitle under the big number, the collapsible
'Add a comment' accordion, and the gauge icon next to the fee line.
Comments ride along as empty strings on the zap payload, so the
backing 8333 publish still works, it just doesn't carry user text.
- Center the 'Add a comment' accordion toggle and make its textarea
span the full width when opened.
- Tighten the 'How does sending Bitcoin work?' FAQ: leads with the
Nostr-key-as-wallet framing, drops the extra paragraph about
balances, and keeps the fee and public/irreversible points concise.
- Move the fee/balance line below the Send button so it reads like a
footnote under the primary action instead of competing with it.
Redesign the on-chain flow around a sleek 'Send $X' experience: click
the big number to edit, presets sit underneath, comment collapses behind
a chevron, recipient address and OR divider are gone, the send button
just says 'Send $5.20' with the fee included, and fee speeds are
deduplicated so duplicate sat/vB tiers don't repeat.
The fee speed now auto-adjusts when the amount changes to keep the fee
below 40% of the send amount — once the user manually picks a speed,
auto-adjustment is disabled for the session.
Dialog framing switches from 'Send a Zap' to 'Send Bitcoin', the
redundant description line is dropped, and the (?) popover routes to
one of two new tab-specific FAQ entries (send-bitcoin-onchain or
send-bitcoin-lightning).
Nav items and the 'Search for...' entry in the autocomplete dropdown had
primary-colored icons on a primary-tinted circle, which lost contrast
against the primary-tinted row background when highlighted. Swap both to
accent-foreground tones when the row is selected.
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.
Render highlight excerpts as pull-quotes with source attribution across
feed cards, detail pages, and quote embeds. Without this, kind 9802
events fell through to UnknownKindContent in cards and — worse — had
their quoted prose fed through the kind-1 tokenizer in embeds,
auto-linkifying URLs and hashtags that were part of the original source,
not the highlight author's post.
Integration points:
- HighlightContent renders the excerpt with an accent blockquote, wraps
the highlighted span in <mark> when a context tag is present, and
attributes the source via EmbeddedNaddr (a-tag), EmbeddedNote
(e-tag), or a sanitized URL chip (r-tag).
- EmbeddedHighlightCard gives quoted highlights a dedicated compact
card instead of the generic-embed fallback.
- Added 9802 to NoteCard + PostDetailPage dispatch, KIND_HEADER_MAP,
CommentContext labels/icons, NOTIFICATION_KIND_NOUNS, and the
EmbeddedNote dispatcher.
- Registered an EXTRA_KINDS entry with feedIncludeHighlights (off by
default) and showHighlights (on), plus a /highlights route backed by
KindFeedPage.
- Added a highlights notification type with its own subscription
template, preference toggle, grouped notification row, and
author-ownership filter so users are only notified when their own
content is highlighted.
- feedUtils hides empty highlights with no source reference.
Three inconsistencies between /i/ pages cleaned up:
* Action bar now always renders three universal interaction buttons
(Comment, React, Share) with the Comment button opening the
compose modal and showing the top-level comment count. ISBN keeps
its Write Review (star) button as a fourth. Previously Bitcoin
and other non-book identifiers showed only React + Share.
* Book content tabs (Comments / Reviews) switched from the flat
shadcn Tabs primitive to SubHeaderBar + TabButton so the curved
arc styling matches Feed, Profile, Search, and every other tabbed
page in the app.
* Extracted ExternalCommentsSection — the inline ComposeBox +
threaded list + loading/empty states are now shared between the
default single-list layout and the ISBN Comments tab instead of
duplicated.
Every context except the Feed composer follows the compose box with
content that needs visual separation (comment lists, wall posts,
threaded replies), so add a default border-b and let Feed opt out with
a new hideBorder prop — it sits directly above SubHeaderBar's arc
background, which already provides its own separator.
Modal usage (forceExpanded) stays borderless since it lives inside
a dialog container.
ExternalContentPage already supported fetching comments for non-URL
external roots (bitcoin:tx:..., isbn:..., iso3166:..., etc.), but the
inline ComposeBox and the FAB's ReplyComposeModal were both gated on
the URL-only commentRootUrl — leaving those pages with no way to post.
Widen ComposeBox's replyTo and ReplyComposeModal's event to accept
`#${string}` NIP-73 identifiers alongside URLs and events, route them
through the existing NIP-22 publish path (usePostComment already
handled string roots), and wire the page up to the combined
commentRoot.
text-primary was applied directly to the Shield icon, overriding the
ghost button's hover:text-accent-foreground. On hover the background
turned into accent while the icon stayed primary, producing a
low-contrast pairing. Move text-primary up to the Button so the
ghost variant's hover rule can take over, and let the icon inherit
currentColor.
- Remove the app-name subtitle under "Permissions"; the site is already
identified by the nav bar directly above the popover.
- Drop the trash icon from the "Revoke all" button so the destructive
action reads as plain text like the row items.
- Remove the per-row allow/deny toggle. Stored permissions are always
"allowed" in practice (a denied prompt doesn't surface a row the user
would want to keep around), so the toggle added noise without a
realistic use case. Users who change their mind can remove the row and
re-prompt.
- Drop the status check/X icon on the left of each row now that the
toggle is gone and every listed permission is implicitly allowed.
- Show the remove button always (was opacity-0 until hover) and switch
it from a trash icon to an X, matching the close-affordance idiom used
elsewhere in the app.
- Drop the siteName prop from NsitePermissionManager; nothing uses it
anymore.
The target pubkey was rendered as a truncated hex string in the prompt,
which is noise to the user: they can't verify it, it doesn't scope the
stored permission (which is global-to-the-app), and showing a pubkey
next to "Allow" misleadingly suggests the decision applies only to that
peer. Drop the field from NsitePromptState and stop threading it through
the four encrypt/decrypt RPC branches.
Sandbox iframes live on a cross-origin subdomain, so most capability APIs
are blocked unless the parent delegates them with allow=. Add a permissive
policy covering media (camera, microphone, display-capture, encrypted-media,
picture-in-picture, autoplay, speaker-selection), sensors (accelerometer,
gyroscope, magnetometer, ambient-light-sensor, compute-pressure, battery),
input (gamepad, midi, keyboard-map, xr-spatial-tracking), and UX features
(fullscreen, geolocation, idle-detection, screen-wake-lock, clipboard-write,
web-share, window-management, storage-access) so nsites and webxdc apps can
use anything a regular web app would.
Deliberately omitted for security: payment, publickey-credentials-*,
otp-credentials, identity-credentials-get (all phishing/account-takeover
vectors), local-fonts (fingerprinting), bluetooth/hid/serial/usb/
clipboard-read (raw device access left off for now).
Drop WebxdcIframe's narrow allow= override so webxdc apps get the same
broad policy instead of downgrading to just autoplay/fullscreen/gamepad.
Also split safe-area-top from content padding in the NsitePreviewDialog
and WebxdcEmbed nav bars: the outer element reserves space for the
notch inset, the inner row keeps a fixed px-3 py-2 flex layout so
content stays vertically centered inside the intended 44px bar height.
Introduces a new sidebar item type for nsites that auto-opens the nsite
preview when clicked, using React Router state to prevent external URLs
from triggering auto-launch.
- Add isNsiteUri/nsiteUriToSubdomain helpers and parseNsiteSubdomain
- Create NsiteSidebarItem with site favicon and link preview title label
- Wire nsite:// dispatch in SidebarNavList, useFeedSettings, SidebarMoreMenu
- NoteMoreMenu pins named nsite events as nsite:// URIs instead of nostr:
- NsiteCard gains a Pin/Unpin button and an autoPlayKey prop that re-opens
the player each time the sidebar item is clicked
- NsitePlayerContext tracks the active subdomain for sidebar highlighting,
provided in MainLayout so sidebar and pages share state
- PostDetailContent consumes nsiteAutoPlay router state and clears it
after consumption so a page refresh doesn't re-trigger auto-play
When a logged-in user opens an nsite preview, a window.nostr provider is
injected into the sandboxed iframe. The provider proxies signEvent, nip04,
and nip44 calls to the parent signer over the existing JSON-RPC bridge.
A permission system gates each operation:
- getPublicKey is auto-allowed (clicking Run implies consent)
- signEvent prompts are granular per event kind (like Amber)
- encrypt/decrypt prompts are per operation type
- Users can check 'Remember for this site' to persist decisions
- Permissions are scoped to (userPubkey, siteId) in localStorage
The nsite preview nav bar gains a shield icon that opens a popover for
managing stored permissions.
Kind labels for the signer nudge, the permission prompt, and the post-
detail loading title now route through a central KIND_LABELS registry
(src/lib/kindLabels.ts) instead of three divergent inline maps.
The native SandboxPlugin (iOS WKWebView / Android WebView overlay) is
removed; SandboxFrame now always uses iframe.diy, so native behavior
matches web. This drops ~1100 lines of native code, the Android-only
blob prefetch workaround in NsitePreviewDialog, and the createPluginCall
registration in MainActivity and capacitor.config.json.
The sing action uses getUserMedia + MediaRecorder, which in a browser is
gated only by the standard web mic prompt. In Capacitor's Android
WebView it additionally requires the RECORD_AUDIO permission to be
declared in AndroidManifest.xml; without it the WebView rejects with
NotAllowedError and no system prompt is ever shown, so tapping record
silently fails on the Android app while working fine in the browser.
Also add MODIFY_AUDIO_SETTINGS, which some devices require for the
echoCancellation / noiseSuppression / autoGainControl constraints that
InlineSingCard passes to getUserMedia.
Separately, reorder AUDIO_MIME_CANDIDATES to prefer audio/mp4/aac over
audio/webm;codecs=opus. iOS WKWebView cannot decode WebM/Opus in an
<audio> element, so the recorded Blob's preview URL failed to load on
iOS. Android WebView and desktop Chromium both support mp4/aac, so
preferring it first is safe cross-platform. This mirrors the ordering
already used by useVoiceRecorder.ts.
When a user tapped "Open Signer App", the dialog previously stayed
frozen on the same screen — same button, same copy-URI fallback, no
feedback — until the login either succeeded (and the dialog dismissed)
or timed out after two minutes. With slow or flaky signers (Amber's
current listening-REQ bug being the immediate trigger, but any NIP-46
signer that takes more than a second or two to respond hits the same
hole) this looked indistinguishable from a hang. Users retapped the
button, closed the dialog, gave up.
Now the dialog swaps the QR / Open Signer App area for a centered
spinner with a live status line as the handshake advances:
- "Waiting for signer connection…" while the signer app has the user
and we're listening on kind 24133 for the connect-ack.
- "Getting public key…" once the connect-ack arrives and we're
issuing the NIP-46 get_public_key RPC.
On mobile the swap happens synchronously when the user taps "Open
Signer App" so they see the progress state the moment they return
from the signer — this is the most important window, since that's
exactly when the original UI left them staring at a button they
were worried they needed to re-tap. On desktop the QR stays visible
through the awaiting-connect phase (they may still be scanning with
a different device) and only swaps in once the signer has
acknowledged.
The progress view includes a Cancel link (primary color, matches the
"Create account" affordance) that aborts the in-flight subscription
and regenerates fresh connect params — equivalent to the existing
Retry path, but reachable while the handshake is live instead of
only after a failure.
The handshake phases are surfaced via the new `onStatus` callback
on `NLogin.fromNostrConnect` in @nostrify/react 0.6.0. Bumps
@nostrify/react to ^0.6.0 and @nostrify/nostrify / @nostrify/types
to their matching versions (^0.52.0 / ^0.37.0) to avoid duplicate
nested package copies that would otherwise split type identity.
Incidental cleanup while editing the dialog: the Copy URI button and
the "Tap to open your signer app" / "Scan with your signer app"
status lines are removed. The primary Open Signer App button is
self-explanatory on mobile, and the QR on desktop doesn't need a
caption.
The nostrconnect listening effect depended on `login`, `onLogin`,
`onClose`, and `isWaitingForConnect`. `login` is a fresh object from
useLoginActions on every render, and every call site passes inline
arrow functions for onLogin/onClose, so the effect re-ran on every
parent render. Each re-run fired the cleanup and flipped a local
`cancelled = true` flag, and when the signer's NIP-46 response
eventually arrived, the success branch saw `cancelled === true` and
silently skipped `onLogin()` / `onClose()` — the user was logged in on
the backend but the dialog never closed.
Stabilize onLogin/onClose/login via latest-value refs, narrow the
effect deps to `[nostrConnectParams]` only, and gate the success
branch on `controller.signal.aborted` (which is only true when the
dialog was explicitly closed or handleRetry fired). Drop the unused
`isWaitingForConnect` state. Also abort the in-flight controller from
handleRetry before regenerating params, so the prior subscription
doesn't linger.
The bug was masked by most signers responding fast enough (<1s) that
parent re-renders didn't happen during the wait. It surfaced during
an upstream Amber bug that delays its listening REQ by ~8+ seconds,
giving render cycles time to fire (https://github.com/greenart7c3/Amber/pull/420).
The Amber bug is getting fixed separately; this Ditto fix stands on
its own — any signer that takes a few seconds to respond could trip
the same race.
The paginated backfill loop could mark itself as 'done' even when pages
timed out or when a retry overlap page returned only already-known events.
This caused counts to differ between reloads depending on network timing.
Changes to useMultiHashtagFeed.ts:
- Add reachedEnd flag: backfillDone only set when a definitive end condition
is confirmed (empty batch, reached since, sub-limit overlap, or MAX_EVENTS)
- Timeout/abort sets completedCleanly=false, preventing premature mark
- Resume cursor derived from eventMap minimum timestamp on retry
- Saturated boundary handling: if overlap page at limit returns zero new
events, fall back to exclusive cursor (oldestInBatch - 1) to probe older
- MAX_EVENTS (10,000) hard cap as explicit constant
- Query DITTO_RELAY directly via NRelay1 to avoid NPool eoseTimeout
Also includes: ConfigDrawer, territorial coverage utilities, content
fallback for municipality attribution, and real relay data integration.
VineMedia (kind 22 / 34236 in regular feeds) always started muted on
click-to-play, even when the "autoplay videos" setting was disabled.
That made click-to-play behave differently from the regular VideoPlayer,
where a user-gesture click plays with sound.
Now VineMedia only starts muted when autoplayVideos is on (browsers
require autoplay to be muted). When the setting is off, a click is an
explicit user gesture and plays with audio. The <video> muted attribute
is also bound to state so it tracks the mute toggle correctly. If play
is rejected unmuted, we retry muted as a fallback.
Regression-of: 23e845eb
- Add stripped venezuelaTerritorial.ts (24 states + 379 municipalities, no parroquias)
- Add useEventDashboardConfig hook with hardcoded default config
- Add useMultiHashtagFeed hook with backfill/polling/dedup and enabled option
- Add useEventDashboard adapter hook: config → feed → aggregation → typed outputs
- Wire EventDashboardPage to real data; show loading skeleton and error states
- RecentActivityList now resolves profiles via useAuthor
- Non-admin users never trigger relay queries (enabled: false guards all queries)
- No ConfigDrawer, parroquias, territorialCoverage, verified badges, or content
fallback ported — those remain deferred to Phase 3
- Add admin-gated /event-dashboard route with visual-only mock dashboard
- Add requiresAdmin support to SidebarItemDef; filter admin-only items
from desktop sidebar, mobile drawer, More menu, and search suggestions
- Dashboard components: KPI grid, activity chart, top regions bar chart,
distribution donut, participants list, recent activity feed, skeleton
- Page-level access control mirrors OrganizersPage pattern (login prompt
for logged-out users, locked card for non-admins, full content for admins)
- Uses noMaxWidth layout for wider charts within the standard app shell
- All data is static mock (Venezuelan municipality codes in real CNE format)
- No relay fetching, config drawer, dedup/backfill, or localStorage yet
Long-form articles (kind 30023) previously ran their content through
react-markdown with no Nostr awareness, so `nostr:` URIs, bare URLs,
hashtags, and custom emoji all surfaced as literal text. This composes
NoteContent into the markdown renderer instead, so mentions get hover
cards, quoted notes render as embedded cards, URLs get link previews,
and custom emoji resolve — matching regular note behavior exactly, with
zero duplicated parsing.
NoteContent gains an `as: 'div' | 'span'` prop so it can be embedded
inline inside markdown elements without invalid-HTML nesting. Markdown
paragraphs render as `<div>` (with reproduced prose paragraph spacing)
so block-level quote cards and images are legal children. Headings
suppress block embeds to avoid quote cards inside an `<h1>`.
Also strips Typography's default always-on link underline in favor of
hover-only, matching the rest of the app.
Replace blobbi reference equality with stable visual-identity primitive
comparisons in MemoizedBlobbiVisual memo and SVG renderer useMemo deps.
This prevents SVG DOM rebuilds (which restart SMIL animateTransform)
when the upstream companion object gets a new reference during the
imperative gradient-drain loop.
Lift useFillLevelUpdate above MemoizedBlobbiVisual so the memo boundary
can block the 60fps re-render cascade during level-only changes. The
arePropsEqual comparison now uses a pre-computed recipeFingerprint
(which excludes angerRise.level) instead of recipe reference equality.
During nausea drain, the structural recipe fingerprint stays constant,
so MemoizedBlobbiVisual blocks all re-renders — keeping the SVG DOM
stable and SMIL spiral-eye animations running uninterrupted. The fill
level still updates imperatively via useFillLevelUpdate called from
BlobbiCompanionVisual's root ref.
Vomit now triggers on any sufficiently hard shake (peakIntensity >= 0.7)
regardless of hunger level. Green nausea fill remains hunger-gated.
Remove the MAX_SPLATS = 3 cap so puddles accumulate freely until the
user clicks/taps them away.
Add a static mouthAnchor lookup table that maps baby/adult forms to
their actual mouth Y-ratio (including the +0.12 visual offset), so
the vomit drop spawns from the correct mouth position per form.
- Falling drop z-index 10002 (above companion) so it visibly exits mouth
- Landed puddle stays at z-index 9998 (below companion)
- Land position changed to 20px below Blobbi's container bottom, clamped
- Spawn position adjusted to config.size * 0.65 for mouth area
- Unified transform anchor to translate(-50%, -100%) in both states
- Keyframe ends at scale(1) opacity(1) to prevent pop on landing
- Reset vomitedThisCycle + peakIntensity in onDragStart during active
cycle so each new qualifying drag can vomit independently