More work on export
This commit is contained in:
parent
94bf35875a
commit
2b2c736311
5 changed files with 327 additions and 299 deletions
|
|
@ -27,14 +27,16 @@
|
||||||
<div v-if="!event.isRelation() && !event.isRedacted() && !event.isRedaction()" :ref="event.getId()">
|
<div v-if="!event.isRelation() && !event.isRedacted() && !event.isRedaction()" :ref="event.getId()">
|
||||||
<div class="message-wrapper">
|
<div class="message-wrapper">
|
||||||
<component
|
<component
|
||||||
:is="componentForEventForExport(event, true)"
|
:is="componentForEvent(event, true)"
|
||||||
:room="room"
|
:room="room"
|
||||||
:originalEvent="event"
|
:originalEvent="event"
|
||||||
:nextEvent="events[index + 1]"
|
:nextEvent="events[index + 1]"
|
||||||
:timelineSet="timelineSet"
|
:timelineSet="timelineSet"
|
||||||
:componentFn="componentForEventForExport"
|
:componentFn="componentForEvent"
|
||||||
ref="exportedEvent"
|
ref="exportedEvent"
|
||||||
v-on:layout-change="onLayoutChange"
|
v-on:layout-change="onLayoutChange"
|
||||||
|
@vue:mounted="addComponent"
|
||||||
|
v-on:componentMounted="addComponent"
|
||||||
/>
|
/>
|
||||||
<!-- <div v-if="debugging" style="user-select:text">EventID: {{ event.getId() }}</div> -->
|
<!-- <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 v-if="debugging" style="user-select:text">Event: {{ JSON.stringify(event) }}</div> -->
|
||||||
|
|
@ -100,6 +102,7 @@ import { EventTimelineSet } from "matrix-js-sdk";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import "../services/jszip.min";
|
import "../services/jszip.min";
|
||||||
import "../services/filesaver.cjs";
|
import "../services/filesaver.cjs";
|
||||||
|
import { toRaw } from "vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "RoomExport",
|
name: "RoomExport",
|
||||||
|
|
@ -180,21 +183,9 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
componentForEventForExport(event, forExport) {
|
addComponent(item) {
|
||||||
const comp = this.componentForEvent(event, forExport);
|
//console.log('Add component of type', item.type.__file);
|
||||||
const self = this;
|
this.exportComponents.push(item);
|
||||||
if (comp) {
|
|
||||||
if (!comp.mixins) {
|
|
||||||
comp.mixins = [];
|
|
||||||
}
|
|
||||||
comp.mixins.push({
|
|
||||||
created: function() {
|
|
||||||
console.error("Created", this.name);
|
|
||||||
self.exportComponents.push(this);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return comp;
|
|
||||||
},
|
},
|
||||||
cancelExport() {
|
cancelExport() {
|
||||||
this.cancelled = true;
|
this.cancelled = true;
|
||||||
|
|
@ -242,297 +233,292 @@ export default {
|
||||||
}
|
}
|
||||||
return fetchedEvents;
|
return fetchedEvents;
|
||||||
},
|
},
|
||||||
doExport() {
|
async doExport() {
|
||||||
var zip = null;
|
var zip = null;
|
||||||
var currentMediaSize = 0;
|
var currentMediaSize = 0;
|
||||||
var maxMediaSize = 1024 * 1024 * 1024; // 1GB
|
var maxMediaSize = 1024 * 1024 * 1024; // 1GB
|
||||||
|
|
||||||
this.exportComponents = [];
|
try {
|
||||||
|
this.exportComponents = [];
|
||||||
|
|
||||||
this.getEvents()
|
const events = await this.getEvents();
|
||||||
.then((events) => {
|
|
||||||
var decryptionPromises = [];
|
let decryptionPromises = [];
|
||||||
for (const event of this.events) {
|
for (const event of events) {
|
||||||
if (event.isEncrypted()) {
|
if (event.isEncrypted()) {
|
||||||
decryptionPromises.push(
|
decryptionPromises.push(
|
||||||
this.$matrix.matrixClient.decryptEventIfNeeded(event, {
|
this.$matrix.matrixClient.decryptEventIfNeeded(event, {
|
||||||
isRetry: true,
|
isRetry: true,
|
||||||
emit: false,
|
emit: false,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return Promise.all(decryptionPromises).then(() => {
|
}
|
||||||
return events;
|
await Promise.all(decryptionPromises);
|
||||||
|
|
||||||
|
// Create a timeline and add the events to that, so that relations etc are aggregated correctly!
|
||||||
|
this.timelineSet = new EventTimelineSet(null, { unstableClientRelationAggregation: true });
|
||||||
|
this.timelineSet.addEventsToTimeline(events.reverse(), true, false, 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) {
|
||||||
|
parentEvent["isMxThread"] = true;
|
||||||
|
event["parentThread"] = parentEvent;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
})
|
this.events
|
||||||
.then((events) => {
|
.filter((event) => event.replyEventId && !event.replyEvent)
|
||||||
// Create a timeline and add the events to that, so that relations etc are aggregated correctly!
|
.forEach((event) => {
|
||||||
this.timelineSet = new EventTimelineSet(null, { unstableClientRelationAggregation: true });
|
const parentEvent =
|
||||||
this.timelineSet.addEventsToTimeline(events.reverse(), true, false, this.timelineSet.getLiveTimeline(), "");
|
this.timelineSet.findEventById(event.replyEventId) || this.room.findEventById(event.replyEventId);
|
||||||
this.events = events;
|
if (parentEvent) {
|
||||||
|
event["replyEvent"] = parentEvent;
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Wait a tick so UI is updated.
|
|
||||||
return new Promise((resolve, ignoredReject) => {
|
|
||||||
this.$nextTick(() => {
|
|
||||||
resolve(true);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
this.totalEvents = this.exportComponents.length;
|
|
||||||
|
|
||||||
// UI updated, start processing events
|
// Wait a tick so UI is updated.
|
||||||
zip = new JSZip();
|
await new Promise((resolve, ignoredReject) => {
|
||||||
var avatarFolder = zip.folder("avatars");
|
this.$nextTick(() => {
|
||||||
var imageFolder = zip.folder("images");
|
resolve(true);
|
||||||
var audioFolder = zip.folder("audio");
|
});
|
||||||
var videoFolder = zip.folder("video");
|
});
|
||||||
var filesFolder = zip.folder("files");
|
|
||||||
|
|
||||||
var downloadPromises = [];
|
this.totalEvents = this.exportComponents.length;
|
||||||
|
|
||||||
for (const comp of this.exportComponents.filter((c) => c.event != undefined)) {
|
let beforeExportPromises = this.exportComponents.filter((c) => c.component.exposed?.beforeExport !== undefined).map((c) => c.component.exposed.beforeExport());
|
||||||
// Avatars need downloading?
|
await Promise.all(beforeExportPromises);
|
||||||
if (comp.$el && comp.$el.nodeType == 1) {
|
|
||||||
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) => {
|
// UI updated, start processing events
|
||||||
for (let avatarIndex = 0; avatarIndex < avatars.length; avatarIndex++) {
|
zip = new JSZip();
|
||||||
const avatarElement = avatars[avatarIndex];
|
var avatarFolder = zip.folder("avatars");
|
||||||
const images = avatarElement.getElementsByTagName("img");
|
var imageFolder = zip.folder("images");
|
||||||
for (let imageIndex = 0; imageIndex < images.length; imageIndex++) {
|
var audioFolder = zip.folder("audio");
|
||||||
const img = images[imageIndex];
|
var videoFolder = zip.folder("video");
|
||||||
img.onerror = undefined;
|
var filesFolder = zip.folder("files");
|
||||||
img.removeAttribute("src");
|
|
||||||
img.setAttribute("data-exported-src", "./avatars/" + fileName);
|
var downloadPromises = [];
|
||||||
}
|
|
||||||
|
for (const comp of this.exportComponents.filter((c) => c.props.originalEvent != undefined)) {
|
||||||
|
const event = comp.props.originalEvent;
|
||||||
|
|
||||||
|
// Avatars need downloading?
|
||||||
|
if (comp.el && comp.el.nodeType == 1) {
|
||||||
|
const avatars = comp.el.getElementsByClassName("v-avatar");
|
||||||
|
if (avatars && avatars.length > 0) {
|
||||||
|
const member = this.room.getMember(event.getSender());
|
||||||
|
if (member) {
|
||||||
|
const fileName = 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.removeAttribute("src");
|
||||||
|
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
|
|
||||||
);
|
|
||||||
if (url) {
|
|
||||||
avatarFolder.file(fileName, "empty");
|
|
||||||
downloadPromises.push(
|
|
||||||
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) => {
|
|
||||||
console.error("Download error: ", err);
|
|
||||||
avatarFolder.remove(fileName);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setSource(fileName);
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!avatarFolder.file(fileName)) {
|
||||||
|
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",
|
||||||
|
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) => {
|
||||||
|
console.error("Download error: ", err);
|
||||||
|
avatarFolder.remove(fileName);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setSource(fileName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let attachment =
|
let attachment =
|
||||||
comp.event && comp.event.getId
|
event && event.getId ? this.$matrix.attachmentManager.getEventAttachment(event) : undefined;
|
||||||
? this.$matrix.attachmentManager.getEventAttachment(comp.event)
|
let componentClass = comp.type
|
||||||
: undefined;
|
? comp.type.__file.split("/").reverse()[0].split(".")[0]
|
||||||
let componentClass = comp.$options
|
: "invalid_component";
|
||||||
? comp.$options.__file.split("/").reverse()[0].split(".")[0]
|
|
||||||
: "invalid_component";
|
|
||||||
console.error("Processi", componentClass, comp.event, comp.originalEvent, attachment);
|
|
||||||
|
|
||||||
|
if (attachment && (attachment.srcSize = 0 || currentMediaSize + attachment.srcSize <= maxMediaSize)) {
|
||||||
|
downloadPromises.push(
|
||||||
|
attachment
|
||||||
|
.loadBlob()
|
||||||
|
.then((res) => {
|
||||||
|
const blob = res.data;
|
||||||
|
if (currentMediaSize + blob.size <= maxMediaSize) {
|
||||||
|
currentMediaSize += blob.size;
|
||||||
|
|
||||||
if (attachment && (attachment.srcSize = 0 || currentMediaSize + attachment.srcSize <= maxMediaSize)) {
|
switch (componentClass) {
|
||||||
downloadPromises.push(
|
case "MessageIncomingImageExport":
|
||||||
attachment
|
case "MessageOutgoingImageExport":
|
||||||
.loadSrc({ asBlob: true })
|
{
|
||||||
.then((res) => {
|
let mime = blob.type;
|
||||||
const blob = res.data;
|
var extension = ".png";
|
||||||
if (currentMediaSize + blob.size <= maxMediaSize) {
|
switch (mime) {
|
||||||
currentMediaSize += blob.size;
|
case "image/jpeg":
|
||||||
|
case "image/jpg":
|
||||||
switch (componentClass) {
|
extension = ".jpg";
|
||||||
case "MessageIncomingImageExport":
|
break;
|
||||||
case "MessageOutgoingImageExport":
|
case "image/gif":
|
||||||
{
|
extension = ".gif";
|
||||||
let mime = blob.type;
|
|
||||||
var extension = ".png";
|
|
||||||
switch (mime) {
|
|
||||||
case "image/jpeg":
|
|
||||||
case "image/jpg":
|
|
||||||
extension = ".jpg";
|
|
||||||
break;
|
|
||||||
case "image/gif":
|
|
||||||
extension = ".gif";
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
|
|
||||||
case "MessageIncomingAudioExport":
|
let fileName = event.getId() + extension;
|
||||||
case "MessageOutgoingAudioExport":
|
imageFolder.file(fileName, blob); // TODO calc bytes
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "MessageIncomingVideoExport":
|
// Update source
|
||||||
case "MessageOutgoingVideoExport":
|
const images = comp.el.getElementsByTagName("img");
|
||||||
{
|
for (let imageIndex = 0; imageIndex < images.length; imageIndex++) {
|
||||||
var extension = ".mp4";
|
const img = images[imageIndex];
|
||||||
let fileName = comp.event.getId() + extension;
|
img.removeAttribute("src");
|
||||||
videoFolder.file(fileName, blob); // TODO calc bytes
|
img.setAttribute("data-exported-src", "./images/" + fileName);
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
break;
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
case "MessageIncomingFileExport":
|
case "MessageIncomingAudioExport":
|
||||||
case "MessageOutgoingFileExport":
|
case "MessageOutgoingAudioExport":
|
||||||
{
|
{
|
||||||
var extension = util.getFileExtension(comp.event);
|
var extension = ".webm";
|
||||||
let fileName = comp.event.getId() + extension;
|
let fileName = event.getId() + extension;
|
||||||
filesFolder.file(fileName, blob);
|
audioFolder.file(fileName, blob); // TODO calc bytes
|
||||||
comp.href = "./files/" + fileName;
|
let elements = comp.el.getElementsByTagName("audio");
|
||||||
this.processedEvents += 1;
|
let element = elements && elements[0];
|
||||||
|
if (element) {
|
||||||
|
element.setAttribute("data-exported-src", "./audio/" + fileName);
|
||||||
}
|
}
|
||||||
break;
|
}
|
||||||
}
|
break;
|
||||||
this.processedEvents += 1;
|
|
||||||
return true;
|
case "MessageIncomingVideoExport":
|
||||||
} else {
|
case "MessageOutgoingVideoExport":
|
||||||
this.processedEvents += 1;
|
{
|
||||||
return false;
|
var extension = ".mp4";
|
||||||
|
let fileName = 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 "MessageIncomingFileExport":
|
||||||
|
case "MessageOutgoingFileExport":
|
||||||
|
{
|
||||||
|
var extension = util.getFileExtension(event);
|
||||||
|
let fileName = event.getId() + extension;
|
||||||
|
filesFolder.file(fileName, blob);
|
||||||
|
comp.component.data.href = "./files/" + fileName;
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
})
|
|
||||||
.catch((ignoredErr) => {
|
|
||||||
this.processedEvents += 1;
|
this.processedEvents += 1;
|
||||||
})
|
return true;
|
||||||
);
|
} else {
|
||||||
} else {
|
this.processedEvents += 1;
|
||||||
this.processedEvents += 1;
|
return false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((ignoredErr) => {
|
||||||
|
this.processedEvents += 1;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.processedEvents += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(downloadPromises);
|
||||||
|
|
||||||
|
console.log("All media added, total size: " + currentMediaSize);
|
||||||
|
|
||||||
|
let root = this.$refs.exportRoot;
|
||||||
|
|
||||||
|
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";
|
||||||
|
for (const rule of sheet.cssRules) {
|
||||||
|
if (rule.constructor.name != "CSSFontFaceRule") {
|
||||||
|
// Strip font face rules for now.
|
||||||
|
doc += rule.cssText + "\n";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Promise.all(downloadPromises);
|
doc += "</style>\n";
|
||||||
})
|
}
|
||||||
.then(() => {
|
doc +=
|
||||||
console.log("All media added, total size: " + currentMediaSize);
|
"</head><body><div class='v-application v-application--is-ltr theme--light' style='height:100%;overflow-y:auto'>";
|
||||||
|
const getCssRules = function (el) {
|
||||||
let root = this.$refs.exportRoot;
|
if (el.classList.contains("op-button")) {
|
||||||
|
el.innerHTML = "";
|
||||||
var doc = '<!DOCTYPE html>\n<html><head>\n<meta charset="utf-8"/>\n';
|
} else {
|
||||||
|
for (let i = 0; i < el.children.length; i++) {
|
||||||
for (const sheet of document.styleSheets) {
|
getCssRules(el.children[i]);
|
||||||
doc += "<style type='text/css'>\n";
|
|
||||||
for (const rule of sheet.cssRules) {
|
|
||||||
if (rule.constructor.name != "CSSFontFaceRule") {
|
|
||||||
// Strip font face rules for now.
|
|
||||||
doc += rule.cssText + "\n";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
doc += "</style>\n";
|
|
||||||
}
|
}
|
||||||
doc +=
|
};
|
||||||
"</head><body><div class='v-application v-application--is-ltr theme--light' style='height:100%;overflow-y:auto'>";
|
getCssRules(root);
|
||||||
const getCssRules = function (el) {
|
|
||||||
if (el.classList.contains("op-button")) {
|
|
||||||
el.innerHTML = "";
|
|
||||||
} else {
|
|
||||||
for (let i = 0; i < el.children.length; i++) {
|
|
||||||
getCssRules(el.children[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
getCssRules(root);
|
|
||||||
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
const contentHtml = this.$refs.exportRoot.outerHTML;
|
const contentHtml = this.$refs.exportRoot.outerHTML;
|
||||||
doc += contentHtml.replaceAll("data-exported-src=", "src=");
|
doc += contentHtml.replaceAll("data-exported-src=", "src=");
|
||||||
doc += "</div></body></html>";
|
doc += "</div></body></html>";
|
||||||
|
|
||||||
zip.file("chat.html", doc);
|
zip.file("chat.html", doc);
|
||||||
zip.generateAsync({ type: "blob" }).then((content) => {
|
zip.generateAsync({ type: "blob" }).then((content) => {
|
||||||
saveAs(
|
saveAs(
|
||||||
content,
|
content,
|
||||||
this.$t("room_export.export_filename", { date: util.formatDay(Date.now().valueOf()) }) + ".zip"
|
this.$t("room_export.export_filename", { date: util.formatDay(Date.now().valueOf()) }) + ".zip"
|
||||||
);
|
);
|
||||||
this.status = "";
|
this.status = "";
|
||||||
this.$emit("close");
|
this.$emit("close");
|
||||||
});
|
|
||||||
});
|
});
|
||||||
})
|
this.events = [];
|
||||||
.catch((err) => {
|
|
||||||
console.error("Failed to export:", err);
|
|
||||||
this.$emit("close");
|
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to export:", error);
|
||||||
|
this.$emit("close");
|
||||||
|
this.events = [];
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onLayoutChange(event) {
|
onLayoutChange(event) {
|
||||||
const { action, element } = event;
|
const { action, element } = event;
|
||||||
|
|
|
||||||
|
|
@ -129,7 +129,6 @@ export default {
|
||||||
componentForEvent(event, isForExport = false) {
|
componentForEvent(event, isForExport = false) {
|
||||||
let component = this.componentForEventInternal(event, isForExport);
|
let component = this.componentForEventInternal(event, isForExport);
|
||||||
if (component) {
|
if (component) {
|
||||||
console.error("COMPONENT", isForExport, component.name);
|
|
||||||
return markRaw(component);
|
return markRaw(component);
|
||||||
}
|
}
|
||||||
return component;
|
return component;
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,23 @@
|
||||||
:class="isIncoming ? 'messageIn-thread' : 'messageOut-thread'"
|
:class="isIncoming ? 'messageIn-thread' : 'messageOut-thread'"
|
||||||
ref="root"
|
ref="root"
|
||||||
v-bind="{ ...$props, ...$attrs }"
|
v-bind="{ ...$props, ...$attrs }"
|
||||||
|
@vue:mounted="
|
||||||
|
(item) => {
|
||||||
|
emits('componentMounted', item);
|
||||||
|
}
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<component :is="textComponent" v-bind="{ ...$props, ...$attrs }" :originalEvent="event" ref="exportedEvent" />
|
<component
|
||||||
|
:is="textComponent"
|
||||||
|
v-bind="{ ...$props, ...$attrs }"
|
||||||
|
:originalEvent="event"
|
||||||
|
ref="exportedEvent"
|
||||||
|
@vue:mounted="
|
||||||
|
(item) => {
|
||||||
|
emits('componentMounted', item);
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
<component
|
<component
|
||||||
v-for="item in items"
|
v-for="item in items"
|
||||||
:is="$props.componentFn(item.event, true)"
|
:is="$props.componentFn(item.event, true)"
|
||||||
|
|
@ -13,6 +28,11 @@
|
||||||
:originalEvent="item.event"
|
:originalEvent="item.event"
|
||||||
:key="item.event.getId()"
|
:key="item.event.getId()"
|
||||||
ref="exportedEvent"
|
ref="exportedEvent"
|
||||||
|
@vue:mounted="
|
||||||
|
(item) => {
|
||||||
|
emits('componentMounted', item);
|
||||||
|
}
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
</component>
|
</component>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -24,7 +44,7 @@ import MessageIncomingText from "../MessageIncomingText.vue";
|
||||||
import MessageOutgoingText from "../MessageOutgoingText.vue";
|
import MessageOutgoingText from "../MessageOutgoingText.vue";
|
||||||
import { MessageEmits, MessageProps, useMessage } from "./useMessage";
|
import { MessageEmits, MessageProps, useMessage } from "./useMessage";
|
||||||
import util from "@/plugins/utils";
|
import util from "@/plugins/utils";
|
||||||
import { computed, inject, onBeforeUnmount, ref, Ref, useTemplateRef, watch } from "vue";
|
import { computed, inject, onBeforeUnmount, ref, Ref, watch, useAttrs } from "vue";
|
||||||
import { EventAttachment } from "../../../models/eventAttachment";
|
import { EventAttachment } from "../../../models/eventAttachment";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import { MatrixEvent, MatrixEventEvent } from "matrix-js-sdk";
|
import { MatrixEvent, MatrixEventEvent } from "matrix-js-sdk";
|
||||||
|
|
@ -33,12 +53,21 @@ const { t } = useI18n();
|
||||||
const $matrix: any = inject("globalMatrix");
|
const $matrix: any = inject("globalMatrix");
|
||||||
|
|
||||||
const emits = defineEmits<
|
const emits = defineEmits<
|
||||||
MessageEmits & { (event: "layout-change", value: { element: Element | undefined; action: () => void }): void }
|
MessageEmits & { (event: "layout-change", value: { element: Element | undefined; action: () => void }): void } & {
|
||||||
|
(event: "componentMounted", value: any): void;
|
||||||
|
}
|
||||||
>();
|
>();
|
||||||
|
|
||||||
const items: Ref<EventAttachment[]> = ref([]);
|
const items: Ref<EventAttachment[]> = ref([]);
|
||||||
const props = defineProps<MessageProps>();
|
const props = defineProps<MessageProps>();
|
||||||
|
|
||||||
|
let beforeExportPromiseResolve: ((value: boolean) => void) | undefined = undefined;
|
||||||
|
let beforeExportPromiseReject: ((value: boolean) => void) | undefined = undefined;
|
||||||
|
const beforeExportPromise = new Promise((resolve, reject) => {
|
||||||
|
beforeExportPromiseResolve = resolve;
|
||||||
|
beforeExportPromiseReject = reject;
|
||||||
|
});
|
||||||
|
|
||||||
const processThread = () => {
|
const processThread = () => {
|
||||||
if (!event.value?.isRedacted()) {
|
if (!event.value?.isRedacted()) {
|
||||||
_processThread();
|
_processThread();
|
||||||
|
|
@ -96,15 +125,22 @@ const _processThread = () => {
|
||||||
const eventItems = props.timelineSet.relations
|
const eventItems = props.timelineSet.relations
|
||||||
.getAllChildEventsForEvent(event.value?.getId() ?? "")
|
.getAllChildEventsForEvent(event.value?.getId() ?? "")
|
||||||
.filter((e: MatrixEvent) => !e.isRedacted() && util.downloadableTypes().includes(e.getContent().msgtype));
|
.filter((e: MatrixEvent) => !e.isRedacted() && util.downloadableTypes().includes(e.getContent().msgtype));
|
||||||
|
|
||||||
console.log("EVENT ITEMS", eventItems);
|
|
||||||
items.value = eventItems.map((e: MatrixEvent) => {
|
items.value = eventItems.map((e: MatrixEvent) => {
|
||||||
let ea = $matrix.attachmentManager.getEventAttachment(e);
|
let ea = $matrix.attachmentManager.getEventAttachment(e);
|
||||||
ea.loadThumbnail();
|
|
||||||
return ea;
|
return ea;
|
||||||
});
|
});
|
||||||
console.log("MAPPED", items.value);
|
if (beforeExportPromiseResolve) {
|
||||||
|
beforeExportPromiseResolve(true);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const beforeExport = () => {
|
||||||
|
return beforeExportPromise;
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
beforeExport,
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@use "@/assets/css/chat.scss" as *;
|
@use "@/assets/css/chat.scss" as *;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk";
|
import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk";
|
||||||
import { EventAttachment, EventAttachmentLoadSrcOptions, EventAttachmentUrlType, KeanuEvent, KeanuEventExtension } from "./eventAttachment";
|
import { EventAttachment, EventAttachmentLoadSrcOptions, EventAttachmentUrlData, EventAttachmentUrlPromise, EventAttachmentUrlType, KeanuEvent, KeanuEventExtension } from "./eventAttachment";
|
||||||
import axios, { AxiosRequestConfig, AxiosResponse } from "axios";
|
import axios, { AxiosRequestConfig, AxiosResponse } from "axios";
|
||||||
import { Counter, ModeOfOperation } from "aes-js";
|
import { Counter, ModeOfOperation } from "aes-js";
|
||||||
import { Attachment, AttachmentBatch, AttachmentSendInfo } from "./attachment";
|
import { Attachment, AttachmentBatch, AttachmentSendInfo } from "./attachment";
|
||||||
|
|
@ -144,22 +144,22 @@ export class AttachmentManager {
|
||||||
autoDownloadable: fileSize <= this.maxSizeAutoDownloads,
|
autoDownloadable: fileSize <= this.maxSizeAutoDownloads,
|
||||||
loadSrc: () => Promise.reject("Not implemented"),
|
loadSrc: () => Promise.reject("Not implemented"),
|
||||||
loadThumbnail: () => Promise.reject("Not implemented"),
|
loadThumbnail: () => Promise.reject("Not implemented"),
|
||||||
|
loadBlob: () => Promise.reject("Not implemented"),
|
||||||
release: () => Promise.reject("Not implemented"),
|
release: () => Promise.reject("Not implemented"),
|
||||||
});
|
});
|
||||||
attachment.loadSrc = (options?: EventAttachmentLoadSrcOptions) => {
|
attachment.loadSrc = () => {
|
||||||
if (attachment.src && !options?.asBlob) {
|
if (attachment.src) {
|
||||||
return Promise.resolve({data: attachment.src, type: "src"});
|
return Promise.resolve({data: attachment.src, type: "src"});
|
||||||
} else if (attachment.srcPromise && !options?.asBlob) {
|
} else if (attachment.srcPromise) {
|
||||||
return attachment.srcPromise;
|
return attachment.srcPromise;
|
||||||
}
|
}
|
||||||
Implement loadBlob somewhere here!
|
attachment.srcPromise = this._loadEventAttachmentOrThumbnail(event, false, false, (percent) => {
|
||||||
attachment.srcPromise = this._loadEventAttachmentOrThumbnail(event, false, !!options?.asBlob, (percent) => {
|
|
||||||
attachment.srcProgress = percent;
|
attachment.srcProgress = percent;
|
||||||
}).then((res) => {
|
}).then((res) => {
|
||||||
attachment.src = res.data as string;
|
attachment.src = (res as EventAttachmentUrlData).data;
|
||||||
return res;
|
return res;
|
||||||
});
|
}) as Promise<EventAttachmentUrlData>;
|
||||||
return attachment.srcPromise;
|
return attachment.srcPromise as Promise<{data:string,type:EventAttachmentUrlType}>;
|
||||||
};
|
};
|
||||||
attachment.loadThumbnail = () => {
|
attachment.loadThumbnail = () => {
|
||||||
if (attachment.thumbnail) {
|
if (attachment.thumbnail) {
|
||||||
|
|
@ -176,9 +176,17 @@ export class AttachmentManager {
|
||||||
attachment.src = res.data as string;
|
attachment.src = res.data as string;
|
||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
});
|
}) as Promise<EventAttachmentUrlData>;
|
||||||
return attachment.thumbnailPromise;
|
return attachment.thumbnailPromise;
|
||||||
};
|
};
|
||||||
|
attachment.loadBlob = () => {
|
||||||
|
const promise = this._loadEventAttachmentOrThumbnail(event, false, true, (percent) => {
|
||||||
|
attachment.srcProgress = percent;
|
||||||
|
}).then((res) => {
|
||||||
|
return {data: res.data as Blob};
|
||||||
|
});
|
||||||
|
return promise;
|
||||||
|
};
|
||||||
attachment.release = (src: boolean, thumbnail: boolean) => {
|
attachment.release = (src: boolean, thumbnail: boolean) => {
|
||||||
// TODO - figure out logic
|
// TODO - figure out logic
|
||||||
if (entry) {
|
if (entry) {
|
||||||
|
|
@ -201,7 +209,7 @@ export class AttachmentManager {
|
||||||
thumbnail: boolean,
|
thumbnail: boolean,
|
||||||
asBlob: boolean,
|
asBlob: boolean,
|
||||||
progress?: (percent: number) => void
|
progress?: (percent: number) => void
|
||||||
): Promise<{data: string | Blob, type: EventAttachmentUrlType}> {
|
): Promise<EventAttachmentUrlData | {data: Blob, type: EventAttachmentUrlType}> {
|
||||||
await this.matrixClient.decryptEventIfNeeded(event);
|
await this.matrixClient.decryptEventIfNeeded(event);
|
||||||
|
|
||||||
let urltype: EventAttachmentUrlType = thumbnail ? "thumbnail" : "src";
|
let urltype: EventAttachmentUrlType = thumbnail ? "thumbnail" : "src";
|
||||||
|
|
@ -293,7 +301,7 @@ export class AttachmentManager {
|
||||||
const response = await axios.get(url, options);
|
const response = await axios.get(url, options);
|
||||||
const bytes = decrypt ? await this.decryptData(file, response) : { buffer: response.data };
|
const bytes = decrypt ? await this.decryptData(file, response) : { buffer: response.data };
|
||||||
const blob = new Blob([bytes.buffer], { type: mime });
|
const blob = new Blob([bytes.buffer], { type: mime });
|
||||||
return {data: asBlob ? blob : URL.createObjectURL(blob), type: urltype};
|
return asBlob ? {data: blob, type: urltype} : {data: URL.createObjectURL(blob), type: urltype};
|
||||||
}
|
}
|
||||||
|
|
||||||
private b64toBuffer(val: any) {
|
private b64toBuffer(val: any) {
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,7 @@ export type KeanuEventExtension = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type EventAttachmentUrlType = "src" | "thumbnail";
|
export type EventAttachmentUrlType = "src" | "thumbnail";
|
||||||
export type EventAttachmentLoadSrcOptions = {
|
export type EventAttachmentUrlData = {data: string, type: EventAttachmentUrlType};
|
||||||
asBlob?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type EventAttachment = {
|
export type EventAttachment = {
|
||||||
event: MatrixEvent & KeanuEventExtension;
|
event: MatrixEvent & KeanuEventExtension;
|
||||||
|
|
@ -19,13 +17,14 @@ export type EventAttachment = {
|
||||||
src?: string;
|
src?: string;
|
||||||
srcSize: number;
|
srcSize: number;
|
||||||
srcProgress: number;
|
srcProgress: number;
|
||||||
srcPromise?: Promise<{data: string | Blob, type: EventAttachmentUrlType}>;
|
srcPromise?: Promise<EventAttachmentUrlData>;
|
||||||
thumbnail?: string;
|
thumbnail?: string;
|
||||||
thumbnailProgress: number;
|
thumbnailProgress: number;
|
||||||
thumbnailPromise?: Promise<{data: string | Blob, type: EventAttachmentUrlType}>;
|
thumbnailPromise?: Promise<EventAttachmentUrlData>;
|
||||||
autoDownloadable: boolean;
|
autoDownloadable: boolean;
|
||||||
loadSrc: (options?: EventAttachmentLoadSrcOptions) => Promise<{data: string | Blob, type: EventAttachmentUrlType}>;
|
loadSrc: () => Promise<EventAttachmentUrlData>;
|
||||||
loadThumbnail: () => Promise<{data: string | Blob, type: EventAttachmentUrlType}>;
|
loadThumbnail: () => Promise<EventAttachmentUrlData>;
|
||||||
|
loadBlob: () => Promise<{data: Blob}>;
|
||||||
release: (src: boolean, thumbnail: boolean) => void;
|
release: (src: boolean, thumbnail: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue