mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-04 16:14:07 +00:00
fix: open files sent from Explorer's Send to and Open with in a compose window, leaving the main window where it is
This commit is contained in:
@@ -211,6 +211,17 @@ describe('a mail link from another program in the desktop app', () => {
|
||||
expect(screen.queryByText('stub composer')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('files from Send to or Open with go to a compose window too, attached, and the main window shows no composer', async () => {
|
||||
const { handOffDraft } = await import('@/lib/launch-handoff');
|
||||
render(<MailApp />);
|
||||
const file = new File(['pdf'], 'report.pdf', { type: 'application/pdf' });
|
||||
act(() => { handOffDraft({ files: [file], fromOs: true }); });
|
||||
await waitFor(() => expect(desktopWindows.handOver).toHaveBeenCalledTimes(1));
|
||||
expect(desktopWindows.handOver).toHaveBeenCalledWith(expect.objectContaining({ mode: 'compose', initialFiles: [file] }));
|
||||
await act(() => new Promise((r) => setTimeout(r, 50)));
|
||||
expect(screen.queryByText('stub composer')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('a link inside the app still opens here', async () => {
|
||||
render(<MailApp />);
|
||||
act(() => { requestInternalMailto('mailto:bob@example.com?subject=Hi'); });
|
||||
|
||||
@@ -1717,7 +1717,7 @@ export function MailApp({ linkSegments: routeSegments }: MailAppProps = {}) {
|
||||
// as Outlook and Thunderbird do, and the main window stays where it was. Plugins get
|
||||
// their say first. False when it has to open here instead.
|
||||
const openMailtoInWindow = useCallback(async (pending: DraftRequest): Promise<boolean> => {
|
||||
if (!pending.fromOs || pending.initialFiles?.length || !hasDesktopWindows() || !popoutsAvailable()) return false;
|
||||
if (!pending.fromOs || !hasDesktopWindows() || !popoutsAvailable()) return false;
|
||||
let prefill: ComposePrefill | null = null;
|
||||
if (emailHooks.onBeforeCompose.size > 0) {
|
||||
prefill = await runBeforeCompose(composeOptionsFor('compose', {
|
||||
@@ -1735,6 +1735,8 @@ export function MailApp({ linkSegments: routeSegments }: MailAppProps = {}) {
|
||||
mode: 'compose',
|
||||
initialData: mailtoComposerDraft(pending, useSettingsStore.getState().plainTextMode),
|
||||
prefill: prefill && Object.keys(prefill).length > 0 ? prefill : null,
|
||||
// Send to and Open with: the files go along (desktop-p2-6).
|
||||
...(pending.initialFiles?.length ? { initialFiles: pending.initialFiles } : {}),
|
||||
});
|
||||
return true;
|
||||
}, [identities, t]);
|
||||
@@ -1857,6 +1859,7 @@ export function MailApp({ linkSegments: routeSegments }: MailAppProps = {}) {
|
||||
body: handoff.body ?? '',
|
||||
initialFiles: handoff.files,
|
||||
unedited: true,
|
||||
...(handoff.fromOs ? { fromOs: true } : {}),
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -516,6 +516,7 @@ function PopoutComposer({ payload, handoffId, onReopen }: {
|
||||
replyTo={payload.replyTo}
|
||||
initialDraftText={payload.initialDraftText}
|
||||
initialData={payload.initialData}
|
||||
initialFiles={payload.initialFiles ?? null}
|
||||
prefill={payload.prefill}
|
||||
sessionKey={sessionKey}
|
||||
sourceEmailId={payload.sourceEmailId ?? undefined}
|
||||
|
||||
@@ -132,6 +132,12 @@ describe('launches', () => {
|
||||
expect(takeHandoff('compose')?.files.map((file) => file.name)).toEqual(['a.pdf', 'b.png']);
|
||||
});
|
||||
|
||||
it('files sent from another program are marked so, for a compose window of their own, as a mail link', async () => {
|
||||
const { takeHandoff } = await import('@/lib/launch-handoff');
|
||||
await run({ type: 'files', reason: 'attach', failed: [], files: [{ name: 'a.pdf', data: text('a') }] });
|
||||
expect(takeHandoff('compose')).toMatchObject({ fromOs: true });
|
||||
});
|
||||
|
||||
it('a file opened with the app asks: new message or Files, a new message first', async () => {
|
||||
const { takeHandoff } = await import('@/lib/launch-handoff');
|
||||
const { listenForFileChoices } = await import('@/lib/file-drop/choice');
|
||||
|
||||
@@ -133,10 +133,13 @@ function handleFiles(command: Extract<DesktopOsCommand, { type: 'files' }>, env:
|
||||
const made = command.files.map(({ name, data }) => new File([new Uint8Array(data)], name));
|
||||
const failed = [...command.failed];
|
||||
if (command.reason === 'attach' && command.ask) {
|
||||
// Opened with the app ("Open with"): the same choice as dropping it on the window.
|
||||
// An older shell's "Open with": the same choice as dropping it on the window.
|
||||
requestFileChoice({ files: made, preferred: 'new-message' });
|
||||
} else if (command.reason === 'attach') {
|
||||
if (made.length > 0) handOffDraft({ files: made });
|
||||
// Send to and Open with: a compose window with the files, as a mail link gets, and the
|
||||
// main window stays where it is. Signed out, the sign-in has to show.
|
||||
if (!useAuthStore.getState().isAuthenticated) showWindow();
|
||||
if (made.length > 0) handOffDraft({ files: made, fromOs: true });
|
||||
} else {
|
||||
for (const file of made) {
|
||||
const kind = kindForFile(file);
|
||||
|
||||
@@ -42,6 +42,8 @@ export interface Handoff {
|
||||
/** New draft only. */
|
||||
subject?: string;
|
||||
body?: string;
|
||||
/** From another program through the desktop app (Send to, Open with): a compose window of its own. */
|
||||
fromOs?: boolean;
|
||||
createdAt: number;
|
||||
}
|
||||
|
||||
@@ -307,13 +309,14 @@ export function handOffFiles(files: File[], kind: HandoffKind): void {
|
||||
}
|
||||
|
||||
/** Opens a new draft with this text and these files. Trusted: what came from outside was asked about first. */
|
||||
export function handOffDraft(draft: { subject?: string; body?: string; files?: File[] }): void {
|
||||
export function handOffDraft(draft: { subject?: string; body?: string; files?: File[]; fromOs?: boolean }): void {
|
||||
const subject = cleanMailtoSubject(draft.subject ?? '');
|
||||
const body = cleanMailtoBody(draft.body ?? '');
|
||||
enqueue({
|
||||
id: newId(),
|
||||
kind: 'compose',
|
||||
files: draft.files ?? [],
|
||||
...(draft.fromOs ? { fromOs: true } : {}),
|
||||
...(subject ? { subject } : {}),
|
||||
...(body ? { body } : {}),
|
||||
createdAt: Date.now(),
|
||||
|
||||
@@ -109,7 +109,9 @@ export function createComposeHandoff(): ComposeHandoff | null {
|
||||
/** Keeps the draft with this window, so a reload starts from it. */
|
||||
export function rememberComposePayload(handoffId: string, payload: ComposePopoutPayload): void {
|
||||
try {
|
||||
sessionStorage.setItem(STORAGE_PREFIX + handoffId, JSON.stringify(payload));
|
||||
// A File doesn't go into JSON: once attached, the draft holds it.
|
||||
const { initialFiles: _files, ...kept } = payload;
|
||||
sessionStorage.setItem(STORAGE_PREFIX + handoffId, JSON.stringify(kept));
|
||||
} catch {
|
||||
// Full or unavailable: a reload then starts from the saved draft's first state.
|
||||
}
|
||||
|
||||
@@ -20,4 +20,9 @@ export interface ComposePopoutPayload {
|
||||
sourceEmailSource?: EmailWriteSource | null;
|
||||
/** The mailbox a new message was started from; picks the identity (see the composer's prop). */
|
||||
composeFromAccountEmail?: string;
|
||||
/**
|
||||
* Files to attach as it opens: Send to and Open with in the desktop app. They cross to the
|
||||
* window as they are (BroadcastChannel clones a File) and aren't kept for a reload.
|
||||
*/
|
||||
initialFiles?: File[];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user