258e3ec620
- fileEntries: a single unreadable file/dir in a dropped folder no longer aborts the whole traversal (try/catch per entry, skip failures) — was discarding ALL dropped files (incl. the flat-file path) + an unhandled rejection; also add .catch in both useFileDrop consumers. - RoomInput: mirror a localStorage-restored draft into the draft atom so the P5-57 indicator reflects a persisted draft after a page reload, not only on same-session room re-entry. - useTauriThumbbar: swallow toggleMicrophone()/hangup() rejections (parity with SMTC) — avoids an unhandled rejection when clicked mid-teardown. - App/DesktopChrome: keep wrapper element types stable across the chrome toggle (display:contents when off) so flipping it no longer remounts RouterProvider. - settings: normalizeComposerToolbarOrder also appends missing keys from the canonical key set (safety net if a new button is absent from the default order). Gates: tsc/eslint/prettier clean, build OK, 559 tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
85 lines
2.6 KiB
TypeScript
85 lines
2.6 KiB
TypeScript
import { useCallback, DragEventHandler, RefObject, useState, useEffect, useRef } from 'react';
|
|
import { collectDroppedFiles } from '../utils/fileEntries';
|
|
|
|
export const useFileDropHandler = (onDrop: (file: File[]) => void): DragEventHandler =>
|
|
useCallback(
|
|
(evt) => {
|
|
// `collectDroppedFiles` synchronously captures the entry list from the
|
|
// DataTransfer before traversing folders asynchronously.
|
|
collectDroppedFiles(evt.dataTransfer)
|
|
.then((files) => {
|
|
if (files) onDrop(files);
|
|
})
|
|
.catch(() => undefined);
|
|
},
|
|
[onDrop],
|
|
);
|
|
|
|
export const useFileDropZone = (
|
|
zoneRef: RefObject<HTMLElement>,
|
|
onDrop: (file: File[]) => void,
|
|
): boolean => {
|
|
const dragCounterRef = useRef(0);
|
|
const [active, setActive] = useState(false);
|
|
|
|
useEffect(() => {
|
|
const target = zoneRef.current;
|
|
const handleDrop = (evt: DragEvent) => {
|
|
evt.preventDefault();
|
|
dragCounterRef.current = 0;
|
|
setActive(false);
|
|
if (!evt.dataTransfer) return;
|
|
// Capture entries synchronously (inside the event) then traverse any
|
|
// dropped folders asynchronously — the DataTransferItemList is emptied
|
|
// once this handler returns.
|
|
collectDroppedFiles(evt.dataTransfer)
|
|
.then((files) => {
|
|
if (files) onDrop(files);
|
|
})
|
|
.catch(() => undefined);
|
|
};
|
|
|
|
target?.addEventListener('drop', handleDrop);
|
|
return () => {
|
|
target?.removeEventListener('drop', handleDrop);
|
|
};
|
|
}, [zoneRef, onDrop]);
|
|
|
|
useEffect(() => {
|
|
const target = zoneRef.current;
|
|
const handleDragEnter = (evt: DragEvent) => {
|
|
if (evt.dataTransfer?.types.includes('Files')) {
|
|
dragCounterRef.current += 1;
|
|
setActive(true);
|
|
}
|
|
};
|
|
const handleDragLeave = (evt: DragEvent) => {
|
|
if (evt.relatedTarget === null) {
|
|
// Mouse left the browser window — reset unconditionally
|
|
dragCounterRef.current = 0;
|
|
setActive(false);
|
|
return;
|
|
}
|
|
dragCounterRef.current -= 1;
|
|
if (dragCounterRef.current <= 0) {
|
|
dragCounterRef.current = 0;
|
|
setActive(false);
|
|
}
|
|
};
|
|
const handleDragOver = (evt: DragEvent) => {
|
|
evt.preventDefault();
|
|
};
|
|
|
|
target?.addEventListener('dragenter', handleDragEnter);
|
|
target?.addEventListener('dragleave', handleDragLeave);
|
|
target?.addEventListener('dragover', handleDragOver);
|
|
return () => {
|
|
target?.removeEventListener('dragenter', handleDragEnter);
|
|
target?.removeEventListener('dragleave', handleDragLeave);
|
|
target?.removeEventListener('dragover', handleDragOver);
|
|
};
|
|
}, [zoneRef]);
|
|
|
|
return active;
|
|
};
|