refactor: clarify image drag constant names

This commit is contained in:
zhc committed 2026-10-19 17:46:59 +08:00
1 parent f0ed41c8dd
commit 9485f9bf40
6 files changed
+23 -20

No files matched your search

+10 -7
View File
@@ -3,12 +3,14 @@ import { randomUUID } from 'node:crypto'
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
import { join } from 'node:path'
const IMAGE_EXTENSIONS: Readonly<Record<string, string>> = {
// Keep supported MIME types in sync with IMAGE_MEDIA_TYPES in
// packages/client/ui-sidebar-documentpreview/src/client/image/ImageBody.tsx (extensions: adjacent index.ts).
const DRAG_FILE_EXTENSION_BY_MIME: Readonly<Record<string, string>> = {
'image/png': 'png', 'image/jpeg': 'jpg', 'image/gif': 'gif', 'image/webp': 'webp',
'image/bmp': 'bmp', 'image/x-icon': 'ico', 'image/vnd.microsoft.icon': 'ico', 'image/svg+xml': 'svg',
}
// Bounds aggregate original bytes in preparation and retained temporary files.
const MAX_IMAGE_BYTES = 64 * 1024 * 1024
const MAX_TOTAL_DRAG_FILE_BYTES = 64 * 1024 * 1024
interface ImageDragFile {
readonly directory: string
@@ -36,16 +38,17 @@ export class ImageDragFiles {
* @returns application-local opaque handle; failures retain no usable handle or partial file.
*/
prepare(bytes: unknown, name: unknown, mediaType: unknown): Promise<string> {
const extension = typeof mediaType === 'string' && Object.hasOwn(IMAGE_EXTENSIONS, mediaType)
? IMAGE_EXTENSIONS[mediaType] : undefined
if (this.disposed || !(bytes instanceof Uint8Array) || bytes.byteLength === 0 || bytes.byteLength > MAX_IMAGE_BYTES - this.stagedBytes
|| typeof name !== 'string' || typeof mediaType !== 'string' || extension === undefined) {
const outputExtension = typeof mediaType === 'string' && Object.hasOwn(DRAG_FILE_EXTENSION_BY_MIME, mediaType)
? DRAG_FILE_EXTENSION_BY_MIME[mediaType] : undefined
if (this.disposed || !(bytes instanceof Uint8Array) || bytes.byteLength === 0
|| bytes.byteLength > MAX_TOTAL_DRAG_FILE_BYTES - this.stagedBytes
|| typeof name !== 'string' || typeof mediaType !== 'string' || outputExtension === undefined) {
return Promise.reject(new Error('Invalid image transfer'))
}
let stem = name.replace(/\.[^.]*$/u, '').replace(/[<>:"/\\|?*\x00-\x1f]/gu, '_').replace(/^[. ]+|[. ]+$/gu, '').slice(0, 120) || 'image'
if (/^(con|prn|aux|nul|com[1-9¹²³]|lpt[1-9¹²³])(?:\.|$)/iu.test(stem)) stem = `_${stem}`
this.stagedBytes += bytes.byteLength
const operation = this.write(bytes, `${stem}.${extension}`).catch((error: unknown) => {
const operation = this.write(bytes, `${stem}.${outputExtension}`).catch((error: unknown) => {
this.stagedBytes -= bytes.byteLength
throw error
})
@@ -1,5 +1,5 @@
/** Document drag-and-drop listeners owned by one mounted attachment view. */
import { IMAGE_DRAG_TYPE } from '@deepseek-ai/dsh-client-ui-primitives'
import { IMAGE_DRAG_TOKEN_TYPE } from '@deepseek-ai/dsh-client-ui-primitives'
import type { ComposerAttachmentsProps } from '@deepseek-ai/dsh-client-ui-conversation/client'
/**
@@ -38,7 +38,7 @@ export function installDocumentDropEvents(
): () => void {
const fileTransfer = (event: globalThis.DragEvent): DataTransfer | null => {
const dataTransfer = event.dataTransfer
if (dataTransfer === null || (!dataTransfer.types.includes('Files') && !dataTransfer.types.includes(IMAGE_DRAG_TYPE))) return null
if (dataTransfer === null || (!dataTransfer.types.includes('Files') && !dataTransfer.types.includes(IMAGE_DRAG_TOKEN_TYPE))) return null
return dataTransfer
}
const reset = (): void => {
@@ -50,7 +50,7 @@ export function installDocumentDropEvents(
if (dataTransfer === null) return
event.preventDefault()
dragDepth.current += 1
if (!dataTransfer.types.includes(IMAGE_DRAG_TYPE)) setDragActive(true)
if (!dataTransfer.types.includes(IMAGE_DRAG_TOKEN_TYPE)) setDragActive(true)
}
const onDragOver = (event: globalThis.DragEvent): void => {
const dataTransfer = fileTransfer(event)
@@ -72,7 +72,7 @@ export function installDocumentDropEvents(
event.preventDefault()
reset()
if (canAcceptDrop) {
const image = dataTransfer.types.includes(IMAGE_DRAG_TYPE) ? readImageDrag(dataTransfer) : undefined
const image = dataTransfer.types.includes(IMAGE_DRAG_TOKEN_TYPE) ? readImageDrag(dataTransfer) : undefined
const files = image === undefined ? [...dataTransfer.files] : [image]
if (files.length > 0) onAddFiles(files, droppedDirectories(dataTransfer, files))
}
@@ -3,8 +3,8 @@ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, use
import { Toast } from './Toast.tsx'
import type { DesktopImageDrag } from './image-drag.ts'
/** Browser drag token; file bytes remain in the owning mounted application. */
export const IMAGE_DRAG_TYPE = 'application/x-dsh-image'
/** DataTransfer type for an application-local drag token; file bytes remain in the owning mounted application. */
export const IMAGE_DRAG_TOKEN_TYPE = 'application/x-dsh-image'
interface ImageTransfer {
desktop: DesktopImageDrag | undefined
@@ -43,7 +43,7 @@ export function ImageDragProvider({ children }: { readonly children: ReactNode }
}, [end])
const read = useCallback((transfer: DataTransfer): File | undefined => {
const current = active.current
return current !== undefined && current.token === transfer.getData(IMAGE_DRAG_TYPE) ? current.file : undefined
return current !== undefined && current.token === transfer.getData(IMAGE_DRAG_TOKEN_TYPE) ? current.file : undefined
}, [])
const value = useMemo(() => ({ desktop, report, begin, end, read }), [desktop, report, begin, end, read])
return <ImageDragFeedback.Provider value={value}>
@@ -1,7 +1,7 @@
/** Original image files supplied to browser and native drag sessions. */
import { useCallback, useEffect, useRef, type DragEvent, type MouseEvent, type PointerEvent } from 'react'
import { IMAGE_DRAG_TYPE, useImageDragTransfer } from './ImageDragProvider.tsx'
import { IMAGE_DRAG_TOKEN_TYPE, useImageDragTransfer } from './ImageDragProvider.tsx'
/** Application-owned native image transfer; prepared handles never expose host paths. */
export interface DesktopImageDrag {
@@ -106,7 +106,7 @@ export function useImageDrag(src: string, name: string, failureText: string, ori
void desktop.start(prepared.handle).then((started) => { if (!started) failed() }, failed)
return
}
event.dataTransfer.setData(IMAGE_DRAG_TYPE, transfer.begin(prepared.file))
event.dataTransfer.setData(IMAGE_DRAG_TOKEN_TYPE, transfer.begin(prepared.file))
event.dataTransfer.setData('DownloadURL', `${prepared.file.type}:${prepared.file.name}:${src}`)
}, [src, desktop, prepare, reportFailure, failureText, transfer])
const onPointerDown = useCallback((event: PointerEvent<HTMLImageElement>): void => {
+1 -1
View File
@@ -67,7 +67,7 @@ export { pointerModality } from './input-modality.ts'
export { Toast } from './Toast.tsx'
export { fileSizeText } from './file-size.ts'
export { writeClipboard } from './clipboard.ts'
export { IMAGE_DRAG_TYPE, ImageDragProvider, ImageDrop } from './ImageDragProvider.tsx'
export { IMAGE_DRAG_TOKEN_TYPE, ImageDragProvider, ImageDrop } from './ImageDragProvider.tsx'
export { DraggableImage } from './DraggableImage.tsx'
export type { DraggableImageProps } from './DraggableImage.tsx'
export type { DesktopImageDrag } from './image-drag.ts'
@@ -3,7 +3,7 @@ import { useEffect } from 'react'
import { File as NodeFile } from 'node:buffer'
import { act, cleanup, fireEvent, render, screen } from '@testing-library/react'
import { afterEach, expect, it, vi } from 'vitest'
import { IMAGE_DRAG_TYPE, ImageDragProvider, useImageDrop } from '../src/ImageDragProvider.tsx'
import { IMAGE_DRAG_TOKEN_TYPE, ImageDragProvider, useImageDrop } from '../src/ImageDragProvider.tsx'
import { DraggableImage } from '../src/DraggableImage.tsx'
import { useImageDrag, type DesktopImageDrag } from '../src/image-drag.ts'
@@ -36,7 +36,7 @@ it('hands original bytes to the browser without alt text or an internal URI payl
await act(async () => { fireEvent.dragStart(screen.getByRole('img'), { dataTransfer }) })
expect(dataTransfer.items.add).not.toHaveBeenCalled()
expect(dataTransfer.items.clear).toHaveBeenCalledOnce()
expect(dataTransfer.setData).toHaveBeenCalledWith(IMAGE_DRAG_TYPE, expect.any(String))
expect(dataTransfer.setData).toHaveBeenCalledWith(IMAGE_DRAG_TOKEN_TYPE, expect.any(String))
expect(dataTransfer.effectAllowed).toBe('copy')
expect(dataTransfer.setData).toHaveBeenCalledWith('DownloadURL', 'image/png:original.png:blob:original')
fireEvent.click(screen.getByRole('img'), { detail: 1 })
@@ -169,7 +169,7 @@ it('fetches a readable source once and rejects a foreign drag token', async () =
expect(fetchImage).toHaveBeenCalledOnce()
const dataTransfer = transfer()
fireEvent.dragStart(screen.getByRole('img'), { dataTransfer })
dataTransfer.setData(IMAGE_DRAG_TYPE, 'foreign')
dataTransfer.setData(IMAGE_DRAG_TOKEN_TYPE, 'foreign')
fireEvent.drop(screen.getByTestId('drop'), { dataTransfer })
expect(receive).toHaveBeenCalledWith(undefined)
})