diff --git a/src/domain/session/room/timeline/tiles/FileTile.js b/src/domain/session/room/timeline/tiles/FileTile.js index eab55136..c87b090f 100644 --- a/src/domain/session/room/timeline/tiles/FileTile.js +++ b/src/domain/session/room/timeline/tiles/FileTile.js @@ -24,12 +24,9 @@ export class FileTile extends MessageTile { this._error = null; this._downloading = false; if (this._isUploading) { - // should really do this with an ObservableValue and waitFor to prevent leaks when the promise never resolves - this._entry.attachment.uploaded().finally(() => { - if (!this.isDisposed) { - this.emitChange("label"); - } - }); + this.track(this._entry.attachments.url.status.subscribe(() => { + this.emitChange("label"); + })); } } @@ -56,28 +53,28 @@ export class FileTile extends MessageTile { get size() { if (this._isUploading) { - return this._entry.attachment.localPreview.size; + return this._entry.attachments.url.localPreview.size; } else { return this._getContent().info?.size; } } get _isUploading() { - return this._entry.attachment && !this._entry.attachment.isUploaded; + return this._entry.attachments?.url && !this._entry.attachments.url.isUploaded; } get label() { if (this._error) { return `Could not decrypt file: ${this._error.message}`; } - if (this._entry.attachment?.error) { - return `Failed to upload: ${this._entry.attachment.error.message}`; + if (this._entry.attachments?.url?.error) { + return `Failed to upload: ${this._entry.attachments.url.error.message}`; } const content = this._getContent(); const filename = content.body; const size = formatSize(this.size); if (this._isUploading) { - return this.i18n`Uploading ${filename} (${size})…`; + return this.i18n`Uploading (${this._entry.attachments.url.status.get()}) ${filename} (${size})…`; } else if (this._downloading) { return this.i18n`Downloading ${filename} (${size})…`; } else { diff --git a/src/matrix/room/AttachmentUpload.js b/src/matrix/room/AttachmentUpload.js index 98d81894..5dd957a0 100644 --- a/src/matrix/room/AttachmentUpload.js +++ b/src/matrix/room/AttachmentUpload.js @@ -15,6 +15,11 @@ limitations under the License. */ import {encryptAttachment} from "../e2ee/attachment.js"; +import {createEnum} from "../../utils/enum.js"; +import {ObservableValue} from "../../observable/ObservableValue.js"; +import {AbortError} from "../../utils/error.js"; + +export const UploadStatus = createEnum("Waiting", "Encrypting", "Uploading", "Uploaded", "Error"); export class AttachmentUpload { constructor({filename, blob, hsApi, platform, isEncrypted}) { @@ -26,44 +31,51 @@ export class AttachmentUpload { this._mxcUrl = null; this._transferredBlob = null; this._encryptionInfo = null; - this._uploadPromise = null; this._uploadRequest = null; this._aborted = false; this._error = null; + this._status = new ObservableValue(UploadStatus.Waiting); } - upload() { - if (!this._uploadPromise) { - this._uploadPromise = this._upload(); + get status() { + return this._status; + } + + async upload() { + if (this._status.get() === UploadStatus.Waiting) { + this._upload(); + } + await this._status.waitFor(s => s === UploadStatus.Error || s === UploadStatus.Uploaded).promise; + if (this._status.get() === UploadStatus.Error) { + throw this._error; } - return this._uploadPromise; } + /** @package */ async _upload() { try { let transferredBlob = this._unencryptedBlob; if (this._isEncrypted) { + this._status.set(UploadStatus.Encrypting); const {info, blob} = await encryptAttachment(this._platform, this._unencryptedBlob); transferredBlob = blob; this._encryptionInfo = info; } if (this._aborted) { - return; + throw new AbortError("upload aborted during encryption"); } + this._status.set(UploadStatus.Uploading); this._uploadRequest = this._hsApi.uploadAttachment(transferredBlob, this._filename); const {content_uri} = await this._uploadRequest.response(); this._mxcUrl = content_uri; this._transferredBlob = transferredBlob; + this._status.set(UploadStatus.Uploaded); } catch (err) { this._error = err; - throw err; + this._status.set(UploadStatus.Error); } } - get isUploaded() { - return !!this._transferredBlob; - } - /** @public */ abort() { this._aborted = true; @@ -80,29 +92,34 @@ export class AttachmentUpload { } /** @package */ - uploaded() { - if (!this._uploadPromise) { - throw new Error("upload has not started yet"); - } - return this._uploadPromise; - } - - /** @package */ - applyToContent(content) { + applyToContent(urlPath, content) { if (!this._mxcUrl) { throw new Error("upload has not finished"); } - content.info = { - size: this._transferredBlob.size, - mimetype: this._unencryptedBlob.mimeType, - }; + let prefix = urlPath.substr(0, urlPath.lastIndexOf("url")); + setPath(`${prefix}info.size`, content, this._transferredBlob.size); + setPath(`${prefix}info.mimetype`, content, this._unencryptedBlob.mimeType); if (this._isEncrypted) { - content.file = Object.assign(this._encryptionInfo, { + setPath(`${prefix}file`, content, Object.assign(this._encryptionInfo, { mimetype: this._unencryptedBlob.mimeType, url: this._mxcUrl - }); + })); } else { - content.url = this._mxcUrl; + setPath(`${prefix}url`, content, this._mxcUrl); } } } + +function setPath(path, content, value) { + const parts = path.split("."); + let obj = content; + for (let i = 0; i < (parts.length - 1); i += 1) { + const key = parts[i]; + if (!obj[key]) { + obj[key] = {}; + } + obj = obj[key]; + } + const propKey = parts[parts.length - 1]; + obj[propKey] = value; +} diff --git a/src/matrix/room/Room.js b/src/matrix/room/Room.js index 109c096e..69829e9c 100644 --- a/src/matrix/room/Room.js +++ b/src/matrix/room/Room.js @@ -352,8 +352,8 @@ export class Room extends EventEmitter { } /** @public */ - sendEvent(eventType, content, attachment) { - return this._sendQueue.enqueueEvent(eventType, content, attachment); + sendEvent(eventType, content, attachments) { + return this._sendQueue.enqueueEvent(eventType, content, attachments); } /** @public */ @@ -633,10 +633,9 @@ export class Room extends EventEmitter { } } - uploadAttachment(blob, filename) { + createAttachment(blob, filename) { const attachment = new AttachmentUpload({blob, filename, hsApi: this._hsApi, platform: this._platform, isEncrypted: this.isEncrypted}); - attachment.upload(); return attachment; } diff --git a/src/matrix/room/sending/PendingEvent.js b/src/matrix/room/sending/PendingEvent.js index 030e57b6..b1e7f5a2 100644 --- a/src/matrix/room/sending/PendingEvent.js +++ b/src/matrix/room/sending/PendingEvent.js @@ -15,9 +15,9 @@ limitations under the License. */ export class PendingEvent { - constructor(data, attachment) { + constructor(data, attachments) { this._data = data; - this.attachment = attachment; + this.attachments = attachments; } get roomId() { return this._data.roomId; } diff --git a/src/matrix/room/sending/SendQueue.js b/src/matrix/room/sending/SendQueue.js index 76ff2c66..23019358 100644 --- a/src/matrix/room/sending/SendQueue.js +++ b/src/matrix/room/sending/SendQueue.js @@ -51,16 +51,15 @@ export class SendQueue { this._amountSent += 1; continue; } - if (pendingEvent.attachment) { - const {attachment} = pendingEvent; + if (pendingEvent.attachments) { try { - await attachment.uploaded(); + await this._uploadAttachments(pendingEvent); } catch (err) { - console.log("upload failed, skip sending message", pendingEvent); + console.log("upload failed, skip sending message", err, pendingEvent); this._amountSent += 1; continue; } - attachment.applyToContent(pendingEvent.content); + console.log("attachments upload, content is now", pendingEvent.content); } if (pendingEvent.needsEncryption) { const {type, content} = await this._roomEncryption.encrypt( @@ -127,8 +126,8 @@ export class SendQueue { } } - async enqueueEvent(eventType, content, attachment) { - const pendingEvent = await this._createAndStoreEvent(eventType, content, attachment); + async enqueueEvent(eventType, content, attachments) { + const pendingEvent = await this._createAndStoreEvent(eventType, content, attachments); this._pendingEvents.set(pendingEvent); console.log("added to _pendingEvents set", this._pendingEvents.length); if (!this._isSending && !this._offline) { @@ -161,7 +160,7 @@ export class SendQueue { await txn.complete(); } - async _createAndStoreEvent(eventType, content, attachment) { + async _createAndStoreEvent(eventType, content, attachments) { console.log("_createAndStoreEvent"); const txn = this._storage.readWriteTxn([this._storage.storeNames.pendingEvents]); let pendingEvent; @@ -178,7 +177,7 @@ export class SendQueue { content, txnId: makeTxnId(), needsEncryption: !!this._roomEncryption - }, attachment); + }, attachments); console.log("_createAndStoreEvent: adding to pendingEventsStore"); pendingEventsStore.add(pendingEvent.data); } catch (err) { @@ -188,4 +187,12 @@ export class SendQueue { await txn.complete(); return pendingEvent; } + + async _uploadAttachments(pendingEvent) { + const {attachments} = pendingEvent; + for (const [urlPath, attachment] of Object.entries(attachments)) { + await attachment.upload(); + attachment.applyToContent(urlPath, pendingEvent.content); + } + } } diff --git a/src/matrix/room/timeline/entries/PendingEventEntry.js b/src/matrix/room/timeline/entries/PendingEventEntry.js index 9f91c80f..e31e56d5 100644 --- a/src/matrix/room/timeline/entries/PendingEventEntry.js +++ b/src/matrix/room/timeline/entries/PendingEventEntry.js @@ -64,8 +64,8 @@ export class PendingEventEntry extends BaseEntry { return this._pendingEvent.txnId; } - get attachment() { - return this._pendingEvent.attachment; + get attachments() { + return this._pendingEvent.attachments; } notifyUpdate() {