The mobile nav drawer in TopNav has its own X button inside the panel
header, but SheetContent was also rendering the shadcn primitive close
button just outside the panel — two X buttons for the same sheet.
Add an opt-in `hideClose` prop to SheetContent and set it on the
TopNav drawer. Other Sheet consumers (MobileDrawer, etc.) keep the
default built-in close.
Install @capacitor/haptics and add a centralized haptics utility
(src/lib/haptics.ts) that uses the native taptic engine on iOS/Android
and falls back to navigator.vibrate() on web.
Haptics added to:
- Switch component (covers 36+ toggle switches app-wide)
- PullToRefresh threshold (covers 15+ pages)
- MobileBottomNav tab taps
- ReactionButton (like/unlike, double-click heart)
- RepostMenu (repost/undo repost)
- ZapDialog button press + payment success (NWC and WebLN)
- FollowButton and ProfilePage follow toggle
- ComposeBox (post, voice message, and poll publish success)
- NoteMoreMenu (bookmark, pin, mute)
- VinesFeedPage reaction and repost buttons
- ProfileReactionButton and ExternalReactionButton
- NoteCard share button
- BlobbiRoomShell swipe navigation
Replaces raw navigator.vibrate() calls in GameControls and
SendAnimation with the new cross-platform haptics utility, fixing
haptic feedback on iOS where the Vibration API is not available.
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)
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.
The keyboard-aware repositioning of dialogs was too aggressive and broken.
Removes the CSS rule, dialog-keyboard-aware class, and global keyboard
detector mount. The useKeyboardVisible hook is preserved for ArticleEditor.
- Add rounded-xl to Dialog and AlertDialog (was sm:rounded-lg only)
- Add consistent gap-2 to footer buttons on mobile (was no gap)
- Use w-[calc(100%-2rem)] for mobile side margins
- Push dialogs to top of viewport only when keyboard is visible via
.keyboard-visible class on <html>, toggled by useKeyboardVisible
- Mount useKeyboardVisible globally in MainLayout so the class is
always available for CSS-only consumers
The Avatar component was initializing maskUrl as '' and loading it in a
useEffect. Since hasCustomShape was true immediately, rounded-full was
removed on the first render, but the mask wasn't applied until after the
effect fired — causing a visible square flash for one frame.
getAvatarMaskUrl is already synchronous (renders emoji to canvas, caches
the data-URL), so compute it inline during render instead of deferring
to an effect. The mask is now applied on the very first paint.
- Pinned SubHeaderBar uses safe-area-inset-top (top offset) instead of
safe-area-top (padding) so the arc stays flush with the tab content
- Throttle signer nudge toasts (8s cooldown) to prevent rapid-fire storm
when relay connection is unstable
- New posts pill fades out when nav hides instead of translating, avoiding
it floating in the safe area zone
- Signer toasts use finite duration (120s) so Radix swipe-to-dismiss works
- Lower toast swipe threshold from 50px to 30px for easier dismissal
- Upgrade react, react-dom to ^19.2.4 and @types/react, @types/react-dom to v19
- Upgrade @nostrify/react to 0.4.0 (peer deps fix for React 19)
- Upgrade vaul to 1.1.2 and react-day-picker to 9.14.0 for React 19 compatibility
- Fix useRef() calls to pass explicit initial values (required in React 19)
- Update RefObject types to include null (React 19 type change)
- Rewrite Calendar component for react-day-picker v9 classNames API
- Add npm overrides for @emoji-mart/react (only remaining React 18 holdout)
On mobile, toasts enter from the top but previously could only be swiped
right to dismiss. Now swipe direction is responsive: swipe up on mobile
(top-positioned), swipe right on desktop (bottom-right positioned). Exit
animations also match the swipe direction at each breakpoint.
- Remove BLOBBI_TOPIC_TAG and BLOBBI_CLIENT_TAG from event building
- Add t and client to DEPRECATED_BLOBBI_TAG_NAMES for migration cleanup
- Update validation functions to not require t tag
- Delete blobbiShapes.ts and BlobbiShapePicker.tsx entirely
- Simplify avatarShape.ts to only support emoji shapes
- Remove blobbi_shape task from useEvolveTasks
- Remove change_shape task from useHatchTasks
- Remove change_shape mission from daily-missions
- Clean up ProfileCard shape picker to only show emoji picker
The app's useNostrPublish hook already adds client tags automatically,
making the explicit client tag redundant. Old events with these tags
will have them stripped on next save.
Moves the arc from the generic Sheet component into MobileDrawer
so it can inherit the theme background image. Uses clip-path
ellipse on an HTML div instead of SVG fill, with the same
bg-background/70 overlay applied when a background image is set.
Increases the arc width from 20px to 36px with a deeper curve,
and changes fill from background/80 to solid background to
match the drawer's actual background color.
Removes the flat right border from the left sheet variant and
adds an SVG arc that curves outward along the right edge,
matching the curved styling used on the horizontal nav bars.
Replaces the flat right border with an SVG arc that curves
outward along the full height of the drawer, matching the
curved design language used on the nav bars.
Amber users on Android who manually approve events must switch from Ditto to
Amber to approve, then switch back. Backgrounding Ditto can freeze its
WebSocket, causing the NIP-46 response to be silently dropped — leaving the
operation hanging with no feedback and no way out. Users with Amber
notifications working correctly are unaffected, as approving via notification
does not background Ditto.
Even with auto-approve enabled, kinds outside Amber's default whitelist
require manual approval. Ditto uses several of these regularly: kind 1059
(NIP-17 gift-wrap DMs), 1111 (comments), 1311 (live chat), 31925 (RSVPs),
24242 (Blossom file upload auth), and 30078 (app settings).
This introduces signerWithNudge, a NostrSigner wrapper with the following
behaviour:
Nudge toast after 4 seconds
If a signing or encryption op is still pending after 4 s, a persistent toast
appears naming what is being approved (e.g. 'Approve file upload auth'), with
a human-readable label derived from the event kind.
Android 'Approve in signer' button
On Android the nudge toast includes an 'Approve in signer' button that opens
Amber via the nostrsigner: URI scheme, keeping the WebSocket alive. After
tapping, the button becomes a spinner and a Cancel button appears.
Automatic retry on foreground resume
When Ditto returns to the foreground after being backgrounded, it
automatically retries the pending NIP-46 request (up to 2 times) and shows a
brief 'Checking for signer response' toast.
Hard 45-second timeout
Operations with no response within 45 s are rejected with a clear error.
Cancel / Skip
The nudge toast has a Skip link throughout. After tapping 'Approve in signer'
it becomes a full Cancel button.
Multi-phase encrypt-then-sign
Saving app settings (kind 30078) and mute lists (kind 10000) require a nip44
encrypt followed immediately by a signEvent. When the encrypt nudge was shown,
a phase-transition toast tells the user a second approval is coming. The check
is kind-specific to avoid false positives.
Success feedback
A brief 'Approved' toast confirms the outcome when the nudge was shown.
Relay connectivity check
At nudge time, if the bunker relay WebSocket is not OPEN the toast warns
'Signer relay unreachable' instead of prompting for an approval that cannot
be delivered.
Accessibility
Toast buttons meet the 44 px touch target minimum. Text size and contrast
were increased for readability on small screens.
CSS mask-image with SVG data URLs doesn't work reliably for complex SVGs
with transforms in some browsers. The picker preview works because it renders
inline SVG directly, but the avatar mask-image was failing for shapes like
droppi, flammi, leafy, mushie, owli, rocky, and rosey.
Changes:
- Rewrite getBlobbiMaskUrl() to render SVG to canvas and export PNG
- Use Blob URL for SVG loading (more reliable than data URL)
- Add async mask generation with proper caching and deduplication
- Update Avatar component to load masks asynchronously via useEffect
- Add getAvatarMaskUrlAsync() for async mask URL retrieval
The picker preview continues to use inline SVG (which works fine),
while the avatar mask now uses rasterized PNG (which works everywhere).
1. Remove stroke="white" from catti shape - let the styling apply colors
2. Fix Avatar component to properly update mask when shape changes:
- Compute maskUrl outside useMemo so it's always fresh
- Use maskUrl directly in dependencies instead of shape string
- This ensures the mask updates immediately when selecting a new shape
- avatar.tsx: consolidate to single isEmojiShape boolean, fix AvatarFallback using same logic as Avatar
- avatarShape.ts: accept NostrMetadata directly in getAvatarShape (no cast needed)
- Remove 'as Record<string, unknown>' casts from all ~50 call sites
- Replace 'circle' magic string with empty string in form defaults and parseShape
- Use isValidAvatarShape instead of string comparison in save logic
- ProfileCard: extract IIFE overlay style into useMemo, use isEmojiShape throughout
Extend the kind-0 'shape' field to accept any emoji string in addition to
the predefined geometric shapes (circle, triangle, hexagon, star, etc.).
When an emoji is chosen, its native OS glyph is rendered onto a canvas,
the alpha channel extracted as a PNG mask, and applied via CSS mask-image.
Implementation:
- Widen AvatarShape type to accept emoji strings alongside predefined names
- isEmoji() detects any short non-ASCII string (avoids fragile Unicode regex)
- getEmojiMaskUrl() renders emoji at 512px, crops to tight bounding box
(with alpha threshold to ignore shadows/glows), squares the crop, scales
to 256px output, converts to white+alpha mask PNG. Cached per emoji.
- Avatar component applies mask-image for emoji, clip-path for geometric
- ProfileCard uses ring-4 instead of border-4 for emoji shapes to avoid
mask/content misalignment (border is inside the element; ring is outside)
- AvatarShapePicker adds emoji-mart picker in a popover
- Form schemas use z.string() to accommodate both predefined and emoji values
- NIP.md documents emoji shape support
- New 'shape' property on kind 0 profile metadata with 7 predefined shapes:
circle, triangle, inverted-triangle, hexagon, star, inverted-star, hexagram
- Avatar component updated with clip-path support for non-circle shapes
- Visual shape picker added to both EditProfileForm and ProfileSettings
- Shape applied across all 49 avatar render sites in the app
- New UserAvatar wrapper component for future simplified avatar rendering
- Unknown shape values gracefully fall back to circle (forward compatible)
- Add 'How do I report harmful content?' help tip to Muted Content and Sensitive Content sections
- Add 'For You page' help tip to Home Feed settings header
- Move wallet help tip from NWC section to Wallet page header
- Add 'What are zaps?' help tip to Zap dialog title
- Add 'Mastodon/Bluesky comparison' help tip to Platform filter in search
- Fix Select dropdown z-index (250->270) so dropdowns render above filter popover (260)
- Raised dialog z-index from z-[150] to z-[250] so the overlay sits above
CommentsSheet's backdrop (z-[200]) and receives touch events
- Removed stopPropagation from DialogOverlay/DialogContent onClick handlers
which was preventing Radix DismissableLayer's deferred click listener from
reaching document on iOS Safari
On iOS touch, Radix registers a one-time click listener on document after
pointerdown. The stopPropagation was intercepting that click before it could
reach document, so the dismiss callback never fired. Z-index stacking now
handles isolation between the dialog and the CommentsSheet backdrop.
Radix Dialog's RemoveScroll blocks wheel events on elements outside the
Dialog's DOM tree. The GIF picker Popover was portaling to document.body,
placing it outside the RemoveScroll boundary. Trackpad scrolling was
blocked while scrollbar dragging still worked.
Added PortalContainerContext so PopoverContent can portal into the modal
element instead of document.body when rendered inside a dialog/modal.
SelectContent used z-50 while PopoverContent used z-[200]. Both are
portaled to the body, so when a Select inside a Popover is opened,
the Select dropdown rendered behind the Popover panel. Raised
SelectContent to z-[250] so it always appears above popovers.
Closes#55
- ComposeBox: invalidate the correct 'event-comments' cache key after
posting a NIP-22 comment so CommentsSheet refreshes immediately
- toast: raise z-index to 300 so it renders above the CommentsSheet
backdrop blur (which sits at z-200)