implement optional image compression when adding images to messages
This commit is contained in:
26
package-lock.json
generated
26
package-lock.json
generated
@@ -15,6 +15,7 @@
|
|||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
"click-outside-vue3": "^4.0.1",
|
"click-outside-vue3": "^4.0.1",
|
||||||
|
"compressorjs": "^1.2.1",
|
||||||
"electron-prompt": "^1.7.0",
|
"electron-prompt": "^1.7.0",
|
||||||
"mitt": "^3.0.1",
|
"mitt": "^3.0.1",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
@@ -1979,6 +1980,11 @@
|
|||||||
"bluebird": "^3.5.5"
|
"bluebird": "^3.5.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/blueimp-canvas-to-blob": {
|
||||||
|
"version": "3.29.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/blueimp-canvas-to-blob/-/blueimp-canvas-to-blob-3.29.0.tgz",
|
||||||
|
"integrity": "sha512-0pcSSGxC0QxT+yVkivxIqW0Y4VlO2XSDPofBAqoJ1qJxgH9eiUDLv50Rixij2cDuEfx4M6DpD9UGZpRhT5Q8qg=="
|
||||||
|
},
|
||||||
"node_modules/boolean": {
|
"node_modules/boolean": {
|
||||||
"version": "3.2.0",
|
"version": "3.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz",
|
||||||
@@ -2414,6 +2420,15 @@
|
|||||||
"node": ">= 10"
|
"node": ">= 10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/compressorjs": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/compressorjs/-/compressorjs-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-+geIjeRnPhQ+LLvvA7wxBQE5ddeLU7pJ3FsKFWirDw6veY3s9iLxAQEw7lXGHnhCJvBujEQWuNnGzZcvCvdkLQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"blueimp-canvas-to-blob": "^3.29.0",
|
||||||
|
"is-blob": "^2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/concat-map": {
|
"node_modules/concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
@@ -3751,6 +3766,17 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-blob": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-blob/-/is-blob-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-SZ/fTft5eUhQM6oF/ZaASFDEdbFVe89Imltn9uZr03wdKMcWNVYSMjQPFtg05QuNkt5l5c135ElvXEQG0rk4tw==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-ci": {
|
"node_modules/is-ci": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz",
|
||||||
|
|||||||
@@ -100,6 +100,7 @@
|
|||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
"click-outside-vue3": "^4.0.1",
|
"click-outside-vue3": "^4.0.1",
|
||||||
|
"compressorjs": "^1.2.1",
|
||||||
"electron-prompt": "^1.7.0",
|
"electron-prompt": "^1.7.0",
|
||||||
"mitt": "^3.0.1",
|
"mitt": "^3.0.1",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
|
|||||||
135
src/frontend/components/messages/AddImageButton.vue
Normal file
135
src/frontend/components/messages/AddImageButton.vue
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
<template>
|
||||||
|
<div class="inline-flex rounded-md shadow-sm">
|
||||||
|
|
||||||
|
<button @click="showMenu" type="button" class="my-auto mr-1 inline-flex items-center gap-x-1 rounded-md bg-gray-500 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-gray-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-500 dark:bg-zinc-800 dark:text-white dark:hover:bg-zinc-700 dark:focus-visible:outline-zinc-500">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-5">
|
||||||
|
<path fill-rule="evenodd" d="M1.5 6a2.25 2.25 0 0 1 2.25-2.25h16.5A2.25 2.25 0 0 1 22.5 6v12a2.25 2.25 0 0 1-2.25 2.25H3.75A2.25 2.25 0 0 1 1.5 18V6ZM3 16.06V18c0 .414.336.75.75.75h16.5A.75.75 0 0 0 21 18v-1.94l-2.69-2.689a1.5 1.5 0 0 0-2.12 0l-.88.879.97.97a.75.75 0 1 1-1.06 1.06l-5.16-5.159a1.5 1.5 0 0 0-2.12 0L3 16.061Zm10.125-7.81a1.125 1.125 0 1 1 2.25 0 1.125 1.125 0 0 1-2.25 0Z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
<span class="ml-1 hidden xl:inline-block whitespace-nowrap">Add Image</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="relative block">
|
||||||
|
<Transition
|
||||||
|
enter-active-class="transition ease-out duration-100"
|
||||||
|
enter-from-class="transform opacity-0 scale-95"
|
||||||
|
enter-to-class="transform opacity-100 scale-100"
|
||||||
|
leave-active-class="transition ease-in duration-75"
|
||||||
|
leave-from-class="transform opacity-100 scale-100"
|
||||||
|
leave-to-class="transform opacity-0 scale-95">
|
||||||
|
<div v-if="isShowingMenu" v-click-outside="hideMenu" class="absolute bottom-0 -ml-11 sm:right-0 sm:ml-0 z-10 mb-10 rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
|
||||||
|
<div class="py-1">
|
||||||
|
<button @click="addImage('low')" type="button" class="w-full block text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 whitespace-nowrap">Low Quality (320x320)</button>
|
||||||
|
<button @click="addImage('medium')" type="button" class="w-full block text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 whitespace-nowrap">Medium Quality (640x640)</button>
|
||||||
|
<button @click="addImage('high')" type="button" class="w-full block text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 whitespace-nowrap">High Quality (1280x1280)</button>
|
||||||
|
<button @click="addImage('original')" type="button" class="w-full block text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 whitespace-nowrap">Original Quality</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- hidden file input for selecting files -->
|
||||||
|
<input ref="image-input" @change="onImageInputChange" type="file" accept="image/*" style="display:none"/>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Compressor from 'compressorjs';
|
||||||
|
import DialogUtils from "../../js/DialogUtils";
|
||||||
|
export default {
|
||||||
|
name: 'AddImageButton',
|
||||||
|
emits: [
|
||||||
|
"add-image",
|
||||||
|
],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isShowingMenu: false,
|
||||||
|
selectedImageQuality: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
showMenu() {
|
||||||
|
this.isShowingMenu = true;
|
||||||
|
},
|
||||||
|
hideMenu() {
|
||||||
|
this.isShowingMenu = false;
|
||||||
|
},
|
||||||
|
addImage(quality) {
|
||||||
|
this.isShowingMenu = false;
|
||||||
|
this.selectedImageQuality = quality;
|
||||||
|
this.$refs["image-input"].click();
|
||||||
|
},
|
||||||
|
clearImageInput: function() {
|
||||||
|
this.$refs["image-input"].value = null;
|
||||||
|
},
|
||||||
|
onImageInputChange: async function(event) {
|
||||||
|
if(event.target.files.length > 0){
|
||||||
|
|
||||||
|
// get selected file
|
||||||
|
const file = event.target.files[0];
|
||||||
|
|
||||||
|
// process file based on selected image quality
|
||||||
|
switch(this.selectedImageQuality) {
|
||||||
|
case "low": {
|
||||||
|
new Compressor(file, {
|
||||||
|
maxWidth: 320,
|
||||||
|
maxHeight: 320,
|
||||||
|
quality: 0.2,
|
||||||
|
mimeType: "image/webp",
|
||||||
|
success: (result) => {
|
||||||
|
this.$emit("add-image", result);
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
DialogUtils.alert(err.message);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "medium": {
|
||||||
|
new Compressor(file, {
|
||||||
|
maxWidth: 640,
|
||||||
|
maxHeight: 640,
|
||||||
|
quality: 0.6,
|
||||||
|
mimeType: "image/webp",
|
||||||
|
success: (result) => {
|
||||||
|
this.$emit("add-image", result);
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
DialogUtils.alert(err.message);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "high": {
|
||||||
|
new Compressor(file, {
|
||||||
|
maxWidth: 1280,
|
||||||
|
maxHeight: 1280,
|
||||||
|
quality: 0.75,
|
||||||
|
mimeType: "image/webp",
|
||||||
|
success: (result) => {
|
||||||
|
this.$emit("add-image", result);
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
DialogUtils.alert(err.message);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "original": {
|
||||||
|
this.$emit("add-image", file);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
DialogUtils.alert(`Unsupported image quality: ${this.selectedImageQuality}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// clear image input to allow selecting the same file after user removed it
|
||||||
|
this.clearImageInput();
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -334,12 +334,9 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- add image -->
|
<!-- add image -->
|
||||||
<button @click="addImageToMessage" type="button" class="my-auto mr-1 inline-flex items-center gap-x-1 rounded-md bg-gray-500 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-gray-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-500 dark:bg-zinc-800 dark:text-white dark:hover:bg-zinc-700 dark:focus-visible:outline-zinc-500">
|
<div>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-5 h-5">
|
<AddImageButton @add-image="onImageSelected"/>
|
||||||
<path fill-rule="evenodd" d="M1.5 6a2.25 2.25 0 0 1 2.25-2.25h16.5A2.25 2.25 0 0 1 22.5 6v12a2.25 2.25 0 0 1-2.25 2.25H3.75A2.25 2.25 0 0 1 1.5 18V6ZM3 16.06V18c0 .414.336.75.75.75h16.5A.75.75 0 0 0 21 18v-1.94l-2.69-2.689a1.5 1.5 0 0 0-2.12 0l-.88.879.97.97a.75.75 0 1 1-1.06 1.06l-5.16-5.159a1.5 1.5 0 0 0-2.12 0L3 16.061Zm10.125-7.81a1.125 1.125 0 1 1 2.25 0 1.125 1.125 0 0 1-2.25 0Z" clip-rule="evenodd" />
|
</div>
|
||||||
</svg>
|
|
||||||
<span class="ml-1 hidden xl:inline-block whitespace-nowrap">Add Image</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- add audio -->
|
<!-- add audio -->
|
||||||
<div>
|
<div>
|
||||||
@@ -369,7 +366,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- hidden file input for selecting files -->
|
<!-- hidden file input for selecting files -->
|
||||||
<input ref="image-input" @change="onImageInputChange" type="file" accept="image/*" style="display:none"/>
|
|
||||||
<input ref="file-input" @change="onFileInputChange" type="file" multiple style="display:none"/>
|
<input ref="file-input" @change="onFileInputChange" type="file" multiple style="display:none"/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -398,10 +394,12 @@ import moment from "moment";
|
|||||||
import SendMessageButton from "./SendMessageButton.vue";
|
import SendMessageButton from "./SendMessageButton.vue";
|
||||||
import MaterialDesignIcon from "../MaterialDesignIcon.vue";
|
import MaterialDesignIcon from "../MaterialDesignIcon.vue";
|
||||||
import ConversationDropDownMenu from "./ConversationDropDownMenu.vue";
|
import ConversationDropDownMenu from "./ConversationDropDownMenu.vue";
|
||||||
|
import AddImageButton from "./AddImageButton.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ConversationViewer',
|
name: 'ConversationViewer',
|
||||||
components: {
|
components: {
|
||||||
|
AddImageButton,
|
||||||
ConversationDropDownMenu,
|
ConversationDropDownMenu,
|
||||||
MaterialDesignIcon,
|
MaterialDesignIcon,
|
||||||
SendMessageButton,
|
SendMessageButton,
|
||||||
@@ -1095,7 +1093,6 @@ export default {
|
|||||||
this.newMessageImageUrl = null;
|
this.newMessageImageUrl = null;
|
||||||
this.newMessageAudio = null;
|
this.newMessageAudio = null;
|
||||||
this.newMessageFiles = [];
|
this.newMessageFiles = [];
|
||||||
this.clearImageInput();
|
|
||||||
this.clearFileInput();
|
this.clearFileInput();
|
||||||
|
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
@@ -1166,11 +1163,10 @@ export default {
|
|||||||
this.newMessageImage = null;
|
this.newMessageImage = null;
|
||||||
this.newMessageImageUrl = null;
|
this.newMessageImageUrl = null;
|
||||||
},
|
},
|
||||||
onImageInputChange: function(event) {
|
onImageSelected: function(imageBlob) {
|
||||||
if(event.target.files.length > 0){
|
|
||||||
|
|
||||||
// update selected file
|
// update selected file
|
||||||
this.newMessageImage = event.target.files[0];
|
this.newMessageImage = imageBlob;
|
||||||
|
|
||||||
// update image url when file is read
|
// update image url when file is read
|
||||||
const fileReader = new FileReader();
|
const fileReader = new FileReader();
|
||||||
@@ -1181,13 +1177,6 @@ export default {
|
|||||||
// convert image to data url
|
// convert image to data url
|
||||||
fileReader.readAsDataURL(this.newMessageImage);
|
fileReader.readAsDataURL(this.newMessageImage);
|
||||||
|
|
||||||
// clear image input to allow selecting the same file after user removed it
|
|
||||||
this.clearImageInput();
|
|
||||||
|
|
||||||
}
|
|
||||||
},
|
|
||||||
clearImageInput: function() {
|
|
||||||
this.$refs["image-input"].value = null;
|
|
||||||
},
|
},
|
||||||
async startRecordingAudioAttachment(args) {
|
async startRecordingAudioAttachment(args) {
|
||||||
|
|
||||||
@@ -1376,9 +1365,6 @@ export default {
|
|||||||
addFilesToMessage: function() {
|
addFilesToMessage: function() {
|
||||||
this.$refs["file-input"].click();
|
this.$refs["file-input"].click();
|
||||||
},
|
},
|
||||||
addImageToMessage: function() {
|
|
||||||
this.$refs["image-input"].click();
|
|
||||||
},
|
|
||||||
findConversation: function(destinationHash) {
|
findConversation: function(destinationHash) {
|
||||||
return this.conversations.find((conversation) => {
|
return this.conversations.find((conversation) => {
|
||||||
return conversation.destination_hash === destinationHash;
|
return conversation.destination_hash === destinationHash;
|
||||||
|
|||||||
Reference in New Issue
Block a user