forked from mystiq/hydrogen-web
support multiple attachments per event
This commit is contained in:
parent
14b3c4b701
commit
d2a4242e5b
6 changed files with 75 additions and 55 deletions
|
@ -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(() => {
|
|
||||||
if (!this.isDisposed) {
|
|
||||||
this.emitChange("label");
|
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 {
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
return this._uploadPromise;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @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;
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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; }
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
Loading…
Reference in a new issue