Work on export and moving to Vue composition API

This commit is contained in:
N-Pex 2025-06-27 16:10:25 +02:00
parent b0fae3396d
commit 9a124c5ab9
22 changed files with 660 additions and 906 deletions

View file

@ -26,9 +26,16 @@
<div v-if="!event.isRelation() && !event.isRedacted() && !event.isRedaction()" :ref="event.getId()">
<div class="message-wrapper">
<component :is="componentForEvent(event, true)" :room="room" :originalEvent="event"
:nextEvent="events[index + 1]" :timelineSet="timelineSet" :componentFn="componentForEventForExport"
ref="exportedEvent" v-on:layout-change="onLayoutChange" />
<component
:is="componentForEvent(event, true)"
:room="room"
:originalEvent="event"
:nextEvent="events[index + 1]"
:timelineSet="timelineSet"
:componentFn="componentForEvent"
ref="exportedEvent"
v-on:layout-change="onLayoutChange"
/>
<!-- <div v-if="debugging" style="user-select:text">EventID: {{ event.getId() }}</div> -->
<!-- <div v-if="debugging" style="user-select:text">Event: {{ JSON.stringify(event) }}</div> -->
</div>
@ -54,16 +61,12 @@
<script>
import MessageIncomingText from "./messages/MessageIncomingText.vue";
import MessageIncomingFile from "./messages/MessageIncomingFile.vue";
import MessageIncomingImage from "./messages/MessageIncomingImage.vue";
import MessageFile from "./messages/composition/MessageFile.vue";
import MessageImage from "./messages/composition/MessageImage.vue";
import MessageIncomingAudio from "./messages/MessageIncomingAudio.vue";
import MessageIncomingVideo from "./messages/MessageIncomingVideo.vue";
import MessageIncomingSticker from "./messages/MessageIncomingSticker.vue";
import MessageOutgoingText from "./messages/MessageOutgoingText.vue";
import MessageOutgoingFile from "./messages/MessageOutgoingFile.vue";
import MessageOutgoingImage from "./messages/MessageOutgoingImage.vue";
import MessageOutgoingAudio from "./messages/MessageOutgoingAudio.vue";
import MessageOutgoingVideo from "./messages/MessageOutgoingVideo.vue";
import MessageOutgoingSticker from "./messages/MessageOutgoingSticker.vue";
import MessageOutgoingPoll from "./messages/MessageOutgoingPoll.vue";
import ContactJoin from "./messages/ContactJoin.vue";
@ -94,7 +97,7 @@ import CreatePollDialog from "./CreatePollDialog.vue";
import chatMixin from "./chatMixin";
import util from "../plugins/utils";
import { EventTimelineSet } from "matrix-js-sdk";
import axios from 'axios';
import axios from "axios";
import "../services/jszip.min";
import "../services/filesaver.cjs";
@ -105,16 +108,12 @@ export default {
components: {
ChatHeader,
MessageIncomingText,
MessageIncomingFile,
MessageIncomingImage,
MessageFile,
MessageImage,
MessageIncomingAudio,
MessageIncomingVideo,
MessageIncomingSticker,
MessageOutgoingText,
MessageOutgoingFile,
MessageOutgoingImage,
MessageOutgoingAudio,
MessageOutgoingVideo,
MessageOutgoingSticker,
MessageOutgoingPoll,
ContactJoin,
@ -180,9 +179,6 @@ export default {
},
},
methods: {
componentForEventForExport(event) {
return this.componentForEvent(event, true);
},
cancelExport() {
this.cancelled = true;
},
@ -258,19 +254,25 @@ export default {
this.events = events;
// Need to set thread root events and replyEvents so stuff is rendered correctly.
this.events.filter(event => (event.threadRootId && !event.parentThread)).forEach(event => {
const parentEvent = this.timelineSet.findEventById(event.threadRootId) || this.room.findEventById(event.threadRootId);
if (parentEvent) {
parentEvent["isMxThread"] = true;
event["parentThread"] = parentEvent;
}
});
this.events.filter(event => (event.replyEventId && !event.replyEvent)).forEach(event => {
const parentEvent = this.timelineSet.findEventById(event.replyEventId) || this.room.findEventById(event.replyEventId);
if (parentEvent) {
event["replyEvent"] = parentEvent;
}
});
this.events
.filter((event) => event.threadRootId && !event.parentThread)
.forEach((event) => {
const parentEvent =
this.timelineSet.findEventById(event.threadRootId) || this.room.findEventById(event.threadRootId);
if (parentEvent) {
parentEvent["isMxThread"] = true;
event["parentThread"] = parentEvent;
}
});
this.events
.filter((event) => event.replyEventId && !event.replyEvent)
.forEach((event) => {
const parentEvent =
this.timelineSet.findEventById(event.replyEventId) || this.room.findEventById(event.replyEventId);
if (parentEvent) {
event["replyEvent"] = parentEvent;
}
});
// Wait a tick so UI is updated.
return new Promise((resolve, ignoredReject) => {
@ -304,8 +306,7 @@ export default {
childComponents.push(parentComp.$refs.exportedEvent);
}
}
for (const comp of childComponents) {
for (const comp of childComponents.filter((c) => c.event != undefined)) {
// Avatars need downloading?
if (comp.$el && comp.$el.nodeType == 1) {
const avatars = comp.$el.getElementsByClassName("v-avatar");
@ -322,29 +323,44 @@ export default {
const img = images[imageIndex];
img.onerror = undefined;
img.removeAttribute("src");
img.setAttribute("data-exported-src", './avatars/' + fileName);
img.setAttribute("data-exported-src", "./avatars/" + fileName);
}
}
}
};
if (!avatarFolder.file(fileName)) {
const url = member.getAvatarUrl(this.$matrix.matrixClient.getHomeserverUrl(), 40, 40, "scale", true, false, this.$matrix.useAuthedMedia);
const url = member.getAvatarUrl(
this.$matrix.matrixClient.getHomeserverUrl(),
40,
40,
"scale",
true,
false,
this.$matrix.useAuthedMedia
);
if (url) {
avatarFolder.file(fileName, "empty");
downloadPromises.push(
axios.get(url, {
responseType: 'blob'
})
.then(result => {
axios
.get(url, {
responseType: "blob",
headers: this.$matrix.useAuthedMedia
? {
Authorization: `Bearer ${this.$matrix.matrixClient.getAccessToken()}`,
}
: undefined,
})
.then((result) => {
if (result.data) {
avatarFolder.file(fileName, result.data);
setSource(fileName);
}
})
.catch(err => {
.catch((err) => {
console.error("Download error: ", err);
avatarFolder.remove(fileName);
}));
})
);
}
} else {
setSource(fileName);
@ -353,139 +369,107 @@ export default {
}
}
let componentClass = comp.$options ? comp.$options.__file.split("/").reverse()[0].split(".")[0] : "invalid_component";
switch (componentClass) {
case "MessageIncomingImageExport":
case "MessageOutgoingImageExport":
// TODO - maybe consider what media to download based on the file size we already have?
// info = comp.event.getContent().info;
// if (info && info.size && currentMediaSize + info.size > maxMediaSize) {
// // No need to even download.
// console.log("Dont download!");
// continue;
// }
let componentClass = comp.$options
? comp.$options.__file.split("/").reverse()[0].split(".")[0]
: "invalid_component";
let attachment =
comp.event && comp.event.getId
? this.$matrix.attachmentManager.getEventAttachment(comp.event)
: undefined;
downloadPromises.push(
util
.getAttachment(this.$matrix.matrixClient, this.$matrix.useAuthedMedia, comp.event, null, true)
.then((blob) => {
return new Promise((resolve, ignoredReject) => {
let mime = blob.type;
var extension = ".png";
switch (mime) {
case "image/jpeg":
case "image/jpg":
extension = ".jpg";
break;
case "image/gif":
extension = ".gif";
}
if (currentMediaSize + blob.size <= maxMediaSize) {
currentMediaSize += blob.size;
if (attachment && (attachment.srcSize = 0 || currentMediaSize + attachment.srcSize <= maxMediaSize)) {
downloadPromises.push(
attachment
.loadSrc({ asBlob: true })
.then((res) => {
const blob = res.data;
if (currentMediaSize + blob.size <= maxMediaSize) {
currentMediaSize += blob.size;
let fileName = comp.event.getId() + extension;
imageFolder.file(fileName, blob); // TODO calc bytes
switch (componentClass) {
case "MessageIncomingImageExport":
case "MessageOutgoingImageExport":
{
let mime = blob.type;
var extension = ".png";
switch (mime) {
case "image/jpeg":
case "image/jpg":
extension = ".jpg";
break;
case "image/gif":
extension = ".gif";
}
// Update source
const images = comp.$el.getElementsByTagName("img");
for (let imageIndex = 0; imageIndex < images.length; imageIndex++) {
const img = images[imageIndex];
img.removeAttribute("src");
img.setAttribute("data-exported-src", './images/' + fileName);
let fileName = comp.event.getId() + extension;
imageFolder.file(fileName, blob); // TODO calc bytes
// Update source
const images = comp.$el.getElementsByTagName("img");
for (let imageIndex = 0; imageIndex < images.length; imageIndex++) {
const img = images[imageIndex];
img.removeAttribute("src");
img.setAttribute("data-exported-src", "./images/" + fileName);
}
this.processedEvents += 1;
}
this.processedEvents += 1;
resolve(true);
}
});
})
.catch((ignoredErr) => {
this.processedEvents += 1;
})
);
break;
case "MessageIncomingAudioExport":
case "MessageOutgoingAudioExport":
downloadPromises.push(
util
.getAttachment(this.$matrix.matrixClient, this.$matrix.useAuthedMedia, comp.event, null, true)
.then((blob) => {
if (currentMediaSize + blob.size <= maxMediaSize) {
currentMediaSize += blob.size;
return new Promise((resolve, ignoredReject) => {
//let mime = blob.type;
var extension = ".webm";
let fileName = comp.event.getId() + extension;
audioFolder.file(fileName, blob); // TODO calc bytes
let elements = comp.$el.getElementsByTagName("audio");
let element = elements && elements[0];
if (element) {
element.setAttribute("data-exported-src", "./audio/" + fileName);
break;
case "MessageIncomingAudioExport":
case "MessageOutgoingAudioExport":
{
var extension = ".webm";
let fileName = comp.event.getId() + extension;
audioFolder.file(fileName, blob); // TODO calc bytes
let elements = comp.$el.getElementsByTagName("audio");
let element = elements && elements[0];
if (element) {
element.setAttribute("data-exported-src", "./audio/" + fileName);
}
this.processedEvents += 1;
}
this.processedEvents += 1;
resolve(true);
});
}
})
.catch((ignoredErr) => {
this.processedEvents += 1;
})
);
break;
case "MessageIncomingVideoExport":
case "MessageOutgoingVideoExport":
downloadPromises.push(
util
.getAttachment(this.$matrix.matrixClient, this.$matrix.useAuthedMedia, comp.event, null, true)
.then((blob) => {
if (currentMediaSize + blob.size <= maxMediaSize) {
currentMediaSize += blob.size;
return new Promise((resolve, ignoredReject) => {
//let mime = blob.type;
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.setAttribute("data-exported-src", "./video/" + fileName);
break;
case "MessageIncomingVideoExport":
case "MessageOutgoingVideoExport":
{
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.setAttribute("data-exported-src", "./video/" + fileName);
}
this.processedEvents += 1;
}
this.processedEvents += 1;
resolve(true);
});
break;
case "MessageIncomingFileExport":
case "MessageOutgoingFileExport":
{
var extension = util.getFileExtension(comp.event);
let fileName = comp.event.getId() + extension;
filesFolder.file(fileName, blob);
comp.href = "./files/" + fileName;
this.processedEvents += 1;
}
break;
}
})
.catch((ignoredErr) => {
this.processedEvents += 1;
})
);
break;
case "MessageIncomingFileExport":
case "MessageOutgoingFileExport":
downloadPromises.push(
util
.getAttachment(this.$matrix.matrixClient, this.$matrix.useAuthedMedia, 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);
});
}
})
.catch((ignoredErr) => {
return true;
} else {
this.processedEvents += 1;
})
);
break;
default:
this.processedEvents += 1;
break;
return false;
}
})
.catch((ignoredErr) => {
this.processedEvents += 1;
})
);
} else {
this.processedEvents += 1;
}
}
}
@ -496,7 +480,7 @@ export default {
let root = this.$refs.exportRoot;
var doc = "<!DOCTYPE html>\n<html><head>\n<meta charset=\"utf-8\"/>\n";
var doc = '<!DOCTYPE html>\n<html><head>\n<meta charset="utf-8"/>\n';
for (const sheet of document.styleSheets) {
doc += "<style type='text/css'>\n";
@ -542,7 +526,8 @@ export default {
this.$emit("close");
});
},
onLayoutChange(action, ignoredelement) {
onLayoutChange(event) {
const { action, element } = event;
action();
},
},
@ -551,7 +536,8 @@ export default {
<style lang="scss">
.chat-root.export {
.messageIn-thread, .messageOut-thread {
.messageIn-thread,
.messageOut-thread {
/** For media threads, hide all duplicated metadata, like
sender, sender avatar, time, quick reactions etc. They are
shown for the root thread event */
@ -561,11 +547,14 @@ export default {
.messageOut {
margin-right: 50px !important;
}
.messageIn, .messageOut {
.quick-reaction-container, .senderAndTime, .avatar {
.messageIn,
.messageOut {
.quick-reaction-container,
.senderAndTime,
.avatar {
display: none;
}
}
}
}
</style>
</style>