On page navigation the companion now briefly pauses and scans the
layout areas that changed. Center content is always scanned first,
followed by the right sidebar if a non-placeholder sidebar is
detected in the DOM.
Implementation:
- useRouteReaction.ts: thin orchestration hook that watches pathname,
determines changed areas, and chains triggerAttention calls via
setTimeout. Cancels on new route change, drag, or unmount.
- useBlobbiCompanion.ts: wires the new hook with existing
triggerAttention/clearAttention from useBlobbiAttention.
No changes to the attention system, state machine, gaze hook, motion
hook, or entry animation. The existing attending state and attend-ui
gaze mode handle all the visual behavior.
Includes an empty ROUTE_REACTIONS map for future per-route overrides.
Android 16 (API 36) enforces edge-to-edge rendering unconditionally,
breaking @capacitor/status-bar's setOverlaysWebView and setBackgroundColor.
Additionally, a Chromium bug (<140) causes env(safe-area-inset-*) to report
0 in some Android WebViews.
- Replace @capacitor/status-bar with SystemBars from @capacitor/core 8+
- Enable insetsHandling: 'css' in capacitor.config.ts so the SystemBars
plugin injects --safe-area-inset-* CSS variables on Android
- Update all safe area CSS utilities and inline styles to use
var(--safe-area-inset-*, env(safe-area-inset-*, 0px)) fallback pattern
- Remove @capacitor/status-bar dependency (no longer needed)
- Use SecAddSharedWebCredential to prompt 'Save Password?' on signup
- Use ASAuthorizationPasswordProvider to restore credentials on login
- Add webcredentials:ditto.pub Associated Domains entitlement
- Deploy apple-app-site-association for domain validation
- Keep existing Chromium PasswordCredential flow as web fallback
- Add saveNsec() helper: native credential manager on iOS/Android,
file download + bonus PasswordCredential on web
- Single 'Continue' button triggers the appropriate save method per platform
Progressive enhancement using PasswordCredential (Chromium-only).
On sign-up, the nsec is offered to the browser's password manager
alongside the existing file download. The prompt appears while the
user is looking at their key on the download step. On login, stored
credentials are retrieved for one-tap login on supported browsers.
Safari/Firefox/iOS silently skip — existing flows are unchanged.
Add a 'Delete Account' pill button to the bottom of the Settings
page (Guideline 5.1.1v). Rename the Danger Zone heading in Advanced
Settings to match. Simplify the deletion dialog to a single screen:
plain-language warning, list of what gets deleted, type DELETE to
confirm, and Cancel/Delete buttons. Always broadcasts to all relays.
The underlying NIP-62 mechanism and components that render vanish
events to other users are unchanged.
File inputs with accept="image/*" present a camera option on iOS.
Without this usage description, WKWebView crashes or fails to show
the permission dialog when the user selects 'Take Photo'.
Nostr events are untrusted user input. Any URL extracted from event tags
or metadata must be validated before use in any context — not just
navigable hrefs, but also img src, CSS url(), and style attributes.
Changes:
- Theme events (kind 16767/36767): validate background and font URLs
through sanitizeUrl() at parse time in themeEvent.ts
- Badge definitions (kind 30009): validate image and thumb URLs through
sanitizeUrl() at parse time in parseBadgeDefinition.ts
- Font family names: sanitize with an allowlist regex before
interpolation into CSS declarations in fontLoader.ts
- Profile fields: replace weak startsWith('http://') checks with
sanitizeUrl() in ProfileRightSidebar and ProfilePage
- Community descriptions: validate extracted URLs through sanitizeUrl()
in CommunityContent.tsx
- AGENTS.md: mandate unconditional URL sanitization for all
event-sourced URLs regardless of rendering context, document CSS
injection prevention guidelines
The vulnerability (GHSA-95h2-gj7x-gx9w) allows bypassing hasDangerousProtocol()
in useHeadSafe() via leading-zero padded HTML entities. Not currently reachable
in this codebase (we only use useSeoMeta), but closes the CVE in the dependency
tree.
Add a shared sanitizeUrl() utility that validates URLs are well-formed
https: before they reach href attributes, window.open(), or openUrl().
Apply sanitization across all components that render untrusted URLs:
- CalendarEventDetailPage: r-tag links
- ZapstoreAppContent: url and repository tags
- ZapstoreReleaseContent: asset url tags passed to openUrl()
- AppHandlerContent: web handler tags and metadata.website
- NsiteCard: source tag
- GitRepoCard: web tag URLs passed to openUrl()
- FileMetadataContent: url tag used in download href
- ProfilePage: metadata.website (tighten weak startsWith check)
- useUserStatus: r-tag URL
Document sanitizeUrl usage in AGENTS.md for future agent use.
Detect lnbc/lntb/lnbcrt/lntbs invoices (with optional lightning: prefix)
in note text and render them as interactive cards with a theme-aware QR
code, decoded amount, copy button, and Open in Wallet action.
- Add lightning-invoice token type to NoteContent tokenizer
- Create LightningInvoiceCard with tap-to-expand square QR, cqw-scaled
amount text, and responsive layout
- Extract shared theme-aware QR color logic into src/lib/qrColors.ts
(deduplicate from FollowQRDialog)
Use capacitor-secure-storage-plugin to persist login credentials
(nsec keys) in iOS Keychain / Android KeyStore instead of plaintext
localStorage. Web behavior is unchanged. Existing native users are
auto-migrated on first launch: if secure storage is empty but
localStorage has data, it is moved over and the plaintext copy is
removed.
Also ignore ios/ directory in ESLint (Capacitor-generated files).
Upgrade to the new version that includes the NLoginStorage interface
and storage/fallback props on NostrLoginProvider for pluggable async
storage backends (e.g. Capacitor Secure Storage).
- Add resolve.dedupe for react/react-dom to prevent dual-React issues
- Update NoteContent tests to use async findBy* queries since the
provider now always awaits storage initialization
SandboxFrame's virtual script serving intercepted /webxdc.js and served
the empty placeholder content before resolveFile was ever called. The
dynamically generated bridge script (which embeds selfAddr etc.) was
never reaching the iframe.
Move bridge serving and HTML injection into resolveFileWithBridge so
the content is served from bridgeScriptRef after onReady populates it.
The sandbox frame was sending init immediately and calling onReady
concurrently, so fetch requests arrived before the archive was
downloaded and unzipped. Now onReady is awaited before init is sent,
matching the original Webxdc behavior.
Extract duplicated sandbox protocol logic from NsitePreviewDialog and
Webxdc into a single SandboxFrame component. Shared utilities (MIME
types, base64, HTML injection, JSON-RPC types) move to src/lib/sandbox/.
Add configurable sandboxDomain to AppConfig so the iframe.diy domain
can be overridden via ditto.json, preparing for native Capacitor
implementations.
Strip unused console/navigation/error RPC from previewInjectedScript,
leaving only the /index.html path normalization.
The Blobbi collection was previously discovered via the profile's has[] tag
list, meaning any blobbi whose d-tag was missing from that secondary index
would be invisible to the user despite existing on the relay.
Now useBlobbisCollection() without args queries all kind 31124 events by
author + ecosystem namespace tag — the user authored these events, so that
is the source of truth. The profile.has[] list is still used for selection
ordering preference, but no longer gates discovery.
The dList parameter remains available for targeted fetches (e.g. the
companion layer only needs one specific blobbi).
Adds a Mail-icon menu item in the profile more menu for other users'
profiles. Navigates to /letters/compose?to={npub} so the recipient is
pre-filled, matching the same flow used by the notification reply button.
Only show the delivery method radio group when push notifications are
enabled. Update the persistent option description to explain it is for
devices that don't support push notifications (e.g. GrapheneOS).
Default to push mode (no foreground service). Persistent mode with
the always-on background polling service is opt-in via the new
Delivery Method section in notification settings.
- Add notificationStyle ('push' | 'persistent') to EncryptedSettings
- Show radio group in NotificationSettings on native platforms
- Pass notificationStyle through Capacitor plugin to SharedPreferences
- DittoNotificationPlugin starts/stops foreground service on style change
- MainActivity only starts service on launch when style is persistent
- Re-enable unread polling on native when push mode is active
FollowPackDetailContent, TeamSoapboxCard, and InitialSyncGate all had
handleFollowAll implementations that queried kind 3 directly (bypassing
fetchFreshEvent) and rebuilt the tag array with only p-tags, silently
dropping all non-p-tags (relay hints, petnames, etc.). They also did
not pass prev for published_at preservation.
Align all three with the safe pattern already used in FollowPage and
useFollowActions.
The /follow route now accepts naddr1 identifiers for follow packs
(kind 39089) and follow sets (kind 30000) in addition to npub/nprofile.
Renders an immersive fullscreen layout with pack info hero, avatar
stack, big Follow All CTA with status indicator, and Feed/Members
tabs using the standard SubHeaderBar arc.
Follow All uses the safe fetch-fresh -> modify -> publish pattern
with prev for published_at preservation.
Shared components (PackFeedTab, MemberCard, MemberCardSkeleton) and
parsePackEvent are reused from FollowPackDetailContent and packUtils.
Also fixes SubHeaderBar tab indicator positioning when innerClassName
centers the tab container (adds containerOffset + ResizeObserver for
layout-dependent recalculation).
- Remove unused 'authors' parameter from useInfiniteHotFeed
- Extract inline query from Feed.tsx into useCuratedDittoFeed hook
- Use content-based fingerprint for query key instead of list length
- Add error state handling so curator fetch failure shows empty state
instead of infinite skeletons for first-time visitors
- Move hardcoded curator pubkey to AppConfig (curatorPubkey) so it
can be overridden via ditto.json without a code change
- Remove LANDING_KINDS/LANDING_WEBXDC_FILTER from Feed.tsx (now in hook)