diff --git a/package-lock.json b/package-lock.json index 97c3ef2..ae81a7a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 59313fa..b207ead 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/lib/crm-inbox-adapter.ts b/src/lib/crm-inbox-adapter.ts index 27e4711..b1bda30 100644 --- a/src/lib/crm-inbox-adapter.ts +++ b/src/lib/crm-inbox-adapter.ts @@ -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 { + async uploadAttachment(file: File, onProgress?: (fraction: number) => void): Promise { 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 }; } diff --git a/src/lib/crm-messaging-adapter.ts b/src/lib/crm-messaging-adapter.ts index 16a5c73..532f87e 100644 --- a/src/lib/crm-messaging-adapter.ts +++ b/src/lib/crm-messaging-adapter.ts @@ -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 { + async upload(file: File, onProgress?: (fraction: number) => void): Promise { 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 }; } diff --git a/src/lib/upload-put.ts b/src/lib/upload-put.ts new file mode 100644 index 0000000..0ff7371 --- /dev/null +++ b/src/lib/upload-put.ts @@ -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 { + await new Promise((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); + }); +}