diff --git a/src/components/Chat.vue b/src/components/Chat.vue index 7b8f8dc..c6bd3dd 100644 --- a/src/components/Chat.vue +++ b/src/components/Chat.vue @@ -211,8 +211,11 @@ {{ $t('message.images') }}
- +
+ + +
{{ currentImageInput.scaledDimensions.width }} x {{ currentImageInput.scaledDimensions.height }} @@ -228,7 +231,7 @@ + v-model="currentImageInput.useScaled" :disabled="currentImageInput.sendInfo" />
@@ -240,6 +243,7 @@
{{ $t('message.file') }}: {{ currentImageInputPath.name }} ({{ formatBytes(currentImageInputPath.size) }}) +
@@ -249,13 +253,12 @@
{{ currentSendError }}
{{ currentSendErrorExceededFile }}
-
{{ currentSendProgress }}
- + {{ $t("menu.cancel") }} - {{ $t("menu.send") }} + {{ $t("menu.send") }} @@ -354,6 +357,7 @@ import BottomSheet from "./BottomSheet.vue"; import ImageResize from "image-resize"; import CreatePollDialog from "./CreatePollDialog.vue"; import chatMixin from "./chatMixin"; +import sendAttachmentsMixin from "./sendAttachmentsMixin"; import AudioLayout from "./AudioLayout.vue"; import FileDropLayout from "./file_mode/FileDropLayout"; import { requestNotificationAndServiceWorker, windowNotificationPermission, notificationCount } from "../plugins/notificationAndServiceWorker.js" @@ -394,7 +398,7 @@ ScrollPosition.prototype.prepareFor = function (direction) { export default { name: "Chat", - mixins: [chatMixin, logoMixin, roomTypeMixin], + mixins: [chatMixin, logoMixin, roomTypeMixin, sendAttachmentsMixin], components: { ChatHeader, MessageOperations, @@ -425,8 +429,6 @@ export default { scrollPosition: null, currentFileInputs: null, - currentSendOperation: null, - currentSendProgress: null, currentSendShowSendButton: true, currentSendError: null, currentSendErrorExceededFile: null, @@ -1204,46 +1206,14 @@ export default { this.$refs.stickerPickerSheet.open(); }, - onUploadProgress(p) { - if (p.total) { - this.currentSendProgress = this.$t("message.upload_progress_with_total", { - count: p.loaded || 0, - total: p.total, - }); - } else { - this.currentSendProgress = this.$t("message.upload_progress", { - count: p.loaded || 0, - }); - } - }, sendAttachment(withText) { this.$refs.attachment.value = null; if (this.isCurrentFileInputsAnArray) { - let inputFiles = this.currentFileInputs.map(entry => { - // other than file type image - if(entry instanceof File) { - return entry; - } else { - if (entry.scaled && entry.useScaled) { - // Send scaled version of image instead! - return entry.scaled; - } else { - // Send actual file image when not scaled! - return entry.actualFile; - } - } - }) - - const promises = inputFiles.map(inputFile => util.sendImage(this.$matrix.matrixClient, this.roomId, inputFile, this.onUploadProgress)); - this.currentSendOperation = promises; - - Promise.all(promises).then(() => { - this.currentSendOperation = null; + const text = withText || ""; + const promise = this.sendAttachments(text, this.currentFileInputs); + promise.then(() => { this.currentFileInputs = null; - this.currentSendProgress = null; - if (withText) { - this.sendMessage(withText); - } + this.sendingStatus = this.sendStatuses.INITIAL; }) .catch((err) => { if (err.name === "AbortError" || err === "Abort") { @@ -1253,22 +1223,17 @@ export default { this.currentSendError = err.LocaleString(); this.currentSendErrorExceededFile = err.LocaleString(); } - this.currentSendOperation = null; - this.currentSendProgress = null; }); } }, cancelSendAttachment() { this.$refs.attachment.value = null; - if (this.currentSendOperation) { - this.currentSendOperation.forEach(o => o.abort()); - } - this.currentSendOperation = null; + this.cancelSendAttachments(); this.currentFileInputs = null; - this.currentSendProgress = null; this.currentSendError = null; this.currentSendErrorExceededFile = null; + this.sendingStatus = this.sendStatuses.INITIAL; }, addAttachment(file) { diff --git a/src/components/chatMixin.js b/src/components/chatMixin.js index a2949bb..0cd8c83 100644 --- a/src/components/chatMixin.js +++ b/src/components/chatMixin.js @@ -14,6 +14,7 @@ import MessageOutgoingAudio from "./messages/MessageOutgoingAudio.vue"; import MessageOutgoingVideo from "./messages/MessageOutgoingVideo.vue"; import MessageOutgoingSticker from "./messages/MessageOutgoingSticker.vue"; import MessageOutgoingPoll from "./messages/MessageOutgoingPoll.vue"; +import MessageOutgoingThread from "./messages/MessageOutgoingThread.vue"; import MessageIncomingImageExport from "./messages/export/MessageIncomingImageExport"; import MessageIncomingAudioExport from "./messages/export/MessageIncomingAudioExport"; import MessageIncomingVideoExport from "./messages/export/MessageIncomingVideoExport"; @@ -66,6 +67,7 @@ export default { MessageOutgoingAudio, MessageOutgoingVideo, MessageOutgoingSticker, + MessageOutgoingThread, MessageOutgoingPoll, ContactJoin, ContactLeave, @@ -128,6 +130,13 @@ export default { }, componentForEvent(event, isForExport = false) { + if (!event.isRelation() && !event.isRedaction() && event.isRedacted()) { + const redaction = event.getRedactionEvent(); + if (redaction && redaction.content && redaction.content.reason === "cancel") { + return null; // Show nothing, it was canceled! + } + } + switch (event.getType()) { case "m.room.member": if (event.getContent().membership == "join") { @@ -188,6 +197,10 @@ export default { } return MessageIncomingText; } else { + if (event.isThreadRoot || event.isThread) { + // Outgoing thread + return MessageOutgoingThread; + } if (event.getContent().msgtype == "m.image") { // For SVG, make downloadable if ( diff --git a/src/components/file_mode/FileDropLayout.vue b/src/components/file_mode/FileDropLayout.vue index 02c9806..1fa355f 100644 --- a/src/components/file_mode/FileDropLayout.vue +++ b/src/components/file_mode/FileDropLayout.vue @@ -44,7 +44,7 @@ sendCurrentTextMessage(); } " /> - {{ $t("menu.send") }} + {{ $t("menu.send") }} @@ -54,13 +54,13 @@ v-if="attachments && attachments.length > 0 && (status == mainStatuses.SENDING || status == mainStatuses.SENT)">
-
+
{{ $t('file_mode.sending_progress') }}
-
- +
+
$vuetify.icons.ic_check_circle @@ -69,18 +69,18 @@
-
- +
+
{{ info.attachment.name }}
-
+
close
{{ $tc((this.messageInput && this.messageInput.length > 0) ? - "file_mode.files_sent_with_note" : "file_mode.files_sent", sentItems.length) }}
+ "file_mode.files_sent_with_note" : "file_mode.files_sent", attachmentsSent.length) }}
@@ -105,11 +105,11 @@ + + + \ No newline at end of file diff --git a/src/components/sendAttachmentsMixin.js b/src/components/sendAttachmentsMixin.js new file mode 100644 index 0000000..3503a11 --- /dev/null +++ b/src/components/sendAttachmentsMixin.js @@ -0,0 +1,159 @@ +import util from "../plugins/utils"; + +export default { + data() { + return { + sendStatuses: Object.freeze({ + INITIAL: 0, + SENDING: 1, + SENT: 2, + CANCELED: 3, + FAILED: 4, + }), + sendingStatus: 0, + sendingPromise: null, + sendingRootEventId: null, + sendingAttachments: [], + } + }, + computed: { + attachmentsSentCount() { + return this.sendingAttachments ? this.sendingAttachments.reduce((a, elem, ignoredidx, ignoredarray) => elem.status == this.sendStatuses.SENT ? a + 1 : a, 0) : 0 + }, + attachmentsSending() { + return this.sendingAttachments ? this.sendingAttachments.filter(elem => elem.status == this.sendStatuses.INITIAL || elem.status == this.sendStatuses.SENDING) : [] + }, + attachmentsSent() { + this.sortSendingAttachments(); + return this.sendingAttachments ? this.sendingAttachments.filter(elem => elem.status == this.sendStatuses.SENT) : [] + }, + }, + methods: { + sendAttachments(text, attachments) { + this.sendingStatus = this.sendStatuses.SENDING; + + this.sendingAttachments = attachments.map((attachment) => { + let file = (() => { + // other than file type image + if(attachment instanceof File) { + return attachment; + } else { + if (attachment.scaled && attachment.useScaled) { + // Send scaled version of image instead! + return attachment.scaled; + } else { + // Send actual file image when not scaled! + return attachment.actualFile; + } + } + })(); + let sendInfo = { + id: attachment.name, + status: this.sendStatuses.INITIAL, + statusDate: Date.now, + attachment: file, + preview: attachment.image, + progress: 0, + randomRotation: 0, + randomTranslationX: 0, + randomTranslationY: 0 + }; + attachment.sendInfo = sendInfo; + return sendInfo; + }); + + this.sendingPromise = util.sendTextMessage(this.$matrix.matrixClient, this.room.roomId, text) + .then((eventId) => { + this.sendingRootEventId = eventId; + + // Use the eventId as a thread root for all the media + let promiseChain = Promise.resolve(); + const getItemPromise = (index) => { + if (index < this.sendingAttachments.length) { + const item = this.sendingAttachments[index]; + if (item.status !== this.sendStatuses.INITIAL) { + return getItemPromise(++index); + } + const itemPromise = util.sendImage(this.$matrix.matrixClient, this.room.roomId, item.attachment, ({ loaded, total }) => { + if (loaded == total) { + item.progress = 100; + } else if (total > 0) { + item.progress = 100 * loaded / total; + } + }, eventId) + .then(() => { + // Look at last item rotation, flipping the sign on this, so looks more like a true stack + let signR = 1; + let signX = 1; + let signY = 1; + if (this.attachmentsSent.length > 0) { + if (this.attachmentsSent[0].randomRotation >= 0) { + signR = -1; + } + if (this.attachmentsSent[0].randomTranslationX >= 0) { + signX = -1; + } + if (this.attachmentsSent[0].randomTranslationY >= 0) { + signY = -1; + } + } + item.randomRotation = signR * (2 + Math.random() * 10); + item.randomTranslationX = signX * Math.random() * 20; + item.randomTranslationY = signY * Math.random() * 20; + item.status = this.sendStatuses.SENT; + item.statusDate = Date.now; + }).catch(ignorederr => { + if (item.promise.aborted) { + item.status = this.sendStatuses.CANCELED; + } else { + console.error("ERROR", ignorederr); + item.status = this.sendStatuses.FAILED; + } + }); + item.promise = itemPromise; + return itemPromise.then(() => getItemPromise(++index)); + } + else return Promise.resolve(); + }; + + return promiseChain.then(() => getItemPromise(0)); + }) + .then(() => { + this.sendingStatus = this.sendStatuses.SENT; + }) + .catch((err) => { + console.error("ERROR", err); + }); + return this.sendingPromise; + }, + + cancelSendAttachments() { + this.sendingAttachments.toReversed().forEach(item => { + this.cancelSendAttachmentItem(item); + }); + this.sendingStatus = this.sendStatuses.CANCELED; + if (this.sendingRootEventId && this.room) { + // Redact the root event. + this.$matrix.matrixClient + .redactEvent(this.room.roomId, this.sendingRootEventId, undefined, { reason: "cancel" }) + .then(() => { + console.log("Message redacted"); + }) + .catch((err) => { + console.log("Redaction failed: ", err); + }); + } + }, + + cancelSendAttachmentItem(item) { + if (item.promise && item.status != this.sendStatuses.INITIAL) { + item.promise.abort(); + } + item.status = this.sendStatuses.CANCELED; + }, + + sortSendingAttachments() { + this.sendingAttachments.sort((a, b) => b.statusDate - a.statusDate); + }, + } +} \ No newline at end of file diff --git a/src/plugins/stickers.js b/src/plugins/stickers.js index c3205d4..171801f 100644 --- a/src/plugins/stickers.js +++ b/src/plugins/stickers.js @@ -21,7 +21,7 @@ class Stickers { } isStickerShortcode(messageBody) { - if (messageBody && messageBody.startsWith(":") && messageBody.startsWith(":") && messageBody.length >= 5) { + if (messageBody && typeof messageBody === "string" && messageBody.startsWith(":") && messageBody.startsWith(":") && messageBody.length >= 5) { const image = this.getStickerImage(messageBody); return image != undefined && image != null; }