diff --git a/packages/kite-chat-component/src/kite-chat.ts b/packages/kite-chat-component/src/kite-chat.ts index c1b32d1..9486cb9 100644 --- a/packages/kite-chat-component/src/kite-chat.ts +++ b/packages/kite-chat-component/src/kite-chat.ts @@ -271,18 +271,19 @@ export class KiteChatElement extends LitElement { private _sendFile(event: Event) { const target = event.target as HTMLInputElement; - const numFiles = target.files?.length ?? 0; - for (let i = 0; i < numFiles; i++) { - const file = target.files?.item(i); - if (!file) continue; + const files = Array.from(target.files || []).filter(file => file); + const batchId = randomStringId(); + files.forEach(file => { const message: FileMsg = { messageId: randomStringId(), timestamp: new Date(), status: MsgStatus.unknown, file, + batchId, + totalFiles: files.length, }; this._dispatchMsg(message) && this.appendMsg(message); - } + }); } private _dispatchMsg(detail: KiteMsg): boolean { diff --git a/packages/kite-chat-component/src/kite-payload.ts b/packages/kite-chat-component/src/kite-payload.ts index fb1a9b2..72a6b1d 100644 --- a/packages/kite-chat-component/src/kite-payload.ts +++ b/packages/kite-chat-component/src/kite-payload.ts @@ -18,6 +18,8 @@ export type PlaintextMsg = BaseMsg & { export type FileMsg = BaseMsg & { file: File; + batchId?: string; + totalFiles?: number; }; export type KiteMsg = PlaintextMsg | FileMsg; diff --git a/packages/kite-chat/src/kite-chat.ts b/packages/kite-chat/src/kite-chat.ts index f000ced..a7e7440 100644 --- a/packages/kite-chat/src/kite-chat.ts +++ b/packages/kite-chat/src/kite-chat.ts @@ -30,9 +30,9 @@ import { KiteDB, openDatabase, getMessages, - addMessage, - messageById, - modifyMessage + addMessage, + modifyMessage, + deleteMessage } from './kite-storage'; export type KiteChatOptions = { @@ -148,11 +148,17 @@ export class KiteChat { if(!this.db) { return; } - messageById(messageId, this.db).then((msg) => { - this.db && modifyMessage(messageId, {...msg, ...updatedMsg}, this.db); - }); + modifyMessage(messageId, updatedMsg, this.db); } + private delete(messageId: string) { + if(!this.db) { + return; + } + deleteMessage(messageId, this.db); + } + + private restore() { if(!this.db) { return; @@ -295,12 +301,17 @@ export class KiteChat { const fileElement = document.querySelector( `${KiteMsgElement.TAG}[messageId="${e.messageId}"] > ${KiteFileElement.TAG}` ) as KiteFileElement | undefined; - if (fileElement) { - fileElement.file = e.file; - } + fileElement && (fileElement.file = e.file); this.update(e.messageId, { file: e.file, } as ContentMsg); + e.zippedIds.forEach(id => { + const msgElement = document.querySelector( + `${KiteMsgElement.TAG}[messageId="${id}"]` + ) as KiteFileElement | undefined; + msgElement?.remove(); + this.delete(id); + }) } protected onFailedMessage(e: FailedMsg) { diff --git a/packages/kite-chat/src/kite-storage.ts b/packages/kite-chat/src/kite-storage.ts index 4d03602..891647b 100644 --- a/packages/kite-chat/src/kite-storage.ts +++ b/packages/kite-chat/src/kite-storage.ts @@ -105,10 +105,26 @@ export async function messageById(messageId: string, db: KiteDB) { * Function to modify an existing message in the database. */ export async function modifyMessage(messageId: string, modifiedMessage: ContentMsg, db: KiteDB) { + const oldMessage = await messageById(messageId, db); + + const tx = db.transaction(MESSAGES_STORE_NAME, 'readwrite'); + const store = tx.objectStore(MESSAGES_STORE_NAME); + + const primaryKey = await store.index(MESSAGES_KEY).getKey(messageId); + await store.put({...oldMessage, ...modifiedMessage}, primaryKey); + await tx.done; +} + +/** + * Function to delete a message by its messageId. + */ +export async function deleteMessage(messageId: string, db: KiteDB) { const tx = db.transaction(MESSAGES_STORE_NAME, 'readwrite'); const store = tx.objectStore(MESSAGES_STORE_NAME); const primaryKey = await store.index(MESSAGES_KEY).getKey(messageId); - await store.put(modifiedMessage, primaryKey); + if (primaryKey) { + await store.delete(primaryKey); + } await tx.done; } \ No newline at end of file diff --git a/packages/kite-chat/src/kite-types.ts b/packages/kite-chat/src/kite-types.ts index 64dd963..66866eb 100644 --- a/packages/kite-chat/src/kite-types.ts +++ b/packages/kite-chat/src/kite-types.ts @@ -74,6 +74,8 @@ export type FileMsg = { file: File; timestamp: Date; status?: MsgStatus; + batchId?: string; + totalFiles?: number; }; export type UploadRequest = { @@ -139,6 +141,7 @@ export type ActiveTab = { export type ZippedMsg = { type: MsgType.ZIPPED; messageId: string; + zippedIds: string[]; file: File; }; diff --git a/packages/kite-chat/src/kite-worker.ts b/packages/kite-chat/src/kite-worker.ts index 39c1602..a5c806b 100644 --- a/packages/kite-chat/src/kite-worker.ts +++ b/packages/kite-chat/src/kite-worker.ts @@ -83,6 +83,10 @@ let joinChannel: JoinChannel | null = null; */ const messageHistory = new Array(); +let batchAccumulator = new Array(); + +let zipQueue = new Array(); + const outgoingQueue = new Array(); const tabPorts = new Set(); @@ -208,17 +212,21 @@ function verifyPlainText(text: string): PlainTextVerification { return PlainTextVerification.SUCCEED; } -async function zipFile(file: File, resultType = "application/zip"): Promise { +async function zipFiles(files: File[], resultType = "application/zip", timestamp = new Date()): Promise { // Import JSZip module dynamically await import(/* @vite-ignore */ JSZIP_CDN); const extendedSelf = self as unknown as typeof self & {JSZip: JSZip}; const zip: JSZip = new extendedSelf.JSZip(); - zip.file(file.name, file); + files.forEach((file) => { + zip.file(file.name, file); + }); + const zipFileName = files.length === 1 + ? `${files[0].name.replace(/\.[^/.]+$/, '')}.zip` + : `${timestamp.toISOString()}.zip`; const blob = await zip.generateAsync({ type: 'blob', compression: 'DEFLATE', compressionOptions: { level: 0 } }); - const zipFileName = `${file.name.replace(/\.[^/.]+$/, '')}.zip`; return new File([blob], zipFileName, {type: resultType}); } @@ -255,9 +263,7 @@ function onPlaintextMessage(payload: PlaintextMsg, tabPort: KiteMessagePort) { } function onFileMessage(payload: FileMsg, tabPort: KiteMessagePort) { - messageHistory.push(payload); - - const uploadFile = (file: File) => { + const uploadFile = (payload: FileMsg, file: File = payload.file) => { const upload: UploadRequest = { type: MsgType.UPLOAD, messageId: payload.messageId, @@ -279,46 +285,68 @@ function onFileMessage(payload: FileMsg, tabPort: KiteMessagePort) { }); } - const zippedFile = (file: File) => { - tabPort.postMessage({ - type: MsgType.ZIPPED, - messageId: payload.messageId, - file: file, - }); - } - const maxSize = (type: string) => formatSize(SUPPORTED_FILE_FORMATS[type as keyof typeof SUPPORTED_FILE_FORMATS]); const result = verifyFile(payload.file); + const {file} = payload; switch (result) { case FileVerification.UNSUPPORTED_TYPE: - if(payload.file.size > SUPPORTED_FILE_FORMATS[ZIP_FILE_FORMAT]) { - failedFile( - FileVerification.EXCEED_SIZE, - `${ZIP_FILE_FORMAT} size exceeds ${maxSize(ZIP_FILE_FORMAT)} limit.` - ); - break; + if(file.size > SUPPORTED_FILE_FORMATS[ZIP_FILE_FORMAT]) { + failedFile(FileVerification.EXCEED_SIZE, `${ZIP_FILE_FORMAT} size exceeds ${maxSize(ZIP_FILE_FORMAT)} limit.`); + } else { + zipQueue.push(payload); } - zipFile(payload.file, ZIP_FILE_FORMAT) - .then(file => { - zippedFile(file); - uploadFile(file); - }) - .catch(error => { - console.error('Error zipping file:', error); - }); break; case FileVerification.EXCEED_SIZE: - failedFile( - FileVerification.EXCEED_SIZE, - `${payload.file.type} size exceeds ${maxSize(payload.file.type)} limit.` - ); + failedFile(FileVerification.EXCEED_SIZE, `${file.type} size exceeds ${maxSize(file.type)} limit.`); break; case FileVerification.SUCCEED: - uploadFile(payload.file); + uploadFile(payload); break; } + + batchAccumulator.push(payload); + + if(payload.totalFiles !== batchAccumulator.length) { + return; + } + + const chunks = new Array(); + + while (zipQueue.length > 0) { + const chunk = zipQueue.reduce((acc, msg) => { + const newSize = acc.size + msg.file.size; + if (newSize <= SUPPORTED_FILE_FORMATS[ZIP_FILE_FORMAT]) { + acc.messages.push(msg); + acc.size = newSize; + } + return acc; + }, { size: 0, messages: new Array()}); + + chunks.push(chunk.messages); + zipQueue = zipQueue.filter(msg => !chunk.messages.includes(msg)); + } + + for(const chunk of chunks) { + zipFiles(chunk.map(msg => msg.file), ZIP_FILE_FORMAT, payload.timestamp) + .then(file => { + const {messageId} = chunk[0]; + const messageIds = chunk.map(msg => msg.messageId); + tabPort.postMessage({ + type: MsgType.ZIPPED, + messageId: messageId, + zippedIds: messageIds.filter(id => id !== messageId), + file: file, + }); + uploadFile(chunk[0], file); + }) + .catch(error => { + console.error('Error zipping file:', error); + }); + } + + batchAccumulator = []; } function onJoinChannel(payload: JoinChannel) {