Conversation
Extract the lightbox block and its state (lightboxUrl/Loading/Error, the loadMediaFile effect, closeLightbox) out of MediaGallery.svelte into a new MediaLightbox.svelte, rendered once from +page.svelte as a sibling of the main layout, outside .gallery-panel. Previously the lightbox was only reachable while the Media Gallery panel was open, because .gallery-panel is `overflow: hidden` and translated off-screen when closed, clipping any `position: fixed` descendant. MediaGallery keeps calling galleryState.setLightbox(path) but no longer renders the lightbox itself. The new component resolves the active item against galleryState.allItems (the UNFILTERED list), not the gallery's filtered `items`, via a new pure helper resolveLightboxItem() in src/lib/helpers/lightbox.ts. The gallery has no unit suite and vitest.config.ts runs only plain tests/**/*.test.ts in node with no Svelte plugin, so the pure resolver is tested directly in tests/gallery/lightbox-resolver.test.ts rather than through createGalleryState (which reads appState and needs a component context). Escape ownership: MediaLightbox now owns Escape for the lightbox via its own <svelte:window onkeydown>. MediaGallery and BookmarksPanel are both non-blocking side panels (chat stays visible/clickable next to them, unlike BookmarkModal's blocking full-screen backdrop), so their own Escape handlers now skip onClose() while the lightbox is open -- mirrors BookmarksPanel's pre-existing `!editingBookmark` guard for BookmarkModal -- so a single Escape press closes exactly one thing.
Wrap the image/sticker <img> in MessageBubble.svelte in a <button> that
calls the new onOpenMedia callback prop with the media file's path;
+page.svelte wires this to galleryState.setLightbox(path), same call
the Media Gallery's thumbnails already use. Threaded down through
ChatView's onOpenMedia prop, matching CLAUDE.md's "callback props for
events" convention.
Video keeps its native <controls> element untouched (clicking the
video itself still plays/pauses/scrubs); a small overlay button in the
corner opens the lightbox instead of hijacking the native controls'
click target. Audio is out of scope per the card.
New Paraglide key message_open_media ("Open media") is the aria-label
for both buttons; alt text on the image stays the file name. Only
messages/en.json is updated here -- the other 9 locales are added in
the following chore(i18n) commit via machine-translate (or a manual
fallback if that service is unavailable; see that commit).
Closes #92
npm run machine-translate failed in this environment: the inlang RPC service (rpc.inlang.com/_rpc, called with no INLANG_GOOGLE_TRANSLATE_API_KEY set) returned HTTP 404 "Application not found" for every request, including a bare ping -- an external service-side issue, not a local network/proxy problem (the same host reaches inlang.com and api.github.com fine). Translated de/es/fr/it/nl/pt/ru/zh/ja by hand instead, cross-checked against each locale's existing "media"/"open" vocabulary already in these files (e.g. media_gallery_title, media_gallery_go_to_message, export_step_1) for consistent phrasing. A reviewer with a working machine-translate setup should re-run it and diff against these values.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements GH-92: clicking an image, sticker, or video inside a chat bubble now opens it in the same standalone viewer the Media tab already uses.
Today's
MessageBubble.svelterenders the media<img>withcursor-pointerand no click handler — the cursor promises something that doesn't happen. This fixes that.What changed
feat(chat): hoist media lightbox to the app root). Extracted the lightbox block and its state out ofMediaGallery.svelteinto a newMediaLightbox.svelte, mounted once from+page.svelteas a sibling of the main layout, outside.gallery-panel. The gallery panel isoverflow: hiddenand translated off-screen when closed, which clips anyposition: fixeddescendant — so previously the lightbox was only reachable while the gallery panel itself was open.MediaGallerystill callsgalleryState.setLightbox(path)but no longer renders the lightbox itself.galleryState.allItems(unfiltered), not the gallery's filtereditems, so a bubble click resolves correctly even while the gallery has an active participant/type filter. This is a pure helper,resolveLightboxItem()insrc/lib/helpers/lightbox.ts, unit tested intests/gallery/lightbox-resolver.test.ts(the gallery has no component test harness —vitest.config.tsruns plain.tsfiles in node with no Svelte plugin — so the pure resolver is what's testable).feat(chat): open chat media in the shared lightbox). Image/sticker<img>is now wrapped in a<button>calling a newonOpenMediacallback prop, threadedMessageBubble→ChatView→+page.svelte→galleryState.setLightbox(path)(callback props, perCLAUDE.md). Video keeps its native<controls>untouched; a small overlay button opens the lightbox without hijacking the controls' click target. Audio is out of scope.MediaLightboxnow owns Escape for itself via its own<svelte:window onkeydown>. Tracing actual reachability turned up a real regression the hoist would otherwise have introduced:MediaGalleryandBookmarksPanelare both non-blocking side panels — the chat stays visible and clickable next to them (unlikeBookmarkModal's blocking full-screen backdrop) — so a bubble click can open the lightbox while either panel is open, and a single Escape press would then close both the lightbox and the panel at once. Both panels now skip their ownonClose()while the lightbox is open, mirroringBookmarksPanel's pre-existing!editingBookmarkguard forBookmarkModal.chore(i18n): translate message_open_media to remaining locales). New keymessage_open_media("Open media") added tomessages/en.jsonfirst.npm run machine-translatefailed in the sandbox this was built in —rpc.inlang.com/_rpcreturned HTTP 404 "Application not found" for every request, including a bare ping, which looks like an external service-side issue (the same host reachesinlang.comandapi.github.comfine). The other 9 locales were translated by hand, cross-checked against each file's existing "media"/"open" vocabulary for consistent phrasing. A reviewer with a working machine-translate setup should re-run it and diff against these values.Decisions recorded (per the originating card)
src/lib/helpers/lightbox.ts+tests/gallery/lightbox-resolver.test.ts(pure resolver, testable in node without a Svelte component context).MediaLightboxowns it;MediaGalleryandBookmarksPanelboth gained a guard (BookmarkModaldid not need one — it's a blocking overlay, mutually exclusive with the lightbox by construction).Verification
npm ci,npm run lint,npm run check,npm test,npm run build, andGITHUB_PAGES=true npm run buildall pass on the tip of this branch, and were also independently re-verified on each individual commit in isolation (viagit stash --keep-index) before it was made, per the "commit after each stage builds green" instruction.libglib-2.0.so.0,libnss3.so, etc. are absent system-wide). I traced the fix logically instead: the new<MediaLightbox>is a direct sibling of the<div class="h-screen ...">root wrapper in+page.svelte, outside every panel'soverflow:hidden/transformancestor, so its ownfixed inset-0 z-50should render unclipped regardless of gallery-panel state — but this is inference from source, not an observed screenshot.npm run dev, importexamples/chats/private-chat, make sure the Media Gallery panel is closed, then click a photo inside a chat bubble. The lightbox should appear centered over the whole window. Then open the Media Gallery, apply a participant or type filter that excludes the chat's media, and click a bubble image for media matching the excluded filter — the lightbox should still open (unfiltered lookup). Finally, open Bookmarks (or Media Gallery), click a bubble image to open the lightbox, and press Escape once — only the lightbox should close, not the side panel too.Out of scope
Prev/next navigation between lightbox items, zoom, download button, changing the gallery's grid — not implemented; flagged here as possible follow-ups per the originating card.
Closes #92