Optionally scale images when sending

Default is "scale". Currently scales image so that longest side is 640px.
This commit is contained in:
N-Pex 2021-05-11 21:03:54 +02:00
parent 91dfb0bc8e
commit 5276a46afa
4 changed files with 331 additions and 83 deletions

97
package-lock.json generated
View file

@ -1,18 +1,21 @@
{
"name": "keanuapp-weblite",
"version": "0.1.4",
"version": "0.1.5",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"version": "0.1.4",
"version": "0.1.5",
"dependencies": {
"aes-js": "^3.1.2",
"axios": "^0.21.0",
"clean-insights-sdk": "^2.4.1",
"core-js": "^3.6.5",
"data-uri-to-buffer": "^3.0.1",
"dayjs": "^1.10.3",
"fix-webm-duration": "^1.0.0",
"image-resize": "^1.1.5",
"image-size": "^1.0.0",
"intersection-observer": "^0.11.0",
"js-sha256": "^0.9.0",
"json-web-key": "^0.4.0",
@ -21,6 +24,7 @@
"matrix-js-sdk": "^9.4.1",
"md-gum-polyfill": "^1.0.0",
"olm": "https://packages.matrix.org/npm/olm/olm-3.2.1.tgz",
"pretty-bytes": "^5.6.0",
"qrcode": "^1.4.4",
"raw-loader": "^4.0.2",
"recordrtc": "^5.6.2",
@ -4820,6 +4824,14 @@
"node": ">=0.10"
}
},
"node_modules/data-uri-to-buffer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz",
"integrity": "sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==",
"engines": {
"node": ">= 6"
}
},
"node_modules/dayjs": {
"version": "1.10.4",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.10.4.tgz",
@ -7319,6 +7331,28 @@
"node": ">= 4"
}
},
"node_modules/image-resize": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/image-resize/-/image-resize-1.1.5.tgz",
"integrity": "sha512-oLSSXVzAmjhwSe36cGNmaSAid8xEXADlS6k+hXqXij3SMPhf9zUI6g3dHuNDV9nVmaD4jja36+k5PWmOiuI1wg==",
"dependencies": {
"jquery": "^3.4.1"
}
},
"node_modules/image-size": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/image-size/-/image-size-1.0.0.tgz",
"integrity": "sha512-JLJ6OwBfO1KcA+TvJT+v8gbE6iWbj24LyDNFgFEN0lzegn6cC6a/p3NIDaepMsJjQjlUWqIC7wJv8lBFxPNjcw==",
"dependencies": {
"queue": "6.0.2"
},
"bin": {
"image-size": "bin/image-size.js"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/import-cwd": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz",
@ -8167,8 +8201,7 @@
"node_modules/jquery": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.0.tgz",
"integrity": "sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw==",
"peer": true
"integrity": "sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw=="
},
"node_modules/js-message": {
"version": "1.0.7",
@ -10782,6 +10815,17 @@
"node": ">=4"
}
},
"node_modules/pretty-bytes": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz",
"integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==",
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/pretty-error": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-2.1.2.tgz",
@ -11093,6 +11137,14 @@
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
"dev": true
},
"node_modules/queue": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz",
"integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==",
"dependencies": {
"inherits": "~2.0.3"
}
},
"node_modules/randombytes": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@ -19479,6 +19531,11 @@
"assert-plus": "^1.0.0"
}
},
"data-uri-to-buffer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz",
"integrity": "sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og=="
},
"dayjs": {
"version": "1.10.4",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.10.4.tgz",
@ -21454,6 +21511,22 @@
"integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==",
"dev": true
},
"image-resize": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/image-resize/-/image-resize-1.1.5.tgz",
"integrity": "sha512-oLSSXVzAmjhwSe36cGNmaSAid8xEXADlS6k+hXqXij3SMPhf9zUI6g3dHuNDV9nVmaD4jja36+k5PWmOiuI1wg==",
"requires": {
"jquery": "^3.4.1"
}
},
"image-size": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/image-size/-/image-size-1.0.0.tgz",
"integrity": "sha512-JLJ6OwBfO1KcA+TvJT+v8gbE6iWbj24LyDNFgFEN0lzegn6cC6a/p3NIDaepMsJjQjlUWqIC7wJv8lBFxPNjcw==",
"requires": {
"queue": "6.0.2"
}
},
"import-cwd": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz",
@ -22075,8 +22148,7 @@
"jquery": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.0.tgz",
"integrity": "sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw==",
"peer": true
"integrity": "sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw=="
},
"js-message": {
"version": "1.0.7",
@ -24229,6 +24301,11 @@
"dev": true,
"optional": true
},
"pretty-bytes": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz",
"integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg=="
},
"pretty-error": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-2.1.2.tgz",
@ -24486,6 +24563,14 @@
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
"dev": true
},
"queue": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz",
"integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==",
"requires": {
"inherits": "~2.0.3"
}
},
"randombytes": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",

View file

@ -12,8 +12,11 @@
"axios": "^0.21.0",
"clean-insights-sdk": "^2.4.1",
"core-js": "^3.6.5",
"data-uri-to-buffer": "^3.0.1",
"dayjs": "^1.10.3",
"fix-webm-duration": "^1.0.0",
"image-resize": "^1.1.5",
"image-size": "^1.0.0",
"intersection-observer": "^0.11.0",
"js-sha256": "^0.9.0",
"json-web-key": "^0.4.0",
@ -22,6 +25,7 @@
"matrix-js-sdk": "^9.4.1",
"md-gum-polyfill": "^1.0.0",
"olm": "https://packages.matrix.org/npm/olm/olm-3.2.1.tgz",
"pretty-bytes": "^5.6.0",
"qrcode": "^1.4.4",
"raw-loader": "^4.0.2",
"recordrtc": "^5.6.2",

View file

@ -16,7 +16,10 @@
ref="messageOperations"
:style="opStyle"
:emojis="recentEmojis"
v-on:close="showContextMenu = false;showContextMenuAnchor = null;"
v-on:close="
showContextMenu = false;
showContextMenuAnchor = null;
"
v-if="selectedEvent && showContextMenu"
v-on:addreaction="addReaction"
v-on:addquickreaction="addQuickReaction"
@ -33,7 +36,10 @@
<avatar-operations
ref="avatarOperations"
:style="avatarOpStyle"
v-on:close="showAvatarMenu = false;showAvatarMenuAnchor = null;"
v-on:close="
showAvatarMenu = false;
showAvatarMenuAnchor = null;
"
v-on:start-private-chat="startPrivateChat($event)"
v-if="selectedEvent && showAvatarMenu"
:room="room"
@ -47,7 +53,10 @@
@notify="handleChatContainerResize"
/>
<CreatedRoomWelcomeHeader v-if="showCreatedRoomWelcomeHeader" v-on:close="closeCreateRoomWelcomeHeader" />
<CreatedRoomWelcomeHeader
v-if="showCreatedRoomWelcomeHeader"
v-on:close="closeCreateRoomWelcomeHeader"
/>
<div
v-for="(event, index) in events"
@ -208,7 +217,10 @@
</v-btn>
</v-col>
<v-col v-if="config.useShortCodeStickers" class="input-area-button text-center flex-grow-0 flex-shrink-1">
<v-col
v-if="config.useShortCodeStickers"
class="input-area-button text-center flex-grow-0 flex-shrink-1"
>
<v-btn
v-if="!showRecorder"
icon
@ -254,16 +266,52 @@
</v-container>
<div v-if="currentImageInputPath">
<v-dialog v-model="currentImageInputPath" class="ma-0 pa-0" width="50%">
<v-dialog v-model="currentImageInputPath" class="ma-0 pa-0" :width="$vuetify.breakpoint.smAndUp ? '50%' : '85%'">
<v-card class="ma-0 pa-0">
<v-card-text class="ma-0 pa-0">
<v-card-text class="ma-0 pa-2">
<v-img
v-if="currentImageInput"
v-if="currentImageInput && currentImageInput.image"
:aspect-ratio="1"
:src="currentImageInput"
:src="currentImageInput.image"
contain
style="max-height: 50vh"
style="max-height: 50vh; background-color: #e2e2e2"
/>
<div>
file: {{ currentImageInputPath.name }}
<span
v-if="
currentImageInput &&
currentImageInput.scaled &&
currentImageInput.useScaled
"
>
{{ currentImageInput.scaledDimensions.width }} x
{{ currentImageInput.scaledDimensions.height }}</span
>
<span
v-else-if="currentImageInput && currentImageInput.dimensions"
>
{{ currentImageInput.dimensions.width }} x
{{ currentImageInput.dimensions.height }}</span
>
<span
v-if="
currentImageInput &&
currentImageInput.scaled &&
currentImageInput.useScaled
"
>
({{ formatBytes(currentImageInput.scaledSize) }})</span
>
<span v-else>
({{ formatBytes(currentImageInputPath.size) }})</span
>
<v-switch
v-if="currentImageInput && currentImageInput.scaled"
label="Scale image"
v-model="currentImageInput.useScaled"
/>
</div>
<div v-if="currentSendError">{{ currentSendError }}</div>
<div v-else>{{ currentSendProgress }}</div>
</v-card-text>
@ -286,7 +334,10 @@
</v-dialog>
</div>
<MessageOperationsBottomSheet ref="messageOperationsSheet" xv-show="showEmojiPicker">
<MessageOperationsBottomSheet
ref="messageOperationsSheet"
xv-show="showEmojiPicker"
>
<MessageOperationsPicker
v-on:close="showEmojiPicker = false"
v-if="selectedEvent"
@ -298,10 +349,18 @@
v-on:download="download(selectedEvent)"
:event="selectedEvent"
/>
<VEmojiPicker ref="emojiPicker" style="width: 100%" @select="emojiSelected" />
<VEmojiPicker
ref="emojiPicker"
style="width: 100%"
@select="emojiSelected"
/>
</MessageOperationsBottomSheet>
<StickerPickerBottomSheet ref="stickerPickerSheet" style="z-index:10" v-on:selectSticker="sendSticker" />
<StickerPickerBottomSheet
ref="stickerPickerSheet"
style="z-index: 10"
v-on:selectSticker="sendSticker"
/>
<!-- "NOT ALLOWED FOR GUEST ACCOUNTS" dialog -->
<v-dialog v-model="showNotAllowedForGuests" class="ma-0 pa-0" width="50%">
@ -342,6 +401,7 @@
</template>
<script>
import Vue from "vue";
import { TimelineWindow, EventTimeline } from "matrix-js-sdk";
import MessageIncomingText from "./messages/MessageIncomingText";
import MessageIncomingFile from "./messages/MessageIncomingFile";
@ -373,14 +433,18 @@ import ChatHeader from "./ChatHeader";
import VoiceRecorder from "./VoiceRecorder";
import RoomInfoBottomSheet from "./RoomInfoBottomSheet";
import CreatedRoomWelcomeHeader from "./CreatedRoomWelcomeHeader";
import MessageOperationsBottomSheet from './MessageOperationsBottomSheet';
import stickers from '../plugins/stickers';
import StickerPickerBottomSheet from './StickerPickerBottomSheet';
import BottomSheet from './BottomSheet.vue';
import MessageOperationsBottomSheet from "./MessageOperationsBottomSheet";
import stickers from "../plugins/stickers";
import StickerPickerBottomSheet from "./StickerPickerBottomSheet";
import BottomSheet from "./BottomSheet.vue";
import config from "../assets/config";
import ImageResize from "image-resize";
const sizeOf = require("image-size");
const dataUriToBuffer = require("data-uri-to-buffer");
const prettyBytes = require("pretty-bytes");
const READ_RECEIPT_TIMEOUT = 5000; /* How long a message must have been visible before the read marker is updated */
const WINDOW_BUFFER_SIZE = 0.3 /** Relative window height of when we start paginating. Always keep this much loaded before and after our scroll position! */
const WINDOW_BUFFER_SIZE = 0.3; /** Relative window height of when we start paginating. Always keep this much loaded before and after our scroll position! */
// from https://kirbysayshi.com/2013/08/19/maintaining-scroll-position-knockoutjs-list.html
function ScrollPosition(node) {
@ -504,7 +568,7 @@ export default {
showCreatedRoomWelcomeHeader: false,
/** An array of recent emojis. Used in the "message operations" popup. */
recentEmojis: []
recentEmojis: [],
};
},
@ -627,7 +691,7 @@ export default {
},
debugging() {
return (window.location.host || "").startsWith("localhost");
}
},
},
watch: {
@ -679,13 +743,18 @@ export default {
methods: {
onRoomJoined(initialEventId) {
// Was this room just created (by you)? Show a small info header in
// that case!
const createEvent = this.room.currentState.getStateEvents("m.room.create","");
const createEvent = this.room.currentState.getStateEvents(
"m.room.create",
""
);
if (createEvent) {
const creatorId = createEvent.getContent().creator;
if (creatorId == this.$matrix.currentUserId && createEvent.getLocalAge() < (5 * 60000) /* 5 minutes */) {
if (
creatorId == this.$matrix.currentUserId &&
createEvent.getLocalAge() < 5 * 60000 /* 5 minutes */
) {
this.showCreatedRoomWelcomeHeader = true;
}
}
@ -713,7 +782,8 @@ export default {
const getMoreIfNeeded = function _getMoreIfNeeded() {
const container = self.$refs.chatContainer;
if (
container.scrollHeight <= (1 + 2 * WINDOW_BUFFER_SIZE) * container.clientHeight &&
container.scrollHeight <=
(1 + 2 * WINDOW_BUFFER_SIZE) * container.clientHeight &&
self.timelineWindow &&
self.timelineWindow.canPaginate(EventTimeline.BACKWARDS)
) {
@ -770,10 +840,17 @@ export default {
},
scrollToEndOfTimeline() {
if (this.timelineWindow && this.timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
if (
this.timelineWindow &&
this.timelineWindow.canPaginate(EventTimeline.FORWARDS)
) {
this.loading = true;
// Instead of paging though ALL history, just reload a timeline at the live marker...
var timelineWindow = new TimelineWindow(this.$matrix.matrixClient, this.room.getUnfilteredTimelineSet(), {});
var timelineWindow = new TimelineWindow(
this.$matrix.matrixClient,
this.room.getUnfilteredTimelineSet(),
{}
);
const self = this;
timelineWindow
.load(null, 20)
@ -876,7 +953,11 @@ export default {
if (event.getSender() != this.$matrix.currentUserId) {
if (event.getContent().msgtype == "m.image") {
// For SVG, make downloadable
if (event.getContent().info && event.getContent().info.mimetype && event.getContent().info.mimetype.startsWith("image/svg")) {
if (
event.getContent().info &&
event.getContent().info.mimetype &&
event.getContent().info.mimetype.startsWith("image/svg")
) {
return MessageIncomingFile;
}
return MessageIncomingImage;
@ -893,7 +974,11 @@ export default {
} else {
if (event.getContent().msgtype == "m.image") {
// For SVG, make downloadable
if (event.getContent().info && event.getContent().info.mimetype && event.getContent().info.mimetype.startsWith("image/svg")) {
if (
event.getContent().info &&
event.getContent().info.mimetype &&
event.getContent().info.mimetype.startsWith("image/svg")
) {
return MessageOutgoingImage;
}
return MessageOutgoingImage;
@ -945,13 +1030,17 @@ export default {
// Scrolled to top
this.handleScrolledToTop();
} else if (
container.scrollHeight - container.scrollTop.toFixed(0) - container.clientHeight <= bufferHeight
container.scrollHeight -
container.scrollTop.toFixed(0) -
container.clientHeight <=
bufferHeight
) {
this.handleScrolledToBottom(false);
}
this.showScrollToEnd =
container.scrollHeight - container.scrollTop.toFixed(0) >
container.clientHeight || (this.timelineWindow &&
container.clientHeight ||
(this.timelineWindow &&
this.timelineWindow.canPaginate(EventTimeline.FORWARDS));
this.restartRRTimer();
@ -1046,9 +1135,61 @@ export default {
if (event.target.files && event.target.files[0]) {
var reader = new FileReader();
reader.onload = (e) => {
const file = event.target.files[0];
this.currentSendShowSendButton = true;
this.currentImageInput = e.target.result;
this.currentImageInputPath = event.target.files[0];
if (file.type.startsWith("image/")) {
this.currentImageInput = {
image: e.target.result,
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.currentImageInputPath = file;
};
reader.readAsDataURL(event.target.files[0]);
}
@ -1068,12 +1209,22 @@ export default {
},
sendAttachment(withText) {
this.$refs.attachment.value = null;
if (this.currentImageInputPath) {
var inputFile = this.currentImageInputPath;
if (
this.currentImageInput &&
this.currentImageInput.scaled &&
this.currentImageInput.useScaled
) {
// Send scaled version of image instead!
inputFile = this.currentImageInput.scaled;
}
this.currentSendProgress = null;
this.currentSendOperation = util.sendImage(
this.$matrix.matrixClient,
this.roomId,
this.currentImageInputPath,
inputFile,
this.onUploadProgress
);
this.currentSendOperation
@ -1095,6 +1246,7 @@ export default {
},
cancelSendAttachment() {
this.$refs.attachment.value = null;
if (this.currentSendOperation) {
this.currentSendOperation.reject("Canceled");
}
@ -1306,21 +1458,26 @@ export default {
},
startPrivateChat(e) {
this.$matrix.getOrCreatePrivateChat(e.event.getSender())
.then(room => {
this.$matrix
.getOrCreatePrivateChat(e.event.getSender())
.then((room) => {
this.$nextTick(() => {
this.$navigation.push(
{
name: "Chat",
params: { roomId: util.sanitizeRoomId(room.getCanonicalAlias() || room.roomId) },
params: {
roomId: util.sanitizeRoomId(
room.getCanonicalAlias() || room.roomId
),
},
},
-1
);
});
})
.catch(err => {
.catch((err) => {
console.error(err);
})
});
},
closeContextMenusIfOpen(e) {
@ -1466,8 +1623,11 @@ export default {
return;
}
this.recentEmojis = [];
}
},
formatBytes(bytes) {
return prettyBytes(bytes);
},
},
};
</script>

View file

@ -562,7 +562,6 @@ export default {
//
const createRoomOptions = {
visibility: "private", // Not listed!
//name: this.roomName,
preset: "private_chat",
initial_state: [
{