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:
Generated
+4
-4
@@ -12,7 +12,7 @@
|
|||||||
"@huggingface/transformers": "^4.2.0",
|
"@huggingface/transformers": "^4.2.0",
|
||||||
"@imgly/background-removal": "^1.7.0",
|
"@imgly/background-removal": "^1.7.0",
|
||||||
"@insignia/iios-kernel-client": "^0.1.6",
|
"@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",
|
"@photo-gallery/sdk": "file:./vendor/photo-gallery-sdk",
|
||||||
"@tensorflow-models/coco-ssd": "^2.2.3",
|
"@tensorflow-models/coco-ssd": "^2.2.3",
|
||||||
"@tensorflow/tfjs": "^4.22.0",
|
"@tensorflow/tfjs": "^4.22.0",
|
||||||
@@ -1110,9 +1110,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@insignia/iios-messaging-ui": {
|
"node_modules/@insignia/iios-messaging-ui": {
|
||||||
"version": "0.1.16",
|
"version": "0.1.17",
|
||||||
"resolved": "https://git.lynkedup.cloud/api/packages/insignia/npm/%40insignia%2Fiios-messaging-ui/-/0.1.16/iios-messaging-ui-0.1.16.tgz",
|
"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-z7bpoN6pA6lGS5w/Ckusc6U7uFmHo/8+nGqvsJlpJ/i6d+87VyK3X2C8CWrwfHNXLrjQAiiID7GKEs02kEGmjA==",
|
"integrity": "sha512-pob/ldsvloFZ15BUyDyVdZb34MYQrH6aFAV27tleTgaJhnOeDf4HlJe19ftpW8S/h2TNhFMRhv6eOnYRLzEtxQ==",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@insignia/iios-kernel-client": "*",
|
"@insignia/iios-kernel-client": "*",
|
||||||
"react": ">=18",
|
"react": ">=18",
|
||||||
|
|||||||
+1
-1
@@ -14,7 +14,7 @@
|
|||||||
"@huggingface/transformers": "^4.2.0",
|
"@huggingface/transformers": "^4.2.0",
|
||||||
"@imgly/background-removal": "^1.7.0",
|
"@imgly/background-removal": "^1.7.0",
|
||||||
"@insignia/iios-kernel-client": "^0.1.6",
|
"@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",
|
"@photo-gallery/sdk": "file:./vendor/photo-gallery-sdk",
|
||||||
"@tensorflow-models/coco-ssd": "^2.2.3",
|
"@tensorflow-models/coco-ssd": "^2.2.3",
|
||||||
"@tensorflow/tfjs": "^4.22.0",
|
"@tensorflow/tfjs": "^4.22.0",
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import type {
|
|||||||
MailPerson,
|
MailPerson,
|
||||||
} from "@insignia/iios-messaging-ui";
|
} from "@insignia/iios-messaging-ui";
|
||||||
import type { DataDoor } from "./crm-messaging-adapter";
|
import type { DataDoor } from "./crm-messaging-adapter";
|
||||||
|
import { putWithProgress } from "./upload-put";
|
||||||
|
|
||||||
const MAX_ATTACHMENT_BYTES = 26 * 1024 * 1024; // matches IIOS's cap
|
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).");
|
if (file.size > MAX_ATTACHMENT_BYTES) throw new Error("File is too large (max 25 MB).");
|
||||||
const mime = mimeForFile(file);
|
const mime = mimeForFile(file);
|
||||||
const { objectKey, uploadUrl } = await this.sdk.command<{ objectKey: string; uploadUrl: string }>("crm.media.presignUpload", { mime, sizeBytes: file.size });
|
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 });
|
await putWithProgress(uploadUrl, file, mime, onProgress);
|
||||||
if (!res.ok) throw new Error(`Upload failed (${res.status}).`);
|
|
||||||
return { contentRef: objectKey, mimeType: mime, sizeBytes: file.size, filename: file.name };
|
return { contentRef: objectKey, mimeType: mime, sizeBytes: file.size, filename: file.name };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import type {
|
|||||||
Unsubscribe,
|
Unsubscribe,
|
||||||
} from "@insignia/iios-messaging-ui";
|
} from "@insignia/iios-messaging-ui";
|
||||||
import type { MessageSocket, Message as KernelMessage } from "@insignia/iios-kernel-client";
|
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. */
|
/** The imperative appshell data door (useAppShell().sdk). Typed structurally, not to its class. */
|
||||||
export interface DataDoor {
|
export interface DataDoor {
|
||||||
@@ -169,11 +170,10 @@ export class CrmMessagingAdapter implements MessagingAdapter {
|
|||||||
return att ? { ...msg, attachment: att } : msg;
|
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 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 { 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 });
|
await putWithProgress(uploadUrl, file, mime, onProgress);
|
||||||
if (!res.ok) throw new Error(`Upload failed (${res.status}).`);
|
|
||||||
const url = await this.downloadUrl(objectKey, mime);
|
const url = await this.downloadUrl(objectKey, mime);
|
||||||
return { url, mime, name: file.name, contentRef: objectKey, sizeBytes: file.size };
|
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