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:
commit
44dd4e9562
4 changed files with 125 additions and 67 deletions
|
|
@ -36,6 +36,9 @@ $small-button-height: 36px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 50%;
|
height: 50%;
|
||||||
background-color: #181719;
|
background-color: #181719;
|
||||||
|
&.drop-target {
|
||||||
|
background-color: #383739;
|
||||||
|
}
|
||||||
border-radius: 19px;
|
border-radius: 19px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
@ -90,6 +93,9 @@ $small-button-height: 36px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 70%;
|
height: 70%;
|
||||||
background-color: #181719;
|
background-color: #181719;
|
||||||
|
&.drop-target {
|
||||||
|
background-color: #383739;
|
||||||
|
}
|
||||||
border-radius: 19px;
|
border-radius: 19px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
.v-image {
|
.v-image {
|
||||||
|
|
@ -242,16 +248,21 @@ $small-button-height: 36px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.file-drop-stack-item {
|
.file-drop-stack-item {
|
||||||
background: linear-gradient(0deg, #3a3a3c 0%, #3a3a3c 100%), #fff;
|
background: #3a3a3c;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
opacity: 0;
|
||||||
.v-image {
|
.v-image {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
&.direct {
|
||||||
|
opacity: 1 !important;
|
||||||
|
}
|
||||||
&.animated {
|
&.animated {
|
||||||
animation-name: fadeInStackItem;
|
animation-name: fadeInStackItem;
|
||||||
|
animation-fill-mode: both;
|
||||||
animation-duration: 1.5s;
|
animation-duration: 1.5s;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,8 @@
|
||||||
v-on:close="showRecorder = false" v-on:file="onVoiceRecording" :sendTypingIndicators="useVoiceMode" />
|
v-on:close="showRecorder = false" v-on:file="onVoiceRecording" :sendTypingIndicators="useVoiceMode" />
|
||||||
|
|
||||||
<FileDropLayout class="file-drop-root" v-if="useFileModeNonAdmin" :room="room"
|
<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:remove-file="currentFileInputs.splice($event, 1)"
|
||||||
v-on:reset="resetAttachments"
|
v-on:reset="resetAttachments"
|
||||||
:attachments="currentFileInputs"
|
:attachments="currentFileInputs"
|
||||||
|
|
@ -1146,6 +1147,10 @@ export default {
|
||||||
this.currentSendError = null;
|
this.currentSendError = null;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
addAttachment(file) {
|
||||||
|
this.handleFileReader(null, file);
|
||||||
|
},
|
||||||
|
|
||||||
resetAttachments() {
|
resetAttachments() {
|
||||||
this.cancelSendAttachment();
|
this.cancelSendAttachment();
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,10 @@
|
||||||
<v-icon>$vuetify.icons.ic_lock</v-icon>
|
<v-icon>$vuetify.icons.ic_lock</v-icon>
|
||||||
<div class="file-drop-title">{{ $t("file_mode.secure_file_send") }}</div>
|
<div class="file-drop-title">{{ $t("file_mode.secure_file_send") }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="background">
|
<div :class="{ 'background': true, 'drop-target': dropTarget }" @drop.prevent="filesDropped"
|
||||||
<v-btn @click="$emit('add-file')" class="large">{{ $t("file_mode.choose_files") }}</v-btn>
|
@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 class="file-format-info">{{ $t("file_mode.any_file_format_accepted") }}</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -15,7 +17,9 @@
|
||||||
<!-- ATTACHMENT SELECTION MODE -->
|
<!-- ATTACHMENT SELECTION MODE -->
|
||||||
<template v-if="attachments && attachments.length > 0 && status == mainStatuses.SELECTING">
|
<template v-if="attachments && attachments.length > 0 && status == mainStatuses.SELECTING">
|
||||||
<div class="attachment-wrapper" ref="attachmentWrapper">
|
<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" />
|
<v-img v-if="currentItemHasImagePreview" :src="attachments[currentItemIndex].image" />
|
||||||
<div v-else class="filename">{{ attachments[currentItemIndex].name }}</div>
|
<div v-else class="filename">{{ attachments[currentItemIndex].name }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -28,7 +32,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="file-drop-thumbnail noborder">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -46,11 +50,12 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- ATTACHMENT SENDING/SENT MODE -->
|
<!-- 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="attachment-wrapper">
|
||||||
<div class="file-drop-sent-stack" ref="stackContainer">
|
<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 && 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>{{ $t('file_mode.sending_progress') }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else v-for="(item, index) in sentItems" :key="item.id" class="file-drop-stack-item animated"
|
<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,
|
FAILED: 3,
|
||||||
}),
|
}),
|
||||||
sendInfo: [],
|
sendInfo: [],
|
||||||
|
dropTarget: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
|
@ -173,6 +179,14 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
filesDropped(e) {
|
||||||
|
this.dropTarget = false;
|
||||||
|
let droppedFiles = e.dataTransfer.files;
|
||||||
|
if (!droppedFiles) return;
|
||||||
|
([...droppedFiles]).forEach(f => {
|
||||||
|
this.$emit('add-file', f);
|
||||||
|
});
|
||||||
|
},
|
||||||
scrollToBottom() {
|
scrollToBottom() {
|
||||||
const el = this.$refs.attachmentWrapper;
|
const el = this.$refs.attachmentWrapper;
|
||||||
if (el) {
|
if (el) {
|
||||||
|
|
@ -216,38 +230,56 @@ export default {
|
||||||
util.sendTextMessage(this.$matrix.matrixClient, this.room.roomId, text)
|
util.sendTextMessage(this.$matrix.matrixClient, this.room.roomId, text)
|
||||||
.then((eventId) => {
|
.then((eventId) => {
|
||||||
// Use the eventId as a thread root for all the media
|
// 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();
|
||||||
if (loaded == total) {
|
const getItemPromise = (index) => {
|
||||||
item.progress = 100;
|
if (index < this.sendInfo.length) {
|
||||||
} else if (total > 0) {
|
const item = this.sendInfo[index];
|
||||||
item.progress = 100 * loaded / total;
|
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));
|
||||||
}
|
}
|
||||||
}, eventId).then(() => {
|
else return Promise.resolve();
|
||||||
// Look at last item rotation, flipping the sign on this, so looks more like a true stack
|
};
|
||||||
let signR = 1;
|
|
||||||
let signX = 1;
|
return promiseChain.then(() => getItemPromise(0));
|
||||||
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(() => {
|
.then(() => {
|
||||||
this.status = this.mainStatuses.SENT;
|
this.status = this.mainStatuses.SENT;
|
||||||
|
|
@ -257,7 +289,9 @@ export default {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
cancelSendingItem(item) {
|
cancelSendingItem(item) {
|
||||||
// TODO
|
if (item.promise && item.status == this.statuses.INITIAL) {
|
||||||
|
item.promise.abort();
|
||||||
|
}
|
||||||
item.status = this.statuses.CANCELED;
|
item.status = this.statuses.CANCELED;
|
||||||
},
|
},
|
||||||
checkDone() {
|
checkDone() {
|
||||||
|
|
|
||||||
|
|
@ -30,27 +30,29 @@ var _browserCanRecordAudioF = function () {
|
||||||
}
|
}
|
||||||
var _browserCanRecordAudio = _browserCanRecordAudioF();
|
var _browserCanRecordAudio = _browserCanRecordAudioF();
|
||||||
|
|
||||||
class UploadPromise extends Promise {
|
class UploadPromise {
|
||||||
constructor(executor) {
|
aborted = false;
|
||||||
const aborter = {
|
onAbort = undefined;
|
||||||
aborted: false,
|
|
||||||
abortablePromise: undefined,
|
constructor(wrappedPromise) {
|
||||||
matrixClient: undefined,
|
this.wrappedPromise = wrappedPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
abort() {
|
||||||
|
this.aborted = true;
|
||||||
|
if (this.onAbort) {
|
||||||
|
this.onAbort();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const normalExecutor = function (resolve, reject) {
|
then(resolve, reject) {
|
||||||
executor(resolve, reject, aborter);
|
this.wrappedPromise = this.wrappedPromise.then(resolve, reject);
|
||||||
};
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
super(normalExecutor);
|
catch(handler) {
|
||||||
this.abort = () => {
|
this.wrappedPromise = this.wrappedPromise.catch(handler);
|
||||||
aborter.aborted = true;
|
return this;
|
||||||
if (aborter.abortablePromise && aborter.matrixClient) {
|
|
||||||
aborter.matrixClient.cancelUpload(aborter.abortablePromise);
|
|
||||||
aborter.matrixClient = undefined;
|
|
||||||
aborter.abortablePromise = undefined;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -340,11 +342,11 @@ class Util {
|
||||||
}
|
}
|
||||||
|
|
||||||
sendImage(matrixClient, roomId, file, onUploadProgress, threadRoot) {
|
sendImage(matrixClient, roomId, file, onUploadProgress, threadRoot) {
|
||||||
return new UploadPromise((resolve, reject, aborter) => {
|
const uploadPromise = new UploadPromise(undefined);
|
||||||
const abortionController = aborter;
|
uploadPromise.wrappedPromise = new Promise((resolve, reject) => {
|
||||||
var reader = new FileReader();
|
var reader = new FileReader();
|
||||||
reader.onload = (e) => {
|
reader.onload = (e) => {
|
||||||
if (abortionController.aborted) {
|
if (uploadPromise.aborted) {
|
||||||
reject("Aborted");
|
reject("Aborted");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -399,9 +401,11 @@ class Util {
|
||||||
|
|
||||||
if (!matrixClient.isRoomEncrypted(roomId)) {
|
if (!matrixClient.isRoomEncrypted(roomId)) {
|
||||||
// Not encrypted.
|
// Not encrypted.
|
||||||
abortionController.matrixClient = matrixClient;
|
const promise = matrixClient.uploadContent(data, opts);
|
||||||
abortionController.abortablePromise = matrixClient.uploadContent(data, opts);
|
uploadPromise.onAbort = () => {
|
||||||
abortionController.abortablePromise
|
matrixClient.cancelUpload(promise);
|
||||||
|
};
|
||||||
|
promise
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
messageContent.url = response.content_uri;
|
messageContent.url = response.content_uri;
|
||||||
return this.sendMessage(matrixClient, roomId, "m.room.message", messageContent)
|
return this.sendMessage(matrixClient, roomId, "m.room.message", messageContent)
|
||||||
|
|
@ -448,9 +452,12 @@ class Util {
|
||||||
// Encrypted data sent as octet-stream!
|
// Encrypted data sent as octet-stream!
|
||||||
opts.type = "application/octet-stream";
|
opts.type = "application/octet-stream";
|
||||||
|
|
||||||
abortionController.abortablePromise = matrixClient.uploadContent(data, opts);
|
const promise = matrixClient.uploadContent(data, opts);
|
||||||
abortionController.abortablePromise
|
uploadPromise.onAbort = () => {
|
||||||
.then((response) => {
|
matrixClient.cancelUpload(promise);
|
||||||
|
};
|
||||||
|
promise
|
||||||
|
.then((response) => {
|
||||||
if (response.error) {
|
if (response.error) {
|
||||||
return reject(response.error);
|
return reject(response.error);
|
||||||
}
|
}
|
||||||
|
|
@ -469,6 +476,7 @@ class Util {
|
||||||
}
|
}
|
||||||
reader.readAsArrayBuffer(file);
|
reader.readAsArrayBuffer(file);
|
||||||
});
|
});
|
||||||
|
return uploadPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue