More work on export

This commit is contained in:
N-Pex 2025-07-02 15:40:43 +02:00
parent 94bf35875a
commit 2b2c736311
5 changed files with 327 additions and 299 deletions

View file

@ -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;

View file

@ -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;

View file

@ -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 *;

View file

@ -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) {

View file

@ -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;
}; };