Finish unification of attachment sending
This commit is contained in:
parent
fd82fd8840
commit
a92d767fc2
11 changed files with 246 additions and 1100 deletions
|
|
@ -26,13 +26,14 @@
|
|||
<VoiceRecorder class="audio-layout" v-if="useVoiceMode" :micButtonRef="$refs.mic_button" :ptt="showRecorderPTT" :show="showRecorder"
|
||||
v-on:close="showRecorder = false" v-on:file="onVoiceRecording" :sendTypingIndicators="useVoiceMode" />
|
||||
|
||||
<FileDropLayout class="file-drop-root" v-if="useFileModeNonAdmin" :room="room"
|
||||
<SendAttachmentsLayout
|
||||
v-if="room && useFileModeNonAdmin"
|
||||
:room="room"
|
||||
v-on:pick-file="showAttachmentPicker(false)"
|
||||
v-on:add-files="(files) => addAttachments(files)"
|
||||
v-on:remove-file="currentFileInputs.splice($event, 1)"
|
||||
v-on:reset="v"
|
||||
:attachments="currentFileInputs"
|
||||
:batch="uploadBatch"
|
||||
v-on:close="closeFileMode"
|
||||
:showBackButton="false"
|
||||
/>
|
||||
|
||||
<div v-if="!useVoiceMode && !useFileModeNonAdmin" :class="{'chat-content': true, 'flex-grow-1': true, 'flex-shrink-1': true, 'invisible': !initialLoadDone}" ref="chatContainer"
|
||||
|
|
@ -318,9 +319,7 @@ import BottomSheet from "./BottomSheet.vue";
|
|||
import imageResize from "image-resize";
|
||||
import CreatePollDialog from "./CreatePollDialog.vue";
|
||||
import chatMixin, { ROOM_READ_MARKER_EVENT_PLACEHOLDER } from "./chatMixin";
|
||||
import sendAttachmentsMixin from "./sendAttachmentsMixin.ts";
|
||||
import AudioLayout from "./AudioLayout.vue";
|
||||
import FileDropLayout from "./file_mode/FileDropLayout";
|
||||
import SendAttachmentsLayout from "./file_mode/SendAttachmentsLayout.vue";
|
||||
import roomTypeMixin from "./roomTypeMixin";
|
||||
import roomMembersMixin from "./roomMembersMixin";
|
||||
|
|
@ -369,7 +368,7 @@ ScrollPosition.prototype.prepareFor = function (direction) {
|
|||
|
||||
export default {
|
||||
name: "Chat",
|
||||
mixins: [chatMixin, roomTypeMixin, sendAttachmentsMixin, roomMembersMixin],
|
||||
mixins: [chatMixin, roomTypeMixin, roomMembersMixin],
|
||||
components: {
|
||||
ChatHeader,
|
||||
ChatHeaderPrivate,
|
||||
|
|
@ -384,7 +383,6 @@ export default {
|
|||
BottomSheet,
|
||||
CreatePollDialog,
|
||||
AudioLayout,
|
||||
FileDropLayout,
|
||||
SendAttachmentsLayout,
|
||||
UserProfileDialog,
|
||||
PurgeRoomDialog,
|
||||
|
|
@ -412,7 +410,6 @@ export default {
|
|||
timelineWindowPaginating: false,
|
||||
|
||||
scrollPosition: null,
|
||||
currentFileInputs: [],
|
||||
uploadBatch: undefined,
|
||||
showEmojiPicker: false,
|
||||
selectedEvent: null,
|
||||
|
|
@ -1385,7 +1382,8 @@ export default {
|
|||
*/
|
||||
showAttachmentPicker(reset) {
|
||||
if (reset) {
|
||||
this.resetAttachments();
|
||||
this.uploadBatch?.cancel();
|
||||
this.uploadBatch = null;
|
||||
}
|
||||
this.$refs.attachment.click();
|
||||
},
|
||||
|
|
@ -1414,43 +1412,6 @@ export default {
|
|||
this.$refs.stickerPickerSheet.open();
|
||||
},
|
||||
|
||||
sendAttachment(withText) {
|
||||
this.$refs.attachment.value = null;
|
||||
if (this.isCurrentFileInputsAnArray) {
|
||||
const text = withText || "";
|
||||
const promise = this.sendAttachments(text, this.currentFileInputs);
|
||||
promise.then(() => {
|
||||
this.sendingAttachments = [];
|
||||
this.currentFileInputs = [];
|
||||
this.sendingStatus = "initial"
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.name === "AbortError" || err === "Abort") {
|
||||
this.currentSendError = null;
|
||||
this.currentSendErrorExceededFile = null;
|
||||
} else {
|
||||
this.currentSendError = err.LocaleString();
|
||||
this.currentSendErrorExceededFile = err.LocaleString();
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
cancelSendAttachment() {
|
||||
this.$refs.attachment.value = null;
|
||||
if (this.sendingStatus != "initial") {
|
||||
this.cancelSendAttachments();
|
||||
}
|
||||
this.currentFileInputs = [];
|
||||
this.currentSendError = null;
|
||||
this.currentSendErrorExceededFile = null;
|
||||
this.sendingStatus = "initial";
|
||||
},
|
||||
|
||||
resetAttachments() {
|
||||
this.cancelSendAttachment();
|
||||
},
|
||||
|
||||
/**
|
||||
* Called by message components that need to change their layout. This will avoid "jumping" in the UI, because
|
||||
* we remember scroll position, apply the layout change, then restore the scroll.
|
||||
|
|
@ -1885,19 +1846,22 @@ export default {
|
|||
},
|
||||
|
||||
onVoiceRecording(event) {
|
||||
this.currentSendShowSendButton = false;
|
||||
// TODO - refactor
|
||||
this.currentFileInputs = Array.isArray(this.currentFileInputs) ? [...this.currentFileInputs, event.file] : [event.file];
|
||||
const batch = this.$matrix.attachmentManager.createUpload(this.room);
|
||||
batch.addAttachment(this.$matrix.attachmentManager.createAttachment(event.file));
|
||||
var text = undefined;
|
||||
if (this.currentInput && this.currentInput.length > 0) {
|
||||
text = this.currentInput;
|
||||
this.currentInput = "";
|
||||
}
|
||||
this.sendAttachment(text);
|
||||
this.showRecorder = false;
|
||||
|
||||
// Log event
|
||||
this.$analytics.event("Audio", "Voice message sent");
|
||||
batch.send(text)
|
||||
.then(() => {
|
||||
this.showRecorder = false;
|
||||
// Log event
|
||||
this.$analytics.event("Audio", "Voice message sent");
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Failed to send voice message", err);
|
||||
})
|
||||
},
|
||||
|
||||
closeRoomWelcomeHeader() {
|
||||
|
|
@ -1973,7 +1937,8 @@ export default {
|
|||
},
|
||||
|
||||
closeFileMode() {
|
||||
this.resetAttachments();
|
||||
this.uploadBatch?.cancel();
|
||||
this.uploadBatch = undefined;
|
||||
this.$matrix.leaveRoomAndNavigate(this.room.roomId)
|
||||
.catch((err) => {
|
||||
console.log("Error leaving", err);
|
||||
|
|
|
|||
|
|
@ -1,216 +0,0 @@
|
|||
<template>
|
||||
<div v-bind="{ ...$props, ...$attrs }">
|
||||
<!-- No attachments view -->
|
||||
<template v-if="!attachments || attachments.length == 0">
|
||||
<div>
|
||||
<v-icon>$vuetify.icons.ic_lock</v-icon>
|
||||
<div class="file-drop-title">{{ $t("file_mode.secure_file_send") }}</div>
|
||||
</div>
|
||||
<div :class="{ 'background': true, 'drop-target': dropTarget }" @drop.prevent="filesDropped"
|
||||
@dragover.prevent="dropTarget = true" @dragleave.prevent="dropTarget = false"
|
||||
@dragenter.prevent="dropTarget = true">
|
||||
<v-btn @click="$emit('pick-file')" class="large">{{ $t("file_mode.choose_files") }}</v-btn>
|
||||
<div class="file-format-info">{{ $t("file_mode.any_file_format_accepted") }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- ATTACHMENT SELECTION MODE -->
|
||||
<template v-if="attachments && attachments.length > 0 && status == mainStatuses.SELECTING">
|
||||
<div class="attachment-wrapper" ref="attachmentWrapper">
|
||||
<div :class="{ 'file-drop-current-item': true, 'drop-target': dropTarget }" @drop.prevent="filesDropped"
|
||||
@dragover.prevent="dropTarget = true" @dragleave.prevent="dropTarget = false"
|
||||
@dragenter.prevent="dropTarget = true">
|
||||
<v-img v-if="currentItemHasImagePreview" :src="attachments[currentItemIndex].image" />
|
||||
<div v-else class="filename">{{ attachments[currentItemIndex].name }}</div>
|
||||
</div>
|
||||
<div class="file-drop-thumbnail-container">
|
||||
<div :class="{ 'file-drop-thumbnail': true, 'clickable': true, 'current': id == currentItemIndex }"
|
||||
@click="currentItemIndex = id" v-for="(currentImageInput, id) in attachments" :key="id">
|
||||
<v-img v-if="currentImageInput && currentImageInput.image" :src="currentImageInput.image" />
|
||||
<div v-if="currentItemIndex == id" class="remove clickable" @click.stop="$emit('remove-file', id)">
|
||||
<v-icon>$vuetify.icons.ic_trash</v-icon>
|
||||
</div>
|
||||
</div>
|
||||
<div class="file-drop-thumbnail noborder">
|
||||
<div class="add clickable" @click.stop="$emit('pick-file')">
|
||||
+
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="file-drop-input-container">
|
||||
<v-textarea theme="dark" ref="input" full-width variant="solo" flat auto-grow v-model="messageInput" no-resize class="input-area-text"
|
||||
rows="1" :placeholder="$t('file_mode.add_a_message')" hide-details background-color="transparent"
|
||||
v-on:keydown.enter.prevent="() => {
|
||||
sendCurrentTextMessage();
|
||||
}
|
||||
" />
|
||||
<v-btn @click="sendAll" :disabled="!attachments || attachments.length == 0">{{ $t("menu.send") }}</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- ATTACHMENT SENDING/SENT MODE -->
|
||||
<template
|
||||
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 && 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="(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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Middle section -->
|
||||
<div v-if="status == mainStatuses.SENDING" class="file-drop-sending-container">
|
||||
<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 :model-value="info.progress"></v-progress-linear>
|
||||
<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">{{ $t((this.messageInput && this.messageInput.length > 0) ?
|
||||
"file_mode.files_sent_with_note" : "file_mode.files_sent", attachmentsSent.length) }}</div>
|
||||
<div class="file-drop-section">
|
||||
<v-textarea disabled full-width variant="solo" flat auto-grow v-model="messageInput" no-resize class="input-area-text"
|
||||
rows="1" hide-details background-color="transparent" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bottom section -->
|
||||
<div v-if="status == mainStatuses.SENDING" class="file-drop-sending-input-container">
|
||||
<v-textarea disabled full-width variant="solo" flat auto-grow v-model="messageInput" no-resize class="input-area-text"
|
||||
rows="1" :placeholder="$t('file_mode.add_a_message')" hide-details background-color="transparent" />
|
||||
<v-btn>{{ $t("file_mode.sending") }}<v-progress-circular indeterminate size="18" width="2"
|
||||
color="#4642F1"></v-progress-circular></v-btn>
|
||||
</div>
|
||||
<div v-else-if="status == mainStatuses.SENT" class="file-drop-sent-input-container">
|
||||
<v-btn @click.stop="reset">{{ $t("file_mode.send_more_files") }}</v-btn>
|
||||
<v-btn class="close" @click.stop="close">{{ $t("file_mode.close") }}</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import messageMixin from "../messages/messageMixin";
|
||||
import sendAttachmentsMixin from "../sendAttachmentsMixin.ts";
|
||||
import prettyBytes from "pretty-bytes";
|
||||
|
||||
export default {
|
||||
mixins: [messageMixin, sendAttachmentsMixin],
|
||||
components: {},
|
||||
props: {
|
||||
attachments: {
|
||||
type: Array,
|
||||
default: function () {
|
||||
return []
|
||||
}
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentItemIndex: 0,
|
||||
messageInput: "",
|
||||
mainStatuses: Object.freeze({
|
||||
SELECTING: 0,
|
||||
SENDING: 1,
|
||||
SENT: 2,
|
||||
}),
|
||||
status: 0,
|
||||
dropTarget: false,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
document.body.classList.add("dark");
|
||||
this.$audioPlayer.setAutoplay(false);
|
||||
},
|
||||
beforeUnmount() {
|
||||
document.body.classList.remove("dark");
|
||||
},
|
||||
computed: {
|
||||
currentItemHasImagePreview() {
|
||||
return this.currentItemIndex >= 0 && this.currentItemIndex < this.attachments.length &&
|
||||
this.attachments[this.currentItemIndex].image
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
attachments(newValue, oldValue) {
|
||||
// Added or removed?
|
||||
if (newValue && oldValue && newValue.length > oldValue.length) {
|
||||
this.currentItemIndex = oldValue.length;
|
||||
} else if (newValue) {
|
||||
this.currentItemIndex = newValue.length - 1;
|
||||
}
|
||||
},
|
||||
messageInput() {
|
||||
this.scrollToBottom();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
filesDropped(e) {
|
||||
this.dropTarget = false;
|
||||
let droppedFiles = e.dataTransfer.files;
|
||||
if (!droppedFiles) return;
|
||||
this.$emit('add-files', [...droppedFiles]);
|
||||
},
|
||||
scrollToBottom() {
|
||||
const el = this.$refs.attachmentWrapper;
|
||||
if (el) {
|
||||
// Ugly - need to wait until input is auto-sized, THEN scroll to bottom.
|
||||
//
|
||||
this.$nextTick(() => {
|
||||
this.$nextTick(() => {
|
||||
this.$nextTick(() => {
|
||||
el.scrollTop = el.scrollHeight
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
formatBytes(bytes) {
|
||||
return prettyBytes(bytes);
|
||||
},
|
||||
reset() {
|
||||
this.$emit('reset');
|
||||
this.sendingAttachments = [];
|
||||
this.status = this.mainStatuses.SELECTING;
|
||||
this.messageInput = "";
|
||||
this.currentItemIndex = 0;
|
||||
},
|
||||
close() {
|
||||
this.$emit('close');
|
||||
},
|
||||
sendAll() {
|
||||
this.status = this.mainStatuses.SENDING;
|
||||
this.sendAttachments((this.messageInput && this.messageInput.length > 0) ? this.messageInput : this.$t('file_mode.files'), this.attachments)
|
||||
.then(() => {
|
||||
this.status = this.mainStatuses.SENT;
|
||||
});
|
||||
},
|
||||
stackItemTransform(item, index) {
|
||||
const size = 0.6 * (this.$refs.stackContainer ? Math.min(this.$refs.stackContainer.clientWidth, this.$refs.stackContainer.clientHeight) : 176);
|
||||
let transform = ""
|
||||
if (item != null && index != -1) {
|
||||
transform = "transform: rotate(" + item.randomRotation + "deg) translate(" + item.randomTranslationX + "px," + item.randomTranslationY + "px); z-index:" + (index + 2) + ";";
|
||||
}
|
||||
return transform + "width:" + size + "px;height:" + size + "px;border-radius:" + (size / 8) + "px";
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@use "@/assets/css/chat.scss" as *;
|
||||
</style>
|
||||
|
|
@ -1,170 +1,139 @@
|
|||
<template>
|
||||
<div v-bind="{ ...$attrs }" class="send-attachments">
|
||||
<div class="send-attachments__title">{{ $t("message.send_attachements_dialog_title") }}</div>
|
||||
<v-btn
|
||||
v-if="showBackButton"
|
||||
class="back-button clickable"
|
||||
icon="arrow_back"
|
||||
size="default"
|
||||
elevation="0"
|
||||
color="black"
|
||||
@click.stop="close"
|
||||
:disabled="backButtonDisabled"
|
||||
></v-btn>
|
||||
|
||||
<!-- ATTACHMENT SELECTION MODE -->
|
||||
<template v-if="batch.attachments.length > 0 && status == mainStatuses.SELECTING">
|
||||
<div class="attachment-wrapper" ref="attachmentWrapper" v-if="currentAttachment">
|
||||
<div
|
||||
:class="{ 'file-drop-current-item': true, 'drop-target': dropTarget }"
|
||||
@drop.prevent="filesDropped"
|
||||
@dragover.prevent="dropTarget = true"
|
||||
@dragleave.prevent="dropTarget = false"
|
||||
@dragenter.prevent="dropTarget = true"
|
||||
>
|
||||
<v-img v-if="currentAttachment.src && currentAttachment.status === 'loaded'" :src="currentAttachment.src" />
|
||||
<div v-else class="filename">
|
||||
<div>{{ currentAttachment.file.name }}</div>
|
||||
<div v-if="currentAttachment.status === 'loading'" style="font-size: 0.7em; opacity: 0.7">
|
||||
{{ $t("message.preparing_to_upload") }}
|
||||
<v-progress-linear indeterminate class="mb-0"></v-progress-linear>
|
||||
</div>
|
||||
<template v-if="status == mainStatuses.SELECTING">
|
||||
<div
|
||||
:class="{ 'send-attachments__selecting__current-item': true, 'drop-target': dropTarget }"
|
||||
@drop.prevent="filesDropped"
|
||||
@dragover.prevent="dropTarget = true"
|
||||
@dragleave.prevent="dropTarget = false"
|
||||
@dragenter.prevent="dropTarget = true"
|
||||
>
|
||||
<v-img
|
||||
v-if="currentAttachment && currentAttachment.src && currentAttachment.status === 'loaded'"
|
||||
:src="currentAttachment.src"
|
||||
/>
|
||||
<div v-else-if="currentAttachment" class="filename">
|
||||
<div>{{ currentAttachment.file.name }}</div>
|
||||
<div v-if="currentAttachment.status === 'loading'" style="font-size: 0.7em; opacity: 0.7">
|
||||
{{ $t("message.preparing_to_upload") }}
|
||||
<v-progress-linear indeterminate class="mb-0"></v-progress-linear>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <div class="send-attachments__current-item__info">
|
||||
<div class="send-attachments__current-item__info__size">
|
||||
<span
|
||||
v-if="currentAttachment.scaledFile && currentAttachment.useScaled && currentAttachment.scaledDimensions"
|
||||
>
|
||||
{{ currentAttachment.scaledDimensions.width }} x {{ currentAttachment.scaledDimensions.height }}</span
|
||||
>
|
||||
<span v-else-if="currentAttachment.dimensions">
|
||||
{{ currentAttachment.dimensions.width }} x {{ currentAttachment.dimensions.height }}
|
||||
</span>
|
||||
|
||||
<span v-if="currentAttachment.scaledFile && currentAttachment.useScaled">
|
||||
({{ formatBytes(currentAttachment.scaledFile.size) }})
|
||||
</span>
|
||||
<span v-else> ({{ formatBytes(currentAttachment.file.size) }}) </span>
|
||||
|
||||
<span
|
||||
class="send-attachments__current-item__info__size__filename"
|
||||
v-if="currentAttachment.src && currentAttachment.file.name"
|
||||
>
|
||||
- {{ currentAttachment.file.name }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<v-switch
|
||||
v-if="currentAttachment.scaledFile"
|
||||
:label="$t('message.scale_image')"
|
||||
v-model="currentAttachment.useScaled"
|
||||
:disabled="currentAttachment.sendInfo !== undefined"
|
||||
/>
|
||||
|
||||
<C2PABadge :proof="currentAttachment.proof" />
|
||||
</div> -->
|
||||
|
||||
<div class="file-drop-thumbnail-container">
|
||||
<div
|
||||
:class="{ 'file-drop-thumbnail': true, clickable: true, current: id == currentItemIndex }"
|
||||
@click="currentItemIndex = id"
|
||||
v-for="(currentImageInput, id) in batch.attachments"
|
||||
:key="id"
|
||||
>
|
||||
<v-img v-if="currentImageInput && currentImageInput.src" :src="currentImageInput.src" />
|
||||
<div
|
||||
v-if="currentItemIndex == id"
|
||||
class="remove clickable"
|
||||
@click.stop="batch.removeAttachment(currentImageInput)"
|
||||
>
|
||||
<v-icon>$vuetify.icons.ic_trash</v-icon>
|
||||
</div>
|
||||
</div>
|
||||
<div class="file-drop-thumbnail noborder">
|
||||
<div class="add clickable" @click.stop="$emit('pick-file')">+</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="file-drop-input-container">
|
||||
<v-textarea
|
||||
ref="input"
|
||||
full-width
|
||||
variant="solo"
|
||||
flat
|
||||
auto-grow
|
||||
v-model="messageInput"
|
||||
no-resize
|
||||
class="input-area-text"
|
||||
rows="1"
|
||||
:placeholder="$t('file_mode.add_a_message')"
|
||||
hide-details
|
||||
background-color="transparent"
|
||||
v-on:keydown.enter.prevent="
|
||||
() => {
|
||||
sendAll();
|
||||
}
|
||||
"
|
||||
/>
|
||||
<div class="input-container__buttons">
|
||||
<v-btn @click="close">{{ $t("menu.cancel") }}</v-btn>
|
||||
<v-btn @click="sendAll" :disabled="batch.attachments.length == 0">{{ $t("menu.send") }}</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
<div class="file-drop-thumbnail-container">
|
||||
<v-tooltip location="top" v-for="(attachment, index) in batch.attachments" :key="index">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-badge :model-value="batch.isTooLarge(attachment)" color="error">
|
||||
<template v-slot:badge
|
||||
><span v-bind="props"> </span></template
|
||||
>
|
||||
<div
|
||||
:class="{ 'file-drop-thumbnail': true, clickable: true, current: index == currentItemIndex }"
|
||||
@click="currentItemIndex = index"
|
||||
>
|
||||
<v-img v-if="attachment && attachment.src" :src="attachment.src" />
|
||||
<div
|
||||
v-if="currentItemIndex == index"
|
||||
class="remove clickable"
|
||||
@click.stop="batch.removeAttachment(attachment)"
|
||||
>
|
||||
<v-icon>$vuetify.icons.ic_trash</v-icon>
|
||||
</div>
|
||||
</div>
|
||||
</v-badge>
|
||||
</template>
|
||||
<span>{{ $t("message.upload_file_too_large") }}</span>
|
||||
</v-tooltip>
|
||||
<div class="file-drop-thumbnail noborder">
|
||||
<div class="add clickable" @click.stop="$emit('pick-file')">+</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="file-drop-input-container">
|
||||
<v-textarea
|
||||
ref="input"
|
||||
full-width
|
||||
variant="solo"
|
||||
flat
|
||||
auto-grow
|
||||
v-model="messageInput"
|
||||
no-resize
|
||||
class="input-area-text"
|
||||
rows="1"
|
||||
:placeholder="$t('file_mode.add_a_message')"
|
||||
hide-details
|
||||
background-color="transparent"
|
||||
v-on:keydown.enter.prevent="
|
||||
() => {
|
||||
sendAll();
|
||||
}
|
||||
"
|
||||
/>
|
||||
<v-btn
|
||||
class="send-button clickable"
|
||||
icon="arrow_upward"
|
||||
size="default"
|
||||
elevation="0"
|
||||
color="black"
|
||||
@click.stop="sendAll"
|
||||
:disabled="sendButtonDisabled"
|
||||
></v-btn>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- ATTACHMENT SENDING/SENT MODE -->
|
||||
<template v-if="batch.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 && batch.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="(info, index) in batch.attachmentsSent"
|
||||
:key="info.file.name"
|
||||
class="file-drop-stack-item animated"
|
||||
:style="stackItemTransform(info, index)"
|
||||
>
|
||||
<v-img v-if="info.src" :src="info.src" />
|
||||
</div>
|
||||
<div v-if="status == mainStatuses.SENT" class="items-sent" :style="stackItemTransform(null, -1)">
|
||||
<v-icon>$vuetify.icons.ic_check_circle</v-icon>
|
||||
</div>
|
||||
<div class="file-drop-sent-stack" ref="stackContainer">
|
||||
<div v-if="status == mainStatuses.SENDING && batch.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="(info, index) in batch.attachmentsSent"
|
||||
:key="info.file.name"
|
||||
class="file-drop-stack-item animated"
|
||||
:style="stackItemTransform(info, index)"
|
||||
>
|
||||
<v-img v-if="info.src" :src="info.src" />
|
||||
</div>
|
||||
<div v-if="status == mainStatuses.SENT" class="items-sent" :style="stackItemTransform(null, -1)">
|
||||
<v-icon>$vuetify.icons.ic_check_circle</v-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Middle section -->
|
||||
<div v-if="status == mainStatuses.SENDING" class="file-drop-sending-container">
|
||||
<div class="file-drop-sending-item" v-for="attachment in batch.attachmentsSending" :key="attachment.file.name">
|
||||
<v-img v-if="attachment.src" :src="attachment.src" />
|
||||
<div v-else class="filename">{{ attachment.file.name }}</div>
|
||||
<v-progress-linear :model-value="attachment.sendInfo?.progress ?? 0"></v-progress-linear>
|
||||
<div class="file-drop-cancel clickable" @click.stop="batch.cancelSendAttachment(attachment)">
|
||||
<v-icon size="14" color="white">close</v-icon>
|
||||
</div>
|
||||
<!-- Middle section -->
|
||||
<div v-if="status == mainStatuses.SENDING" class="file-drop-sending-container">
|
||||
<div class="file-drop-sending-item" v-for="attachment in batch.attachmentsSending" :key="attachment.file.name">
|
||||
<v-img v-if="attachment.src" :src="attachment.src" />
|
||||
<div v-else class="filename">{{ attachment.file.name }}</div>
|
||||
<v-progress-linear :model-value="attachment.sendInfo?.progress ?? 0"></v-progress-linear>
|
||||
<div class="file-drop-cancel clickable" @click.stop="batch.cancelSendAttachment(attachment)">
|
||||
<v-icon size="14" color="white">close</v-icon>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="status == mainStatuses.SENT" class="file-drop-sending-container">
|
||||
<div class="file-drop-files-sent">
|
||||
{{
|
||||
$t(
|
||||
messageInput && messageInput.length > 0 ? "file_mode.files_sent_with_note" : "file_mode.files_sent",
|
||||
batch.attachmentsSent.length
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
<div class="file-drop-section">
|
||||
<v-textarea
|
||||
disabled
|
||||
full-width
|
||||
variant="solo"
|
||||
flat
|
||||
auto-grow
|
||||
v-model="messageInput"
|
||||
no-resize
|
||||
class="input-area-text"
|
||||
rows="1"
|
||||
hide-details
|
||||
background-color="transparent"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="status == mainStatuses.SENT" class="file-drop-sending-container">
|
||||
<div class="file-drop-files-sent">
|
||||
{{
|
||||
$t(
|
||||
messageInput && messageInput.length > 0 ? "file_mode.files_sent_with_note" : "file_mode.files_sent",
|
||||
batch.attachmentsSent.length
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
|
||||
<!-- Bottom section -->
|
||||
<div v-if="status == mainStatuses.SENDING" class="file-drop-sending-input-container">
|
||||
<div class="file-drop-section">
|
||||
<v-textarea
|
||||
disabled
|
||||
full-width
|
||||
|
|
@ -175,39 +144,63 @@
|
|||
no-resize
|
||||
class="input-area-text"
|
||||
rows="1"
|
||||
:placeholder="$t('file_mode.add_a_message')"
|
||||
hide-details
|
||||
background-color="transparent"
|
||||
/>
|
||||
<v-btn
|
||||
>{{ $t("file_mode.sending")
|
||||
}}<v-progress-circular indeterminate size="18" width="2" color="#4642F1"></v-progress-circular
|
||||
></v-btn>
|
||||
</div>
|
||||
<div v-else-if="status == mainStatuses.SENT" class="file-drop-sent-input-container">
|
||||
<v-btn class="close" @click.stop="close">{{ $t("file_mode.close") }}</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bottom section -->
|
||||
<div v-if="status == mainStatuses.SENDING" class="file-drop-sending-input-container">
|
||||
<v-textarea
|
||||
disabled
|
||||
full-width
|
||||
variant="solo"
|
||||
flat
|
||||
auto-grow
|
||||
v-model="messageInput"
|
||||
no-resize
|
||||
class="input-area-text"
|
||||
rows="1"
|
||||
:placeholder="$t('file_mode.add_a_message')"
|
||||
hide-details
|
||||
background-color="transparent"
|
||||
/>
|
||||
<v-btn
|
||||
>{{ $t("file_mode.sending")
|
||||
}}<v-progress-circular indeterminate size="18" width="2" color="#4642F1"></v-progress-circular
|
||||
></v-btn>
|
||||
</div>
|
||||
<div v-else-if="status == mainStatuses.SENT" class="file-drop-sent-input-container">
|
||||
<v-btn class="close" @click.stop="close">{{ $t("file_mode.close") }}</v-btn>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
import { defineComponent, reactive } from "vue";
|
||||
import messageMixin from "../messages/messageMixin";
|
||||
import prettyBytes from "pretty-bytes";
|
||||
import { Attachment } from "../../models/attachment";
|
||||
import C2PABadge from "../c2pa/C2PABadge.vue";
|
||||
import { createUploadBatch } from "../../models/attachmentManager";
|
||||
|
||||
export default defineComponent({
|
||||
mixins: [messageMixin],
|
||||
components: { C2PABadge },
|
||||
emits: ["add-files", "pick-file", "close"],
|
||||
emits: ["pick-file", "close"],
|
||||
props: {
|
||||
showBackButton: {
|
||||
type: Boolean,
|
||||
default: function () {
|
||||
return true;
|
||||
},
|
||||
},
|
||||
batch: {
|
||||
type: Object,
|
||||
default: function () {
|
||||
return {}
|
||||
return reactive(createUploadBatch(null, null, 0))
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -228,6 +221,12 @@ export default defineComponent({
|
|||
this.$audioPlayer.setAutoplay(false);
|
||||
},
|
||||
computed: {
|
||||
backButtonDisabled() {
|
||||
return this.status == this.mainStatuses.SENDING;
|
||||
},
|
||||
sendButtonDisabled() {
|
||||
return !this.batch.canSend;
|
||||
},
|
||||
currentAttachment(): Attachment | undefined {
|
||||
if (this.currentItemIndex >= 0 && this.currentItemIndex < this.batch.attachments.length) {
|
||||
return this.batch.attachments[this.currentItemIndex];
|
||||
|
|
@ -254,29 +253,15 @@ export default defineComponent({
|
|||
},
|
||||
deep: true,
|
||||
},
|
||||
messageInput() {
|
||||
this.scrollToBottom();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
filesDropped(e: DragEvent) {
|
||||
this.dropTarget = false;
|
||||
let droppedFiles: FileList | undefined = e.dataTransfer?.files;
|
||||
if (!droppedFiles) return;
|
||||
this.$emit("add-files", [...droppedFiles]);
|
||||
},
|
||||
scrollToBottom() {
|
||||
const el = this.$refs.attachmentWrapper;
|
||||
if (el) {
|
||||
// Ugly - need to wait until input is auto-sized, THEN scroll to bottom.
|
||||
//
|
||||
this.$nextTick(() => {
|
||||
this.$nextTick(() => {
|
||||
this.$nextTick(() => {
|
||||
el.scrollTop = el.scrollHeight;
|
||||
});
|
||||
});
|
||||
});
|
||||
for (let i = 0; i < droppedFiles.length; i++) {
|
||||
const file = droppedFiles.item(i);
|
||||
this.batch.addAttachment(this.$matrix.attachmentManager.createAttachment(file));
|
||||
}
|
||||
},
|
||||
formatBytes(bytes: number) {
|
||||
|
|
@ -291,11 +276,11 @@ export default defineComponent({
|
|||
},
|
||||
sendAll() {
|
||||
this.status = this.mainStatuses.SENDING;
|
||||
this.batch.send(
|
||||
this.messageInput && this.messageInput.length > 0 ? this.messageInput : this.$t("file_mode.files")
|
||||
).then(() => {
|
||||
this.status = this.mainStatuses.SENT;
|
||||
});
|
||||
this.batch
|
||||
.send(this.messageInput && this.messageInput.length > 0 ? this.messageInput : this.$t("file_mode.files"))
|
||||
.then(() => {
|
||||
this.status = this.mainStatuses.SENT;
|
||||
});
|
||||
},
|
||||
stackItemTransform(attachment: Attachment, index: number) {
|
||||
const size =
|
||||
|
|
|
|||
1
src/components/sendAttachmentsMixin.d.ts
vendored
1
src/components/sendAttachmentsMixin.d.ts
vendored
|
|
@ -1 +0,0 @@
|
|||
declare module 'sendAttachmentsMixin';
|
||||
|
|
@ -1,195 +0,0 @@
|
|||
import { defineComponent, reactive } from "vue";
|
||||
import util from "../plugins/utils";
|
||||
import { Attachment, AttachmentSendInfo } from "../models/attachment";
|
||||
|
||||
export default defineComponent({
|
||||
data(): {
|
||||
sendingStatus: "initial" | "sending" | "sent" | "canceled" | "failed";
|
||||
sendingRootEventId: string | null;
|
||||
sendingPromise: Promise<any> | null;
|
||||
sendingAttachments: Attachment[];
|
||||
} {
|
||||
return {
|
||||
// sendStatuses: Object.freeze({
|
||||
// INITIAL: 0,
|
||||
// SENDING: 1,
|
||||
// SENT: 2,
|
||||
// CANCELED: 3,
|
||||
// FAILED: 4,
|
||||
// }),
|
||||
sendingStatus: "initial",
|
||||
sendingPromise: null,
|
||||
sendingRootEventId: null,
|
||||
sendingAttachments: [] as Attachment[],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
attachmentsSentCount(): number {
|
||||
return this.sendingAttachments
|
||||
? this.sendingAttachments.reduce((a, elem) => (elem.sendInfo?.status == "sent" ? a + 1 : a), 0)
|
||||
: 0;
|
||||
},
|
||||
attachmentsSending(): Attachment[] {
|
||||
return this.sendingAttachments
|
||||
? this.sendingAttachments.filter(
|
||||
(elem) => elem.sendInfo?.status == "initial" || elem.sendInfo?.status == "sending"
|
||||
)
|
||||
: [];
|
||||
},
|
||||
attachmentsSent(): Attachment[] {
|
||||
this.sortSendingAttachments();
|
||||
return this.sendingAttachments ? this.sendingAttachments.filter((elem) => elem.sendInfo?.status == "sent") : [];
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
sendAttachments(text: string, attachments: Attachment[]) {
|
||||
this.sendingStatus = "sending";
|
||||
|
||||
this.sendingAttachments = attachments.map((attachment) => {
|
||||
let sendInfo: AttachmentSendInfo = {
|
||||
status: "initial",
|
||||
statusDate: Date.now(),
|
||||
mediaEventId: undefined,
|
||||
progress: 0,
|
||||
randomRotation: 0,
|
||||
randomTranslationX: 0,
|
||||
randomTranslationY: 0,
|
||||
promise: undefined,
|
||||
};
|
||||
attachment.sendInfo = reactive(sendInfo);
|
||||
return attachment;
|
||||
});
|
||||
|
||||
this.sendingPromise = util
|
||||
.sendTextMessage(this.$matrix.matrixClient, this.room.roomId, text)
|
||||
.then((eventId: string) => {
|
||||
this.sendingRootEventId = eventId;
|
||||
|
||||
// Use the eventId as a thread root for all the media
|
||||
let promiseChain = Promise.resolve();
|
||||
const getItemPromise = (index: number) => {
|
||||
if (index < this.sendingAttachments.length) {
|
||||
const attachment = this.sendingAttachments[index];
|
||||
const item = attachment.sendInfo!;
|
||||
if (item.status !== "initial") {
|
||||
return getItemPromise(++index);
|
||||
}
|
||||
item.status = "sending";
|
||||
|
||||
let file = (() => {
|
||||
if (attachment.scaledFile && attachment.useScaled) {
|
||||
// Send scaled version of image instead!
|
||||
return attachment.scaledFile;
|
||||
} else {
|
||||
// Send actual file image when not scaled!
|
||||
return attachment.file;
|
||||
}
|
||||
})();
|
||||
|
||||
const itemPromise = util
|
||||
.sendFile(
|
||||
this.$matrix.matrixClient,
|
||||
this.room.roomId,
|
||||
file,
|
||||
({ loaded, total }: { loaded: number; total: number }) => {
|
||||
if (loaded == total) {
|
||||
item.progress = 100;
|
||||
} else if (total > 0) {
|
||||
item.progress = (100 * loaded) / total;
|
||||
}
|
||||
},
|
||||
eventId,
|
||||
attachment.dimensions
|
||||
)
|
||||
.then((mediaEventId: string) => {
|
||||
// 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].sendInfo!.randomRotation >= 0) {
|
||||
signR = -1;
|
||||
}
|
||||
if (this.attachmentsSent[0].sendInfo!.randomTranslationX >= 0) {
|
||||
signX = -1;
|
||||
}
|
||||
if (this.attachmentsSent[0].sendInfo!.randomTranslationY >= 0) {
|
||||
signY = -1;
|
||||
}
|
||||
}
|
||||
item.randomRotation = signR * (2 + Math.random() * 10);
|
||||
item.randomTranslationX = signX * Math.random() * 20;
|
||||
item.randomTranslationY = signY * Math.random() * 20;
|
||||
item.mediaEventId = mediaEventId;
|
||||
item.status = "sent";
|
||||
item.statusDate = Date.now();
|
||||
})
|
||||
.catch((ignorederr: any) => {
|
||||
if (item.promise?.aborted) {
|
||||
item.status = "canceled";
|
||||
} else {
|
||||
console.error("ERROR", ignorederr);
|
||||
item.status = "failed";
|
||||
}
|
||||
return Promise.resolve();
|
||||
});
|
||||
item.promise = itemPromise;
|
||||
return itemPromise.then(() => getItemPromise(++index));
|
||||
} else return Promise.resolve();
|
||||
};
|
||||
|
||||
return promiseChain.then(() => getItemPromise(0));
|
||||
})
|
||||
.then(() => {
|
||||
this.sendingStatus = "sent";
|
||||
this.sendingRootEventId = null;
|
||||
})
|
||||
.catch((err: any) => {
|
||||
console.error("ERROR", err);
|
||||
});
|
||||
return this.sendingPromise;
|
||||
},
|
||||
|
||||
cancelSendAttachments() {
|
||||
this.sendingAttachments.toReversed().forEach((item) => {
|
||||
this.cancelSendAttachmentItem(item);
|
||||
});
|
||||
this.sendingStatus = "canceled";
|
||||
if (this.sendingRootEventId && this.room) {
|
||||
// Redact all media we already sent, plus the root event
|
||||
let promises = this.sendingAttachments
|
||||
.filter((item) => item.sendInfo?.mediaEventId !== undefined)
|
||||
.map((item) =>
|
||||
this.$matrix.matrixClient.redactEvent(this.room.roomId, item.sendInfo!.mediaEventId, undefined, {
|
||||
reason: "cancel",
|
||||
})
|
||||
);
|
||||
promises.push(
|
||||
this.$matrix.matrixClient.redactEvent(this.room.roomId, this.sendingRootEventId, undefined, {
|
||||
reason: "cancel",
|
||||
})
|
||||
);
|
||||
Promise.allSettled(promises)
|
||||
.then(() => {
|
||||
console.log("Message redacted");
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log("Redaction failed: ", err);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
cancelSendAttachmentItem(item: Attachment) {
|
||||
if (item.sendInfo) {
|
||||
if (item.sendInfo.promise && item.sendInfo.status != "initial") {
|
||||
item.sendInfo.promise.abort();
|
||||
}
|
||||
item.sendInfo.status = "canceled";
|
||||
}
|
||||
},
|
||||
|
||||
sortSendingAttachments() {
|
||||
this.sendingAttachments.sort((a, b) => (b.sendInfo?.statusDate ?? 0) - (a.sendInfo?.statusDate ?? 0));
|
||||
},
|
||||
},
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue