Bring back the mobile bottom navigation bar with Home, Notifications, and
Search buttons. The nav slides out of view when scrolling down and reappears
on upward scroll, keeping the viewport uncluttered while reading. Search
retains the popover account search sheet for fast profile lookup without
navigating away. Updated CSS height calculations for full-screen pages
(AI chat, vines, livestream) and FAB positioning to account for the nav.
Replace border/ring with a crisp drop-shadow outline (via emojiAvatarBorderStyle)
so the border follows the emoji mask shape without clipping. Shrink emoji avatars
by the border width so they match the visual footprint of bordered circular ones.
The mute list (kind 10000) was only reading encrypted content, ignoring
public tags in the event's tags array. Per NIP-51, mute items can be
stored as either public tags or private encrypted content. Most Nostr
clients store mutes as public tags, so these were being silently ignored.
Changes:
- Parse both public tags and encrypted content from kind 10000 events
- Add NIP-04 backward compatibility (detect by '?iv=' in ciphertext)
- Deduplicate items that appear in both public and private sections
- Apply same fixes to useInitialSync for login-time bootstrapping
Fixes: https://gitlab.com/soapbox-pub/ditto/-/work_items/96
- 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
Move avatar shape selection from standalone sections in EditProfileForm and
ProfileSettings into a dropdown menu on the ProfileCard avatar itself.
Clicking the avatar now shows a context menu instead of immediately opening
the file browser.
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
clip-path: path() uses pixel coordinates, making a 0-1 normalized path
invisible. Replaced with a parametric heart curve sampled into a 50-point
polygon using percentage coordinates, consistent with all other shapes.
- Add 'heart' as the 8th avatar shape using SVG path() clip-path with cubic beziers
- Document the kind 0 'shape' metadata extension in NIP.md with all 8 defined values,
client behavior rules, and forward-compatibility guidance
Resolve merge conflicts from badges, books, help, and emojis rename.
Fix biome lint: add type="button" to all buttons, use template literals,
format new components with project conventions.
- 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)
Replace Vite 6 (esbuild + Rollup) with Vite 8 (Rolldown), reducing
build times from ~12s to ~3.5s. Switch from the archived
@vitejs/plugin-react-swc to @vitejs/plugin-react v6 which uses Oxc
for React Refresh transforms.
Pass markLoaded directly to LightboxSlot/LightboxImage as (url: string) => void,
and derive a stable handleLoaded callback via useCallback inside LightboxImage.
This removes the eslint-disable comment and properly declares all useEffect deps.
The onLoad callback passed to LightboxImage was recreated on every render
(inline arrow function). When combined with a cached image triggering the
useEffect, this caused: effect fires → onLoad() → setState → re-render →
new onLoad ref → effect fires again. Remove onLoad from the dependency
array since the check only needs to run when src changes.
The MediaGrid now renders its own skeleton rows when hasNextPage is true,
so the external Loader2 spinner is no longer needed. Keep the intersection
observer sentinel div for infinite scroll triggering.
When hasNextPage is true and the last row is incomplete (not fully packed),
hide it to prevent oversized straggler thumbnails. Show 2 skeleton rows as
a loading placeholder. Once the next page loads, those items get properly
packed with new neighbors.
Items in a row now all stretch to the full row height. The row's aspect
ratio is the sum of its items' aspect ratios (since at equal height, total
width = sum of ARs * height). Individual items use flexGrow proportional
to their AR and h-full to fill the row.
On mobile (<768px), limit rows to 2 items max with a taller target row
height (45% vs 30%), so each image gets more screen real estate. Skeleton
loading state also adapts to show 2 items per row on mobile.
The paddingBottom percentage approach doesn't work correctly in flex layouts
because percentages are relative to the containing block's width, not the
element's own computed width. Switch to CSS aspect-ratio property with
flexGrow proportional to each item's aspect ratio, which correctly sizes
items within flex rows.
Use a Google Photos-style justified row layout that respects image aspect
ratios from imeta dim tags. Images without dim data default to 1:1 ratio.
Applies to both the profile Media tab (MediaGrid) and the sidebar Media
widget (ProfileRightSidebar).
nos.lol requires 28-bit proof-of-work for EVENT submissions, which the
app doesn't implement. Every publish attempt was failing against this
relay. Changed to read-only so we still benefit from reading its data
without broken write attempts.
The theme dropdown was appearing behind the account popover because
DropdownMenuContent (z-200) portals below PopoverContent (z-260).
Bumped theme dropdown z-index to 270 so it renders above. Also capped
featured preset themes to 5 in both sidebar dropdowns to keep the menu
manageable, with a 'More...' link for the full list.
The URL→state sync effect was overwriting the input with stale
debounced values. When the debounce fired (e.g. 'hello'), it updated
?q in the URL, which triggered the reverse sync to overwrite the
input — even if the user had already typed further (e.g. 'hello w').
Use a ref guard so the URL→state sync only runs for external URL
changes (browser navigation, sidebar search), not for our own writes.
Remove SearchPageKeyed wrapper that used ?q as a React key, causing
the entire SearchPage to remount (and lose input focus) whenever the
URL query parameter changed. Debounce the search query before syncing
it to the URL and passing it to data-fetching hooks, preventing
excessive re-renders and relay queries while typing.