diff --git a/static/style.css b/static/style.css index 21df92e8..dfa603b9 100644 --- a/static/style.css +++ b/static/style.css @@ -894,7 +894,8 @@ .attach-chip--audio,.attach-chip--video{max-width:260px;} .attach-media-icon{display:inline-flex;align-items:center;color:var(--accent-text);} .attach-chip-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;} - .attach-thumb{width:56px;height:56px;object-fit:cover;border-radius:4px;display:block;cursor:default;} + .attach-thumb{width:56px;height:56px;object-fit:cover;border-radius:4px;display:block;cursor:zoom-in;transition:filter .12s ease, transform .12s ease;} + .attach-thumb:hover{filter:brightness(1.05);transform:scale(1.04);} textarea#msg{width:100%;background:transparent;border:none;outline:none;color:var(--text);font-size:16px;line-height:1.65;padding:12px 16px 6px;resize:none;min-height:44px;max-height:200px;font-family:inherit;} textarea#msg::placeholder{color:var(--muted);} .composer-footer{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:6px 10px 10px;position:relative;container-type:inline-size;container-name:composer-footer;} diff --git a/static/ui.js b/static/ui.js index 38ce6810..1004e050 100644 --- a/static/ui.js +++ b/static/ui.js @@ -296,9 +296,19 @@ function _closeImgLightbox(lb) { } document.addEventListener('click', e => { - const img = e.target && e.target.closest ? e.target.closest('.msg-media-img') : null; - if(!img) return; - _openImgLightbox(img.src, img.alt); + if(!e.target || !e.target.closest) return; + // Message-attached images (already wired since v0.50.x). + let img = e.target.closest('.msg-media-img'); + if(img){ _openImgLightbox(img.src, img.alt); return; } + // Composer attach-tray image thumbnails — click any pasted/dropped image + // chip to lightbox-zoom it before sending. Excludes audio/video chips, + // which keep their inline media controls. SVG thumbnails (.attach-thumb--svg) + // are still images visually, so they qualify. + img = e.target.closest('.attach-thumb'); + if(img && img.tagName === 'IMG'){ + _openImgLightbox(img.src, img.alt || img.title || 'Attached image'); + return; + } }); const _IMAGE_EXTS=/\.(png|jpg|jpeg|gif|webp|bmp|ico|avif)$/i; diff --git a/tests/test_composer_chip_lightbox.py b/tests/test_composer_chip_lightbox.py new file mode 100644 index 00000000..dbf04334 --- /dev/null +++ b/tests/test_composer_chip_lightbox.py @@ -0,0 +1,100 @@ +"""Regression tests for composer attach-thumb lightbox click behaviour. + +User pasted/dropped/picked an image and wants to verify the right one +attached before sending. Clicking the thumbnail in the composer's +attach-tray should open the existing image lightbox (the same one +that's wired to message-attached images). + +This file pins the wiring at the source level — the document-level +delegated click handler must: + - Continue handling .msg-media-img (existing v0.50.x behaviour). + - Also handle .attach-thumb on IMG elements (new in this PR). + - NOT trigger on the chip's × remove button (sibling element). + - NOT trigger on audio/video chips (those have native controls). + +It also pins the CSS cursor affordance so users discover the feature. +""" +from pathlib import Path + + +ROOT = Path(__file__).resolve().parent.parent +UI = ROOT / "static" / "ui.js" +STYLE = ROOT / "static" / "style.css" + + +class TestComposerChipLightboxDelegate: + def test_delegate_handles_attach_thumb_clicks(self): + """The document click handler must pick up clicks on .attach-thumb + (composer image chips) and route them to _openImgLightbox(). + + Previously the handler only looked for .msg-media-img. + """ + src = UI.read_text(encoding="utf-8") + assert "e.target.closest('.attach-thumb')" in src, ( + "Document click delegate must also match .attach-thumb" + ) + # And it must call _openImgLightbox in that path. + # Use a tighter anchor block to ensure both branches are wired. + anchor = ( + "img = e.target.closest('.attach-thumb');\n" + " if(img && img.tagName === 'IMG'){\n" + ) + assert anchor in src + + def test_delegate_still_handles_message_attached_images(self): + """Existing .msg-media-img wiring must not regress.""" + src = UI.read_text(encoding="utf-8") + # The message-image branch must come first (so _openImgLightbox + # fires for them without falling through to the .attach-thumb check). + msg_branch = "let img = e.target.closest('.msg-media-img');\n if(img){ _openImgLightbox(img.src, img.alt); return; }" + assert msg_branch in src + + def test_delegate_excludes_audio_video_chips(self): + """Audio/video chips have their own inline controls (native