Merge pull request #4349 from nesquena/stage-4339
Release v0.51.472 — paste image + text together (#4339)
This commit is contained in:
3 files changed
+49
-15
No files matched your search
@@ -3,6 +3,12 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [v0.51.472] — 2026-06-17 — Release QG (paste image + text together)
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Pasting an image copied from a browser, Notes, or Slack now attaches the image again (#1620 regression).** When the clipboard carries both text and an image (the normal shape for "right-click → Copy Image" and most rich-source copies), the composer paste handler previously bailed entirely and silently dropped the image. It now attaches any real image (`kind==='file'` image item) while still letting the browser paste accompanying text normally — matching how the major AI chat UIs handle paste. Pure screenshots still attach as before, plain text still pastes with no attachment, and a rich-text editor's rendered HTML preview (a `text/html` string, not a file) is not misclassified as an image. Thanks @kaishi00.
|
||||
|
||||
## [v0.51.471] — 2026-06-17 — Release QF (searchable settings)
|
||||
|
||||
### Added
|
||||
|
||||
+8
-9
@@ -1531,16 +1531,15 @@ document.addEventListener('keydown',async e=>{
|
||||
});
|
||||
$('msg').addEventListener('paste',e=>{
|
||||
const items=Array.from(e.clipboardData?.items||[]);
|
||||
// When the clipboard carries BOTH text and an image (common from Notes,
|
||||
// Word, browsers, Slack — the OS attaches a rendered preview alongside
|
||||
// the plain text), prefer the text and let the browser paste normally.
|
||||
// Only intercept when the clipboard is image-only (true screenshot paste).
|
||||
// Tighten the image filter to kind==='file' so string items advertising an
|
||||
// image MIME (e.g. text/html with an embedded data URI) are not misclassified.
|
||||
const hasText=items.some(i=>i.kind==='string'&&(i.type==='text/plain'||i.type==='text/html'));
|
||||
// Extract image items (kind==='file' filter avoids misclassifying text/html
|
||||
// with embedded data URIs as images).
|
||||
const imageItems=items.filter(i=>i.kind==='file'&&i.type.startsWith('image/'));
|
||||
if(!imageItems.length||hasText)return;
|
||||
e.preventDefault();
|
||||
if(!imageItems.length)return;
|
||||
// If text is also present (common when copying images from browsers, Notes,
|
||||
// Slack, etc.), let the browser paste the text normally AND attach the image.
|
||||
// Only preventDefault when the clipboard is image-only (true screenshot paste).
|
||||
const hasText=items.some(i=>i.kind==='string'&&(i.type==='text/plain'||i.type==='text/html'));
|
||||
if(!hasText)e.preventDefault();
|
||||
const pasteTs=Date.now();
|
||||
const files=imageItems.map((i,idx)=>{
|
||||
const blob=i.getAsFile();
|
||||
|
||||
@@ -67,15 +67,44 @@ class TestPasteHandlerTextWithImage:
|
||||
body,
|
||||
), "imageItems filter must use kind === 'file' && type.startsWith('image/')"
|
||||
|
||||
def test_handler_skips_attach_when_text_present(self):
|
||||
"""The early-return guard must short-circuit when text is in the clipboard,
|
||||
so the browser's default text-paste runs and no image is attached."""
|
||||
def test_handler_skips_preventDefault_when_text_present(self):
|
||||
"""When text is in the clipboard alongside an image, preventDefault() must
|
||||
NOT be called so the browser's native text-paste runs. The handler should
|
||||
still attach the image via addFiles(). The #1620 intent (don't swallow text)
|
||||
is preserved by gating preventDefault on !hasText instead of bailing entirely.
|
||||
Additionally, text-only clipboards (no image items) must early-return before
|
||||
reaching the attach path."""
|
||||
body = _paste_handler_body()
|
||||
# Guard shape: if(!imageItems.length || hasText) return;
|
||||
# Text-only early-out: if no image items, return before any attach logic.
|
||||
assert re.search(
|
||||
r"if\s*\(\s*!\s*imageItems\.length\s*\|\|\s*hasText\s*\)\s*return\s*;",
|
||||
r"if\s*\(\s*!\s*imageItems\.length\s*\)\s*return\s*;",
|
||||
body,
|
||||
), "guard must early-return when there are no image files OR text is present"
|
||||
), "handler must early-return when there are no image files (text-only paste)"
|
||||
# preventDefault must be gated on !hasText so text+image pastes let the
|
||||
# browser insert text natively.
|
||||
assert re.search(
|
||||
r"if\s*\(\s*!\s*hasText\s*\)\s*e\.preventDefault\s*\(\s*\)\s*;",
|
||||
body,
|
||||
), "preventDefault must be gated on !hasText (only intercept image-only paste)"
|
||||
|
||||
def test_handler_attaches_image_when_both_text_and_image_present(self):
|
||||
"""When the clipboard carries both text and image items (e.g. browser
|
||||
Copy Image), addFiles() must still be reached so the image is attached.
|
||||
The image attach path must NOT be guarded by hasText."""
|
||||
body = _paste_handler_body()
|
||||
# addFiles must appear AFTER the preventDefault gate, reachable regardless
|
||||
# of whether text is present.
|
||||
prevent_idx = body.find("preventDefault")
|
||||
addfiles_idx = body.find("addFiles(files)")
|
||||
assert addfiles_idx > prevent_idx, (
|
||||
"addFiles(files) must come after the preventDefault gate and be "
|
||||
"reachable even when hasText is true"
|
||||
)
|
||||
# No guard that bails before addFiles when hasText is true.
|
||||
assert not re.search(
|
||||
r"hasText\s*\)\s*return\s*;",
|
||||
body,
|
||||
), "handler must NOT early-return on hasText before addFiles (regression for #1620 fix)"
|
||||
|
||||
def test_handler_still_intercepts_pure_screenshot_paste(self):
|
||||
"""Pure-screenshot paste (image-only clipboard) must still call preventDefault()
|
||||
|
||||
Reference in new issue
Block a user