Lots of fixes to "media threads"
This commit is contained in:
parent
fe081edc62
commit
8bcceafcff
23 changed files with 867 additions and 333 deletions
|
|
@ -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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue