feat(attachments): real upload progress via XHR; SDK 0.1.17 picker
Both adapters uploaded with fetch(), which has no upload-progress event — so the new picker's progress bar could only ever have been indeterminate. putWithProgress does the presigned PUT over XHR and reports true byte progress; when a server sends no content-length the fraction is withheld rather than guessed, and the bar falls back to indeterminate. Picks up the attachment picker, the Gmail-style Compose button, and the mail reply row fix. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,7 @@ import type {
|
||||
MailPerson,
|
||||
} from "@insignia/iios-messaging-ui";
|
||||
import type { DataDoor } from "./crm-messaging-adapter";
|
||||
import { putWithProgress } from "./upload-put";
|
||||
|
||||
const MAX_ATTACHMENT_BYTES = 26 * 1024 * 1024; // matches IIOS's cap
|
||||
|
||||
@@ -84,12 +85,11 @@ export class CrmInboxAdapter implements InboxAdapter {
|
||||
});
|
||||
}
|
||||
|
||||
async uploadAttachment(file: File): Promise<MailAttachment> {
|
||||
async uploadAttachment(file: File, onProgress?: (fraction: number) => void): Promise<MailAttachment> {
|
||||
if (file.size > MAX_ATTACHMENT_BYTES) throw new Error("File is too large (max 25 MB).");
|
||||
const mime = mimeForFile(file);
|
||||
const { objectKey, uploadUrl } = await this.sdk.command<{ objectKey: string; uploadUrl: string }>("crm.media.presignUpload", { mime, sizeBytes: file.size });
|
||||
const res = await fetch(uploadUrl, { method: "PUT", body: file });
|
||||
if (!res.ok) throw new Error(`Upload failed (${res.status}).`);
|
||||
await putWithProgress(uploadUrl, file, mime, onProgress);
|
||||
return { contentRef: objectKey, mimeType: mime, sizeBytes: file.size, filename: file.name };
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import type {
|
||||
Unsubscribe,
|
||||
} from "@insignia/iios-messaging-ui";
|
||||
import type { MessageSocket, Message as KernelMessage } from "@insignia/iios-kernel-client";
|
||||
import { putWithProgress } from "./upload-put";
|
||||
|
||||
/** The imperative appshell data door (useAppShell().sdk). Typed structurally, not to its class. */
|
||||
export interface DataDoor {
|
||||
@@ -169,11 +170,10 @@ export class CrmMessagingAdapter implements MessagingAdapter {
|
||||
return att ? { ...msg, attachment: att } : msg;
|
||||
}
|
||||
|
||||
async upload(file: File): Promise<Attachment> {
|
||||
async upload(file: File, onProgress?: (fraction: number) => void): Promise<Attachment> {
|
||||
const mime = file.type || "application/octet-stream";
|
||||
const { objectKey, uploadUrl } = await this.sdk.command<{ objectKey: string; uploadUrl: string }>("crm.media.presignUpload", { mime, sizeBytes: file.size });
|
||||
const res = await fetch(uploadUrl, { method: "PUT", body: file });
|
||||
if (!res.ok) throw new Error(`Upload failed (${res.status}).`);
|
||||
await putWithProgress(uploadUrl, file, mime, onProgress);
|
||||
const url = await this.downloadUrl(objectKey, mime);
|
||||
return { url, mime, name: file.name, contentRef: objectKey, sizeBytes: file.size };
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* PUT a file to a presigned URL, reporting real byte-level progress.
|
||||
*
|
||||
* Deliberately XHR and not fetch: fetch has no upload-progress event at all (request streams are
|
||||
* still not usable for this in browsers), so a fetch-based upload can only ever show a spinner or a
|
||||
* made-up percentage. XHR's `upload.onprogress` gives the true figure, which is what the picker's
|
||||
* progress bar renders.
|
||||
*/
|
||||
export async function putWithProgress(
|
||||
url: string,
|
||||
file: File,
|
||||
mime: string,
|
||||
onProgress?: (fraction: number) => void,
|
||||
): Promise<void> {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open("PUT", url, true);
|
||||
xhr.setRequestHeader("Content-Type", mime);
|
||||
|
||||
xhr.upload.onprogress = (e) => {
|
||||
// Not every server/proxy reports a total; without one a fraction would be a guess, so we
|
||||
// stay silent and the UI shows its indeterminate bar instead.
|
||||
if (e.lengthComputable && e.total > 0) onProgress?.(e.loaded / e.total);
|
||||
};
|
||||
xhr.onload = () => {
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
onProgress?.(1);
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(`Upload failed (${xhr.status}).`));
|
||||
}
|
||||
};
|
||||
xhr.onerror = () => reject(new Error("Upload failed — check your connection and try again."));
|
||||
xhr.onabort = () => reject(new Error("Upload cancelled."));
|
||||
xhr.send(file);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user