Skip to content

feat(chat): open chat images and stickers in the media lightbox (GH-92) - #94

Open
rodrigogs wants to merge 3 commits into
devfrom
feat/gh-92-chat-lightbox
Open

rodrigogs wants to merge 3 commits into
devfrom
feat/gh-92-chat-lightbox

Conversation

@rodrigogs

Copy link
Copy Markdown
Owner

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.svelte renders the media <img> with cursor-pointer and no click handler — the cursor promises something that doesn't happen. This fixes that.

What changed

  1. Hoisted the lightbox (feat(chat): hoist media lightbox to the app root). Extracted the lightbox block and its state out of MediaGallery.svelte into a new MediaLightbox.svelte, mounted once from +page.svelte as a sibling of the main layout, outside .gallery-panel. The gallery panel is overflow: hidden and translated off-screen when closed, which clips any position: fixed descendant — so previously the lightbox was only reachable while the gallery panel itself was open. MediaGallery still calls galleryState.setLightbox(path) but no longer renders the lightbox itself.
  2. Unfiltered lookup. The new component resolves the active item against galleryState.allItems (unfiltered), not the gallery's filtered items, so a bubble click resolves correctly even while the gallery has an active participant/type filter. This is a pure helper, resolveLightboxItem() in src/lib/helpers/lightbox.ts, unit tested in tests/gallery/lightbox-resolver.test.ts (the gallery has no component test harness — vitest.config.ts runs plain .ts files in node with no Svelte plugin — so the pure resolver is what's testable).
  3. Bubble wiring (feat(chat): open chat media in the shared lightbox). Image/sticker <img> is now wrapped in a <button> calling a new onOpenMedia callback prop, threaded MessageBubbleChatView+page.sveltegalleryState.setLightbox(path) (callback props, per CLAUDE.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.
  4. Escape ownership. MediaLightbox now owns Escape for itself via its own <svelte:window onkeydown>. Tracing actual reachability turned up a real regression the hoist would otherwise have introduced: MediaGallery and BookmarksPanel are both non-blocking side panels — the chat stays visible and clickable next to them (unlike BookmarkModal'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 own onClose() while the lightbox is open, mirroring BookmarksPanel's pre-existing !editingBookmark guard for BookmarkModal.
  5. i18n (chore(i18n): translate message_open_media to remaining locales). New key message_open_media ("Open media") added to messages/en.json first. npm run machine-translate failed in the sandbox this was built in — rpc.inlang.com/_rpc returned HTTP 404 "Application not found" for every request, including a bare ping, which looks like an external service-side issue (the same host reaches inlang.com and api.github.com fine). 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)

  • Helper/test location: src/lib/helpers/lightbox.ts + tests/gallery/lightbox-resolver.test.ts (pure resolver, testable in node without a Svelte component context).
  • Escape ownership: MediaLightbox owns it; MediaGallery and BookmarksPanel both gained a guard (BookmarkModal did not need one — it's a blocking overlay, mutually exclusive with the lightbox by construction).
  • Out of scope (follow-ups, not implemented): prev/next navigation between lightbox items, zoom, download button, changing the gallery's grid.

Verification

  • npm ci, npm run lint, npm run check, npm test, npm run build, and GITHUB_PAGES=true npm run build all pass on the tip of this branch, and were also independently re-verified on each individual commit in isolation (via git stash --keep-index) before it was made, per the "commit after each stage builds green" instruction.
  • Manual DOM check NOT performed — no browser could be launched on the build host for this PR (Playwright's Chromium is present but its shared-library dependencies are missing, and there is no sudo/package-manager access to install them: 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's overflow:hidden/transform ancestor, so its own fixed inset-0 z-50 should render unclipped regardless of gallery-panel state — but this is inference from source, not an observed screenshot.
    • What the reviewer should manually verify: run npm run dev, import examples/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

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE] Clicking on media in the chat should open in a standalone viewer when clicked

1 participant