Lots of fixes to "media threads"

This commit is contained in:
N Pex 2023-11-06 15:28:26 +00:00
parent fe081edc62
commit 8bcceafcff
23 changed files with 867 additions and 333 deletions

View file

@ -1,6 +1,6 @@
<template>
<div class="chat-root">
<div class="chat-root d-flex flex-column" ref="exportRoot">
<div class="chat-root export d-flex flex-column" ref="exportRoot">
<!-- Header-->
<v-container fluid class="chat-header flex-grow-0 flex-shrink-0">
<v-row class="chat-header-row flex-nowrap">
@ -18,18 +18,17 @@
<div class="chat-content flex-grow-1 flex-shrink-1" ref="chatContainer">
<div v-for="(event, index) in events" :key="event.getId()" :eventId="event.getId()">
<!-- DAY Marker, shown for every new day in the timeline -->
<div v-if="showDayMarkerBeforeEvent(event)" class="day-marker"><div class="line"></div><div class="text">{{ dayForEvent(event) }}</div><div class="line"></div></div>
<div v-if="showDayMarkerBeforeEvent(event)" class="day-marker">
<div class="line"></div>
<div class="text">{{ dayForEvent(event) }}</div>
<div class="line"></div>
</div>
<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"
ref="exportedEvent"
/>
<component :is="componentForEvent(event, true)" :room="room" :originalEvent="event"
:nextEvent="events[index + 1]" :timelineSet="timelineSet" :componentFn="componentForEventForExport"
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,6 +53,7 @@
</template>
<script>
import Vue from "vue";
import MessageIncomingText from "./messages/MessageIncomingText.vue";
import MessageIncomingFile from "./messages/MessageIncomingFile.vue";
import MessageIncomingImage from "./messages/MessageIncomingImage.vue";
@ -98,6 +98,7 @@ import util from "../plugins/utils";
import JSZip from "jszip";
import { saveAs } from "file-saver";
import { EventTimelineSet } from "matrix-js-sdk";
import axios from 'axios';
export default {
name: "RoomExport",
@ -146,7 +147,7 @@ export default {
props: {
room: {
type: Object,
default: function() {
default: function () {
return null;
},
},
@ -181,6 +182,9 @@ export default {
},
},
methods: {
componentForEventForExport(event) {
return this.componentForEvent(event, true);
},
cancelExport() {
this.cancelled = true;
},
@ -254,6 +258,21 @@ export default {
this.timelineSet.addEventsToTimeline(events.reverse(), true, this.timelineSet.getLiveTimeline(), "");
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) {
Vue.set(parentEvent, "isMxThread", true);
Vue.set(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) {
Vue.set(event, "replyEvent", parentEvent);
}
});
// Wait a tick so UI is updated.
return new Promise((resolve, ignoredReject) => {
this.$nextTick(() => {
@ -264,129 +283,192 @@ export default {
.then(() => {
// UI updated, start processing events
zip = new JSZip();
var avatarFolder = zip.folder("avatars");
var imageFolder = zip.folder("images");
var audioFolder = zip.folder("audio");
var videoFolder = zip.folder("video");
var downloadPromises = [];
let components = this.$refs.exportedEvent;
for (const comp of components) {
let componentClass = comp.$vnode.tag.split("-").reverse()[0];
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;
// }
for (const parentComp of components) {
let childComponents = [parentComp];
downloadPromises.push(
util
.getAttachment(this.$matrix.matrixClient, 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";
// Some components, i.e. the media threads, have subcomponents
// that we want to export. So pickup subcomponents here as well.
if (parentComp.$refs && parentComp.$refs.exportedEvent) {
if (Array.isArray(parentComp.$refs.exportedEvent)) {
for (const child of parentComp.$refs.exportedEvent) {
childComponents.push(child);
}
} else {
childComponents.push(parentComp.$refs.exportedEvent);
}
}
for (const comp of childComponents) {
// Avatars need downloading?
if (comp.$el) {
const avatars = comp.$el.getElementsByClassName("v-avatar");
if (avatars && avatars.length > 0) {
const member = this.room.getMember(comp.event.getSender());
if (member) {
const fileName = comp.event.getSender() + ".png";
const setSource = (fileName) => {
for (let avatarIndex = 0; avatarIndex < avatars.length; avatarIndex++) {
const avatarElement = avatars[avatarIndex];
const images = avatarElement.getElementsByTagName("img");
for (let imageIndex = 0; imageIndex < images.length; imageIndex++) {
const img = images[imageIndex];
img.onerror = undefined;
img.src = './avatars/' + fileName;
}
}
}
if (!avatarFolder.file(fileName)) {
const url = member.getAvatarUrl(this.$matrix.matrixClient.getHomeserverUrl(), 40, 40, "scale", true);
if (url) {
avatarFolder.file(fileName, "empty");
downloadPromises.push(
axios.get(url, {
responseType: 'blob'
})
.then(result => {
if (result.data) {
avatarFolder.file(fileName, result.data);
setSource(fileName);
}
})
.catch(err => {
console.error("Download error: ", err);
avatarFolder.remove(fileName);
}));
}
} else {
setSource(fileName);
}
}
}
}
let componentClass = comp.$vnode.tag.split("-").reverse()[0];
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;
// }
downloadPromises.push(
util
.getAttachment(this.$matrix.matrixClient, 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;
let fileName = comp.event.getId() + extension;
imageFolder.file(fileName, blob); // TODO calc bytes
let blobUrl = URL.createObjectURL(blob);
comp.src = blobUrl;
this.$nextTick(() => {
// Update source
let elements = comp.$el.getElementsByClassName("v-image__image");
let element = elements && elements[0];
if (element) {
element.style.backgroundImage = 'url("./images/' + fileName + '")';
element.classList.remove("v-image__image--preload");
}
URL.revokeObjectURL(blobUrl); // Give the blob back
this.processedEvents += 1;
resolve(true);
});
}
});
})
.catch((ignoredErr) => {
this.processedEvents += 1;
})
);
break;
case "MessageIncomingAudioExport":
case "MessageOutgoingAudioExport":
downloadPromises.push(
util
.getAttachment(this.$matrix.matrixClient, comp.event, null, true)
.then((blob) => {
if (currentMediaSize + blob.size <= maxMediaSize) {
currentMediaSize += blob.size;
let fileName = comp.event.getId() + extension;
imageFolder.file(fileName, blob); // TODO calc bytes
let blobUrl = URL.createObjectURL(blob);
comp.src = blobUrl;
this.$nextTick(() => {
// Update source
let elements = comp.$el.getElementsByClassName("v-image__image");
return new Promise((resolve, ignoredReject) => {
//let mime = blob.type;
var extension = ".mp3";
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.style.backgroundImage = 'url("./images/' + fileName + '")';
element.classList.remove("v-image__image--preload");
element.src = "./audio/" + fileName;
}
URL.revokeObjectURL(blobUrl); // Give the blob back
this.processedEvents += 1;
resolve(true);
});
}
});
})
.catch((ignoredErr) => {
this.processedEvents += 1;
})
);
break;
case "MessageIncomingAudioExport":
case "MessageOutgoingAudioExport":
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) => {
//let mime = blob.type;
var extension = ".mp3";
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.src = "./audio/" + fileName;
}
this.processedEvents += 1;
resolve(true);
});
}
})
.catch((ignoredErr) => {
this.processedEvents += 1;
})
);
break;
case "MessageIncomingVideoExport":
case "MessageOutgoingVideoExport":
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) => {
//let mime = blob.type;
var extension = ".mp4";
let fileName = comp.event.getId() + extension;
videoFolder.file(fileName, blob); // TODO calc bytes
let elements = comp.$el.getElementsByTagName("video");
let element = elements && elements[0];
if (element) {
element.src = "./video/" + fileName;
}
this.processedEvents += 1;
})
.catch((ignoredErr) => {
this.processedEvents += 1;
})
);
break;
case "MessageIncomingVideoExport":
case "MessageOutgoingVideoExport":
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) => {
//let mime = blob.type;
var extension = ".mp4";
let fileName = comp.event.getId() + extension;
videoFolder.file(fileName, blob); // TODO calc bytes
let elements = comp.$el.getElementsByTagName("video");
let element = elements && elements[0];
if (element) {
element.src = "./video/" + fileName;
}
this.processedEvents += 1;
resolve(true);
});
}
})
.catch((ignoredErr) => {
this.processedEvents += 1;
})
);
break;
default:
this.processedEvents += 1;
break;
resolve(true);
});
}
})
.catch((ignoredErr) => {
this.processedEvents += 1;
})
);
break;
default:
this.processedEvents += 1;
break;
}
}
}
return Promise.all(downloadPromises);
@ -410,7 +492,7 @@ export default {
}
doc +=
"</head><body><div class='v-application v-application--is-ltr theme--light' style='height:100%;overflow-y:auto'>";
const getCssRules = function(el) {
const getCssRules = function (el) {
if (el.classList.contains("op-button")) {
el.innerHTML = "";
} else {
@ -441,6 +523,30 @@ export default {
this.$emit("close");
});
},
onLayoutChange(action, ignoredelement) {
action();
},
},
};
</script>
<style lang="scss">
.chat-root.export {
.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 */
.messageIn {
margin-left: 50px !important;
}
.messageOut {
margin-right: 50px !important;
}
.messageIn, .messageOut {
.quick-reaction-container, .senderAndTime, .avatar {
display: none;
}
}
}
}
</style>