Merge branch 'file-mode-dragdrop' into 'dev'

File mode: support drag drop and fix cancellation of uploads

See merge request keanuapp/keanuapp-weblite!209
This commit is contained in:
N Pex 2023-07-06 09:20:58 +00:00
commit 44dd4e9562
4 changed files with 125 additions and 67 deletions

View file

@ -36,6 +36,9 @@ $small-button-height: 36px;
width: 100%;
height: 50%;
background-color: #181719;
&.drop-target {
background-color: #383739;
}
border-radius: 19px;
display: flex;
flex-direction: column;
@ -90,6 +93,9 @@ $small-button-height: 36px;
width: 100%;
height: 70%;
background-color: #181719;
&.drop-target {
background-color: #383739;
}
border-radius: 19px;
overflow: hidden;
.v-image {
@ -242,16 +248,21 @@ $small-button-height: 36px;
}
}
.file-drop-stack-item {
background: linear-gradient(0deg, #3a3a3c 0%, #3a3a3c 100%), #fff;
background: #3a3a3c;
position: absolute;
overflow: hidden;
opacity: 0;
.v-image {
width: 100%;
height: 100%;
object-fit: cover;
}
&.direct {
opacity: 1 !important;
}
&.animated {
animation-name: fadeInStackItem;
animation-fill-mode: both;
animation-duration: 1.5s;
}
}

View file

@ -16,7 +16,8 @@
v-on:close="showRecorder = false" v-on:file="onVoiceRecording" :sendTypingIndicators="useVoiceMode" />
<FileDropLayout class="file-drop-root" v-if="useFileModeNonAdmin" :room="room"
v-on:add-file="showAttachmentPicker()"
v-on:pick-file="showAttachmentPicker()"
v-on:add-file="addAttachment($event)"
v-on:remove-file="currentFileInputs.splice($event, 1)"
v-on:reset="resetAttachments"
:attachments="currentFileInputs"
@ -1146,6 +1147,10 @@ export default {
this.currentSendError = null;
},
addAttachment(file) {
this.handleFileReader(null, file);
},
resetAttachments() {
this.cancelSendAttachment();
},

View file

@ -6,8 +6,10 @@
<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="{ '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>
@ -15,7 +17,9 @@
<!-- 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">
<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>
@ -28,7 +32,7 @@
</div>
</div>
<div class="file-drop-thumbnail noborder">
<div class="add clickable" @click.stop="$emit('add-file')">
<div class="add clickable" @click.stop="$emit('pick-file')">
+
</div>
</div>
@ -46,11 +50,12 @@
</template>
<!-- ATTACHMENT SENDING/SENT MODE -->
<template v-if="attachments && attachments.length > 0 && (status == mainStatuses.SENDING || status == mainStatuses.SENT)">
<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 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"
@ -130,6 +135,7 @@ export default {
FAILED: 3,
}),
sendInfo: [],
dropTarget: false,
};
},
mounted() {
@ -173,6 +179,14 @@ export default {
},
},
methods: {
filesDropped(e) {
this.dropTarget = false;
let droppedFiles = e.dataTransfer.files;
if (!droppedFiles) return;
([...droppedFiles]).forEach(f => {
this.$emit('add-file', f);
});
},
scrollToBottom() {
const el = this.$refs.attachmentWrapper;
if (el) {
@ -216,13 +230,21 @@ export default {
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 }) => {
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(() => {
}, 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;
@ -244,10 +266,20 @@ export default {
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;
}));
return Promise.allSettled(promises)
}
});
item.promise = itemPromise;
return itemPromise.then(() => getItemPromise(++index));
}
else return Promise.resolve();
};
return promiseChain.then(() => getItemPromise(0));
})
.then(() => {
this.status = this.mainStatuses.SENT;
@ -257,7 +289,9 @@ export default {
});
},
cancelSendingItem(item) {
// TODO
if (item.promise && item.status == this.statuses.INITIAL) {
item.promise.abort();
}
item.status = this.statuses.CANCELED;
},
checkDone() {

View file

@ -30,27 +30,29 @@ var _browserCanRecordAudioF = function () {
}
var _browserCanRecordAudio = _browserCanRecordAudioF();
class UploadPromise extends Promise {
constructor(executor) {
const aborter = {
aborted: false,
abortablePromise: undefined,
matrixClient: undefined,
class UploadPromise {
aborted = false;
onAbort = undefined;
constructor(wrappedPromise) {
this.wrappedPromise = wrappedPromise;
}
const normalExecutor = function (resolve, reject) {
executor(resolve, reject, aborter);
};
super(normalExecutor);
this.abort = () => {
aborter.aborted = true;
if (aborter.abortablePromise && aborter.matrixClient) {
aborter.matrixClient.cancelUpload(aborter.abortablePromise);
aborter.matrixClient = undefined;
aborter.abortablePromise = undefined;
abort() {
this.aborted = true;
if (this.onAbort) {
this.onAbort();
}
};
}
then(resolve, reject) {
this.wrappedPromise = this.wrappedPromise.then(resolve, reject);
return this;
}
catch(handler) {
this.wrappedPromise = this.wrappedPromise.catch(handler);
return this;
}
}
@ -340,11 +342,11 @@ class Util {
}
sendImage(matrixClient, roomId, file, onUploadProgress, threadRoot) {
return new UploadPromise((resolve, reject, aborter) => {
const abortionController = aborter;
const uploadPromise = new UploadPromise(undefined);
uploadPromise.wrappedPromise = new Promise((resolve, reject) => {
var reader = new FileReader();
reader.onload = (e) => {
if (abortionController.aborted) {
if (uploadPromise.aborted) {
reject("Aborted");
return;
}
@ -399,9 +401,11 @@ class Util {
if (!matrixClient.isRoomEncrypted(roomId)) {
// Not encrypted.
abortionController.matrixClient = matrixClient;
abortionController.abortablePromise = matrixClient.uploadContent(data, opts);
abortionController.abortablePromise
const promise = matrixClient.uploadContent(data, opts);
uploadPromise.onAbort = () => {
matrixClient.cancelUpload(promise);
};
promise
.then((response) => {
messageContent.url = response.content_uri;
return this.sendMessage(matrixClient, roomId, "m.room.message", messageContent)
@ -448,8 +452,11 @@ class Util {
// Encrypted data sent as octet-stream!
opts.type = "application/octet-stream";
abortionController.abortablePromise = matrixClient.uploadContent(data, opts);
abortionController.abortablePromise
const promise = matrixClient.uploadContent(data, opts);
uploadPromise.onAbort = () => {
matrixClient.cancelUpload(promise);
};
promise
.then((response) => {
if (response.error) {
return reject(response.error);
@ -469,6 +476,7 @@ class Util {
}
reader.readAsArrayBuffer(file);
});
return uploadPromise;
}
/**