Experimental "file drop" mode
This commit is contained in:
parent
791fa5936a
commit
ebadd509e9
19 changed files with 1038 additions and 85 deletions
285
src/components/file_mode/FileDropLayout.vue
Normal file
285
src/components/file_mode/FileDropLayout.vue
Normal file
|
|
@ -0,0 +1,285 @@
|
|||
<template>
|
||||
<div v-bind="{ ...$props, ...$attrs }" v-on="$listeners">
|
||||
<!-- 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">
|
||||
<v-btn @click="$emit('add-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">
|
||||
<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('add-file')">
|
||||
+
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="file-drop-input-container">
|
||||
<v-textarea ref="input" full-width 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="send" :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 && countSent == 0" class="no-items">
|
||||
<div class="file-drop-stack-item" :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>
|
||||
<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 sendingItems" :key="index">
|
||||
<v-img v-if="info.attachment && info.attachment.image" :src="info.attachment.image" />
|
||||
<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)">
|
||||
<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>
|
||||
<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" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bottom section -->
|
||||
<div v-if="status == mainStatuses.SENDING" class="file-drop-sending-input-container">
|
||||
<v-textarea disabled full-width 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.return_to_home") }}</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import messageMixin from "../messages/messageMixin";
|
||||
import util from "../../plugins/utils";
|
||||
const prettyBytes = require("pretty-bytes");
|
||||
|
||||
export default {
|
||||
mixins: [messageMixin],
|
||||
components: {},
|
||||
props: {
|
||||
attachments: {
|
||||
type: Array,
|
||||
default: function () {
|
||||
return []
|
||||
}
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentItemIndex: 0,
|
||||
messageInput: "",
|
||||
mainStatuses: Object.freeze({
|
||||
SELECTING: 0,
|
||||
SENDING: 1,
|
||||
SENT: 2,
|
||||
}),
|
||||
status: 0,
|
||||
statuses: Object.freeze({
|
||||
INITIAL: 0,
|
||||
SENT: 1,
|
||||
CANCELED: 2,
|
||||
FAILED: 3,
|
||||
}),
|
||||
sendInfo: [],
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
document.body.classList.add("dark");
|
||||
this.$audioPlayer.setAutoplay(false);
|
||||
},
|
||||
beforeDestroy() {
|
||||
document.body.classList.remove("dark");
|
||||
},
|
||||
computed: {
|
||||
currentItemHasImagePreview() {
|
||||
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) {
|
||||
// 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: {
|
||||
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.sendInfo = [];
|
||||
this.status = this.mainStatuses.SELECTING;
|
||||
this.messageInput = "";
|
||||
this.currentItemIndex = 0;
|
||||
},
|
||||
send() {
|
||||
this.status = this.mainStatuses.SENDING;
|
||||
this.sendInfo = this.attachments.map((attachment) => {
|
||||
return {
|
||||
id: attachment.name,
|
||||
status: this.statuses.INITIAL,
|
||||
statusDate: Date.now,
|
||||
attachment: 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
|
||||
const promises = this.sendInfo.map(item => 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 => {
|
||||
console.error("ERROR", ignorederr);
|
||||
item.status = this.statuses.FAILED;
|
||||
}));
|
||||
return Promise.allSettled(promises)
|
||||
})
|
||||
.then(() => {
|
||||
this.status = this.mainStatuses.SENT;
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("ERROR", err);
|
||||
});
|
||||
},
|
||||
cancelSendingItem(item) {
|
||||
// TODO
|
||||
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 = ""
|
||||
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">
|
||||
@import "@/assets/css/chat.scss";
|
||||
</style>
|
||||
Loading…
Add table
Add a link
Reference in a new issue