File mode: support drag drop and fix cancellation of uploads
This commit is contained in:
parent
fbacf68651
commit
2f29b72594
4 changed files with 125 additions and 67 deletions
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue