forked from mystiq/hydrogen-web
merge state machine from AttachmentUpload into PendingEvent
to have less state machines, and we are mostly interested in the aggregate status of all attachments of an event this will also drive updates through the pending events collection that already exists rather than an extra observablevalue, so less housekeeping to update the UI.
This commit is contained in:
parent
91f7970d66
commit
fd81111bfb
5 changed files with 215 additions and 124 deletions
|
@ -15,81 +15,33 @@ 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, platform}) {
|
||||||
this._filename = filename;
|
this._filename = filename;
|
||||||
|
// need to keep around for local preview while uploading
|
||||||
this._unencryptedBlob = blob;
|
this._unencryptedBlob = blob;
|
||||||
this._isEncrypted = isEncrypted;
|
this._transferredBlob = this._unencryptedBlob;
|
||||||
this._platform = platform;
|
this._platform = platform;
|
||||||
this._hsApi = hsApi;
|
|
||||||
this._mxcUrl = null;
|
this._mxcUrl = null;
|
||||||
this._transferredBlob = null;
|
|
||||||
this._encryptionInfo = null;
|
this._encryptionInfo = 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);
|
this._sentBytes = 0;
|
||||||
this._progress = new ObservableValue(0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get status() {
|
/** important to call after encrypt() if encryption is needed */
|
||||||
return this._status;
|
get size() {
|
||||||
|
return this._transferredBlob.size;
|
||||||
}
|
}
|
||||||
|
|
||||||
get uploadProgress() {
|
get sentBytes() {
|
||||||
return this._progress;
|
return this._sentBytes;
|
||||||
}
|
|
||||||
|
|
||||||
async upload() {
|
|
||||||
if (this._status.get() === UploadStatus.Waiting) {
|
|
||||||
this._upload();
|
|
||||||
}
|
|
||||||
await this._status.waitFor(s => {
|
|
||||||
return s === UploadStatus.Error || s === UploadStatus.Uploaded;
|
|
||||||
}).promise;
|
|
||||||
if (this._status.get() === UploadStatus.Error) {
|
|
||||||
throw this._error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @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) {
|
|
||||||
throw new AbortError("upload aborted during encryption");
|
|
||||||
}
|
|
||||||
this._progress.set(0);
|
|
||||||
this._status.set(UploadStatus.Uploading);
|
|
||||||
this._uploadRequest = this._hsApi.uploadAttachment(transferredBlob, this._filename, {
|
|
||||||
uploadProgress: sentBytes => this._progress.set(sentBytes / transferredBlob.size)
|
|
||||||
});
|
|
||||||
const {content_uri} = await this._uploadRequest.response();
|
|
||||||
this._progress.set(1);
|
|
||||||
this._mxcUrl = content_uri;
|
|
||||||
this._transferredBlob = transferredBlob;
|
|
||||||
this._status.set(UploadStatus.Uploaded);
|
|
||||||
} catch (err) {
|
|
||||||
this._error = err;
|
|
||||||
this._status.set(UploadStatus.Error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
abort() {
|
abort() {
|
||||||
this._aborted = true;
|
|
||||||
this._uploadRequest?.abort();
|
this._uploadRequest?.abort();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -98,8 +50,26 @@ export class AttachmentUpload {
|
||||||
return this._unencryptedBlob;
|
return this._unencryptedBlob;
|
||||||
}
|
}
|
||||||
|
|
||||||
get error() {
|
/** @package */
|
||||||
return this._error;
|
async encrypt() {
|
||||||
|
if (this._encryptionInfo) {
|
||||||
|
throw new Error("already encrypted");
|
||||||
|
}
|
||||||
|
const {info, blob} = await encryptAttachment(this._platform, this._transferredBlob);
|
||||||
|
this._transferredBlob = blob;
|
||||||
|
this._encryptionInfo = info;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @package */
|
||||||
|
async upload(hsApi, progressCallback) {
|
||||||
|
this._uploadRequest = hsApi.uploadAttachment(this._transferredBlob, this._filename, {
|
||||||
|
uploadProgress: sentBytes => {
|
||||||
|
this._sentBytes = sentBytes;
|
||||||
|
progressCallback();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const {content_uri} = await this._uploadRequest.response();
|
||||||
|
this._mxcUrl = content_uri;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @package */
|
/** @package */
|
||||||
|
@ -110,7 +80,7 @@ export class AttachmentUpload {
|
||||||
let prefix = urlPath.substr(0, urlPath.lastIndexOf("url"));
|
let prefix = urlPath.substr(0, urlPath.lastIndexOf("url"));
|
||||||
setPath(`${prefix}info.size`, content, this._transferredBlob.size);
|
setPath(`${prefix}info.size`, content, this._transferredBlob.size);
|
||||||
setPath(`${prefix}info.mimetype`, content, this._unencryptedBlob.mimeType);
|
setPath(`${prefix}info.mimetype`, content, this._unencryptedBlob.mimeType);
|
||||||
if (this._isEncrypted) {
|
if (this._encryptionInfo) {
|
||||||
setPath(`${prefix}file`, content, 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
|
||||||
|
|
|
@ -634,9 +634,7 @@ export class Room extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
createAttachment(blob, filename) {
|
createAttachment(blob, filename) {
|
||||||
const attachment = new AttachmentUpload({blob, filename,
|
return new AttachmentUpload({blob, filename, platform: this._platform});
|
||||||
hsApi: this._hsApi, platform: this._platform, isEncrypted: this.isEncrypted});
|
|
||||||
return attachment;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dispose() {
|
dispose() {
|
||||||
|
|
|
@ -13,11 +13,31 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
import {createEnum} from "../../../utils/enum.js";
|
||||||
|
import {AbortError} from "../../../utils/error.js";
|
||||||
|
|
||||||
|
export const SendStatus = createEnum(
|
||||||
|
"Waiting",
|
||||||
|
"EncryptingAttachments",
|
||||||
|
"UploadingAttachments",
|
||||||
|
"Encrypting",
|
||||||
|
"Sending",
|
||||||
|
"Sent",
|
||||||
|
"Error",
|
||||||
|
);
|
||||||
|
|
||||||
export class PendingEvent {
|
export class PendingEvent {
|
||||||
constructor(data, attachments) {
|
constructor({data, remove, emitUpdate, attachments}) {
|
||||||
this._data = data;
|
this._data = data;
|
||||||
this.attachments = attachments;
|
this._attachments = attachments;
|
||||||
|
this._emitUpdate = () => {
|
||||||
|
console.log("PendingEvent status", this.status, this._attachments && Object.entries(this._attachments).map(([key, a]) => `${key}: ${a.sentBytes}/${a.size}`));
|
||||||
|
emitUpdate();
|
||||||
|
};
|
||||||
|
this._removeFromQueueCallback = remove;
|
||||||
|
this._aborted = false;
|
||||||
|
this._status = SendStatus.Waiting;
|
||||||
|
this._sendRequest = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
get roomId() { return this._data.roomId; }
|
get roomId() { return this._data.roomId; }
|
||||||
|
@ -25,14 +45,111 @@ export class PendingEvent {
|
||||||
get eventType() { return this._data.eventType; }
|
get eventType() { return this._data.eventType; }
|
||||||
get txnId() { return this._data.txnId; }
|
get txnId() { return this._data.txnId; }
|
||||||
get remoteId() { return this._data.remoteId; }
|
get remoteId() { return this._data.remoteId; }
|
||||||
set remoteId(value) { this._data.remoteId = value; }
|
|
||||||
get content() { return this._data.content; }
|
get content() { return this._data.content; }
|
||||||
get needsEncryption() { return this._data.needsEncryption; }
|
|
||||||
get data() { return this._data; }
|
get data() { return this._data; }
|
||||||
|
|
||||||
|
getAttachment(key) {
|
||||||
|
return this._attachments && this._attachments[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
get needsSending() {
|
||||||
|
return !this.remoteId && !this.aborted;
|
||||||
|
}
|
||||||
|
|
||||||
|
get needsEncryption() {
|
||||||
|
return this._data.needsEncryption && !this.aborted;
|
||||||
|
}
|
||||||
|
|
||||||
|
get needsUpload() {
|
||||||
|
return this._data.needsUpload && !this.aborted;
|
||||||
|
}
|
||||||
|
|
||||||
|
setEncrypting() {
|
||||||
|
this._status = SendStatus.Encrypting;
|
||||||
|
this._emitUpdate("status");
|
||||||
|
}
|
||||||
|
|
||||||
setEncrypted(type, content) {
|
setEncrypted(type, content) {
|
||||||
this._data.eventType = type;
|
this._data.encryptedEventType = type;
|
||||||
this._data.content = content;
|
this._data.encryptedContent = content;
|
||||||
this._data.needsEncryption = false;
|
this._data.needsEncryption = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setError(error) {
|
||||||
|
this._status = SendStatus.Error;
|
||||||
|
this._error = error;
|
||||||
|
this._emitUpdate("status");
|
||||||
|
}
|
||||||
|
|
||||||
|
get status() { return this._status; }
|
||||||
|
get error() { return this._error; }
|
||||||
|
|
||||||
|
get attachmentsTotalBytes() {
|
||||||
|
return Object.values(this._attachments).reduce((t, a) => t + a.size, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
get attachmentsSentBytes() {
|
||||||
|
return Object.values(this._attachments).reduce((t, a) => t + a.sentBytes, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
async uploadAttachments(hsApi) {
|
||||||
|
if (!this.needsUpload) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.needsEncryption) {
|
||||||
|
this._status = SendStatus.EncryptingAttachments;
|
||||||
|
this._emitUpdate("status");
|
||||||
|
for (const attachment of Object.values(this._attachments)) {
|
||||||
|
await attachment.encrypt();
|
||||||
|
if (this.aborted) {
|
||||||
|
throw new AbortError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._status = SendStatus.UploadingAttachments;
|
||||||
|
this._emitUpdate("status");
|
||||||
|
for (const [urlPath, attachment] of Object.entries(this._attachments)) {
|
||||||
|
await attachment.upload(hsApi, () => {
|
||||||
|
this._emitUpdate("attachmentsSentBytes");
|
||||||
|
});
|
||||||
|
attachment.applyToContent(urlPath, this.content);
|
||||||
|
}
|
||||||
|
this._data.needsUpload = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
abort() {
|
||||||
|
if (!this._aborted) {
|
||||||
|
this._aborted = true;
|
||||||
|
if (this._attachments) {
|
||||||
|
for (const attachment of Object.values(this._attachments)) {
|
||||||
|
attachment.abort();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._sendRequest?.abort();
|
||||||
|
this._removeFromQueueCallback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get aborted() {
|
||||||
|
return this._aborted;
|
||||||
|
}
|
||||||
|
|
||||||
|
async send(hsApi) {
|
||||||
|
console.log(`sending event ${this.eventType} in ${this.roomId}`);
|
||||||
|
this._status = SendStatus.Sending;
|
||||||
|
this._emitUpdate("status");
|
||||||
|
const eventType = this._data.encryptedEventType || this._data.eventType;
|
||||||
|
const content = this._data.encryptedContent || this._data.content;
|
||||||
|
this._sendRequest = hsApi.send(
|
||||||
|
this.roomId,
|
||||||
|
eventType,
|
||||||
|
this.txnId,
|
||||||
|
content
|
||||||
|
);
|
||||||
|
const response = await this._sendRequest.response();
|
||||||
|
this._sendRequest = null;
|
||||||
|
this._data.remoteId = response.event_id;
|
||||||
|
this._status = SendStatus.Sent;
|
||||||
|
this._emitUpdate("status");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,13 +29,22 @@ export class SendQueue {
|
||||||
if (pendingEvents.length) {
|
if (pendingEvents.length) {
|
||||||
console.info(`SendQueue for room ${roomId} has ${pendingEvents.length} pending events`, pendingEvents);
|
console.info(`SendQueue for room ${roomId} has ${pendingEvents.length} pending events`, pendingEvents);
|
||||||
}
|
}
|
||||||
this._pendingEvents.setManyUnsorted(pendingEvents.map(data => new PendingEvent(data)));
|
this._pendingEvents.setManyUnsorted(pendingEvents.map(data => this._createPendingEvent(data)));
|
||||||
this._isSending = false;
|
this._isSending = false;
|
||||||
this._offline = false;
|
this._offline = false;
|
||||||
this._amountSent = 0;
|
|
||||||
this._roomEncryption = null;
|
this._roomEncryption = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_createPendingEvent(data, attachments = null) {
|
||||||
|
const pendingEvent = new PendingEvent({
|
||||||
|
data,
|
||||||
|
remove: () => this._removeEvent(pendingEvent),
|
||||||
|
emitUpdate: () => this._pendingEvents.set(pendingEvent),
|
||||||
|
attachments
|
||||||
|
});
|
||||||
|
return pendingEvent;
|
||||||
|
}
|
||||||
|
|
||||||
enableEncryption(roomEncryption) {
|
enableEncryption(roomEncryption) {
|
||||||
this._roomEncryption = roomEncryption;
|
this._roomEncryption = roomEncryption;
|
||||||
}
|
}
|
||||||
|
@ -43,53 +52,44 @@ export class SendQueue {
|
||||||
async _sendLoop() {
|
async _sendLoop() {
|
||||||
this._isSending = true;
|
this._isSending = true;
|
||||||
try {
|
try {
|
||||||
console.log("start sending", this._amountSent, "<", this._pendingEvents.length);
|
for (let i = 0; i < this._pendingEvents.length; i += 1) {
|
||||||
while (this._amountSent < this._pendingEvents.length) {
|
const pendingEvent = this._pendingEvents.get(i);
|
||||||
const pendingEvent = this._pendingEvents.get(this._amountSent);
|
try {
|
||||||
console.log("trying to send", pendingEvent.content.body);
|
await this._sendEvent(pendingEvent);
|
||||||
if (pendingEvent.remoteId) {
|
} catch(err) {
|
||||||
this._amountSent += 1;
|
if (err instanceof ConnectionError) {
|
||||||
continue;
|
this._offline = true;
|
||||||
}
|
break;
|
||||||
if (pendingEvent.attachments) {
|
} else {
|
||||||
try {
|
pendingEvent.setError(err);
|
||||||
await this._uploadAttachments(pendingEvent);
|
|
||||||
} catch (err) {
|
|
||||||
console.log("upload failed, skip sending message", err, pendingEvent);
|
|
||||||
this._amountSent += 1;
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
console.log("attachments upload, content is now", pendingEvent.content);
|
|
||||||
}
|
}
|
||||||
if (pendingEvent.needsEncryption) {
|
|
||||||
const {type, content} = await this._roomEncryption.encrypt(
|
|
||||||
pendingEvent.eventType, pendingEvent.content, this._hsApi);
|
|
||||||
pendingEvent.setEncrypted(type, content);
|
|
||||||
await this._tryUpdateEvent(pendingEvent);
|
|
||||||
}
|
|
||||||
console.log("really sending now");
|
|
||||||
const response = await this._hsApi.send(
|
|
||||||
pendingEvent.roomId,
|
|
||||||
pendingEvent.eventType,
|
|
||||||
pendingEvent.txnId,
|
|
||||||
pendingEvent.content
|
|
||||||
).response();
|
|
||||||
pendingEvent.remoteId = response.event_id;
|
|
||||||
//
|
|
||||||
console.log("writing remoteId now");
|
|
||||||
await this._tryUpdateEvent(pendingEvent);
|
|
||||||
console.log("keep sending?", this._amountSent, "<", this._pendingEvents.length);
|
|
||||||
this._amountSent += 1;
|
|
||||||
}
|
|
||||||
} catch(err) {
|
|
||||||
if (err instanceof ConnectionError) {
|
|
||||||
this._offline = true;
|
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
this._isSending = false;
|
this._isSending = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async _sendEvent(pendingEvent) {
|
||||||
|
if (pendingEvent.needsUpload) {
|
||||||
|
await pendingEvent.uploadAttachments(this._hsApi);
|
||||||
|
console.log("attachments upload, content is now", pendingEvent.content);
|
||||||
|
await this._tryUpdateEvent(pendingEvent);
|
||||||
|
}
|
||||||
|
if (pendingEvent.needsEncryption) {
|
||||||
|
pendingEvent.setEncrypting();
|
||||||
|
const {type, content} = await this._roomEncryption.encrypt(
|
||||||
|
pendingEvent.eventType, pendingEvent.content, this._hsApi);
|
||||||
|
pendingEvent.setEncrypted(type, content);
|
||||||
|
await this._tryUpdateEvent(pendingEvent);
|
||||||
|
}
|
||||||
|
if (pendingEvent.needsSending) {
|
||||||
|
await pendingEvent.send(this._hsApi);
|
||||||
|
console.log("writing remoteId");
|
||||||
|
await this._tryUpdateEvent(pendingEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
removeRemoteEchos(events, txn) {
|
removeRemoteEchos(events, txn) {
|
||||||
const removed = [];
|
const removed = [];
|
||||||
for (const event of events) {
|
for (const event of events) {
|
||||||
|
@ -109,11 +109,24 @@ export class SendQueue {
|
||||||
return removed;
|
return removed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async _removeEvent(pendingEvent) {
|
||||||
|
const idx = this._pendingEvents.array.indexOf(pendingEvent);
|
||||||
|
if (idx !== -1) {
|
||||||
|
const txn = this._storage.readWriteTxn([this._storage.storeNames.pendingEvents]);
|
||||||
|
try {
|
||||||
|
txn.pendingEvents.remove(pendingEvent.roomId, pendingEvent.queueIndex);
|
||||||
|
} catch (err) {
|
||||||
|
txn.abort();
|
||||||
|
}
|
||||||
|
await txn.complete();
|
||||||
|
this._pendingEvents.remove(idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
emitRemovals(pendingEvents) {
|
emitRemovals(pendingEvents) {
|
||||||
for (const pendingEvent of pendingEvents) {
|
for (const pendingEvent of pendingEvents) {
|
||||||
const idx = this._pendingEvents.array.indexOf(pendingEvent);
|
const idx = this._pendingEvents.array.indexOf(pendingEvent);
|
||||||
if (idx !== -1) {
|
if (idx !== -1) {
|
||||||
this._amountSent -= 1;
|
|
||||||
this._pendingEvents.remove(idx);
|
this._pendingEvents.remove(idx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -170,13 +183,14 @@ export class SendQueue {
|
||||||
const maxQueueIndex = await pendingEventsStore.getMaxQueueIndex(this._roomId) || 0;
|
const maxQueueIndex = await pendingEventsStore.getMaxQueueIndex(this._roomId) || 0;
|
||||||
console.log("_createAndStoreEvent got maxQueueIndex", maxQueueIndex);
|
console.log("_createAndStoreEvent got maxQueueIndex", maxQueueIndex);
|
||||||
const queueIndex = maxQueueIndex + 1;
|
const queueIndex = maxQueueIndex + 1;
|
||||||
pendingEvent = new PendingEvent({
|
pendingEvent = this._createPendingEvent({
|
||||||
roomId: this._roomId,
|
roomId: this._roomId,
|
||||||
queueIndex,
|
queueIndex,
|
||||||
eventType,
|
eventType,
|
||||||
content,
|
content,
|
||||||
txnId: makeTxnId(),
|
txnId: makeTxnId(),
|
||||||
needsEncryption: !!this._roomEncryption
|
needsEncryption: !!this._roomEncryption,
|
||||||
|
needsUpload: !!attachments
|
||||||
}, attachments);
|
}, attachments);
|
||||||
console.log("_createAndStoreEvent: adding to pendingEventsStore");
|
console.log("_createAndStoreEvent: adding to pendingEventsStore");
|
||||||
pendingEventsStore.add(pendingEvent.data);
|
pendingEventsStore.add(pendingEvent.data);
|
||||||
|
@ -187,12 +201,4 @@ 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 attachments() {
|
get pendingEvent() {
|
||||||
return this._pendingEvent.attachments;
|
return this._pendingEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
notifyUpdate() {
|
notifyUpdate() {
|
||||||
|
|
Loading…
Reference in a new issue