file types and exports

This commit is contained in:
N-Pex 2023-12-04 11:29:23 +01:00
parent db04080463
commit 324ccd70b3
21 changed files with 339 additions and 55 deletions

14
package-lock.json generated
View file

@ -28,7 +28,7 @@
"jszip": "^3.9.1",
"linkify-html": "^4.1.0",
"linkifyjs": "^4.1.0",
"material-design-icons-iconfont": "^6.1",
"material-design-icons-iconfont": "^6.7.0",
"matrix-js-sdk": "^23.4.0",
"md-gum-polyfill": "^1.0.0",
"mic-recorder-to-mp3": "^2.2.2",
@ -9936,9 +9936,9 @@
}
},
"node_modules/material-design-icons-iconfont": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/material-design-icons-iconfont/-/material-design-icons-iconfont-6.1.0.tgz",
"integrity": "sha512-wRJtOo1v1ch+gN8PRsj0IGJznk+kQ8mz13ds/nuhLI+Qyf/931ZlRpd92oq0IRPpZIb+bhX8pRjzIVdcPDKmiQ=="
"version": "6.7.0",
"resolved": "https://registry.npmjs.org/material-design-icons-iconfont/-/material-design-icons-iconfont-6.7.0.tgz",
"integrity": "sha512-lSj71DgVv20kO0kGbs42icDzbRot61gEDBLQACzkUuznRQBUYmbxzEkGU6dNBb5fRWHMaScYlAXX96HQ4/cJWA=="
},
"node_modules/matrix-events-sdk": {
"version": "0.0.1",
@ -24023,9 +24023,9 @@
}
},
"material-design-icons-iconfont": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/material-design-icons-iconfont/-/material-design-icons-iconfont-6.1.0.tgz",
"integrity": "sha512-wRJtOo1v1ch+gN8PRsj0IGJznk+kQ8mz13ds/nuhLI+Qyf/931ZlRpd92oq0IRPpZIb+bhX8pRjzIVdcPDKmiQ=="
"version": "6.7.0",
"resolved": "https://registry.npmjs.org/material-design-icons-iconfont/-/material-design-icons-iconfont-6.7.0.tgz",
"integrity": "sha512-lSj71DgVv20kO0kGbs42icDzbRot61gEDBLQACzkUuznRQBUYmbxzEkGU6dNBb5fRWHMaScYlAXX96HQ4/cJWA=="
},
"matrix-events-sdk": {
"version": "0.0.1",

View file

@ -29,7 +29,7 @@
"jszip": "^3.9.1",
"linkify-html": "^4.1.0",
"linkifyjs": "^4.1.0",
"material-design-icons-iconfont": "^6.1",
"material-design-icons-iconfont": "^6.7.0",
"matrix-js-sdk": "^23.4.0",
"md-gum-polyfill": "^1.0.0",
"mic-recorder-to-mp3": "^2.2.2",

View file

@ -783,12 +783,23 @@ body {
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.4);
align-items: center;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.download-text {
width: 100%;
color: white;
}
.download-size {
font-size: 70%;
color: white;
}
.download-icon {
width: 32px;
height: 32px;
color: white;
}
}
.room-name,

View file

@ -213,7 +213,7 @@
</v-container>
<input ref="attachment" type="file" name="attachment" @change="handlePickedAttachment($event)"
accept="image/*, audio/*, video/*, .pdf, .apk, .ipa, .zip" class="d-none" multiple/>
accept="image/*,audio/*,video/*,.pdf,application/pdf,.apk,application/vnd.android.package-archive,.ipa,.zip,application/zip,application/x-zip-compressed,multipart/x-zip" class="d-none" multiple/>
<div v-if="currentFileInputsDialog && !useFileModeNonAdmin">
<v-dialog v-model="currentFileInputsDialog" class="ma-0 pa-0" :width="$vuetify.breakpoint.smAndUp ? '50%' : '85%'" persistent scrollable>
@ -1545,6 +1545,7 @@ export default {
},
download(event) {
console.error("DOWNLOAD!!!");
if ((event.isThreadRoot || event.isMxThread) && this.timelineSet) {
const children = this.timelineSet.relations.getAllChildEventsForEvent(event.getId()).filter(e => util.downloadableTypes().includes(e.getContent().msgtype));
children.forEach(child => util.download(this.$matrix.matrixClient, child));

View file

@ -287,6 +287,7 @@ export default {
var imageFolder = zip.folder("images");
var audioFolder = zip.folder("audio");
var videoFolder = zip.folder("video");
var filesFolder = zip.folder("files");
var downloadPromises = [];
let components = this.$refs.exportedEvent;
@ -321,7 +322,8 @@ export default {
for (let imageIndex = 0; imageIndex < images.length; imageIndex++) {
const img = images[imageIndex];
img.onerror = undefined;
img.src = './avatars/' + fileName;
img.removeAttribute("src");
img.setAttribute("data-exported-src", './avatars/' + fileName);
}
}
}
@ -421,13 +423,15 @@ export default {
var extension = ".mp3";
let fileName = comp.event.getId() + extension;
audioFolder.file(fileName, blob); // TODO calc bytes
//this.$nextTick(() => {
let elements = comp.$el.getElementsByTagName("audio");
let element = elements && elements[0];
if (element) {
element.src = "./audio/" + fileName;
element.setAttribute("data-exported-src", "./audio/" + fileName);
}
this.processedEvents += 1;
resolve(true);
//});
});
}
})
@ -449,13 +453,36 @@ export default {
var extension = ".mp4";
let fileName = comp.event.getId() + extension;
videoFolder.file(fileName, blob); // TODO calc bytes
// comp.src = "./video/" + fileName;
let elements = comp.$el.getElementsByTagName("video");
let element = elements && elements[0];
if (element) {
element.src = "./video/" + fileName;
element.setAttribute("data-exported-src", "./video/" + fileName);
}
this.processedEvents += 1;
resolve(true);
});
}
})
.catch((ignoredErr) => {
this.processedEvents += 1;
})
);
break;
case "MessageIncomingFileExport":
case "MessageOutgoingFileExport":
downloadPromises.push(
util
.getAttachment(this.$matrix.matrixClient, comp.event, null, true)
.then((blob) => {
if (currentMediaSize + blob.size <= maxMediaSize) {
currentMediaSize += blob.size;
return new Promise((resolve, ignoredReject) => {
var extension = util.getFileExtension(comp.event);
let fileName = comp.event.getId() + extension;
filesFolder.file(fileName, blob);
comp.href="./files/" + fileName;
this.processedEvents += 1;
resolve(true);
});
}
@ -504,7 +531,8 @@ export default {
getCssRules(root);
this.$nextTick(() => {
doc += this.$refs.exportRoot.outerHTML;
const contentHtml = this.$refs.exportRoot.outerHTML;
doc += contentHtml.replaceAll("data-exported-src=", "src=");
doc += "</div></body></html>";
zip.file("chat.html", doc);

View file

@ -19,10 +19,12 @@ import MessageIncomingImageExport from "./messages/export/MessageIncomingImageEx
import MessageIncomingAudioExport from "./messages/export/MessageIncomingAudioExport";
import MessageIncomingVideoExport from "./messages/export/MessageIncomingVideoExport";
import MessageIncomingThreadExport from "./messages/export/MessageIncomingThreadExport";
import MessageIncomingFileExport from "./messages/export/MessageIncomingFileExport";
import MessageOutgoingImageExport from "./messages/export/MessageOutgoingImageExport";
import MessageOutgoingAudioExport from "./messages/export/MessageOutgoingAudioExport";
import MessageOutgoingVideoExport from "./messages/export/MessageOutgoingVideoExport";
import MessageOutgoingThreadExport from "./messages/export/MessageOutgoingThreadExport";
import MessageOutgoingFileExport from "./messages/export/MessageOutgoingFileExport";
import ContactJoin from "./messages/ContactJoin.vue";
import ContactLeave from "./messages/ContactLeave.vue";
import ContactInvited from "./messages/ContactInvited.vue";
@ -172,6 +174,9 @@ export default {
event.getContent().info.mimetype &&
event.getContent().info.mimetype.startsWith("image/svg")
) {
if (isForExport) {
return MessageIncomingFileExport;
}
return MessageIncomingFile;
}
if (isForExport) {
@ -189,6 +194,9 @@ export default {
}
return MessageIncomingVideo;
} else if (event.getContent().msgtype == "m.file") {
if (isForExport) {
return MessageIncomingFileExport;
}
return MessageIncomingFile;
} else if (stickers.isStickerShortcode(event.getContent().body)) {
return MessageIncomingSticker;
@ -223,6 +231,9 @@ export default {
}
return MessageOutgoingVideo;
} else if (event.getContent().msgtype == "m.file") {
if (isForExport) {
return MessageOutgoingFileExport;
}
return MessageOutgoingFile;
} else if (stickers.isStickerShortcode(event.getContent().body)) {
return MessageOutgoingSticker;

View file

@ -1,19 +1,22 @@
<template>
<v-responsive v-if="item.event.getContent().msgtype == 'm.video'" :class="{'thumbnail-item': true, 'preview': previewOnly}"
<v-responsive v-if="item.event.getContent().msgtype == 'm.video' && item.src" :class="{'thumbnail-item': true, 'preview': previewOnly}"
@click.stop="$emit('itemclick', {item: item})">
<video :src="item.src" :controls="!previewOnly" class="w-100 h-100">
{{ $t('fallbacks.video_file') }}
</video>
</v-responsive>
<v-img v-else-if="item.event.getContent().msgtype == 'm.image'" :aspect-ratio="previewOnly ? (16 / 9) : undefined" :class="{'thumbnail-item': true, 'preview': previewOnly}" :src="item.src" :contain="!previewOnly" :cover="previewOnly"
<v-img v-else-if="item.event.getContent().msgtype == 'm.image' && item.src" :aspect-ratio="previewOnly ? (16 / 9) : undefined" :class="{'thumbnail-item': true, 'preview': previewOnly}" :src="item.src" :contain="!previewOnly" :cover="previewOnly"
@click.stop="$emit('itemclick', {item: item})" />
<div v-else :class="{'thumbnail-item': true, 'preview': previewOnly, 'file-item': true}" @click.stop="$emit('itemclick', {item: item})">
<v-icon>description</v-icon>
{{ $sanitize(item.event.getContent().body) }}
<v-icon>{{ fileTypeIcon }}</v-icon>
<b>{{ $sanitize(fileName) }}</b>
<div>{{ fileSize }}</div>
</div>
</template>
<script>
import util from "../../plugins/utils";
export default {
props: {
/**
@ -32,6 +35,26 @@ export default {
}
},
},
computed: {
fileTypeIcon() {
if (util.isFileTypeAPK(this.item.event)) {
return "phone_android";
} else if (util.isFileTypeIPA(this.item.event)) {
return "phone_iphone";
} else if (util.isFileTypePDF(this.item.event)) {
return "picture_as_pdf";
} else if (util.isFileTypeZip(this.item.event)) {
return "folder_zip";
}
return "description"
},
fileName() {
return util.getFileName(this.item.event);
},
fileSize() {
return util.getFileSizeFormatted(this.item.event);
}
}
}
</script>

View file

@ -10,12 +10,7 @@
</div>
<div class="message">
<span>{{ $t('message.file_prefix') }}</span>
<span
class="cursor-pointer"
@click.stop="$emit('download')"
v-html="linkify($sanitize(messageText))"
/>
<ThumbnailView class="clickable" v-on:itemclick="$emit('download')" :item="{ event: event, src: null }" />
<span class="edit-marker" v-if="event.replacingEventId()"
>{{ $t('message.edited') }}</span
>
@ -25,11 +20,12 @@
</template>
<script>
import ThumbnailView from '../file_mode/ThumbnailView.vue';
import MessageIncoming from "./MessageIncoming.vue";
export default {
extends: MessageIncoming,
components: { MessageIncoming }
components: { MessageIncoming, ThumbnailView }
};
</script>

View file

@ -10,6 +10,15 @@
{{ $t('message.download_progress',{percentage: downloadProgress}) }}
</div>
</div>
<div v-else-if="userInitiatedDownloadsOnly && !src" class="download-overlay">
<div class="text-center download-text">
{{ fileName }}
</div>
<div class="text-center download-size">
{{ fileSize }}
</div>
<v-icon size="32" color="white" class="clickable" @click="loadAttachmentSource(event, true)">download</v-icon>
</div>
</v-responsive>
</div>
</message-incoming>

View file

@ -11,12 +11,7 @@
<div class="message">
<span>{{ $t('message.file_prefix') }}</span>
<span
class="cursor-pointer"
@click.stop="$emit('download')"
v-html="linkify($sanitize(messageText))"
/>
<ThumbnailView class="clickable" v-on:itemclick="$emit('download')" :item="{ event: event, src: null }" />
<span class="edit-marker" v-if="event.replacingEventId()"
>{{ $t('message.edited') }}</span
>
@ -26,11 +21,12 @@
</template>
<script>
import ThumbnailView from '../file_mode/ThumbnailView.vue';
import MessageOutgoing from "./MessageOutgoing.vue";
export default {
extends: MessageOutgoing,
components: { MessageOutgoing },
components: { MessageOutgoing, ThumbnailView },
};
</script>
<style lang="scss">

View file

@ -5,6 +5,20 @@
<video :src="src" controls class="w-100 h-100">
{{$t('fallbacks.video_file')}}
</video>
<div v-if="downloadProgress" class="download-overlay">
<div class="text-center download-text">
{{ $t('message.download_progress',{percentage: downloadProgress}) }}
</div>
</div>
<div v-else-if="userInitiatedDownloadsOnly && !src" class="download-overlay">
<div class="text-center download-text">
{{ fileName }}
</div>
<div class="text-center download-size">
{{ fileSize }}
</div>
<v-icon size="32" color="white" class="clickable" @click="loadAttachmentSource(event, true)">download</v-icon>
</div>
</v-responsive>
</div>
</message-outgoing>

View file

@ -4,7 +4,8 @@ export default {
data() {
return {
src: null,
downloadProgress: null
downloadProgress: null,
userInitiatedDownloadsOnly: false,
}
},
watch: {
@ -21,14 +22,27 @@ export default {
beforeDestroy() {
this.loadAttachmentSource(null); // Release
},
computed: {
fileName() {
return util.getFileName(this.event);
},
fileSize() {
return util.getFileSizeFormatted(this.event);
}
},
methods: {
loadAttachmentSource(event) {
loadAttachmentSource(event, userInitiated = false) {
if (this.src) {
const objectUrl = this.src;
this.src = null;
URL.revokeObjectURL(objectUrl);
}
if (event) {
const fileSize = util.getFileSize(event);
if (!userInitiated && (fileSize == 0 || fileSize > 1000000)) {
this.userInitiatedDownloadsOnly = true;
return;
}
util
.getAttachment(this.$matrix.matrixClient, event, (progress) => {
this.downloadProgress = progress;

View file

@ -1,17 +1,17 @@
<template>
<message-incoming v-bind="{ ...$props, ...$attrs }" v-on="$listeners">
<audio controls :src="src">{{ $t("fallbacks.audio_file") }}</audio>
<audio controls>{{ $t("fallbacks.audio_file") }}</audio>
</message-incoming>
</template>
<script>
import attachmentMixin from "../attachmentMixin";
import exportedAttachmentMixin from "./exportedAttachmentMixin";
import MessageIncoming from "../MessageIncoming.vue";
export default {
name: "MessageIncomingAudioExport",
extends: MessageIncoming,
mixins: [attachmentMixin],
mixins: [exportedAttachmentMixin],
components: { MessageIncoming },
};
</script>

View file

@ -0,0 +1,51 @@
<template>
<message-incoming v-bind="{ ...$props, ...$attrs }" v-on="$listeners">
<a style="text-decoration: none;color: currentColor" class="bubble" target="_blank" :href="href" >
<div :class="{ 'thumbnail-item': true, 'preview': true, 'file-item': true }">
<b>{{ $sanitize(fileName) }}</b>
<div>{{ fileSize }}</div>
</div>
</a>
</message-incoming>
</template>
<script>
import exportedAttachmentMixin from "./exportedAttachmentMixin";
import MessageIncoming from "../MessageIncoming.vue";
export default {
name: "MessageIncomingFileExport",
extends: MessageIncoming,
mixins: [exportedAttachmentMixin],
components: { MessageIncoming },
data() {
return {
href: ""
}
}
};
</script>
<style lang="scss">
@import "@/assets/css/chat.scss";
.thumbnail-item {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
}
.file-item {
display: flex;
align-items: center;
justify-content: center;
font-size: 0.6rem;
flex-direction: column;
padding: 20px;
.v-icon {
margin-bottom: 10px;
color: currentColor;
}
}
</style>

View file

@ -2,7 +2,7 @@
<message-incoming v-bind="{ ...$props, ...$attrs }" v-on="$listeners">
<div class="bubble image-bubble">
<v-responsive :aspect-ratio="16 / 9" :src="src">
<video :src="src" controls class="w-100 h-100">
<video controls class="w-100 h-100">
{{ $t("fallbacks.video_file") }}
</video>
</v-responsive>
@ -11,14 +11,14 @@
</template>
<script>
import attachmentMixin from "../attachmentMixin";
import exportedAttachmentMixin from "./exportedAttachmentMixin";
import MessageIncoming from "../MessageIncoming.vue";
export default {
name: "MessageIncomingVideoExport",
extends: MessageIncoming,
components: { MessageIncoming },
mixins: [attachmentMixin],
mixins: [exportedAttachmentMixin],
};
</script>

View file

@ -1,18 +1,18 @@
<template>
<message-outgoing v-bind="{ ...$props, ...$attrs }" v-on="$listeners">
<audio controls :src="src">{{ $t("fallbacks.audio_file") }}</audio>
<audio controls>{{ $t("fallbacks.audio_file") }}</audio>
</message-outgoing>
</template>
<script>
import attachmentMixin from "../attachmentMixin";
import exportedAttachmentMixin from "./exportedAttachmentMixin";
import MessageOutgoing from "../MessageOutgoing.vue";
export default {
name: "MessageOutgoingAudioExport",
extends: MessageOutgoing,
components: { MessageOutgoing },
mixins: [attachmentMixin],
mixins: [exportedAttachmentMixin],
};
</script>

View file

@ -0,0 +1,51 @@
<template>
<message-outgoing v-bind="{ ...$props, ...$attrs }" v-on="$listeners">
<a style="text-decoration: none;color: currentColor" class="bubble" target="_blank" :href="href" >
<div :class="{ 'thumbnail-item': true, 'preview': true, 'file-item': true }">
<b>{{ $sanitize(fileName) }}</b>
<div>{{ fileSize }}</div>
</div>
</a>
</message-outgoing>
</template>
<script>
import exportedAttachmentMixin from "./exportedAttachmentMixin";
import MessageOutgoing from "../MessageOutgoing.vue";
export default {
name: "MessageOutgoingFileExport",
extends: MessageOutgoing,
mixins: [exportedAttachmentMixin],
components: { MessageOutgoing },
data() {
return {
href: ""
}
}
};
</script>
<style lang="scss">
@import "@/assets/css/chat.scss";
.thumbnail-item {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
}
.file-item {
display: flex;
align-items: center;
justify-content: center;
font-size: 0.6rem;
flex-direction: column;
padding: 20px;
.v-icon {
margin-bottom: 10px;
color: currentColor;
}
}
</style>

View file

@ -2,7 +2,7 @@
<message-outgoing v-bind="{ ...$props, ...$attrs }" v-on="$listeners">
<div class="bubble image-bubble">
<v-responsive :aspect-ratio="16 / 9" class="ma-0 pa-0">
<video :src="src" controls class="w-100 h-100">
<video controls class="w-100 h-100">
{{ $t("fallbacks.video_file") }}
</video>
</v-responsive>
@ -11,14 +11,14 @@
</template>
<script>
import attachmentMixin from "../attachmentMixin";
import exportedAttachmentMixin from "./exportedAttachmentMixin";
import MessageOutgoing from "../MessageOutgoing.vue";
export default {
name: "MessageOutgoingVideoExport",
extends: MessageOutgoing,
components: { MessageOutgoing },
mixins: [attachmentMixin],
mixins: [exportedAttachmentMixin],
};
</script>

View file

@ -0,0 +1,17 @@
import util from "../../../plugins/utils";
export default {
data() {
return {
src: null,
}
},
computed: {
fileName() {
return util.getFileName(this.event);
},
fileSize() {
return util.getFileSizeFormatted(this.event);
}
},
}

View file

@ -74,7 +74,7 @@ export default {
if (item.status !== this.sendStatuses.INITIAL) {
return getItemPromise(++index);
}
const itemPromise = util.sendImage(this.$matrix.matrixClient, this.room.roomId, item.attachment, ({ loaded, total }) => {
const itemPromise = util.sendFile(this.$matrix.matrixClient, this.room.roomId, item.attachment, ({ loaded, total }) => {
if (loaded == total) {
item.progress = 100;
} else if (total > 0) {

View file

@ -4,6 +4,7 @@ import dataUriToBuffer from "data-uri-to-buffer";
import ImageResize from "image-resize";
import { AutoDiscovery } from 'matrix-js-sdk';
import User from '../models/user';
const prettyBytes = require("pretty-bytes");
export const STATE_EVENT_ROOM_DELETION_NOTICE = "im.keanu.room_deletion_notice";
export const STATE_EVENT_ROOM_DELETED = "im.keanu.room_deleted";
@ -159,7 +160,7 @@ class Util {
// true
// );
url = matrixClient.mxcUrlToHttp(file.url);
} else if (content.file && content.file.url) {
} else if (content.file && content.file.url && this.getMimeType(event).startsWith("image/")) {
// No thumb, use real url
file = content.file;
url = matrixClient.mxcUrlToHttp(file.url);
@ -348,7 +349,7 @@ class Util {
});
}
sendImage(matrixClient, roomId, file, onUploadProgress, threadRoot) {
sendFile(matrixClient, roomId, file, onUploadProgress, threadRoot) {
const uploadPromise = new UploadPromise(undefined);
uploadPromise.wrappedPromise = new Promise((resolve, reject) => {
var reader = new FileReader();
@ -371,13 +372,13 @@ class Util {
}
var description = file.name;
var msgtype = 'm.image';
if (file.type.startsWith("audio/")) {
var msgtype = 'm.file';
if (file.type.startsWith("image/")) {
msgtype = 'm.image';
} else if (file.type.startsWith("audio/")) {
msgtype = 'm.audio';
} else if (file.type.startsWith("video/")) {
msgtype = 'm.video';
} else if (file.type.startsWith("application/pdf")) {
msgtype = 'm.file';
}
const opts = {
@ -913,7 +914,6 @@ class Util {
link.download = event.getContent().body || this.$t("fallbacks.download_name");
document.body.appendChild(link);
link.click();
setTimeout(function () {
document.body.removeChild(link);
URL.revokeObjectURL(url);
@ -951,6 +951,68 @@ class Util {
}
return Promise.resolve(config.defaultBaseUrl);
}
getMimeType(event) {
const content = event.getContent();
return (content.info && content.info.mimetype) ? content.info.mimetype : (content.file && content.file.mimetype) ? content.file.mimetype : "";
}
getFileName(event) {
const content = event.getContent();
return (content.body || content.filename || "").toLowerCase();
}
getFileExtension(event) {
const fileName = this.getFileName(event);
const parts = fileName.split(".");
if (parts.length > 1) {
return "." + parts[parts.length - 1].toLowerCase();
}
return "";
}
getFileSize(event) {
const content = event.getContent();
if (content.info) {
return content.info.size;
}
return 0;
}
getFileSizeFormatted(event) {
return prettyBytes(this.getFileSize(event));
}
isFileTypeAPK(event) {
const mime = this.getMimeType(event);
if (mime === "application/vnd.android.package-archive" || this.getFileName(event).endsWith(".apk")) {
return true;
}
return false;
}
isFileTypeIPA(event) {
if (this.getFileName(event).endsWith(".ipa")) {
return true;
}
return false;
}
isFileTypePDF(event) {
const mime = this.getMimeType(event);
if (mime === "application/pdf" || this.getFileName(event).endsWith(".pdf")) {
return true;
}
return false;
}
isFileTypeZip(event) {
const mime = this.getMimeType(event);
if (["application/zip", "application/x-zip-compressed", "multipart/x-zip"].includes(mime) || this.getFileName(event).endsWith(".zip")) {
return true;
}
return false;
}
}
export default new Util();