Files
webmail/lib/file-drop/files-target.ts
T
Linus Rath e211ae905e feat: offer attach or upload when files are dragged onto any screen
Files dragged into the window from outside now bring up a full-window
overlay with large drop zones: attach to a new message and upload to
Files, with the open composer first, "Upload to <folder>" on the Files
screen, and add to calendar, add to contacts, open or import for .ics,
.vcf and .eml files. At most four zones, the one under the pointer
highlights, Esc or dragging out cancels, a drop beside the zones does
nothing. Drags that start in the app never show it. A file pasted
outside a text field gets the same choices as a keyboard dialog.

The desktop app used to turn any file dropped on the window into a new
draft (lib/desktop/window-files.ts). That handling moved to the
overlay, so the "files on the window" tests in os-window.test.tsx went
with it: the same cases are now in components/file-drop/__tests__.
2026-11-04 05:37:35 +01:00

60 lines
1.7 KiB
TypeScript

/**
* The Files screen's end of the file drop overlay. While it's open on a folder that takes
* uploads it says so here, and the overlay offers "Upload to <folder>". From anywhere else
* the files wait here while the app goes to Files, which takes them once it has listed a
* folder they can go in.
*/
export interface FilesUpload {
files: File[];
/** A dropped folder: the files carry their paths, and the upload keeps them. */
asFolder: boolean;
}
interface FilesTarget {
/** The folder in view, as the user sees its name. Null at the top of Files. */
folder: string | null;
take: (upload: FilesUpload) => unknown;
/** The Pro shell keeps Files mounted on a tab out of sight. */
visible: () => boolean;
}
let target: FilesTarget | null = null;
let waiting: FilesUpload | null = null;
const listeners = new Set<() => void>();
export function setFilesDropTarget(next: FilesTarget | null): () => void {
target = next;
return () => {
if (target === next) target = null;
};
}
/** The Files screen on view, if it can take files where it is. */
export function filesDropTarget(): FilesTarget | null {
return target?.visible() ? target : null;
}
/** Files for the Files screen to upload when it's ready. A newer drop replaces an older one nobody took. */
export function queueFilesUpload(upload: FilesUpload): void {
waiting = upload;
for (const listener of listeners) listener();
}
export function takeQueuedFilesUpload(): FilesUpload | null {
const upload = waiting;
waiting = null;
return upload;
}
export function hasQueuedFilesUpload(): boolean {
return waiting !== null;
}
export function subscribeQueuedFilesUpload(listener: () => void): () => void {
listeners.add(listener);
return () => {
listeners.delete(listener);
};
}