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:
2026-07-29 12:54:19 +05:30
parent 3f6a5b9b1f
commit 7221bec250
5 changed files with 50 additions and 11 deletions
+4 -4
View File
@@ -12,7 +12,7 @@
"@huggingface/transformers": "^4.2.0",
"@imgly/background-removal": "^1.7.0",
"@insignia/iios-kernel-client": "^0.1.6",
"@insignia/iios-messaging-ui": "^0.1.16",
"@insignia/iios-messaging-ui": "^0.1.17",
"@photo-gallery/sdk": "file:./vendor/photo-gallery-sdk",
"@tensorflow-models/coco-ssd": "^2.2.3",
"@tensorflow/tfjs": "^4.22.0",
@@ -1110,9 +1110,9 @@
}
},
"node_modules/@insignia/iios-messaging-ui": {
"version": "0.1.16",
"resolved": "https://git.lynkedup.cloud/api/packages/insignia/npm/%40insignia%2Fiios-messaging-ui/-/0.1.16/iios-messaging-ui-0.1.16.tgz",
"integrity": "sha512-z7bpoN6pA6lGS5w/Ckusc6U7uFmHo/8+nGqvsJlpJ/i6d+87VyK3X2C8CWrwfHNXLrjQAiiID7GKEs02kEGmjA==",
"version": "0.1.17",
"resolved": "https://git.lynkedup.cloud/api/packages/insignia/npm/%40insignia%2Fiios-messaging-ui/-/0.1.17/iios-messaging-ui-0.1.17.tgz",
"integrity": "sha512-pob/ldsvloFZ15BUyDyVdZb34MYQrH6aFAV27tleTgaJhnOeDf4HlJe19ftpW8S/h2TNhFMRhv6eOnYRLzEtxQ==",
"peerDependencies": {
"@insignia/iios-kernel-client": "*",
"react": ">=18",
+1 -1
View File
@@ -14,7 +14,7 @@
"@huggingface/transformers": "^4.2.0",
"@imgly/background-removal": "^1.7.0",
"@insignia/iios-kernel-client": "^0.1.6",
"@insignia/iios-messaging-ui": "^0.1.16",
"@insignia/iios-messaging-ui": "^0.1.17",
"@photo-gallery/sdk": "file:./vendor/photo-gallery-sdk",
"@tensorflow-models/coco-ssd": "^2.2.3",
"@tensorflow/tfjs": "^4.22.0",
+3 -3
View File
@@ -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 };
}
+3 -3
View File
@@ -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 };
}
+39
View File
@@ -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);
});
}