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 class="message-wrapper">
|
||||
<component
|
||||
:is="componentForEventForExport(event, true)"
|
||||
:is="componentForEvent(event, true)"
|
||||
:room="room"
|
||||
:originalEvent="event"
|
||||
:nextEvent="events[index + 1]"
|
||||
:timelineSet="timelineSet"
|
||||
:componentFn="componentForEventForExport"
|
||||
:componentFn="componentForEvent"
|
||||
ref="exportedEvent"
|
||||
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">Event: {{ JSON.stringify(event) }}</div> -->
|
||||
|
|
@ -100,6 +102,7 @@ import { EventTimelineSet } from "matrix-js-sdk";
|
|||
import axios from "axios";
|
||||
import "../services/jszip.min";
|
||||
import "../services/filesaver.cjs";
|
||||
import { toRaw } from "vue";
|
||||
|
||||
export default {
|
||||
name: "RoomExport",
|
||||
|
|
@ -180,21 +183,9 @@ export default {
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
componentForEventForExport(event, forExport) {
|
||||
const comp = this.componentForEvent(event, forExport);
|
||||
const self = this;
|
||||
if (comp) {
|
||||
if (!comp.mixins) {
|
||||
comp.mixins = [];
|
||||
}
|
||||
comp.mixins.push({
|
||||
created: function() {
|
||||
console.error("Created", this.name);
|
||||
self.exportComponents.push(this);
|
||||
}
|
||||
});
|
||||
}
|
||||
return comp;
|
||||
addComponent(item) {
|
||||
//console.log('Add component of type', item.type.__file);
|
||||
this.exportComponents.push(item);
|
||||
},
|
||||
cancelExport() {
|
||||
this.cancelled = true;
|
||||
|
|
@ -242,297 +233,292 @@ export default {
|
|||
}
|
||||
return fetchedEvents;
|
||||
},
|
||||
doExport() {
|
||||
async doExport() {
|
||||
var zip = null;
|
||||
var currentMediaSize = 0;
|
||||
var maxMediaSize = 1024 * 1024 * 1024; // 1GB
|
||||
|
||||
this.exportComponents = [];
|
||||
try {
|
||||
this.exportComponents = [];
|
||||
|
||||
this.getEvents()
|
||||
.then((events) => {
|
||||
var decryptionPromises = [];
|
||||
for (const event of this.events) {
|
||||
if (event.isEncrypted()) {
|
||||
decryptionPromises.push(
|
||||
this.$matrix.matrixClient.decryptEventIfNeeded(event, {
|
||||
isRetry: true,
|
||||
emit: false,
|
||||
})
|
||||
);
|
||||
}
|
||||
const events = await this.getEvents();
|
||||
|
||||
let decryptionPromises = [];
|
||||
for (const event of events) {
|
||||
if (event.isEncrypted()) {
|
||||
decryptionPromises.push(
|
||||
this.$matrix.matrixClient.decryptEventIfNeeded(event, {
|
||||
isRetry: true,
|
||||
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;
|
||||
}
|
||||
});
|
||||
})
|
||||
.then((events) => {
|
||||
// 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
|
||||
.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);
|
||||
});
|
||||
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;
|
||||
}
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
this.totalEvents = this.exportComponents.length;
|
||||
|
||||
// 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 filesFolder = zip.folder("files");
|
||||
// Wait a tick so UI is updated.
|
||||
await new Promise((resolve, ignoredReject) => {
|
||||
this.$nextTick(() => {
|
||||
resolve(true);
|
||||
});
|
||||
});
|
||||
|
||||
var downloadPromises = [];
|
||||
this.totalEvents = this.exportComponents.length;
|
||||
|
||||
for (const comp of this.exportComponents.filter((c) => c.event != undefined)) {
|
||||
// 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(comp.event.getSender());
|
||||
if (member) {
|
||||
const fileName = comp.event.getSender() + ".png";
|
||||
let beforeExportPromises = this.exportComponents.filter((c) => c.component.exposed?.beforeExport !== undefined).map((c) => c.component.exposed.beforeExport());
|
||||
await Promise.all(beforeExportPromises);
|
||||
|
||||
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);
|
||||
}
|
||||
// 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 filesFolder = zip.folder("files");
|
||||
|
||||
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 =
|
||||
comp.event && comp.event.getId
|
||||
? this.$matrix.attachmentManager.getEventAttachment(comp.event)
|
||||
: undefined;
|
||||
let componentClass = comp.$options
|
||||
? comp.$options.__file.split("/").reverse()[0].split(".")[0]
|
||||
: "invalid_component";
|
||||
console.error("Processi", componentClass, comp.event, comp.originalEvent, attachment);
|
||||
let attachment =
|
||||
event && event.getId ? this.$matrix.attachmentManager.getEventAttachment(event) : undefined;
|
||||
let componentClass = comp.type
|
||||
? comp.type.__file.split("/").reverse()[0].split(".")[0]
|
||||
: "invalid_component";
|
||||
|
||||
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)) {
|
||||
downloadPromises.push(
|
||||
attachment
|
||||
.loadSrc({ asBlob: true })
|
||||
.then((res) => {
|
||||
const blob = res.data;
|
||||
if (currentMediaSize + blob.size <= maxMediaSize) {
|
||||
currentMediaSize += blob.size;
|
||||
|
||||
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";
|
||||
}
|
||||
|
||||
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;
|
||||
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";
|
||||
}
|
||||
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;
|
||||
}
|
||||
break;
|
||||
let fileName = event.getId() + extension;
|
||||
imageFolder.file(fileName, blob); // TODO calc bytes
|
||||
|
||||
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;
|
||||
// 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);
|
||||
}
|
||||
break;
|
||||
}
|
||||
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;
|
||||
case "MessageIncomingAudioExport":
|
||||
case "MessageOutgoingAudioExport":
|
||||
{
|
||||
var extension = ".webm";
|
||||
let fileName = 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;
|
||||
}
|
||||
this.processedEvents += 1;
|
||||
return true;
|
||||
} else {
|
||||
this.processedEvents += 1;
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
case "MessageIncomingVideoExport":
|
||||
case "MessageOutgoingVideoExport":
|
||||
{
|
||||
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;
|
||||
})
|
||||
);
|
||||
} else {
|
||||
this.processedEvents += 1;
|
||||
return true;
|
||||
} else {
|
||||
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);
|
||||
})
|
||||
.then(() => {
|
||||
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";
|
||||
}
|
||||
doc += "</style>\n";
|
||||
}
|
||||
doc +=
|
||||
"</head><body><div class='v-application v-application--is-ltr theme--light' style='height:100%;overflow-y:auto'>";
|
||||
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]);
|
||||
}
|
||||
doc += "</style>\n";
|
||||
}
|
||||
doc +=
|
||||
"</head><body><div class='v-application v-application--is-ltr theme--light' style='height:100%;overflow-y:auto'>";
|
||||
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);
|
||||
};
|
||||
getCssRules(root);
|
||||
|
||||
this.$nextTick(() => {
|
||||
const contentHtml = this.$refs.exportRoot.outerHTML;
|
||||
doc += contentHtml.replaceAll("data-exported-src=", "src=");
|
||||
doc += "</div></body></html>";
|
||||
this.$nextTick(() => {
|
||||
const contentHtml = this.$refs.exportRoot.outerHTML;
|
||||
doc += contentHtml.replaceAll("data-exported-src=", "src=");
|
||||
doc += "</div></body></html>";
|
||||
|
||||
zip.file("chat.html", doc);
|
||||
zip.generateAsync({ type: "blob" }).then((content) => {
|
||||
saveAs(
|
||||
content,
|
||||
this.$t("room_export.export_filename", { date: util.formatDay(Date.now().valueOf()) }) + ".zip"
|
||||
);
|
||||
this.status = "";
|
||||
this.$emit("close");
|
||||
});
|
||||
zip.file("chat.html", doc);
|
||||
zip.generateAsync({ type: "blob" }).then((content) => {
|
||||
saveAs(
|
||||
content,
|
||||
this.$t("room_export.export_filename", { date: util.formatDay(Date.now().valueOf()) }) + ".zip"
|
||||
);
|
||||
this.status = "";
|
||||
this.$emit("close");
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Failed to export:", err);
|
||||
this.$emit("close");
|
||||
this.events = [];
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to export:", error);
|
||||
this.$emit("close");
|
||||
this.events = [];
|
||||
}
|
||||
},
|
||||
onLayoutChange(event) {
|
||||
const { action, element } = event;
|
||||
|
|
|
|||
|
|
@ -129,7 +129,6 @@ export default {
|
|||
componentForEvent(event, isForExport = false) {
|
||||
let component = this.componentForEventInternal(event, isForExport);
|
||||
if (component) {
|
||||
console.error("COMPONENT", isForExport, component.name);
|
||||
return markRaw(component);
|
||||
}
|
||||
return component;
|
||||
|
|
|
|||
|
|
@ -4,8 +4,23 @@
|
|||
:class="isIncoming ? 'messageIn-thread' : 'messageOut-thread'"
|
||||
ref="root"
|
||||
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
|
||||
v-for="item in items"
|
||||
:is="$props.componentFn(item.event, true)"
|
||||
|
|
@ -13,6 +28,11 @@
|
|||
:originalEvent="item.event"
|
||||
:key="item.event.getId()"
|
||||
ref="exportedEvent"
|
||||
@vue:mounted="
|
||||
(item) => {
|
||||
emits('componentMounted', item);
|
||||
}
|
||||
"
|
||||
/>
|
||||
</component>
|
||||
</template>
|
||||
|
|
@ -24,7 +44,7 @@ import MessageIncomingText from "../MessageIncomingText.vue";
|
|||
import MessageOutgoingText from "../MessageOutgoingText.vue";
|
||||
import { MessageEmits, MessageProps, useMessage } from "./useMessage";
|
||||
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 { useI18n } from "vue-i18n";
|
||||
import { MatrixEvent, MatrixEventEvent } from "matrix-js-sdk";
|
||||
|
|
@ -33,12 +53,21 @@ const { t } = useI18n();
|
|||
const $matrix: any = inject("globalMatrix");
|
||||
|
||||
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 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 = () => {
|
||||
if (!event.value?.isRedacted()) {
|
||||
_processThread();
|
||||
|
|
@ -96,15 +125,22 @@ const _processThread = () => {
|
|||
const eventItems = props.timelineSet.relations
|
||||
.getAllChildEventsForEvent(event.value?.getId() ?? "")
|
||||
.filter((e: MatrixEvent) => !e.isRedacted() && util.downloadableTypes().includes(e.getContent().msgtype));
|
||||
|
||||
console.log("EVENT ITEMS", eventItems);
|
||||
items.value = eventItems.map((e: MatrixEvent) => {
|
||||
let ea = $matrix.attachmentManager.getEventAttachment(e);
|
||||
ea.loadThumbnail();
|
||||
return ea;
|
||||
});
|
||||
console.log("MAPPED", items.value);
|
||||
if (beforeExportPromiseResolve) {
|
||||
beforeExportPromiseResolve(true);
|
||||
}
|
||||
};
|
||||
|
||||
const beforeExport = () => {
|
||||
return beforeExportPromise;
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
beforeExport,
|
||||
});
|
||||
</script>
|
||||
<style lang="scss">
|
||||
@use "@/assets/css/chat.scss" as *;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
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 { Counter, ModeOfOperation } from "aes-js";
|
||||
import { Attachment, AttachmentBatch, AttachmentSendInfo } from "./attachment";
|
||||
|
|
@ -144,22 +144,22 @@ export class AttachmentManager {
|
|||
autoDownloadable: fileSize <= this.maxSizeAutoDownloads,
|
||||
loadSrc: () => Promise.reject("Not implemented"),
|
||||
loadThumbnail: () => Promise.reject("Not implemented"),
|
||||
loadBlob: () => Promise.reject("Not implemented"),
|
||||
release: () => Promise.reject("Not implemented"),
|
||||
});
|
||||
attachment.loadSrc = (options?: EventAttachmentLoadSrcOptions) => {
|
||||
if (attachment.src && !options?.asBlob) {
|
||||
attachment.loadSrc = () => {
|
||||
if (attachment.src) {
|
||||
return Promise.resolve({data: attachment.src, type: "src"});
|
||||
} else if (attachment.srcPromise && !options?.asBlob) {
|
||||
} else if (attachment.srcPromise) {
|
||||
return attachment.srcPromise;
|
||||
}
|
||||
Implement loadBlob somewhere here!
|
||||
attachment.srcPromise = this._loadEventAttachmentOrThumbnail(event, false, !!options?.asBlob, (percent) => {
|
||||
attachment.srcPromise = this._loadEventAttachmentOrThumbnail(event, false, false, (percent) => {
|
||||
attachment.srcProgress = percent;
|
||||
}).then((res) => {
|
||||
attachment.src = res.data as string;
|
||||
attachment.src = (res as EventAttachmentUrlData).data;
|
||||
return res;
|
||||
});
|
||||
return attachment.srcPromise;
|
||||
}) as Promise<EventAttachmentUrlData>;
|
||||
return attachment.srcPromise as Promise<{data:string,type:EventAttachmentUrlType}>;
|
||||
};
|
||||
attachment.loadThumbnail = () => {
|
||||
if (attachment.thumbnail) {
|
||||
|
|
@ -176,9 +176,17 @@ export class AttachmentManager {
|
|||
attachment.src = res.data as string;
|
||||
}
|
||||
return res;
|
||||
});
|
||||
}) as Promise<EventAttachmentUrlData>;
|
||||
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) => {
|
||||
// TODO - figure out logic
|
||||
if (entry) {
|
||||
|
|
@ -201,7 +209,7 @@ export class AttachmentManager {
|
|||
thumbnail: boolean,
|
||||
asBlob: boolean,
|
||||
progress?: (percent: number) => void
|
||||
): Promise<{data: string | Blob, type: EventAttachmentUrlType}> {
|
||||
): Promise<EventAttachmentUrlData | {data: Blob, type: EventAttachmentUrlType}> {
|
||||
await this.matrixClient.decryptEventIfNeeded(event);
|
||||
|
||||
let urltype: EventAttachmentUrlType = thumbnail ? "thumbnail" : "src";
|
||||
|
|
@ -293,7 +301,7 @@ export class AttachmentManager {
|
|||
const response = await axios.get(url, options);
|
||||
const bytes = decrypt ? await this.decryptData(file, response) : { buffer: response.data };
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -9,9 +9,7 @@ export type KeanuEventExtension = {
|
|||
}
|
||||
|
||||
export type EventAttachmentUrlType = "src" | "thumbnail";
|
||||
export type EventAttachmentLoadSrcOptions = {
|
||||
asBlob?: boolean;
|
||||
}
|
||||
export type EventAttachmentUrlData = {data: string, type: EventAttachmentUrlType};
|
||||
|
||||
export type EventAttachment = {
|
||||
event: MatrixEvent & KeanuEventExtension;
|
||||
|
|
@ -19,13 +17,14 @@ export type EventAttachment = {
|
|||
src?: string;
|
||||
srcSize: number;
|
||||
srcProgress: number;
|
||||
srcPromise?: Promise<{data: string | Blob, type: EventAttachmentUrlType}>;
|
||||
srcPromise?: Promise<EventAttachmentUrlData>;
|
||||
thumbnail?: string;
|
||||
thumbnailProgress: number;
|
||||
thumbnailPromise?: Promise<{data: string | Blob, type: EventAttachmentUrlType}>;
|
||||
thumbnailPromise?: Promise<EventAttachmentUrlData>;
|
||||
autoDownloadable: boolean;
|
||||
loadSrc: (options?: EventAttachmentLoadSrcOptions) => Promise<{data: string | Blob, type: EventAttachmentUrlType}>;
|
||||
loadThumbnail: () => Promise<{data: string | Blob, type: EventAttachmentUrlType}>;
|
||||
loadSrc: () => Promise<EventAttachmentUrlData>;
|
||||
loadThumbnail: () => Promise<EventAttachmentUrlData>;
|
||||
loadBlob: () => Promise<{data: Blob}>;
|
||||
release: (src: boolean, thumbnail: boolean) => void;
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue