Merge branch '367-send-multiple-files-at-once' into 'dev'

Resolve "Not able to send multiple files at once on IM"

Closes #367

See merge request keanuapp/keanuapp-weblite!188
This commit is contained in:
N Pex 2023-06-05 06:49:20 +00:00
commit 26f693ca85
2 changed files with 150 additions and 115 deletions

View file

@ -90,7 +90,11 @@
"incoming_message_deleted_text": "This message was deleted.", "incoming_message_deleted_text": "This message was deleted.",
"not_allowed_to_send": "Only admins and moderators are allowed to send to the room", "not_allowed_to_send": "Only admins and moderators are allowed to send to the room",
"reaction_count_more": "{reactionCount} more", "reaction_count_more": "{reactionCount} more",
"seen_by": "Seen by no members | Seen by 1 member | Seen by {count} members" "seen_by": "Seen by no members | Seen by 1 member | Seen by {count} members",
"file": "File",
"files": "Files",
"images": "Images",
"send_attachements_dialog_title": "Do you want to send following attachments ?"
}, },
"room": { "room": {
"invitations": "You have no invitations | You have 1 invitation | You have {count} invitations", "invitations": "You have no invitations | You have 1 invitation | You have {count} invitations",

View file

@ -188,35 +188,52 @@
</v-container> </v-container>
<input ref="attachment" type="file" name="attachment" @change="handlePickedAttachment($event)" <input ref="attachment" type="file" name="attachment" @change="handlePickedAttachment($event)"
accept="image/*, audio/*, video/*, .pdf" class="d-none" /> accept="image/*, audio/*, video/*, .pdf" class="d-none" multiple/>
<div v-if="currentImageInputPath"> <div v-if="currentFileInputsDialog">
<v-dialog v-model="currentImageInputPath" class="ma-0 pa-0" :width="$vuetify.breakpoint.smAndUp ? '50%' : '85%'"> <v-dialog v-model="currentFileInputsDialog" class="ma-0 pa-0" :width="$vuetify.breakpoint.smAndUp ? '50%' : '85%'" persistent scrollable>
<v-card class="ma-0 pa-0"> <v-card class="ma-0 pa-0">
<v-card-text class="ma-0 pa-2"> <v-card-title>{{ $t('message.send_attachements_dialog_title') }}</v-card-title>
<v-img v-if="currentImageInput && currentImageInput.image" :aspect-ratio="1" :src="currentImageInput.image" <v-divider></v-divider>
contain class="current-image-input-path" /> <template v-if="Array.isArray(currentImageInputs) && currentImageInputs.length">
<div> <v-card-title v-if="currentImageInputs.length > 1"> {{ $t('message.images') }} </v-card-title>
file: {{ currentImageInputPath.name }} <v-card-text :class="{'ma-0 pa-2' : true, 'd-flex flex-wrap justify-center': currentImageInputs.length > 1}">
<span v-if="currentImageInput && currentImageInput.scaled && currentImageInput.useScaled"> <div :class="{'col-4': currentImageInputs.length > 1}" v-for="(currentImageInput, id) in currentImageInputs" :key="id">
{{ currentImageInput.scaledDimensions.width }} x {{ currentImageInput.scaledDimensions.height }}</span> <v-img v-if="currentImageInput && currentImageInput.image" :aspect-ratio="1" :src="currentImageInput.image"
<span v-else-if="currentImageInput && currentImageInput.dimensions"> contain class="current-image-input-path" />
{{ currentImageInput.dimensions.width }} x {{ currentImageInput.dimensions.height }}</span> <div>
<span v-if="currentImageInput && currentImageInput.scaled && currentImageInput.useScaled"> <span v-if="currentImageInput && currentImageInput.scaled && currentImageInput.useScaled">
({{ formatBytes(currentImageInput.scaledSize) }})</span> {{ currentImageInput.scaledDimensions.width }} x {{ currentImageInput.scaledDimensions.height }}</span>
<span v-else> ({{ formatBytes(currentImageInputPath.size) }})</span> <span v-else-if="currentImageInput && currentImageInput.dimensions">
<v-switch v-if="currentImageInput && currentImageInput.scaled" :label="$t('message.scale_image')" {{ currentImageInput.dimensions.width }} x {{ currentImageInput.dimensions.height }}</span>
v-model="currentImageInput.useScaled" /> <span v-if="currentImageInput && currentImageInput.scaled && currentImageInput.useScaled">
</div> ({{ formatBytes(currentImageInput.scaledSize) }})</span>
<div v-if="currentSendError">{{ currentSendError }}</div> <v-switch v-if="currentImageInput && currentImageInput.scaled" :label="$t('message.scale_image')"
<div v-else>{{ currentSendProgress }}</div> v-model="currentImageInput.useScaled" />
</v-card-text> </div>
</div>
</v-card-text>
</template>
<template v-if="Array.isArray(currentFileInputs) && currentFileInputs.length">
<v-card-title v-if="nonImageFiles.length > 1">{{ $t('message.files') }}</v-card-title>
<v-card-text>
<div v-for="(currentImageInputPath, id) in currentFileInputs" :key="id">
<div v-if="!currentImageInputPath.type.includes('image/')">
<span> {{ $t('message.file') }}: {{ currentImageInputPath.name }}</span>
<span> ({{ formatBytes(currentImageInputPath.size) }})</span>
</div>
</div>
</v-card-text>
</template>
<v-divider></v-divider> <v-divider></v-divider>
<v-card-actions> <v-card-actions>
<v-spacer></v-spacer> <v-spacer>
<v-btn color="primary" text @click="cancelSendAttachment" id="btn-attachment-cancel">{{ <div v-if="currentSendError">{{ currentSendError }}</div>
$t("menu.cancel") <div v-else>{{ currentSendProgress }}</div>
}}</v-btn> </v-spacer>
<v-btn color="primary" text @click="cancelSendAttachment" id="btn-attachment-cancel">
{{ $t("menu.cancel") }}
</v-btn>
<v-btn id="btn-attachment-send" color="primary" text @click="sendAttachment" <v-btn id="btn-attachment-send" color="primary" text @click="sendAttachment"
v-if="currentSendShowSendButton" :disabled="currentSendOperation != null">{{ $t("menu.send") }}</v-btn> v-if="currentSendShowSendButton" :disabled="currentSendOperation != null">{{ $t("menu.send") }}</v-btn>
</v-card-actions> </v-card-actions>
@ -342,8 +359,8 @@ export default {
timelineWindowPaginating: false, timelineWindowPaginating: false,
scrollPosition: null, scrollPosition: null,
currentImageInput: null, currentImageInputs: null,
currentImageInputPath: null, currentFileInputs: null,
currentSendOperation: null, currentSendOperation: null,
currentSendProgress: null, currentSendProgress: null,
currentSendShowSendButton: true, currentSendShowSendButton: true,
@ -444,6 +461,20 @@ export default {
}, },
computed: { computed: {
nonImageFiles() {
return this.isCurrentFileInputsAnArray && this.currentFileInputs.filter(file => !file.type.includes("image/"))
},
isCurrentFileInputsAnArray() {
return Array.isArray(this.currentFileInputs)
},
currentFileInputsDialog: {
get() {
return this.isCurrentFileInputsAnArray
},
set() {
this.currentFileInputs = null
}
},
chatContainer() { chatContainer() {
const container = this.$refs.chatContainer; const container = this.$refs.chatContainer;
if (this.useVoiceMode) { if (this.useVoiceMode) {
@ -950,64 +981,64 @@ export default {
this.$refs.attachment.click(); this.$refs.attachment.click();
}, },
/** optimizeImage(e,event,file) {
* Handle picked attachment let currentImageInput = {
*/ image: e.target.result,
handlePickedAttachment(event) { dimensions: null,
if (event.target.files && event.target.files[0]) { };
try {
currentImageInput.dimensions = sizeOf(dataUriToBuffer(e.target.result));
// Need to resize?
const w = currentImageInput.dimensions.width;
const h = currentImageInput.dimensions.height;
if (w > 640 || h > 640) {
var aspect = w / h;
var newWidth = parseInt((w > h ? 640 : 640 * aspect).toFixed());
var newHeight = parseInt((w > h ? 640 / aspect : 640).toFixed());
var imageResize = new ImageResize({
format: "png",
width: newWidth,
height: newHeight,
outputType: "blob",
});
imageResize
.play(event.target)
.then((img) => {
Vue.set(
currentImageInput,
"scaled",
new File([img], file.name, {
type: img.type,
lastModified: Date.now(),
})
);
Vue.set(currentImageInput, "useScaled", true);
Vue.set(currentImageInput, "scaledSize", img.size);
Vue.set(currentImageInput, "scaledDimensions", {
width: newWidth,
height: newHeight,
});
})
.catch((err) => {
console.error("Resize failed:", err);
});
}
} catch (error) {
console.error("Failed to get image dimensions: " + error);
}
return currentImageInput
},
handleFileReader(event, file) {
if (file) {
var reader = new FileReader(); var reader = new FileReader();
reader.onload = (e) => { reader.onload = (e) => {
const file = event.target.files[0];
if (file.type.startsWith("image/")) { if (file.type.startsWith("image/")) {
this.currentImageInput = { const currentImageInput = this.optimizeImage(e, event, file)
image: e.target.result, this.currentImageInputs = Array.isArray(this.currentImageInputs) ? [...this.currentImageInputs, currentImageInput] : [currentImageInput]
dimensions: null,
};
try {
this.currentImageInput.dimensions = sizeOf(dataUriToBuffer(e.target.result));
// Need to resize?
const w = this.currentImageInput.dimensions.width;
const h = this.currentImageInput.dimensions.height;
if (w > 640 || h > 640) {
var aspect = w / h;
var newWidth = parseInt((w > h ? 640 : 640 * aspect).toFixed());
var newHeight = parseInt((w > h ? 640 / aspect : 640).toFixed());
var imageResize = new ImageResize({
format: "png",
width: newWidth,
height: newHeight,
outputType: "blob",
});
imageResize
.play(event.target)
.then((img) => {
Vue.set(
this.currentImageInput,
"scaled",
new File([img], file.name, {
type: img.type,
lastModified: Date.now(),
})
);
Vue.set(this.currentImageInput, "useScaled", true);
Vue.set(this.currentImageInput, "scaledSize", img.size);
Vue.set(this.currentImageInput, "scaledDimensions", {
width: newWidth,
height: newHeight,
});
})
.catch((err) => {
console.error("Resize failed:", err);
});
}
} catch (error) {
console.error("Failed to get image dimensions: " + error);
}
} }
console.log(this.currentImageInput);
this.$matrix.matrixClient.getMediaConfig().then((config) => { this.$matrix.matrixClient.getMediaConfig().then((config) => {
this.currentImageInputPath = file; this.currentFileInputs = Array.isArray(this.currentFileInputs) ? [...this.currentFileInputs, file] : [file];
if (config["m.upload.size"] && file.size > config["m.upload.size"]) { if (config["m.upload.size"] && file.size > config["m.upload.size"]) {
this.currentSendError = this.$t("message.upload_file_too_large"); this.currentSendError = this.$t("message.upload_file_too_large");
this.currentSendShowSendButton = false; this.currentSendShowSendButton = false;
@ -1016,9 +1047,15 @@ export default {
} }
}); });
}; };
reader.readAsDataURL(event.target.files[0]); reader.readAsDataURL(file);
} }
}, },
/**
* Handle picked attachment
*/
handlePickedAttachment(event) {
Object.values(event.target.files).forEach(file => this.handleFileReader(event, file));
},
showStickerPicker() { showStickerPicker() {
this.$refs.stickerPickerSheet.open(); this.$refs.stickerPickerSheet.open();
@ -1036,41 +1073,35 @@ export default {
}); });
} }
}, },
sendAttachment(withText) { sendAttachment(withText) {
this.$refs.attachment.value = null; this.$refs.attachment.value = null;
if (this.currentImageInputPath) { if (this.isCurrentFileInputsAnArray) {
var inputFile = this.currentImageInputPath; let inputFiles = this.currentFileInputs;
if (this.currentImageInput && this.currentImageInput.scaled && this.currentImageInput.useScaled) { if (Array.isArray(this.currentImageInputs) && this.currentImageInputs.scaled && this.currentImageInputs.useScaled) {
// Send scaled version of image instead! // Send scaled version of image instead!
inputFile = this.currentImageInput.scaled; inputFiles = this.currentImageInputs.map(({scaled}) => scaled)
} }
const promises = inputFiles.map(inputFile => util.sendImage(this.$matrix.matrixClient, this.roomId, inputFile, this.onUploadProgress));
Promise.all(promises).then(() => {
this.currentSendOperation = null;
this.currentImageInputs = null;
this.currentFileInputs = null;
this.currentSendProgress = null; this.currentSendProgress = null;
this.currentSendOperation = util.sendImage( if (withText) {
this.$matrix.matrixClient, this.sendMessage(withText);
this.roomId, }
inputFile, })
this.onUploadProgress .catch((err) => {
); if (err.name === "AbortError" || err === "Abort") {
this.currentSendOperation this.currentSendError = null;
.then(() => { } else {
this.currentSendOperation = null; this.currentSendError = err.LocaleString();
this.currentImageInput = null; }
this.currentImageInputPath = null; this.currentSendOperation = null;
this.currentSendProgress = null; this.currentSendProgress = null;
if (withText) { });
this.sendMessage(withText);
}
})
.catch((err) => {
if (err.name === "AbortError" || err === "Abort") {
this.currentSendError = null;
} else {
this.currentSendError = err.LocaleString();
}
this.currentSendOperation = null;
this.currentSendProgress = null;
});
} }
}, },
@ -1080,8 +1111,8 @@ export default {
this.currentSendOperation.abort(); this.currentSendOperation.abort();
} }
this.currentSendOperation = null; this.currentSendOperation = null;
this.currentImageInput = null; this.currentImageInputs = null;
this.currentImageInputPath = null; this.currentFileInputs = null;
this.currentSendProgress = null; this.currentSendProgress = null;
this.currentSendError = null; this.currentSendError = null;
}, },
@ -1461,7 +1492,7 @@ export default {
onVoiceRecording(event) { onVoiceRecording(event) {
this.currentSendShowSendButton = false; this.currentSendShowSendButton = false;
this.currentImageInputPath = event.file; this.currentFileInputs = Array.isArray(this.currentFileInputs) ? [...this.currentFileInputs, event.file] : [event.file];
var text = undefined; var text = undefined;
if (this.currentInput && this.currentInput.length > 0) { if (this.currentInput && this.currentInput.length > 0) {
text = this.currentInput; text = this.currentInput;