Files
cinny/src/app/hooks/useFileDrop.ts
T
jared 258e3ec620
CI / Build & Quality Checks (push) Successful in 10m40s
CI / Trigger Desktop Build (push) Successful in 8s
fix(desktop): address code-review findings on the desktop wave
- 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>
2026-07-01 10:40:31 -04:00

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;
};