support multiple attachments per event

This commit is contained in:
Bruno Windels 2020-11-13 17:19:19 +01:00
parent 14b3c4b701
commit d2a4242e5b
6 changed files with 75 additions and 55 deletions

View file

@ -24,12 +24,9 @@ export class FileTile extends MessageTile {
this._error = null; this._error = null;
this._downloading = false; this._downloading = false;
if (this._isUploading) { if (this._isUploading) {
// should really do this with an ObservableValue and waitFor to prevent leaks when the promise never resolves this.track(this._entry.attachments.url.status.subscribe(() => {
this._entry.attachment.uploaded().finally(() => { this.emitChange("label");
if (!this.isDisposed) { }));
this.emitChange("label");
}
});
} }
} }
@ -56,28 +53,28 @@ export class FileTile extends MessageTile {
get size() { get size() {
if (this._isUploading) { if (this._isUploading) {
return this._entry.attachment.localPreview.size; return this._entry.attachments.url.localPreview.size;
} else { } else {
return this._getContent().info?.size; return this._getContent().info?.size;
} }
} }
get _isUploading() { get _isUploading() {
return this._entry.attachment && !this._entry.attachment.isUploaded; return this._entry.attachments?.url && !this._entry.attachments.url.isUploaded;
} }
get label() { get label() {
if (this._error) { if (this._error) {
return `Could not decrypt file: ${this._error.message}`; return `Could not decrypt file: ${this._error.message}`;
} }
if (this._entry.attachment?.error) { if (this._entry.attachments?.url?.error) {
return `Failed to upload: ${this._entry.attachment.error.message}`; return `Failed to upload: ${this._entry.attachments.url.error.message}`;
} }
const content = this._getContent(); const content = this._getContent();
const filename = content.body; const filename = content.body;
const size = formatSize(this.size); const size = formatSize(this.size);
if (this._isUploading) { 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) { } else if (this._downloading) {
return this.i18n`Downloading ${filename} (${size})…`; return this.i18n`Downloading ${filename} (${size})…`;
} else { } else {

View file

@ -15,6 +15,11 @@ limitations under the License.
*/ */
import {encryptAttachment} from "../e2ee/attachment.js"; 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 { export class AttachmentUpload {
constructor({filename, blob, hsApi, platform, isEncrypted}) { constructor({filename, blob, hsApi, platform, isEncrypted}) {
@ -26,44 +31,51 @@ export class AttachmentUpload {
this._mxcUrl = null; this._mxcUrl = null;
this._transferredBlob = null; this._transferredBlob = null;
this._encryptionInfo = null; this._encryptionInfo = null;
this._uploadPromise = null;
this._uploadRequest = null; this._uploadRequest = null;
this._aborted = false; this._aborted = false;
this._error = null; this._error = null;
this._status = new ObservableValue(UploadStatus.Waiting);
} }
upload() { get status() {
if (!this._uploadPromise) { return this._status;
this._uploadPromise = this._upload(); }
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() { async _upload() {
try { try {
let transferredBlob = this._unencryptedBlob; let transferredBlob = this._unencryptedBlob;
if (this._isEncrypted) { if (this._isEncrypted) {
this._status.set(UploadStatus.Encrypting);
const {info, blob} = await encryptAttachment(this._platform, this._unencryptedBlob); const {info, blob} = await encryptAttachment(this._platform, this._unencryptedBlob);
transferredBlob = blob; transferredBlob = blob;
this._encryptionInfo = info; this._encryptionInfo = info;
} }
if (this._aborted) { if (this._aborted) {
return; throw new AbortError("upload aborted during encryption");
} }
this._status.set(UploadStatus.Uploading);
this._uploadRequest = this._hsApi.uploadAttachment(transferredBlob, this._filename); this._uploadRequest = this._hsApi.uploadAttachment(transferredBlob, this._filename);
const {content_uri} = await this._uploadRequest.response(); const {content_uri} = await this._uploadRequest.response();
this._mxcUrl = content_uri; this._mxcUrl = content_uri;
this._transferredBlob = transferredBlob; this._transferredBlob = transferredBlob;
this._status.set(UploadStatus.Uploaded);
} catch (err) { } catch (err) {
this._error = err; this._error = err;
throw err; this._status.set(UploadStatus.Error);
} }
} }
get isUploaded() {
return !!this._transferredBlob;
}
/** @public */ /** @public */
abort() { abort() {
this._aborted = true; this._aborted = true;
@ -80,29 +92,34 @@ export class AttachmentUpload {
} }
/** @package */ /** @package */
uploaded() { applyToContent(urlPath, content) {
if (!this._uploadPromise) {
throw new Error("upload has not started yet");
}
return this._uploadPromise;
}
/** @package */
applyToContent(content) {
if (!this._mxcUrl) { if (!this._mxcUrl) {
throw new Error("upload has not finished"); throw new Error("upload has not finished");
} }
content.info = { let prefix = urlPath.substr(0, urlPath.lastIndexOf("url"));
size: this._transferredBlob.size, setPath(`${prefix}info.size`, content, this._transferredBlob.size);
mimetype: this._unencryptedBlob.mimeType, setPath(`${prefix}info.mimetype`, content, this._unencryptedBlob.mimeType);
};
if (this._isEncrypted) { if (this._isEncrypted) {
content.file = Object.assign(this._encryptionInfo, { setPath(`${prefix}file`, content, Object.assign(this._encryptionInfo, {
mimetype: this._unencryptedBlob.mimeType, mimetype: this._unencryptedBlob.mimeType,
url: this._mxcUrl url: this._mxcUrl
}); }));
} else { } 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;
}

View file

@ -352,8 +352,8 @@ export class Room extends EventEmitter {
} }
/** @public */ /** @public */
sendEvent(eventType, content, attachment) { sendEvent(eventType, content, attachments) {
return this._sendQueue.enqueueEvent(eventType, content, attachment); return this._sendQueue.enqueueEvent(eventType, content, attachments);
} }
/** @public */ /** @public */
@ -633,10 +633,9 @@ export class Room extends EventEmitter {
} }
} }
uploadAttachment(blob, filename) { createAttachment(blob, filename) {
const attachment = new AttachmentUpload({blob, filename, const attachment = new AttachmentUpload({blob, filename,
hsApi: this._hsApi, platform: this._platform, isEncrypted: this.isEncrypted}); hsApi: this._hsApi, platform: this._platform, isEncrypted: this.isEncrypted});
attachment.upload();
return attachment; return attachment;
} }

View file

@ -15,9 +15,9 @@ limitations under the License.
*/ */
export class PendingEvent { export class PendingEvent {
constructor(data, attachment) { constructor(data, attachments) {
this._data = data; this._data = data;
this.attachment = attachment; this.attachments = attachments;
} }
get roomId() { return this._data.roomId; } get roomId() { return this._data.roomId; }

View file

@ -51,16 +51,15 @@ export class SendQueue {
this._amountSent += 1; this._amountSent += 1;
continue; continue;
} }
if (pendingEvent.attachment) { if (pendingEvent.attachments) {
const {attachment} = pendingEvent;
try { try {
await attachment.uploaded(); await this._uploadAttachments(pendingEvent);
} catch (err) { } catch (err) {
console.log("upload failed, skip sending message", pendingEvent); console.log("upload failed, skip sending message", err, pendingEvent);
this._amountSent += 1; this._amountSent += 1;
continue; continue;
} }
attachment.applyToContent(pendingEvent.content); console.log("attachments upload, content is now", pendingEvent.content);
} }
if (pendingEvent.needsEncryption) { if (pendingEvent.needsEncryption) {
const {type, content} = await this._roomEncryption.encrypt( const {type, content} = await this._roomEncryption.encrypt(
@ -127,8 +126,8 @@ export class SendQueue {
} }
} }
async enqueueEvent(eventType, content, attachment) { async enqueueEvent(eventType, content, attachments) {
const pendingEvent = await this._createAndStoreEvent(eventType, content, attachment); const pendingEvent = await this._createAndStoreEvent(eventType, content, attachments);
this._pendingEvents.set(pendingEvent); this._pendingEvents.set(pendingEvent);
console.log("added to _pendingEvents set", this._pendingEvents.length); console.log("added to _pendingEvents set", this._pendingEvents.length);
if (!this._isSending && !this._offline) { if (!this._isSending && !this._offline) {
@ -161,7 +160,7 @@ export class SendQueue {
await txn.complete(); await txn.complete();
} }
async _createAndStoreEvent(eventType, content, attachment) { async _createAndStoreEvent(eventType, content, attachments) {
console.log("_createAndStoreEvent"); console.log("_createAndStoreEvent");
const txn = this._storage.readWriteTxn([this._storage.storeNames.pendingEvents]); const txn = this._storage.readWriteTxn([this._storage.storeNames.pendingEvents]);
let pendingEvent; let pendingEvent;
@ -178,7 +177,7 @@ export class SendQueue {
content, content,
txnId: makeTxnId(), txnId: makeTxnId(),
needsEncryption: !!this._roomEncryption needsEncryption: !!this._roomEncryption
}, attachment); }, attachments);
console.log("_createAndStoreEvent: adding to pendingEventsStore"); console.log("_createAndStoreEvent: adding to pendingEventsStore");
pendingEventsStore.add(pendingEvent.data); pendingEventsStore.add(pendingEvent.data);
} catch (err) { } catch (err) {
@ -188,4 +187,12 @@ export class SendQueue {
await txn.complete(); await txn.complete();
return pendingEvent; return pendingEvent;
} }
async _uploadAttachments(pendingEvent) {
const {attachments} = pendingEvent;
for (const [urlPath, attachment] of Object.entries(attachments)) {
await attachment.upload();
attachment.applyToContent(urlPath, pendingEvent.content);
}
}
} }

View file

@ -64,8 +64,8 @@ export class PendingEventEntry extends BaseEntry {
return this._pendingEvent.txnId; return this._pendingEvent.txnId;
} }
get attachment() { get attachments() {
return this._pendingEvent.attachment; return this._pendingEvent.attachments;
} }
notifyUpdate() { notifyUpdate() {