fix(desktop): dedupe clipboard image paste
Chromium exposes the same pasted image on both DataTransfer.items and .files as distinct Blob objects, which attached twice. Prefer items and skip the files mirror when items already yielded images.
This commit is contained in:
@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { detectTrigger } from './text-utils'
|
||||
import { blobDedupeKey, detectTrigger, extractClipboardImageBlobs } from './text-utils'
|
||||
|
||||
describe('detectTrigger', () => {
|
||||
it('detects a bare slash trigger with an empty query', () => {
|
||||
@ -23,3 +23,55 @@ describe('detectTrigger', () => {
|
||||
expect(detectTrigger('hello there')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('extractClipboardImageBlobs', () => {
|
||||
it('dedupes the same image exposed on both items and files', () => {
|
||||
const image = new File([new Uint8Array([1, 2, 3])], 'paste.png', {
|
||||
type: 'image/png',
|
||||
lastModified: 1_700_000_000_000
|
||||
})
|
||||
|
||||
const clipboard = {
|
||||
files: {
|
||||
length: 1,
|
||||
item: (index: number) => (index === 0 ? image : null)
|
||||
},
|
||||
getData: () => '',
|
||||
items: [
|
||||
{
|
||||
kind: 'file',
|
||||
type: 'image/png',
|
||||
getAsFile: () => image
|
||||
}
|
||||
]
|
||||
} as unknown as DataTransfer
|
||||
|
||||
expect(extractClipboardImageBlobs(clipboard)).toEqual([image])
|
||||
})
|
||||
|
||||
it('falls back to files when items has no image', () => {
|
||||
const image = new File([new Uint8Array([4, 5])], 'shot.jpg', {
|
||||
type: 'image/jpeg',
|
||||
lastModified: 1_700_000_000_001
|
||||
})
|
||||
|
||||
const clipboard = {
|
||||
files: {
|
||||
length: 1,
|
||||
item: (index: number) => (index === 0 ? image : null)
|
||||
},
|
||||
getData: () => '',
|
||||
items: []
|
||||
} as unknown as DataTransfer
|
||||
|
||||
expect(extractClipboardImageBlobs(clipboard)).toEqual([image])
|
||||
})
|
||||
})
|
||||
|
||||
describe('blobDedupeKey', () => {
|
||||
it('uses file metadata for File blobs', () => {
|
||||
const file = new File([], 'a.png', { type: 'image/png', lastModified: 42 })
|
||||
|
||||
expect(blobDedupeKey(file)).toBe('file:a.png:0:image/png:42')
|
||||
})
|
||||
})
|
||||
|
||||
@ -8,16 +8,31 @@ export interface TriggerState {
|
||||
|
||||
const TRIGGER_RE = /(?:^|[\s])([@/])([^\s@/]*)$/
|
||||
|
||||
/** Stable key for paste dedupe — `items` and `files` often mirror the same image as different objects. */
|
||||
export function blobDedupeKey(blob: Blob): string {
|
||||
if (blob instanceof File) {
|
||||
return `file:${blob.name}:${blob.size}:${blob.type}:${blob.lastModified}`
|
||||
}
|
||||
|
||||
return `blob:${blob.size}:${blob.type}`
|
||||
}
|
||||
|
||||
export function extractClipboardImageBlobs(clipboard: DataTransfer): Blob[] {
|
||||
const blobs: Blob[] = []
|
||||
const seen = new Set<Blob>()
|
||||
const seen = new Set<string>()
|
||||
|
||||
const push = (blob: Blob | null) => {
|
||||
if (!blob || blob.size === 0 || seen.has(blob)) {
|
||||
if (!blob || blob.size === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
seen.add(blob)
|
||||
const key = blobDedupeKey(blob)
|
||||
|
||||
if (seen.has(key)) {
|
||||
return
|
||||
}
|
||||
|
||||
seen.add(key)
|
||||
blobs.push(blob)
|
||||
}
|
||||
|
||||
@ -29,7 +44,8 @@ export function extractClipboardImageBlobs(clipboard: DataTransfer): Blob[] {
|
||||
}
|
||||
}
|
||||
|
||||
if (clipboard.files?.length) {
|
||||
// Chromium/Electron expose the same pasted image on both `items` and `files`.
|
||||
if (blobs.length === 0 && clipboard.files?.length) {
|
||||
for (let i = 0; i < clipboard.files.length; i += 1) {
|
||||
const file = clipboard.files.item(i)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user