Resolve "when uploading multiple media files, send and display it as a thread"
This commit is contained in:
parent
a5ff54842c
commit
5ac61eac7c
7 changed files with 369 additions and 173 deletions
|
|
@ -211,8 +211,11 @@
|
|||
<v-card-title v-if="imageFiles.length > 1"> {{ $t('message.images') }} </v-card-title>
|
||||
<v-card-text :class="{'ma-0 pa-2' : true, 'd-flex flex-wrap justify-center': imageFiles.length > 1}">
|
||||
<div :class="{'col-4': imageFiles.length > 1}" v-for="(currentImageInput, id) in imageFiles" :key="id">
|
||||
<v-img v-if="currentImageInput && currentImageInput.image" :aspect-ratio="1" :src="currentImageInput.image"
|
||||
contain class="current-image-input-path" />
|
||||
<div style="position: relative">
|
||||
<v-img v-if="currentImageInput && currentImageInput.image" :aspect-ratio="1" :src="currentImageInput.image"
|
||||
contain class="current-image-input-path" />
|
||||
<v-progress-linear :style="{ position: 'absolute', left: '0', right: '0', bottom: '0', opacity: currentImageInput.sendInfo ? '1' : '0' }" :value="currentImageInput.sendInfo ? currentImageInput.sendInfo.progress : 0"></v-progress-linear>
|
||||
</div>
|
||||
<div>
|
||||
<span v-if="currentImageInput && currentImageInput.scaled && currentImageInput.useScaled">
|
||||
{{ currentImageInput.scaledDimensions.width }} x {{ currentImageInput.scaledDimensions.height }}</span>
|
||||
|
|
@ -228,7 +231,7 @@
|
|||
</span>
|
||||
|
||||
<v-switch v-if="currentImageInput && currentImageInput.scaled" :label="$t('message.scale_image')"
|
||||
v-model="currentImageInput.useScaled" />
|
||||
v-model="currentImageInput.useScaled" :disabled="currentImageInput.sendInfo" />
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
|
@ -240,6 +243,7 @@
|
|||
<div v-if="!currentImageInputPath.type.includes('image/')">
|
||||
<span> {{ $t('message.file') }}: {{ currentImageInputPath.name }}</span>
|
||||
<span> ({{ formatBytes(currentImageInputPath.size) }})</span>
|
||||
<v-progress-linear :style="{ opacity: currentImageInputPath.sendInfo ? '1' : '0' }" :value="currentImageInputPath.sendInfo ? currentImageInputPath.sendInfo.progress : 0"></v-progress-linear>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
|
@ -249,13 +253,12 @@
|
|||
<v-spacer>
|
||||
<div v-if="currentSendError">{{ currentSendError }}</div>
|
||||
<div v-if="currentSendErrorExceededFile" class="red--text">{{ currentSendErrorExceededFile }}</div>
|
||||
<div v-else>{{ currentSendProgress }}</div>
|
||||
</v-spacer>
|
||||
<v-btn color="primary" text @click="cancelSendAttachment" id="btn-attachment-cancel">
|
||||
<v-btn color="primary" text @click="cancelSendAttachment" id="btn-attachment-cancel" :disabled="sendingStatus != sendStatuses.SENDING && sendingStatus != sendStatuses.INITIAL">
|
||||
{{ $t("menu.cancel") }}
|
||||
</v-btn>
|
||||
<v-btn id="btn-attachment-send" color="primary" text @click="sendAttachment"
|
||||
v-if="currentSendShowSendButton" :disabled="currentSendOperation != null">{{ $t("menu.send") }}</v-btn>
|
||||
<v-btn id="btn-attachment-send" color="primary" text @click="sendAttachment(undefined)"
|
||||
v-if="currentSendShowSendButton" :disabled="sendingStatus != sendStatuses.INITIAL">{{ $t("menu.send") }}</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@
|
|||
sendCurrentTextMessage();
|
||||
}
|
||||
" />
|
||||
<v-btn @click="send" :disabled="!attachments || attachments.length == 0">{{ $t("menu.send") }}</v-btn>
|
||||
<v-btn @click="sendAll" :disabled="!attachments || attachments.length == 0">{{ $t("menu.send") }}</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -54,13 +54,13 @@
|
|||
v-if="attachments && attachments.length > 0 && (status == mainStatuses.SENDING || status == mainStatuses.SENT)">
|
||||
<div class="attachment-wrapper">
|
||||
<div class="file-drop-sent-stack" ref="stackContainer">
|
||||
<div v-if="status == mainStatuses.SENDING && countSent == 0" class="no-items">
|
||||
<div v-if="status == mainStatuses.SENDING && attachmentsSentCount == 0" class="no-items">
|
||||
<div class="file-drop-stack-item direct" :style="stackItemTransform(null, -1)"></div>
|
||||
<div>{{ $t('file_mode.sending_progress') }}</div>
|
||||
</div>
|
||||
<div v-else v-for="(item, index) in sentItems" :key="item.id" class="file-drop-stack-item animated"
|
||||
:style="stackItemTransform(item, index)">
|
||||
<v-img v-if="item.attachment && item.attachment.image" :src="item.attachment.image" />
|
||||
<div v-else v-for="(info, index) in attachmentsSent" :key="info.id" class="file-drop-stack-item animated"
|
||||
:style="stackItemTransform(info, index)">
|
||||
<v-img v-if="info.preview" :src="info.preview" />
|
||||
</div>
|
||||
<div v-if="status == mainStatuses.SENT" class="items-sent" :style="stackItemTransform(null, -1)">
|
||||
<v-icon>$vuetify.icons.ic_check_circle</v-icon>
|
||||
|
|
@ -69,18 +69,18 @@
|
|||
|
||||
<!-- Middle section -->
|
||||
<div v-if="status == mainStatuses.SENDING" class="file-drop-sending-container">
|
||||
<div class="file-drop-sending-item" v-for="(info, index) in sendingItems" :key="index">
|
||||
<v-img v-if="info.attachment && info.attachment.image" :src="info.attachment.image" />
|
||||
<div class="file-drop-sending-item" v-for="(info, index) in attachmentsSending" :key="index">
|
||||
<v-img v-if="info.preview" :src="info.preview" />
|
||||
<div v-else class="filename">{{ info.attachment.name }}</div>
|
||||
<v-progress-linear :value="info.progress"></v-progress-linear>
|
||||
<div class="file-drop-cancel clickable" @click.stop="cancelSendingItem(info)">
|
||||
<div class="file-drop-cancel clickable" @click.stop="cancelSendAttachmentItem(info)">
|
||||
<v-icon size="14" color="white">close</v-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="status == mainStatuses.SENT" class="file-drop-sending-container">
|
||||
<div class="file-drop-files-sent">{{ $tc((this.messageInput && this.messageInput.length > 0) ?
|
||||
"file_mode.files_sent_with_note" : "file_mode.files_sent", sentItems.length) }}</div>
|
||||
"file_mode.files_sent_with_note" : "file_mode.files_sent", attachmentsSent.length) }}</div>
|
||||
<div class="file-drop-section">
|
||||
<v-textarea disabled full-width solo flat auto-grow v-model="messageInput" no-resize class="input-area-text"
|
||||
rows="1" hide-details background-color="transparent" />
|
||||
|
|
@ -105,11 +105,11 @@
|
|||
|
||||
<script>
|
||||
import messageMixin from "../messages/messageMixin";
|
||||
import util from "../../plugins/utils";
|
||||
import sendAttachmentsMixin from "../sendAttachmentsMixin";
|
||||
const prettyBytes = require("pretty-bytes");
|
||||
|
||||
export default {
|
||||
mixins: [messageMixin],
|
||||
mixins: [messageMixin, sendAttachmentsMixin],
|
||||
components: {},
|
||||
props: {
|
||||
attachments: {
|
||||
|
|
@ -129,13 +129,6 @@ export default {
|
|||
SENT: 2,
|
||||
}),
|
||||
status: 0,
|
||||
statuses: Object.freeze({
|
||||
INITIAL: 0,
|
||||
SENT: 1,
|
||||
CANCELED: 2,
|
||||
FAILED: 3,
|
||||
}),
|
||||
sendInfo: [],
|
||||
dropTarget: false,
|
||||
};
|
||||
},
|
||||
|
|
@ -151,20 +144,6 @@ export default {
|
|||
return this.currentItemIndex >= 0 && this.currentItemIndex < this.attachments.length &&
|
||||
this.attachments[this.currentItemIndex].image
|
||||
},
|
||||
countSent() {
|
||||
return this.sendInfo ? this.sendInfo.reduce((a, elem, ignoredidx, ignoredarray) => elem.status == this.statuses.SENT ? a + 1 : a, 0) : 0
|
||||
},
|
||||
sendingItems() {
|
||||
return this.sendInfo ? this.sendInfo.filter(elem => elem.status == this.statuses.INITIAL) : []
|
||||
},
|
||||
sentItems() {
|
||||
this.sortSendinfo();
|
||||
return this.sendInfo ? this.sendInfo.filter(elem => elem.status == this.statuses.SENT) : []
|
||||
},
|
||||
sentItemsReversed() {
|
||||
const array = this.sentItems;
|
||||
return array.map((ignoreditem, idx) => array[array.length - 1 - idx])
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
attachments(newValue, oldValue) {
|
||||
|
|
@ -207,7 +186,7 @@ export default {
|
|||
},
|
||||
reset() {
|
||||
this.$emit('reset');
|
||||
this.sendInfo = [];
|
||||
this.sendingAttachments = [];
|
||||
this.status = this.mainStatuses.SELECTING;
|
||||
this.messageInput = "";
|
||||
this.currentItemIndex = 0;
|
||||
|
|
@ -218,97 +197,13 @@ export default {
|
|||
console.log("Error leaving", err);
|
||||
});
|
||||
},
|
||||
send() {
|
||||
sendAll() {
|
||||
this.status = this.mainStatuses.SENDING;
|
||||
this.sendInfo = this.attachments.map((attachment) => {
|
||||
return {
|
||||
id: attachment.name,
|
||||
status: this.statuses.INITIAL,
|
||||
statusDate: Date.now,
|
||||
attachment: attachment.actualFile || attachment,
|
||||
progress: 0,
|
||||
randomRotation: 0,
|
||||
randomTranslationX: 0,
|
||||
randomTranslationY: 0
|
||||
}
|
||||
});
|
||||
|
||||
const text = (this.messageInput && this.messageInput.length > 0) ? this.messageInput : this.$t('file_mode.files');
|
||||
util.sendTextMessage(this.$matrix.matrixClient, this.room.roomId, text)
|
||||
.then((eventId) => {
|
||||
// Use the eventId as a thread root for all the media
|
||||
let promiseChain = Promise.resolve();
|
||||
const getItemPromise = (index) => {
|
||||
if (index < this.sendInfo.length) {
|
||||
const item = this.sendInfo[index];
|
||||
if (item.status !== this.statuses.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.sentItems.length > 0) {
|
||||
if (this.sentItems[0].randomRotation >= 0) {
|
||||
signR = -1;
|
||||
}
|
||||
if (this.sentItems[0].randomTranslationX >= 0) {
|
||||
signX = -1;
|
||||
}
|
||||
if (this.sentItems[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.statuses.SENT;
|
||||
item.statusDate = Date.now;
|
||||
}).catch(ignorederr => {
|
||||
if (item.promise.aborted) {
|
||||
item.status = this.statuses.CANCELED;
|
||||
} else {
|
||||
console.error("ERROR", ignorederr);
|
||||
item.status = this.statuses.FAILED;
|
||||
}
|
||||
});
|
||||
item.promise = itemPromise;
|
||||
return itemPromise.then(() => getItemPromise(++index));
|
||||
}
|
||||
else return Promise.resolve();
|
||||
};
|
||||
|
||||
return promiseChain.then(() => getItemPromise(0));
|
||||
})
|
||||
this.sendAttachments((this.messageInput && this.messageInput.length > 0) ? this.messageInput : this.$t('file_mode.files'), this.attachments)
|
||||
.then(() => {
|
||||
this.status = this.mainStatuses.SENT;
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("ERROR", err);
|
||||
});
|
||||
},
|
||||
cancelSendingItem(item) {
|
||||
if (item.promise && item.status == this.statuses.INITIAL) {
|
||||
item.promise.abort();
|
||||
}
|
||||
item.status = this.statuses.CANCELED;
|
||||
},
|
||||
checkDone() {
|
||||
if (!this.sendInfo.some(a => a.status == this.statuses.INITIAL)) {
|
||||
this.status = this.mainStatuses.SENT;
|
||||
}
|
||||
},
|
||||
sortSendinfo() {
|
||||
this.sendInfo.sort((a, b) => b.statusDate - a.statusDate);
|
||||
},
|
||||
stackItemTransform(item, index) {
|
||||
const size = 0.6 * (this.$refs.stackContainer ? Math.min(this.$refs.stackContainer.clientWidth, this.$refs.stackContainer.clientHeight) : 176);
|
||||
let transform = ""
|
||||
|
|
|
|||
|
|
@ -1,6 +1,15 @@
|
|||
<template>
|
||||
<message-incoming v-bind="{ ...$props, ...$attrs }" v-on="$listeners">
|
||||
<div class="bubble">
|
||||
<div class="original-message" v-if="inReplyToText">
|
||||
<div class="original-message-sender">
|
||||
{{ $t('message.user_said', {user: inReplyToSender || "Someone"}) }}
|
||||
</div>
|
||||
<div
|
||||
class="original-message-text"
|
||||
v-html="linkify($sanitize(inReplyToText))"
|
||||
/>
|
||||
</div>
|
||||
<div class="message">
|
||||
<v-container fluid class="imageCollection">
|
||||
<v-row wrap>
|
||||
|
|
|
|||
155
src/components/messages/MessageOutgoingThread.vue
Normal file
155
src/components/messages/MessageOutgoingThread.vue
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
<template>
|
||||
<message-outgoing v-bind="{ ...$props, ...$attrs }" v-on="$listeners">
|
||||
<div class="bubble">
|
||||
<div class="original-message" v-if="inReplyToText">
|
||||
<div class="original-message-sender">
|
||||
{{ $t('message.user_said', { user: inReplyToSender || "Someone" }) }}
|
||||
</div>
|
||||
<div class="original-message-text" v-html="linkify($sanitize(inReplyToText))" />
|
||||
</div>
|
||||
|
||||
<div class="message">
|
||||
<v-container fluid class="imageCollection">
|
||||
<v-row wrap>
|
||||
<v-col v-for="({ size, item }) in layoutedItems()" :key="item.event.getId()" :cols="size">
|
||||
<v-img :aspect-ratio="16 / 9" :src="item.src" cover @click.stop="dialogSrc = item.src; dialog = true" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
<div style="text-align: end">
|
||||
<v-btn class="download-all-button" @click.stop="downloadAll">{{ $t("message.download_all") }} <v-icon
|
||||
color="white">arrow_downward</v-icon></v-btn>
|
||||
</div>
|
||||
<i v-if="event.isRedacted()" class="deleted-text">
|
||||
<v-icon size="small">block</v-icon>
|
||||
{{ $t('message.outgoing_message_deleted_text') }}
|
||||
</i>
|
||||
<span v-html="linkify($sanitize(messageText))" v-else />
|
||||
<span class="edit-marker" v-if="event.replacingEventId() && !event.isRedacted()">
|
||||
{{ $t('message.edited') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</message-outgoing>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MessageOutgoing from "./MessageOutgoing.vue";
|
||||
import messageMixin from "./messageMixin";
|
||||
import util from "../../plugins/utils";
|
||||
|
||||
export default {
|
||||
extends: MessageOutgoing,
|
||||
components: { MessageOutgoing },
|
||||
mixins: [messageMixin],
|
||||
data() {
|
||||
return {
|
||||
items: [],
|
||||
dialog: false,
|
||||
dialogSrc: null,
|
||||
thread: null,
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.thread = this.timelineSet.relations.getChildEventsForEvent(this.event.getId(), "m.thread", "m.room.message");
|
||||
this.processThread();
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.thread = null;
|
||||
},
|
||||
watch: {
|
||||
thread: {
|
||||
handler(newValue, oldValue) {
|
||||
if (oldValue) {
|
||||
oldValue.off('Relations.add', this.onAddRelation);
|
||||
}
|
||||
if (newValue) {
|
||||
newValue.on('Relations.add', this.onAddRelation);
|
||||
}
|
||||
this.processThread();
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onAddRelation() {
|
||||
this.processThread();
|
||||
},
|
||||
processThread() {
|
||||
this.items = this.timelineSet.relations.getAllChildEventsForEvent(this.event.getId()).map(e => {
|
||||
let ret = {
|
||||
event: e,
|
||||
src: null,
|
||||
};
|
||||
ret.promise =
|
||||
util
|
||||
.getThumbnail(this.$matrix.matrixClient, e, 100, 100)
|
||||
.then((url) => {
|
||||
ret.src = url;
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log("Failed to fetch thumbnail: ", err);
|
||||
});
|
||||
return ret;
|
||||
});
|
||||
},
|
||||
layoutedItems() {
|
||||
if (!this.items || this.items.length == 0) { return [] }
|
||||
let array = this.items.slice(0);
|
||||
let rows = []
|
||||
while (array.length > 0) {
|
||||
if (array.length >= 7) {
|
||||
rows.push({ size: 6, item: array[0] });
|
||||
rows.push({ size: 6, item: array[1] });
|
||||
rows.push({ size: 12, item: array[2] });
|
||||
rows.push({ size: 3, item: array[3] });
|
||||
rows.push({ size: 3, item: array[4] });
|
||||
rows.push({ size: 3, item: array[5] });
|
||||
rows.push({ size: 3, item: array[6] });
|
||||
array = array.slice(7);
|
||||
} else if (array.length >= 3) {
|
||||
rows.push({ size: 6, item: array[0] });
|
||||
rows.push({ size: 6, item: array[1] });
|
||||
rows.push({ size: 12, item: array[2] });
|
||||
array = array.slice(3);
|
||||
} else if (array.length >= 2) {
|
||||
rows.push({ size: 6, item: array[0] });
|
||||
rows.push({ size: 6, item: array[1] });
|
||||
array = array.slice(2);
|
||||
} else {
|
||||
rows.push({ size: 12, item: array[0] });
|
||||
array = array.slice(1);
|
||||
}
|
||||
}
|
||||
return rows
|
||||
},
|
||||
downloadAll() {
|
||||
this.items.forEach(item => util.download(this.$matrix.matrixClient, item.event));
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style lang="scss">
|
||||
@import "@/assets/css/chat.scss";
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.bubble {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.imageCollection {
|
||||
border-radius: 15px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
|
||||
.row {
|
||||
margin: -4px; // Compensate for column padding, so the border-radius above looks round!
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.col {
|
||||
padding: 2px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
159
src/components/sendAttachmentsMixin.js
Normal file
159
src/components/sendAttachmentsMixin.js
Normal file
|
|
@ -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);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue