When the media tab was already mounted, clicking a media item in the sidebar
set sidebarMediaUrl (initialOpenUrl) but flatIndex state didn't update because
useState only uses its initial value at mount time. Add a useEffect to sync
flatIndex whenever initialIndex (derived from initialOpenUrl) changes.
- Make main a flex container on mobile so chat flex-1 works correctly
- Add overflow-hidden to chat wrapper to complete flex scroll chain
- Remove horizontal padding and border-radius from video on mobile
- Apply xl:rounded-2xl to player component (main and error states)
The cleanup guard in useLayoutOptions compared store.getSnapshot() (a LayoutSnapshot spread copy) against prev.current (the original LayoutOptions reference), which are always different objects. The guard was never true, so store.reset() never fired on unmount and the ProfileRightSidebar leaked to other pages. Added getOptions() to expose the raw options reference for a correct identity comparison.
- Resize reaction emojis to h-4 w-4 in stats row and reaction button
- Fix vertical alignment of emojis using inline-flex and leading-none
- Split reposts/quotes into separate stat buttons each opening correct tab
- Fix interactions modal tab sync when already open
- Un-react by clicking emoji button on already-reacted post
- Disable hover-to-open picker when already reacted
- Remove gap between triple emojis in stats row, suppress underline on hover
- Make client tag a link when value is a valid hostname
- 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)
Replace RepostMenu popover with a dedicated VineRepostButton that works
like VineHeartButton — single tap to repost, tap again to undo — with
optimistic cache updates so the button color changes immediately.
The popover menu was invisible on the dark fullscreen video background
and provided no visible confirmation of the action.
- VineHeartButton: add optimistic cache updates and query invalidation on
reaction so the heart fills and count increments immediately without
a page refresh
- RepostMenu: include NIP-18 'a' tag (kind:pubkey:d-tag) when reposting
addressable events (30000–39999) so kind 16 events are properly linked
back to the original vine
Use IntersectionObserver on the ComposeBox wrapper in Feed.tsx to track
its visibility. When visible, hide the FAB to avoid redundancy. When the
user scrolls past the compose area, the FAB smoothly fades in with an
upward slide animation. Extends LayoutStore with fabHidden state that
is part of the snapshot so useSyncExternalStore triggers re-renders.
Add responsive padding to lightbox swipe slots to clear the top bar and
bottom bar overlays. Change LightboxImage from w-full/h-full to
max-w-full/max-h-full so images scale within the padded area instead of
stretching behind the UI controls.
Register touchmove handler via addEventListener with passive:false
instead of React's onTouchMove prop, fixing the 'Unable to
preventDefault inside passive event listener' console warning.
Fix swiped images never appearing by checking img.complete on mount —
cached images from the pre-rendered prev/next slots may not fire onLoad,
leaving them stuck at opacity-0.
React Query serves cached data instantly for the previous event while the
new fetch is in-flight, causing stale replies to flash. Track the event id
the current data belongs to and treat any in-flight fetch for a different
event as a loading state, showing skeletons instead of stale replies.
Previously the query was gated on 'open', so when swiping to a new image
the old event's replies stayed visible until the new query resolved.
Now the event is always passed so the query key updates instantly.
- MediaGrid/Lightbox: support video (VideoPlayer) and audio (AudioVisualizer)
with proper per-type rendering in grid thumbs and lightbox strip slots
- Audio thumb shows author avatar with idle-ring animation instead of plain icon
- LightboxSlot renders VideoPlayer/AudioVisualizer constrained to max-w-lg
matching the audio visualizer width; resolves author avatar for audio via useAuthor
- useProfileMedia: add audio kinds 36787, 34139, 30054, 30055 to query
- CommentsSheet: kind-1 events query only kind-1 replies (not 1111), show
'Replies' header and 'Add a reply…' placeholder; all other events use 1111
- PhotoBottomBar: simplified back to single CommentsSheet; removed ReplyComposeModal
- CommentsSheet z-index raised to z-[200] so it appears above the Lightbox (z-[100])
- ProfileRightSidebar: onMediaClick callback; ProfilePage wires it to switch
to media tab and open lightbox at the clicked URL via initialOpenUrl prop
- MediaGrid flat entries now carry indexInEvent + countInEvent; counter in
top bar shows '2 / 4' for the current event only, hidden when single image
- swipeCommitted ref + rAF reset so strip snaps invisibly after commit
- Commit threshold lowered from 30% to 20% viewport width
Swipe: replace React state-driven translation (caused re-renders + jarring
snap-back) with direct DOM mutation via ref. Axis is locked on first
significant movement; at edges resistance is 20% rubber-band. On release,
if past the 30% threshold the next/prev fires and the position resets
without animation; otherwise it springs back with a single cubic-bezier
transition.
Bottom bar: matches top bar rhythm (px-4 py-3 + safe-area-bottom).
Gradient scrim is now a fixed h-32 pseudo-element that extends upward from
the bottom of the bar, independent of the content height, so it fades
cleanly over the image without needing extra pt-10 padding on the strip.
- Extract PhotoBottomBar into its own component; remove the bottom-nav clearance
padding (the full-screen lightbox overlay covers the nav bar, only safe-area
inset is needed)
- Create MediaGrid: generic 3-col square-thumbnail grid that handles imeta tags
(NIP-68/94 native kinds) and kind-1 inline image URLs; self-contains overlay
state and opens Lightbox with PhotoBottomBar + cross-event navigation
- PhotosFeedPage now delegates entirely to MediaGrid + MediaGridSkeleton
- Profile media tab replaced: was a NoteCard list, now renders MediaGrid for a
proper Instagram-style grid experience matching the /photos page
Replace the bespoke PhotoSlide / PhotoCard / PhotoDetailOverlay stack (~250 lines) with a
thin extension of the existing Lightbox component. Lightbox gains bottomBar, onNextEvent,
and onPrevEvent props; ImageGallery forwards them through. PhotosFeedPage now opens a plain
Lightbox with author info + reactions injected via bottomBar, and cross-event swipe/keyboard
navigation wired through onNextEvent/onPrevEvent. Page shrinks from 626 → ~300 lines.
Remove the invisible spacer div hack (w-8 shrink-0) and restore gap-3
on the search row. The actual fix is min-w-0 on the input, which lets
it shrink below its intrinsic minimum width in the flex container.
The search input with flex-1 extended ~32px further right than item
labels below (which have a Plus button consuming that space). Added a
matching w-8 spacer and min-w-0 so the input aligns with item labels
and cannot force the row wider than the dropdown.
The bg-muted/50 background on the search row extended wider than the
item rows below (which have a Plus button consuming right-side space),
creating a visible purple block and visual misalignment that made
content appear truncated on the left.
The More... trigger sits at the bottom of the sidebar, so the dropdown
was opening downward into very little available space, getting clipped.
Use side="top" so it opens upward where there is ample room, and add
collisionPadding for a safety margin from viewport edges.
The original border-b border-border was removed from NoteCard in 9bd646b
for a photos overlay feature that no longer uses NoteCard. This forced
every container to add divide-y divide-border as a workaround, but
several pages were missed (Bookmarks, Events, Trends, Search, etc).
Restore NoteCard's self-bordering and remove the now-redundant divide-y
wrappers. Existing overrides (EmbeddedNaddr !border-b, Notifications
border-0) continue to work correctly.