fix: give large attachment offloads time to finish and let cloud pickers write links into the text

This commit is contained in:
Linus Rath
2026-10-14 07:40:32 +02:00
parent ae2130f5c8
commit 289e975d38
5 changed files with 23 additions and 8 deletions
@@ -271,6 +271,15 @@ describe('composer-above-body and composer-below-body', () => {
}
});
it('let a cloud picker put share links into the text', () => {
offerSlots('p1', ['composer-attachment-source']);
render(<EmailComposer initialData={DRAFT} onClose={vi.fn()} />);
const props = lastSlotProps('composer-attachment-source');
expect(typeof props.onAttach).toBe('function');
expect(typeof props.insertText).toBe('function');
});
it('draw nothing without a plugin', () => {
const { container } = render(<EmailComposer initialData={DRAFT} onClose={vi.fn()} />);
expect(container.querySelector('[data-plugin-slot="composer-above-body"]')).toBeNull();
+5 -1
View File
@@ -2070,7 +2070,6 @@ export function EmailComposer({
}, [composerClient, t]);
// Stable props, so a re-render of the composer doesn't post them to the
// plugin's iframe again.
const pluginAttachmentSourceProps = useMemo(() => ({ onAttach: handlePluginAttachmentAdd }), [handlePluginAttachmentAdd]);
const pluginToolbarOffers = usePluginSlotOffers('composer-toolbar');
const pluginAttachmentSourceOffers = usePluginSlotOffers('composer-attachment-source');
const hasAboveBodyOffers = usePluginSlotOffers('composer-above-body').length > 0;
@@ -2469,6 +2468,11 @@ export function EmailComposer({
() => ({ insertText: pluginInsertText, getSelectedText: pluginSelectedText }),
[pluginInsertText, pluginSelectedText],
);
// A cloud picker attaches files, or puts share links in the text.
const pluginAttachmentSourceProps = useMemo(
() => ({ onAttach: handlePluginAttachmentAdd, insertText: pluginInsertText }),
[handlePluginAttachmentAdd, pluginInsertText],
);
const sendMenuContext = useCallback(() => ({ draft: draftSnapshotRef.current() }), []);
// Trigger auto-save when content changes (only if user modified something)
+6 -5
View File
@@ -63,11 +63,12 @@ const DEFAULT_TIMEOUT_MS = 5000;
// reply-all, mailto, attachment upload), so they need a much longer budget
// than observer / transform hooks.
const INTERCEPT_TIMEOUT_MS = 60_000;
// onBeforeBlobUpload handlers may push the attachment to an external store
// before handing back a link (see ExternalAttachmentResult), which is a real
// upload over the wire - 5s would kill every offload of a file worth
// offloading, and three of those in a minute would auto-disable the plugin.
const BLOB_UPLOAD_TIMEOUT_MS = 120_000;
// onBeforeBlobUpload handlers may ask the user and then push the attachment
// to an external store before handing back a link (see
// ExternalAttachmentResult): a real upload of a file too big to mail, which
// takes minutes on a slow line. Two minutes cut off large offloads and
// counted them toward auto-disable.
const BLOB_UPLOAD_TIMEOUT_MS = 15 * 60_000;
class HookTimeoutError extends Error {}
+2 -1
View File
@@ -490,7 +490,8 @@ function buildPluginApi(manifest: PluginManifest) {
openExternalUrl: (url: string, target?: string) =>
callApi('ui.openExternalUrl', [url, target]) as Promise<void>,
/** Downloads a file generated by the plugin. Not a user's file or attachment. */
downloadFile: (opts: { content: string; filename: string; contentType?: string }) =>
/** `content` is text, or the file's bytes (ArrayBuffer / Uint8Array). */
downloadFile: (opts: { content: string | ArrayBuffer | Uint8Array; filename: string; contentType?: string }) =>
callApi('ui.downloadFile', [opts]) as Promise<void>,
/**
* Buttons or menu entries on an action surface (see the slot catalogue:
+1 -1
View File
@@ -81,7 +81,7 @@ export const SLOT_CATALOG = {
'composer-below-body': S('composer', 'frame', ['ui:composer-toolbar', 'ui:composer-sidebar'], { draft: 'email:send', insertText: 'compose:open', getSelectedText: 'email:send' }, 'below the message text, above the attachments'),
'composer-sidebar': S('composer', 'frame', ['ui:composer-sidebar'], {}, 'a panel left of the composer'),
'composer-sidebar-right': S('composer', 'frame', ['ui:composer-sidebar'], {}, 'a panel right of the composer'),
'composer-attachment-source': S('composer', 'frame', ['ui:composer-toolbar', 'ui:composer-sidebar'], { onAttach: null }, 'the composer\'s attach menu, as a source of files', true),
'composer-attachment-source': S('composer', 'frame', ['ui:composer-toolbar', 'ui:composer-sidebar'], { onAttach: null, insertText: 'compose:open' }, 'the composer\'s attach menu, as a source of files', true),
'composer-send-menu': S('composer', 'action', ['ui:composer-toolbar'], { draft: 'email:send' }, 'entries in the menu next to Send'),
'composer-attach-menu': S('composer', 'action', ['ui:composer-toolbar'], {}, 'entries in the attach menu (for example "From Nextcloud")'),