Work on export and moving to Vue composition API

This commit is contained in:
N-Pex 2025-06-27 16:10:25 +02:00
parent b0fae3396d
commit 9a124c5ab9
22 changed files with 660 additions and 906 deletions

View file

@ -26,9 +26,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 :is="componentForEvent(event, true)" :room="room" :originalEvent="event" <component
:nextEvent="events[index + 1]" :timelineSet="timelineSet" :componentFn="componentForEventForExport" :is="componentForEvent(event, true)"
ref="exportedEvent" v-on:layout-change="onLayoutChange" /> :room="room"
:originalEvent="event"
:nextEvent="events[index + 1]"
:timelineSet="timelineSet"
:componentFn="componentForEvent"
ref="exportedEvent"
v-on:layout-change="onLayoutChange"
/>
<!-- <div v-if="debugging" style="user-select:text">EventID: {{ event.getId() }}</div> --> <!-- <div v-if="debugging" style="user-select:text">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> -->
</div> </div>
@ -54,16 +61,12 @@
<script> <script>
import MessageIncomingText from "./messages/MessageIncomingText.vue"; import MessageIncomingText from "./messages/MessageIncomingText.vue";
import MessageIncomingFile from "./messages/MessageIncomingFile.vue"; import MessageFile from "./messages/composition/MessageFile.vue";
import MessageIncomingImage from "./messages/MessageIncomingImage.vue"; import MessageImage from "./messages/composition/MessageImage.vue";
import MessageIncomingAudio from "./messages/MessageIncomingAudio.vue"; import MessageIncomingAudio from "./messages/MessageIncomingAudio.vue";
import MessageIncomingVideo from "./messages/MessageIncomingVideo.vue";
import MessageIncomingSticker from "./messages/MessageIncomingSticker.vue"; import MessageIncomingSticker from "./messages/MessageIncomingSticker.vue";
import MessageOutgoingText from "./messages/MessageOutgoingText.vue"; import MessageOutgoingText from "./messages/MessageOutgoingText.vue";
import MessageOutgoingFile from "./messages/MessageOutgoingFile.vue";
import MessageOutgoingImage from "./messages/MessageOutgoingImage.vue";
import MessageOutgoingAudio from "./messages/MessageOutgoingAudio.vue"; import MessageOutgoingAudio from "./messages/MessageOutgoingAudio.vue";
import MessageOutgoingVideo from "./messages/MessageOutgoingVideo.vue";
import MessageOutgoingSticker from "./messages/MessageOutgoingSticker.vue"; import MessageOutgoingSticker from "./messages/MessageOutgoingSticker.vue";
import MessageOutgoingPoll from "./messages/MessageOutgoingPoll.vue"; import MessageOutgoingPoll from "./messages/MessageOutgoingPoll.vue";
import ContactJoin from "./messages/ContactJoin.vue"; import ContactJoin from "./messages/ContactJoin.vue";
@ -94,7 +97,7 @@ import CreatePollDialog from "./CreatePollDialog.vue";
import chatMixin from "./chatMixin"; import chatMixin from "./chatMixin";
import util from "../plugins/utils"; import util from "../plugins/utils";
import { EventTimelineSet } from "matrix-js-sdk"; 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";
@ -105,16 +108,12 @@ export default {
components: { components: {
ChatHeader, ChatHeader,
MessageIncomingText, MessageIncomingText,
MessageIncomingFile, MessageFile,
MessageIncomingImage, MessageImage,
MessageIncomingAudio, MessageIncomingAudio,
MessageIncomingVideo,
MessageIncomingSticker, MessageIncomingSticker,
MessageOutgoingText, MessageOutgoingText,
MessageOutgoingFile,
MessageOutgoingImage,
MessageOutgoingAudio, MessageOutgoingAudio,
MessageOutgoingVideo,
MessageOutgoingSticker, MessageOutgoingSticker,
MessageOutgoingPoll, MessageOutgoingPoll,
ContactJoin, ContactJoin,
@ -180,9 +179,6 @@ export default {
}, },
}, },
methods: { methods: {
componentForEventForExport(event) {
return this.componentForEvent(event, true);
},
cancelExport() { cancelExport() {
this.cancelled = true; this.cancelled = true;
}, },
@ -258,19 +254,25 @@ export default {
this.events = events; this.events = events;
// Need to set thread root events and replyEvents so stuff is rendered correctly. // Need to set thread root events and replyEvents so stuff is rendered correctly.
this.events.filter(event => (event.threadRootId && !event.parentThread)).forEach(event => { this.events
const parentEvent = this.timelineSet.findEventById(event.threadRootId) || this.room.findEventById(event.threadRootId); .filter((event) => event.threadRootId && !event.parentThread)
if (parentEvent) { .forEach((event) => {
parentEvent["isMxThread"] = true; const parentEvent =
event["parentThread"] = parentEvent; this.timelineSet.findEventById(event.threadRootId) || this.room.findEventById(event.threadRootId);
} if (parentEvent) {
}); parentEvent["isMxThread"] = true;
this.events.filter(event => (event.replyEventId && !event.replyEvent)).forEach(event => { event["parentThread"] = parentEvent;
const parentEvent = this.timelineSet.findEventById(event.replyEventId) || this.room.findEventById(event.replyEventId); }
if (parentEvent) { });
event["replyEvent"] = 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. // Wait a tick so UI is updated.
return new Promise((resolve, ignoredReject) => { return new Promise((resolve, ignoredReject) => {
@ -304,8 +306,7 @@ export default {
childComponents.push(parentComp.$refs.exportedEvent); childComponents.push(parentComp.$refs.exportedEvent);
} }
} }
for (const comp of childComponents) { for (const comp of childComponents.filter((c) => c.event != undefined)) {
// Avatars need downloading? // Avatars need downloading?
if (comp.$el && comp.$el.nodeType == 1) { if (comp.$el && comp.$el.nodeType == 1) {
const avatars = comp.$el.getElementsByClassName("v-avatar"); const avatars = comp.$el.getElementsByClassName("v-avatar");
@ -322,29 +323,44 @@ export default {
const img = images[imageIndex]; const img = images[imageIndex];
img.onerror = undefined; img.onerror = undefined;
img.removeAttribute("src"); img.removeAttribute("src");
img.setAttribute("data-exported-src", './avatars/' + fileName); img.setAttribute("data-exported-src", "./avatars/" + fileName);
} }
} }
} };
if (!avatarFolder.file(fileName)) { if (!avatarFolder.file(fileName)) {
const url = member.getAvatarUrl(this.$matrix.matrixClient.getHomeserverUrl(), 40, 40, "scale", true, false, this.$matrix.useAuthedMedia); const url = member.getAvatarUrl(
this.$matrix.matrixClient.getHomeserverUrl(),
40,
40,
"scale",
true,
false,
this.$matrix.useAuthedMedia
);
if (url) { if (url) {
avatarFolder.file(fileName, "empty"); avatarFolder.file(fileName, "empty");
downloadPromises.push( downloadPromises.push(
axios.get(url, { axios
responseType: 'blob' .get(url, {
}) responseType: "blob",
.then(result => { headers: this.$matrix.useAuthedMedia
? {
Authorization: `Bearer ${this.$matrix.matrixClient.getAccessToken()}`,
}
: undefined,
})
.then((result) => {
if (result.data) { if (result.data) {
avatarFolder.file(fileName, result.data); avatarFolder.file(fileName, result.data);
setSource(fileName); setSource(fileName);
} }
}) })
.catch(err => { .catch((err) => {
console.error("Download error: ", err); console.error("Download error: ", err);
avatarFolder.remove(fileName); avatarFolder.remove(fileName);
})); })
);
} }
} else { } else {
setSource(fileName); setSource(fileName);
@ -353,139 +369,107 @@ export default {
} }
} }
let componentClass = comp.$options ? comp.$options.__file.split("/").reverse()[0].split(".")[0] : "invalid_component"; let componentClass = comp.$options
switch (componentClass) { ? comp.$options.__file.split("/").reverse()[0].split(".")[0]
case "MessageIncomingImageExport": : "invalid_component";
case "MessageOutgoingImageExport": let attachment =
// TODO - maybe consider what media to download based on the file size we already have? comp.event && comp.event.getId
// info = comp.event.getContent().info; ? this.$matrix.attachmentManager.getEventAttachment(comp.event)
// if (info && info.size && currentMediaSize + info.size > maxMediaSize) { : undefined;
// // No need to even download.
// console.log("Dont download!");
// continue;
// }
downloadPromises.push( if (attachment && (attachment.srcSize = 0 || currentMediaSize + attachment.srcSize <= maxMediaSize)) {
util downloadPromises.push(
.getAttachment(this.$matrix.matrixClient, this.$matrix.useAuthedMedia, comp.event, null, true) attachment
.then((blob) => { .loadSrc({ asBlob: true })
return new Promise((resolve, ignoredReject) => { .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":
extension = ".jpg";
break;
case "image/gif":
extension = ".gif";
}
if (currentMediaSize + blob.size <= maxMediaSize) {
currentMediaSize += blob.size;
let fileName = comp.event.getId() + extension; switch (componentClass) {
imageFolder.file(fileName, blob); // TODO calc bytes 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";
}
// Update source let fileName = comp.event.getId() + extension;
const images = comp.$el.getElementsByTagName("img"); imageFolder.file(fileName, blob); // TODO calc bytes
for (let imageIndex = 0; imageIndex < images.length; imageIndex++) {
const img = images[imageIndex]; // Update source
img.removeAttribute("src"); const images = comp.$el.getElementsByTagName("img");
img.setAttribute("data-exported-src", './images/' + fileName); 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;
} }
this.processedEvents += 1; break;
resolve(true);
} case "MessageIncomingAudioExport":
}); case "MessageOutgoingAudioExport":
}) {
.catch((ignoredErr) => { var extension = ".webm";
this.processedEvents += 1; let fileName = comp.event.getId() + extension;
}) audioFolder.file(fileName, blob); // TODO calc bytes
); let elements = comp.$el.getElementsByTagName("audio");
break; let element = elements && elements[0];
case "MessageIncomingAudioExport": if (element) {
case "MessageOutgoingAudioExport": element.setAttribute("data-exported-src", "./audio/" + fileName);
downloadPromises.push( }
util this.processedEvents += 1;
.getAttachment(this.$matrix.matrixClient, this.$matrix.useAuthedMedia, comp.event, null, true)
.then((blob) => {
if (currentMediaSize + blob.size <= maxMediaSize) {
currentMediaSize += blob.size;
return new Promise((resolve, ignoredReject) => {
//let mime = blob.type;
var extension = ".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;
resolve(true);
}); case "MessageIncomingVideoExport":
} case "MessageOutgoingVideoExport":
}) {
.catch((ignoredErr) => { var extension = ".mp4";
this.processedEvents += 1; let fileName = comp.event.getId() + extension;
}) videoFolder.file(fileName, blob); // TODO calc bytes
); // comp.src = "./video/" + fileName;
break; let elements = comp.$el.getElementsByTagName("video");
case "MessageIncomingVideoExport": let element = elements && elements[0];
case "MessageOutgoingVideoExport": if (element) {
downloadPromises.push( element.setAttribute("data-exported-src", "./video/" + fileName);
util }
.getAttachment(this.$matrix.matrixClient, this.$matrix.useAuthedMedia, comp.event, null, true) this.processedEvents += 1;
.then((blob) => {
if (currentMediaSize + blob.size <= maxMediaSize) {
currentMediaSize += blob.size;
return new Promise((resolve, ignoredReject) => {
//let mime = blob.type;
var extension = ".mp4";
let fileName = comp.event.getId() + extension;
videoFolder.file(fileName, blob); // TODO calc bytes
// 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;
resolve(true);
}); 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;
}
break;
} }
})
.catch((ignoredErr) => {
this.processedEvents += 1; this.processedEvents += 1;
}) return true;
); } else {
break;
case "MessageIncomingFileExport":
case "MessageOutgoingFileExport":
downloadPromises.push(
util
.getAttachment(this.$matrix.matrixClient, this.$matrix.useAuthedMedia, comp.event, null, true)
.then((blob) => {
if (currentMediaSize + blob.size <= maxMediaSize) {
currentMediaSize += blob.size;
return new Promise((resolve, ignoredReject) => {
var extension = util.getFileExtension(comp.event);
let fileName = comp.event.getId() + extension;
filesFolder.file(fileName, blob);
comp.href="./files/" + fileName;
this.processedEvents += 1;
resolve(true);
});
}
})
.catch((ignoredErr) => {
this.processedEvents += 1; this.processedEvents += 1;
}) return false;
); }
break; })
default: .catch((ignoredErr) => {
this.processedEvents += 1; this.processedEvents += 1;
break; })
);
} else {
this.processedEvents += 1;
} }
} }
} }
@ -496,7 +480,7 @@ export default {
let root = this.$refs.exportRoot; let root = this.$refs.exportRoot;
var doc = "<!DOCTYPE html>\n<html><head>\n<meta charset=\"utf-8\"/>\n"; var doc = '<!DOCTYPE html>\n<html><head>\n<meta charset="utf-8"/>\n';
for (const sheet of document.styleSheets) { for (const sheet of document.styleSheets) {
doc += "<style type='text/css'>\n"; doc += "<style type='text/css'>\n";
@ -542,7 +526,8 @@ export default {
this.$emit("close"); this.$emit("close");
}); });
}, },
onLayoutChange(action, ignoredelement) { onLayoutChange(event) {
const { action, element } = event;
action(); action();
}, },
}, },
@ -551,7 +536,8 @@ export default {
<style lang="scss"> <style lang="scss">
.chat-root.export { .chat-root.export {
.messageIn-thread, .messageOut-thread { .messageIn-thread,
.messageOut-thread {
/** For media threads, hide all duplicated metadata, like /** For media threads, hide all duplicated metadata, like
sender, sender avatar, time, quick reactions etc. They are sender, sender avatar, time, quick reactions etc. They are
shown for the root thread event */ shown for the root thread event */
@ -561,8 +547,11 @@ export default {
.messageOut { .messageOut {
margin-right: 50px !important; margin-right: 50px !important;
} }
.messageIn, .messageOut { .messageIn,
.quick-reaction-container, .senderAndTime, .avatar { .messageOut {
.quick-reaction-container,
.senderAndTime,
.avatar {
display: none; display: none;
} }
} }

View file

@ -1,30 +1,25 @@
import { markRaw } from "vue"; import { markRaw } from "vue";
import util, { ROOM_TYPE_CHANNEL, STATE_EVENT_ROOM_DELETION_NOTICE } from "../plugins/utils"; import util, { ROOM_TYPE_CHANNEL, STATE_EVENT_ROOM_DELETION_NOTICE } from "../plugins/utils";
import MessageIncomingText from "./messages/MessageIncomingText"; import MessageIncomingText from "./messages/MessageIncomingText";
import MessageIncomingFile from "./messages/MessageIncomingFile"; import MessageFile from "./messages/composition/MessageFile.vue";
import MessageIncomingImage from "./messages/MessageIncomingImage.vue"; import MessageImage from "./messages/composition/MessageImage.vue";
import MessageIncomingAudio from "./messages/MessageIncomingAudio.vue"; import MessageIncomingAudio from "./messages/MessageIncomingAudio.vue";
import MessageIncomingVideo from "./messages/MessageIncomingVideo.vue"; import MessageVideo from "./messages/composition/MessageVideo.vue";
import MessageIncomingSticker from "./messages/MessageIncomingSticker.vue"; import MessageIncomingSticker from "./messages/MessageIncomingSticker.vue";
import MessageIncomingPoll from "./messages/MessageIncomingPoll.vue"; import MessageIncomingPoll from "./messages/MessageIncomingPoll.vue";
import MessageIncomingThread from "./messages/composition/MessageIncomingThread.vue"; import MessageThread from "./messages/composition/MessageThread.vue";
import MessageOutgoingText from "./messages/MessageOutgoingText"; import MessageOutgoingText from "./messages/MessageOutgoingText";
import MessageOutgoingFile from "./messages/MessageOutgoingFile";
import MessageOutgoingImage from "./messages/MessageOutgoingImage.vue";
import MessageOutgoingAudio from "./messages/MessageOutgoingAudio.vue"; import MessageOutgoingAudio from "./messages/MessageOutgoingAudio.vue";
import MessageOutgoingVideo from "./messages/MessageOutgoingVideo.vue";
import MessageOutgoingSticker from "./messages/MessageOutgoingSticker.vue"; import MessageOutgoingSticker from "./messages/MessageOutgoingSticker.vue";
import MessageOutgoingPoll from "./messages/MessageOutgoingPoll.vue"; import MessageOutgoingPoll from "./messages/MessageOutgoingPoll.vue";
import MessageOutgoingThread from "./messages/composition/MessageOutgoingThread.vue";
import MessageIncomingImageExport from "./messages/export/MessageIncomingImageExport"; import MessageIncomingImageExport from "./messages/export/MessageIncomingImageExport";
import MessageIncomingAudioExport from "./messages/export/MessageIncomingAudioExport"; import MessageIncomingAudioExport from "./messages/export/MessageIncomingAudioExport";
import MessageIncomingVideoExport from "./messages/export/MessageIncomingVideoExport"; import MessageIncomingVideoExport from "./messages/export/MessageIncomingVideoExport";
import MessageIncomingThreadExport from "./messages/export/MessageIncomingThreadExport"; import MessageThreadExport from "./messages/composition/MessageThreadExport.vue";
import MessageIncomingFileExport from "./messages/export/MessageIncomingFileExport"; import MessageIncomingFileExport from "./messages/export/MessageIncomingFileExport";
import MessageOutgoingImageExport from "./messages/export/MessageOutgoingImageExport"; import MessageOutgoingImageExport from "./messages/export/MessageOutgoingImageExport";
import MessageOutgoingAudioExport from "./messages/export/MessageOutgoingAudioExport"; import MessageOutgoingAudioExport from "./messages/export/MessageOutgoingAudioExport";
import MessageOutgoingVideoExport from "./messages/export/MessageOutgoingVideoExport"; import MessageOutgoingVideoExport from "./messages/export/MessageOutgoingVideoExport";
import MessageOutgoingThreadExport from "./messages/export/MessageOutgoingThreadExport";
import MessageOutgoingFileExport from "./messages/export/MessageOutgoingFileExport"; import MessageOutgoingFileExport from "./messages/export/MessageOutgoingFileExport";
import ContactJoin from "./messages/ContactJoin.vue"; import ContactJoin from "./messages/ContactJoin.vue";
import ContactLeave from "./messages/ContactLeave.vue"; import ContactLeave from "./messages/ContactLeave.vue";
@ -65,19 +60,15 @@ export default {
components: { components: {
ChatHeader, ChatHeader,
MessageIncomingText, MessageIncomingText,
MessageIncomingFile, MessageFile,
MessageIncomingImage, MessageImage,
MessageIncomingAudio, MessageIncomingAudio,
MessageIncomingVideo, MessageVideo,
MessageIncomingSticker, MessageIncomingSticker,
MessageIncomingThread, MessageThread,
MessageOutgoingText, MessageOutgoingText,
MessageOutgoingFile,
MessageOutgoingImage,
MessageOutgoingAudio, MessageOutgoingAudio,
MessageOutgoingVideo,
MessageOutgoingSticker, MessageOutgoingSticker,
MessageOutgoingThread,
MessageOutgoingPoll, MessageOutgoingPoll,
ContactJoin, ContactJoin,
ContactLeave, ContactLeave,
@ -138,6 +129,7 @@ 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;
@ -189,7 +181,7 @@ export default {
} }
if (event.isMxThread) { if (event.isMxThread) {
// Incoming thread, e.g. a file drop! // Incoming thread, e.g. a file drop!
return isForExport ? MessageIncomingThreadExport : MessageIncomingThread; return isForExport ? MessageThreadExport : MessageThread;
} }
if (event.getContent().msgtype == "m.image") { if (event.getContent().msgtype == "m.image") {
// For SVG, make downloadable // For SVG, make downloadable
@ -201,12 +193,12 @@ export default {
if (isForExport) { if (isForExport) {
return MessageIncomingFileExport; return MessageIncomingFileExport;
} }
return MessageIncomingFile; return MessageFile;
} }
if (isForExport) { if (isForExport) {
return MessageIncomingImageExport; return MessageIncomingImageExport;
} }
return MessageIncomingImage; return MessageImage;
} else if (event.getContent().msgtype == "m.audio") { } else if (event.getContent().msgtype == "m.audio") {
if (isForExport) { if (isForExport) {
return MessageIncomingAudioExport; return MessageIncomingAudioExport;
@ -216,12 +208,12 @@ export default {
if (isForExport) { if (isForExport) {
return MessageIncomingVideoExport; return MessageIncomingVideoExport;
} }
return MessageIncomingVideo; return MessageVideo;
} else if (event.getContent().msgtype == "m.file") { } else if (event.getContent().msgtype == "m.file") {
if (isForExport) { if (isForExport) {
return MessageIncomingFileExport; return MessageIncomingFileExport;
} }
return MessageIncomingFile; return MessageFile;
} else if (stickers.isStickerShortcode(event.getContent().body)) { } else if (stickers.isStickerShortcode(event.getContent().body)) {
return MessageIncomingSticker; return MessageIncomingSticker;
} }
@ -236,7 +228,7 @@ export default {
} }
if (event.isMxThread) { if (event.isMxThread) {
// Outgoing thread // Outgoing thread
return isForExport ? MessageOutgoingThreadExport : MessageOutgoingThread; return isForExport ? MessageThreadExport : MessageThread;
} }
if (event.getContent().msgtype == "m.image") { if (event.getContent().msgtype == "m.image") {
// For SVG, make downloadable // For SVG, make downloadable
@ -245,12 +237,12 @@ export default {
event.getContent().info.mimetype && event.getContent().info.mimetype &&
event.getContent().info.mimetype.startsWith("image/svg") event.getContent().info.mimetype.startsWith("image/svg")
) { ) {
return MessageOutgoingImage; return MessageImage;
} }
if (isForExport) { if (isForExport) {
return MessageOutgoingImageExport; return MessageOutgoingImageExport;
} }
return MessageOutgoingImage; return MessageImage;
} else if (event.getContent().msgtype == "m.audio") { } else if (event.getContent().msgtype == "m.audio") {
if (isForExport) { if (isForExport) {
return MessageOutgoingAudioExport; return MessageOutgoingAudioExport;
@ -260,12 +252,12 @@ export default {
if (isForExport) { if (isForExport) {
return MessageOutgoingVideoExport; return MessageOutgoingVideoExport;
} }
return MessageOutgoingVideo; return MessageVideo;
} else if (event.getContent().msgtype == "m.file") { } else if (event.getContent().msgtype == "m.file") {
if (isForExport) { if (isForExport) {
return MessageOutgoingFileExport; return MessageOutgoingFileExport;
} }
return MessageOutgoingFile; return MessageFile;
} else if (stickers.isStickerShortcode(event.getContent().body)) { } else if (stickers.isStickerShortcode(event.getContent().body)) {
return MessageOutgoingSticker; return MessageOutgoingSticker;
} }

View file

@ -1,34 +0,0 @@
<template>
<message-incoming v-bind="{...$props, ...$attrs}">
<div class="bubble">
<div class="original-message" v-if="inReplyToText">
<div class="original-message-sender">{{ inReplyToSender }}</div>
<div
class="original-message-text"
v-html="linkify($sanitize(inReplyToText))"
/>
</div>
<div class="message">
<ThumbnailView class="clickable" v-on:itemclick="$emit('download')" :item="{ event: event, src: null }" />
<span class="edit-marker" v-if="event.replacingEventId()"
>{{ $t('message.edited') }}</span
>
</div>
</div>
</message-incoming>
</template>
<script>
import ThumbnailView from '../file_mode/ThumbnailView.vue';
import MessageIncoming from "./MessageIncoming.vue";
export default {
extends: MessageIncoming,
components: { MessageIncoming, ThumbnailView }
};
</script>
<style lang="scss">
@use "@/assets/css/chat.scss" as *;
</style>

View file

@ -1,85 +0,0 @@
<template>
<message-incoming v-bind="{ ...$props, ...$attrs }" v-intersect="onIntersect">
<div class="bubble image-bubble" ref="imageRef">
<ImageWithProgress
:aspect-ratio="16 / 9"
ref="image"
:src="eventAttachment.src ? eventAttachment.src : eventAttachment.thumbnail"
:cover="cover"
:contain="contain"
:loadingProgress="eventAttachment.thumbnailProgress"
/>
</div>
<v-dialog v-model="dialog" :width="$vuetify.display.smAndUp ? '940px' : '90%'">
<ImageWithProgress :src="eventAttachment.src ? eventAttachment.src : eventAttachment.thumbnail" :loadingProgress="eventAttachment.srcProgress" />
</v-dialog>
</message-incoming>
</template>
<script>
import util from "../../plugins/utils";
import ImageWithProgress from "../ImageWithProgress.vue";
import MessageIncoming from "./MessageIncoming.vue";
export default {
extends: MessageIncoming,
components: { MessageIncoming, ImageWithProgress },
data() {
return {
eventAttachment: {},
cover: true,
contain: false,
dialog: false,
isVisible: false,
};
},
methods: {
// listen for custom hammerJs singletab click to differentiate it from double click(heart animation).
initMessageInImageHammerJs(element) {
const hammerInstance = util.singleOrDoubleTabRecognizer(element);
hammerInstance.on("singletap doubletap", (ev) => {
if (ev.type === "singletap") {
this.eventAttachment?.loadSrc();
this.dialog = true;
}
});
},
loadThumbnail() {
if (this.isVisible) {
this.eventAttachment?.loadThumbnail();
}
},
onIntersect(isIntersecting, entries, observer) {
this.isVisible = isIntersecting;
this.loadThumbnail();
}
},
mounted() {
//console.log("Mounted with event:", JSON.stringify(this.event.getContent()));
const info = this.event.getContent().info;
// JPEGs use cover, PNG and GIF ect contain. This is because PNG and GIF are expected to
// be stickers and small emoji type things.
if (info && info.mimetype && info.mimetype.startsWith("image/jp")) {
this.cover = true;
this.contain = false;
} else {
this.cover = false;
this.contain = true;
}
if (this.$refs.imageRef) {
this.initMessageInImageHammerJs(this.$refs.imageRef);
}
this.eventAttachment = this.$matrix.attachmentManager.getEventAttachment(this.event);
this.loadThumbnail();
},
beforeUnmount() {
this.eventAttachment?.release();
},
};
</script>
<style lang="scss">
@use "@/assets/css/chat.scss" as *;
</style>

View file

@ -1,40 +0,0 @@
<template>
<message-incoming v-bind="{ ...$props, ...$attrs }">
<div class="bubble image-bubble">
<v-responsive :aspect-ratio="16 / 9" :src="src">
<video :src="src" controls class="w-100 h-100">
{{$t('fallbacks.video_file')}}
</video>
<div v-if="downloadProgress" class="download-overlay">
<div class="text-center download-text">
{{ $t('message.download_progress',{percentage: downloadProgress}) }}
</div>
</div>
<div v-else-if="userInitiatedDownloadsOnly && !src" class="download-overlay">
<div class="text-center download-text">
{{ fileName }}
</div>
<div class="text-center download-size">
{{ fileSize }}
</div>
<v-icon size="32" color="white" class="clickable" @click="loadAttachmentSource(event, true)">download</v-icon>
</div>
</v-responsive>
</div>
</message-incoming>
</template>
<script>
import attachmentMixin from "./attachmentMixin";
import MessageIncoming from "./MessageIncoming.vue";
export default {
extends: MessageIncoming,
components: { MessageIncoming },
mixins: [attachmentMixin],
};
</script>
<style lang="scss">
@use "@/assets/css/chat.scss" as *;
</style>

View file

@ -1,34 +0,0 @@
<template>
<message-outgoing v-bind="{ ...$props, ...$attrs }">
<div class="bubble">
<div class="original-message" v-if="inReplyToText">
<div class="original-message-sender">{{ inReplyToSender }}</div>
<div
class="original-message-text"
v-html="linkify($sanitize(inReplyToText))"
/>
</div>
<div class="message">
<ThumbnailView class="clickable" v-on:itemclick="$emit('download')" :item="{ event: event, src: null }" />
<span class="edit-marker" v-if="event.replacingEventId()"
>{{ $t('message.edited') }}</span
>
</div>
</div>
</message-outgoing>
</template>
<script>
import ThumbnailView from '../file_mode/ThumbnailView.vue';
import MessageOutgoing from "./MessageOutgoing.vue";
export default {
extends: MessageOutgoing,
components: { MessageOutgoing, ThumbnailView },
};
</script>
<style lang="scss">
@use "@/assets/css/chat.scss" as *;
</style>

View file

@ -1,83 +0,0 @@
<template>
<message-outgoing v-bind="{ ...$props, ...$attrs }" v-intersect="onIntersect">
<div class="bubble image-bubble" ref="imageRef">
<ImageWithProgress
:aspect-ratio="16 / 9"
ref="image"
:src="eventAttachment.src ? eventAttachment.src : eventAttachment.thumbnail"
:cover="cover"
:contain="contain"
:loadingProgress="eventAttachment.thumbnailProgress"
/>
</div>
<v-dialog v-model="dialog" :width="$vuetify.display.smAndUp ? '940px' : '90%'">
<ImageWithProgress :src="eventAttachment.src ? eventAttachment.src : eventAttachment.thumbnail" :loadingProgress="eventAttachment.srcProgress" />
</v-dialog>
</message-outgoing>
</template>
<script>
import util from "../../plugins/utils";
import ImageWithProgress from "../ImageWithProgress.vue";
import MessageOutgoing from "./MessageOutgoing.vue";
export default {
extends: MessageOutgoing,
components: { MessageOutgoing, ImageWithProgress },
data() {
return {
eventAttachment: {},
cover: true,
contain: false,
dialog: false,
isVisible: false,
};
},
methods: {
// listen for custom hammerJs singletab click to differentiate it from double click(heart animation).
initMessageOutImageHammerJs(element) {
const hammerInstance = util.singleOrDoubleTabRecognizer(element);
hammerInstance.on("singletap doubletap", (ev) => {
if (ev.type === "singletap") {
this.eventAttachment?.loadSrc();
this.dialog = true;
}
});
},
loadThumbnail() {
if (this.isVisible) {
this.eventAttachment?.loadThumbnail();
}
},
onIntersect(isIntersecting, entries, observer) {
this.isVisible = isIntersecting;
this.loadThumbnail();
}
},
mounted() {
const info = this.event.getContent().info;
// JPEGs use cover, PNG and GIF ect contain. This is because PNG and GIF are expected to
// be stickers and small emoji type things.
if (info && info.mimetype && info.mimetype.startsWith("image/jp")) {
this.cover = true;
this.contain = false;
} else {
this.cover = false;
this.contain = true;
}
if (this.$refs.imageRef) {
this.initMessageOutImageHammerJs(this.$refs.imageRef);
}
this.eventAttachment = this.$matrix.attachmentManager.getEventAttachment(this.event);
this.loadThumbnail();
},
beforeUnmount() {
this.eventAttachment?.release();
},
};
</script>
<style lang="scss">
@use "@/assets/css/chat.scss" as *;
</style>

View file

@ -1,40 +0,0 @@
<template>
<message-outgoing v-bind="{ ...$props, ...$attrs }">
<div class="bubble image-bubble">
<v-responsive :aspect-ratio="16 / 9" class="ma-0 pa-0">
<video :src="src" controls class="w-100 h-100">
{{$t('fallbacks.video_file')}}
</video>
<div v-if="downloadProgress" class="download-overlay">
<div class="text-center download-text">
{{ $t('message.download_progress',{percentage: downloadProgress}) }}
</div>
</div>
<div v-else-if="userInitiatedDownloadsOnly && !src" class="download-overlay">
<div class="text-center download-text">
{{ fileName }}
</div>
<div class="text-center download-size">
{{ fileSize }}
</div>
<v-icon size="32" color="white" class="clickable" @click="loadAttachmentSource(event, true)">download</v-icon>
</div>
</v-responsive>
</div>
</message-outgoing>
</template>
<script>
import attachmentMixin from "./attachmentMixin";
import MessageOutgoing from "./MessageOutgoing.vue";
export default {
extends: MessageOutgoing,
components: { MessageOutgoing },
mixins: [attachmentMixin],
};
</script>
<style lang="scss">
@use "@/assets/css/chat.scss" as *;
</style>

View file

@ -0,0 +1,55 @@
<template>
<component :is="rootComponent" v-bind="{ ...$props, ...$attrs }">
<div class="bubble">
{{ inOut }}
<div class="original-message" v-if="inReplyToText">
<div class="original-message-sender">{{ inReplyToSender }}</div>
<div class="original-message-text" v-html="linkify($$sanitize(inReplyToText))" />
</div>
<div class="message">
<ThumbnailView class="clickable" v-on:itemclick="onDownload" :item="attachment" />
<span class="edit-marker" v-if="event?.replacingEventId()">{{ $t("message.edited") }}</span>
</div>
</div>
</component>
</template>
<script setup lang="ts">
import { computed, inject, ref, Ref } from "vue";
import MessageIncoming from "./MessageIncoming.vue";
import MessageOutgoing from "./MessageOutgoing.vue";
import ThumbnailView from "../../file_mode/ThumbnailView.vue";
import { useI18n } from "vue-i18n";
import { MessageEmits, MessageProps, useMessage } from "./useMessage";
import { KeanuEvent } from "../../../models/eventAttachment";
const { t } = useI18n();
const $matrix: any = inject("globalMatrix");
const $$sanitize: any = inject("globalSanitize");
const inOut: Ref<"in" | "out"> = ref("in");
const emits = defineEmits<MessageEmits & { (event: "download", value: KeanuEvent | undefined): void }>();
const props = defineProps<MessageProps>();
const { event, isIncoming, attachment, inReplyToText, inReplyToSender, linkify } = useMessage(
$matrix,
t,
props,
emits,
undefined
);
const rootComponent = computed(() => {
return isIncoming.value ? MessageIncoming : MessageOutgoing;
});
const onDownload = () => {
emits("download", event.value);
};
</script>
<style lang="scss">
@use "@/assets/css/chat.scss" as *;
</style>

View file

@ -0,0 +1,97 @@
<template>
<component
:is="rootComponent"
ref="root"
v-bind="{ ...$props, ...$attrs }">
<div class="bubble image-bubble" ref="imageRef">
<ImageWithProgress v-if="attachment"
:aspect-ratio="16 / 9"
ref="image"
:src="attachment.src ? attachment.src : attachment.thumbnail"
:cover="cover"
:contain="contain"
:loadingProgress="attachment.thumbnailProgress"
/>
</div>
<v-dialog v-model="dialog" :width="smAndUp ? '940px' : '90%'">
<ImageWithProgress :src="attachment?.src ? attachment.src : attachment?.thumbnail" :loadingProgress="attachment?.srcProgress" />
</v-dialog>
</component>
</template>
<script setup lang="ts">
import { computed, inject, onMounted, ref, useTemplateRef, watch } from "vue";
import MessageIncoming from "./MessageIncoming.vue";
import MessageOutgoing from "./MessageOutgoing.vue";
import ImageWithProgress from "../../ImageWithProgress.vue";
import { useLazyLoad } from "./useLazyLoad";
import { useI18n } from "vue-i18n";
import { MessageEmits, MessageProps, useMessage } from "./useMessage";
import { EventAttachment } from "../../../models/eventAttachment";
import { useDisplay } from "vuetify";
import utils from "@/plugins/utils";
import Hammer from "hammerjs";
const { t } = useI18n()
const $matrix: any = inject('globalMatrix');
type RootType = InstanceType<typeof MessageOutgoing | typeof MessageIncoming>
const rootRef = useTemplateRef<RootType>("root");
const imageRef = useTemplateRef("imageRef");
const emits = defineEmits<MessageEmits>();
const props = defineProps<MessageProps>();
const cover = ref(true);
const contain = ref(false);
const dialog = ref(false);
const { smAndUp } = useDisplay();
const {
isVisible
} = useLazyLoad({ root: rootRef });
const {
event,
isIncoming,
attachment,
} = useMessage($matrix, t, props, emits, undefined);
const rootComponent = computed(() => {
return isIncoming.value ? MessageIncoming : MessageOutgoing;
})
watch([isVisible, attachment], ([_v, _a]: [_v: boolean, _a: EventAttachment | undefined]) => {
if (_v && _a) {
_a.loadThumbnail();
}
});
onMounted(() => {
const info = event.value?.getContent().info;
// JPEGs use cover, PNG and GIF ect contain. This is because PNG and GIF are expected to
// be stickers and small emoji type things.
if (info && info.mimetype && info.mimetype.startsWith("image/jp")) {
cover.value = true;
contain.value = false;
} else {
cover.value = false;
contain.value = true;
}
if (imageRef.value) {
const hammerInstance = utils.singleOrDoubleTabRecognizer(imageRef.value);
hammerInstance.on("singletap doubletap", (ev: Hammer.HammerInput) => {
if (ev.type === "singletap") {
attachment.value?.loadSrc();
dialog.value = true;
}
});
}
});
</script>
<style lang="scss">
@use "@/assets/css/chat.scss" as *;
</style>

View file

@ -31,7 +31,7 @@
<script setup lang="ts"> <script setup lang="ts">
import SeenBy from "../SeenBy.vue"; import SeenBy from "../SeenBy.vue";
import { MessageEmits, MessageProps, useMessage } from "./messageMixin"; import { MessageEmits, MessageProps, useMessage } from "./useMessage";
import util, { ROOM_TYPE_CHANNEL } from "@/plugins/utils"; import util, { ROOM_TYPE_CHANNEL } from "@/plugins/utils";
import QuickReactions from "../QuickReactions.vue"; import QuickReactions from "../QuickReactions.vue";
import QuickReactionsChannel from "../channel/QuickReactionsChannel.vue"; import QuickReactionsChannel from "../channel/QuickReactionsChannel.vue";

View file

@ -37,7 +37,7 @@
<script setup lang="ts"> <script setup lang="ts">
import SeenBy from "../SeenBy.vue"; import SeenBy from "../SeenBy.vue";
import { MessageEmits, MessageProps, useMessage } from "./messageMixin"; import { MessageEmits, MessageProps, useMessage } from "./useMessage";
import util, { ROOM_TYPE_CHANNEL } from "@/plugins/utils"; import util, { ROOM_TYPE_CHANNEL } from "@/plugins/utils";
import QuickReactions from "../QuickReactions.vue"; import QuickReactions from "../QuickReactions.vue";
import QuickReactionsChannel from "../channel/QuickReactionsChannel.vue"; import QuickReactionsChannel from "../channel/QuickReactionsChannel.vue";

View file

@ -1,228 +0,0 @@
<template>
<MessageOutgoing
ref="root"
v-bind="{ ...$props, ...$attrs }"
v-if="showMultiview"
v-intersect="onIntersect"
>
<div class="bubble">
<div class="original-message" v-if="inReplyToText">
<div class="original-message-sender">{{ inReplyToSender }}</div>
<div class="original-message-text" v-html="linkify($$sanitize(inReplyToText))" />
</div>
<div class="message">
<SwipeableThumbnailsView
:items="items"
v-if="event && !event.isRedacted() && room.displayType == ROOM_TYPE_CHANNEL"
v-bind="$attrs"
/>
<v-container v-else-if="event && !event.isRedacted()" fluid class="imageCollection">
<v-row wrap>
<v-col v-for="{ size, item } in layoutedItems" :key="item.event.getId()" :cols="size">
<ThumbnailView :item="item" :previewOnly="true" v-on:itemclick="onItemClick($event)" />
</v-col>
</v-row>
</v-container>
<i v-if="event && event.isRedacted()" class="deleted-text">
<v-icon size="small">block</v-icon>
{{
redactedBySomeoneElse(event)
? t("message.incoming_message_deleted_text")
: t("message.outgoing_message_deleted_text")
}}
</i>
<span v-html="linkify($$sanitize(messageText))" v-else-if="messageText" />
<span class="edit-marker" v-if="event && event.replacingEventId() && !event.isRedacted()">
{{ t("message.edited") }}
</span>
</div>
</div>
<GalleryItemsView
:originalEvent="originalEvent"
:items="items"
:initialItem="showItem"
v-if="!!showItem"
v-on:close="showItem = undefined"
/>
</MessageOutgoing>
<component
v-else-if="items.length == 1"
:is="componentFn(items[0].event)"
v-bind="{ ...$props, ...$attrs }"
:originalEvent="items[0].event"
/>
</template>
<script setup lang="ts">
import MessageOutgoing from "./MessageOutgoing.vue";
import { MessageEmits, MessageProps, useMessage } from "./messageMixin";
import util, { ROOM_TYPE_CHANNEL } from "@/plugins/utils";
import GalleryItemsView from "../../file_mode/GalleryItemsView.vue";
import ThumbnailView from "../../file_mode/ThumbnailView.vue";
import SwipeableThumbnailsView from "../channel/SwipeableThumbnailsView.vue";
import { computed, inject, onBeforeUnmount, ref, Ref, watch } from "vue";
import { EventAttachment } from "../../../models/eventAttachment";
import { useI18n } from 'vue-i18n'
import { MatrixEvent, MatrixEventEvent } from "matrix-js-sdk";
const { t } = useI18n()
const $matrix: any = inject('globalMatrix');
const $$sanitize: any = inject('globalSanitize');
const root = ref(undefined);
const emits = defineEmits<MessageEmits & {(event: "layout-change", value: {element: Element | undefined, action: () => void}): void}>();
const items: Ref<EventAttachment[]> = ref([]);
const showItem: Ref<EventAttachment | undefined> = ref(undefined);
const isVisible: Ref<boolean> = ref(false);
const props = defineProps<MessageProps>();
const { room } = props;
const processThread = () => {
if (!event.value?.isRedacted()) {
emits("layout-change", {element: root.value, action: _processThread});
}
};
const {
event,
thread,
inReplyToSender,
inReplyToText,
messageText,
redactedBySomeoneElse,
linkify,
} = useMessage($matrix, t, props, emits, processThread);
watch(event, () => {
if (event.value) {
if (thread.value === undefined) {
thread.value = props.timelineSet.relations.getChildEventsForEvent(
event.value.getId() ?? "",
util.threadMessageType(),
"m.room.message"
);
}
if (!thread.value) {
event.value.on(MatrixEventEvent.RelationsCreated, onRelationsCreated);
}
}
}, { immediate: true});
onBeforeUnmount(() => {
event.value?.off(MatrixEventEvent.RelationsCreated, onRelationsCreated);
});
const showMultiview = computed((): boolean => {
return items.value?.length > 1 ||
(event.value && event.value.isRedacted()) ||
(props.room.displayType == ROOM_TYPE_CHANNEL && items.value.length == 1 && util.isFileTypePDF(items.value[0].event)) ||
messageText.value?.length > 0
});
const onRelationsCreated = () => {
if (event.value) {
thread.value = props.timelineSet.relations.getChildEventsForEvent(
event.value.getId() ?? "",
util.threadMessageType(),
"m.room.message"
);
event.value.off(MatrixEventEvent.RelationsCreated, onRelationsCreated);
}
};
const onItemClick = (event: any) => {
showItem.value = event.item;
};
const _processThread = () => {
const eventItems = props.timelineSet.relations
.getAllChildEventsForEvent(event.value?.getId() ?? "")
.filter((e: MatrixEvent) => !e.isRedacted() && util.downloadableTypes().includes(e.getContent().msgtype));
items.value = eventItems.map((e: MatrixEvent) => {
let ea = $matrix.attachmentManager.getEventAttachment(e);
if (showMultiview.value && isVisible.value) {
ea.loadThumbnail();
}
return ea;
});
};
const layoutedItems = computed(() => {
if (!items.value || items.value.length == 0) {
return [];
}
let array = items.value.slice(0);
let rows = [];
while (array.length > 0) {
if (array.length >= 7) {
rows.push({ size: 6, item: array[0] });
rows.push({ size: 6, item: array[1] });
rows.push({ size: 12, item: array[2] });
rows.push({ size: 3, item: array[3] });
rows.push({ size: 3, item: array[4] });
rows.push({ size: 3, item: array[5] });
rows.push({ size: 3, item: array[6] });
array = array.slice(7);
} else if (array.length >= 3) {
rows.push({ size: 6, item: array[0] });
rows.push({ size: 6, item: array[1] });
rows.push({ size: 12, item: array[2] });
array = array.slice(3);
} else if (array.length >= 2) {
rows.push({ size: 6, item: array[0] });
rows.push({ size: 6, item: array[1] });
array = array.slice(2);
} else {
rows.push({ size: 12, item: array[0] });
array = array.slice(1);
}
}
return rows;
});
const onIntersect = (isIntersecting: boolean, entries: any, observer: any) => {
isVisible.value = isIntersecting;
if (showMultiview.value && isIntersecting) {
items.value.forEach((a) => a.loadThumbnail());
}
};
</script>
<style lang="scss">
@use "@/assets/css/chat.scss" as *;
</style>
<style lang="scss" scoped>
.bubble {
width: 100%;
}
.imageCollection {
border-radius: 15px;
padding: 0;
overflow: hidden;
.row {
margin: -4px; // Compensate for column padding, so the border-radius above looks round!
padding: 0;
}
.col {
padding: 2px;
}
.file-item {
display: flex;
align-items: center;
justify-content: center;
font-size: 0.6rem;
flex-direction: column;
padding: 20px;
}
}
</style>

View file

@ -1,9 +1,9 @@
<template> <template>
<MessageIncoming <component
:is="rootComponent"
ref="root" ref="root"
v-bind="{ ...$props, ...$attrs }" v-bind="{ ...$props, ...$attrs }"
v-if="showMultiview" v-if="showMultiview"
v-intersect="onIntersect"
> >
<div class="bubble"> <div class="bubble">
<div class="original-message" v-if="inReplyToText"> <div class="original-message" v-if="inReplyToText">
@ -25,7 +25,7 @@
</v-row> </v-row>
</v-container> </v-container>
<i v-if="event && event.isRedacted()" class="deleted-text"> <i v-if="event && event.isRedacted()" class="deleted-text">
<v-icon :color="senderIsAdminOrModerator(event) ? 'white' : ''" size="small">block</v-icon> <v-icon :color="isIncoming && senderIsAdminOrModerator(event) ? 'white' : ''" size="small">block</v-icon>
{{ {{
redactedBySomeoneElse(event) redactedBySomeoneElse(event)
? $t("message.incoming_message_deleted_text") ? $t("message.incoming_message_deleted_text")
@ -45,10 +45,10 @@
v-if="!!showItem" v-if="!!showItem"
v-on:close="showItem = undefined" v-on:close="showItem = undefined"
/> />
</MessageIncoming> </component>
<component <component
v-else-if="items.length == 1" v-else-if="items.length == 1"
:is="componentFn(items[0].event)" :is="$props.componentFn(items[0].event, false)"
v-bind="{ ...$props, ...$attrs }" v-bind="{ ...$props, ...$attrs }"
:originalEvent="items[0].event" :originalEvent="items[0].event"
/> />
@ -56,26 +56,29 @@
<script setup lang="ts"> <script setup lang="ts">
import MessageIncoming from "./MessageIncoming.vue"; import MessageIncoming from "./MessageIncoming.vue";
import { MessageEmits, MessageProps, useMessage } from "./messageMixin"; import MessageOutgoing from "./MessageOutgoing.vue";
import { MessageEmits, MessageProps, useMessage } from "./useMessage";
import util, { ROOM_TYPE_CHANNEL, ROOM_TYPE_FILE_MODE } from "@/plugins/utils"; import util, { ROOM_TYPE_CHANNEL, ROOM_TYPE_FILE_MODE } from "@/plugins/utils";
import GalleryItemsView from "../../file_mode/GalleryItemsView.vue"; import GalleryItemsView from "../../file_mode/GalleryItemsView.vue";
import ThumbnailView from "../../file_mode/ThumbnailView.vue"; import ThumbnailView from "../../file_mode/ThumbnailView.vue";
import SwipeableThumbnailsView from "../channel/SwipeableThumbnailsView.vue"; import SwipeableThumbnailsView from "../channel/SwipeableThumbnailsView.vue";
import { computed, inject, onBeforeUnmount, ref, Ref, watch } from "vue"; import { computed, inject, onBeforeUnmount, ref, Ref, useTemplateRef, watch } 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";
import { useLazyLoad } from "./useLazyLoad";
const { t } = useI18n() const { t } = useI18n()
const $matrix: any = inject('globalMatrix'); const $matrix: any = inject('globalMatrix');
const $$sanitize: any = inject('globalSanitize'); const $$sanitize: any = inject('globalSanitize');
const root = ref(undefined); type RootType = InstanceType<typeof MessageOutgoing | typeof MessageIncoming>
const rootRef = useTemplateRef<RootType>("root");
const emits = defineEmits<MessageEmits & {(event: "layout-change", value: {element: Element | undefined, action: () => void}): void}>(); const emits = defineEmits<MessageEmits & {(event: "layout-change", value: {element: Element | undefined, action: () => void}): void}>();
const items: Ref<EventAttachment[]> = ref([]); const items: Ref<EventAttachment[]> = ref([]);
const showItem: Ref<EventAttachment | undefined> = ref(undefined); const showItem: Ref<EventAttachment | undefined> = ref(undefined);
const isVisible: Ref<boolean> = ref(false);
const props = defineProps<MessageProps>(); const props = defineProps<MessageProps>();
@ -83,13 +86,19 @@ const { room } = props;
const processThread = () => { const processThread = () => {
if (!event.value?.isRedacted()) { if (!event.value?.isRedacted()) {
emits("layout-change", {element: root.value, action: _processThread}); const el = rootRef.value?.$el;
emits("layout-change", {element: el, action: _processThread});
} }
}; };
const {
isVisible
} = useLazyLoad({ root: rootRef });
const { const {
event, event,
thread, thread,
isIncoming,
senderIsAdminOrModerator, senderIsAdminOrModerator,
inReplyToSender, inReplyToSender,
inReplyToText, inReplyToText,
@ -98,6 +107,10 @@ const {
linkify, linkify,
} = useMessage($matrix, t, props, emits, processThread); } = useMessage($matrix, t, props, emits, processThread);
const rootComponent = computed(() => {
return isIncoming.value ? MessageIncoming : MessageOutgoing;
})
const onRelationsCreated = () => { const onRelationsCreated = () => {
if (event.value) { if (event.value) {
thread.value = props.timelineSet.relations.getChildEventsForEvent( thread.value = props.timelineSet.relations.getChildEventsForEvent(
@ -129,13 +142,19 @@ onBeforeUnmount(() => {
}); });
const showMultiview = computed((): boolean => { const showMultiview = computed((): boolean => {
return props.room.displayType == ROOM_TYPE_FILE_MODE || return (isIncoming.value && props.room.displayType == ROOM_TYPE_FILE_MODE) ||
items.value?.length > 1 || items.value?.length > 1 ||
(event.value && event.value.isRedacted()) || (event.value && event.value.isRedacted()) ||
(props.room.displayType == ROOM_TYPE_CHANNEL && items.value.length == 1 && util.isFileTypePDF(items.value[0].event)) || (props.room.displayType == ROOM_TYPE_CHANNEL && items.value.length == 1 && util.isFileTypePDF(items.value[0].event)) ||
messageText.value?.length > 0 messageText.value?.length > 0
}); });
watch(isVisible, (visible) => {
if (showMultiview.value && visible) {
items.value.forEach((a) => a.loadThumbnail());
}
});
const onItemClick = (event: any) => { const onItemClick = (event: any) => {
showItem.value = event.item; showItem.value = event.item;
}; };
@ -187,13 +206,6 @@ const layoutedItems = computed(() => {
return rows; return rows;
}); });
const onIntersect = (isIntersecting: boolean, entries: any, observer: any) => {
isVisible.value = isIntersecting;
if (showMultiview.value && isIntersecting) {
items.value.forEach((a) => a.loadThumbnail());
}
};
</script> </script>
<style lang="scss"> <style lang="scss">
@use "@/assets/css/chat.scss" as *; @use "@/assets/css/chat.scss" as *;

View file

@ -0,0 +1,115 @@
<template>
<component
:is="rootComponent"
:class="isIncoming ? 'messageIn-thread' : 'messageOut-thread'"
ref="root"
v-bind="{ ...$props, ...$attrs }"
>
<component :is="textComponent" v-bind="{ ...$props, ...$attrs }" :originalEvent="event" ref="exportedEvent" />
<component
v-for="item in items"
:is="$props.componentFn(item.event, true)"
v-bind="{ ...$props, ...$attrs }"
:originalEvent="item.event"
:key="item.event.getId()"
ref="exportedEvent"
/>
</component>
</template>
<script setup lang="ts">
import MessageIncoming from "./MessageIncoming.vue";
import MessageOutgoing from "./MessageOutgoing.vue";
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 { EventAttachment } from "../../../models/eventAttachment";
import { useI18n } from "vue-i18n";
import { MatrixEvent, MatrixEventEvent } from "matrix-js-sdk";
const { t } = useI18n();
const $matrix: any = inject("globalMatrix");
type RootType = InstanceType<typeof MessageOutgoing | typeof MessageIncoming>;
const rootRef = useTemplateRef<RootType>("root");
const emits = defineEmits<
MessageEmits & { (event: "layout-change", value: { element: Element | undefined; action: () => void }): void }
>();
const items: Ref<EventAttachment[]> = ref([]);
const props = defineProps<MessageProps>();
const processThread = () => {
if (!event.value?.isRedacted()) {
const el = rootRef.value?.$el;
emits("layout-change", { element: el, action: _processThread });
}
};
const { event, thread, isIncoming, messageText } = useMessage($matrix, t, props, emits, processThread);
const rootComponent = computed(() => {
return isIncoming.value ? MessageIncoming : MessageOutgoing;
});
const textComponent = computed(() => {
if (messageText.value && messageText.value.length > 0) {
return isIncoming.value ? MessageIncomingText : MessageOutgoingText;
}
return undefined;
});
const onRelationsCreated = () => {
if (event.value) {
thread.value = props.timelineSet.relations.getChildEventsForEvent(
event.value.getId() ?? "",
util.threadMessageType(),
"m.room.message"
);
event.value.off(MatrixEventEvent.RelationsCreated, onRelationsCreated);
}
};
watch(
event,
() => {
if (event.value) {
if (thread.value === undefined) {
thread.value = props.timelineSet.relations.getChildEventsForEvent(
event.value.getId() ?? "",
util.threadMessageType(),
"m.room.message"
);
}
if (!thread.value) {
event.value.on(MatrixEventEvent.RelationsCreated, onRelationsCreated);
}
}
},
{ immediate: true }
);
onBeforeUnmount(() => {
event.value?.off(MatrixEventEvent.RelationsCreated, onRelationsCreated);
});
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);
};
</script>
<style lang="scss">
@use "@/assets/css/chat.scss" as *;
</style>

View file

@ -0,0 +1,73 @@
<template>
<component
:is="rootComponent"
ref="root"
v-bind="{ ...$props, ...$attrs }">
<div class="bubble image-bubble">
<v-responsive :aspect-ratio="16 / 9" class="ma-0 pa-0" v-if="attachment">
<video :src="attachment.src" controls class="w-100 h-100">
{{$t('fallbacks.video_file')}}
</video>
<div v-if="!attachment.src && attachment.srcProgress >= 0" class="download-overlay">
<div class="text-center download-text">
{{ $t('message.download_progress',{percentage: attachment.srcProgress}) }}
</div>
</div>
<div v-else-if="!attachment.autoDownloadable && !attachment.src" class="download-overlay">
<div class="text-center download-text">
{{ attachment?.name }}
</div>
<div class="text-center download-size">
{{ prettyBytes(attachment.srcSize) }}
</div>
<v-icon size="32" color="white" class="clickable" @click="() => attachment?.loadSrc()">download</v-icon>
</div>
</v-responsive>
</div>
</component>
</template>
<script setup lang="ts">
import { computed, inject, useTemplateRef, watch } from "vue";
import MessageIncoming from "./MessageIncoming.vue";
import MessageOutgoing from "./MessageOutgoing.vue";
import { useLazyLoad } from "./useLazyLoad";
import { useI18n } from "vue-i18n";
import { MessageEmits, MessageProps, useMessage } from "./useMessage";
import { EventAttachment } from "../../../models/eventAttachment";
import prettyBytes from "pretty-bytes";
const { t } = useI18n()
const $matrix: any = inject('globalMatrix');
type RootType = InstanceType<typeof MessageOutgoing | typeof MessageIncoming>
const rootRef = useTemplateRef<RootType>("root");
const emits = defineEmits<MessageEmits>();
const props = defineProps<MessageProps>();
const {
isVisible
} = useLazyLoad({ root: rootRef });
const {
isIncoming,
attachment,
} = useMessage($matrix, t, props, emits, undefined);
const rootComponent = computed(() => {
return isIncoming.value ? MessageIncoming : MessageOutgoing;
})
watch([isVisible, attachment], ([_v, _a]: [_v: boolean, _a: EventAttachment | undefined]) => {
if (_v && _a) {
if (_a.autoDownloadable) {
_a.loadSrc();
}
}
});
</script>
<style lang="scss">
@use "@/assets/css/chat.scss" as *;
</style>

View file

@ -0,0 +1,39 @@
import { ComponentInstance, onBeforeUnmount, Ref, ref, ShallowRef, watch } from "vue";
import Intersect, { ObserveDirectiveBinding } from "vuetify/directives/intersect";
export const useLazyLoad = (props: { root: Readonly<ShallowRef<ComponentInstance<any>>> }) => {
const isVisible: Ref<boolean> = ref(false);
const binding: Ref<ObserveDirectiveBinding | undefined> = ref(undefined);
watch(
props.root,
(newval: ComponentInstance<any>) => {
if (newval && !binding.value) {
const binding: ObserveDirectiveBinding = {
modifiers: {
once: false,
quiet: false,
},
value(isIntersecting, entries, observer) {
isVisible.value = isIntersecting;
},
instance: newval,
oldValue: undefined,
dir: {},
};
Intersect.mounted(newval.$el, binding);
}
},
{ immediate: true }
);
onBeforeUnmount(() => {
if (binding.value && binding.value.instance) {
Intersect.unmounted(binding.value.instance.$el, binding.value);
}
});
return {
isVisible,
};
};

View file

@ -8,14 +8,14 @@ linkify.options.defaults.target = { url: "_blank" };
import { computed, onBeforeUnmount, Ref, ref, watch } from "vue"; import { computed, onBeforeUnmount, Ref, ref, watch } from "vue";
import { EventTimelineSet, Relations, RelationsEvent } from "matrix-js-sdk"; import { EventTimelineSet, Relations, RelationsEvent } from "matrix-js-sdk";
import { KeanuEvent, KeanuRoom } from "../../../models/eventAttachment"; import { EventAttachment, KeanuEvent, KeanuRoom } from "../../../models/eventAttachment";
export interface MessageProps { export interface MessageProps {
room: KeanuRoom; room: KeanuRoom;
originalEvent: KeanuEvent; originalEvent: KeanuEvent;
nextEvent: KeanuEvent | null | undefined; nextEvent: KeanuEvent | null | undefined;
timelineSet: EventTimelineSet; timelineSet: EventTimelineSet;
componentFn: (event: KeanuEvent) => any; componentFn: (event: KeanuEvent, forExport: boolean) => any;
} }
export type MessageEmits = { export type MessageEmits = {
@ -33,8 +33,11 @@ export const useMessage = (
processThread?: () => void processThread?: () => void
) => { ) => {
const event: Ref<KeanuEvent | undefined> = ref(undefined); const event: Ref<KeanuEvent | undefined> = ref(undefined);
const attachment: Ref<EventAttachment | undefined> = ref(undefined);
const thread: Ref<Relations | undefined> = ref(undefined); const thread: Ref<Relations | undefined> = ref(undefined);
const isIncoming = ref(props.originalEvent.getSender() != $matrix.currentUserId);
onBeforeUnmount(() => { onBeforeUnmount(() => {
thread.value = undefined; thread.value = undefined;
}); });
@ -43,6 +46,7 @@ export const useMessage = (
props.originalEvent, props.originalEvent,
(originalEvent) => { (originalEvent) => {
event.value = originalEvent; event.value = originalEvent;
attachment.value = $matrix.attachmentManager.getEventAttachment(event.value);
// Check not null and not {} // Check not null and not {}
if (originalEvent && originalEvent.isBeingDecrypted && originalEvent.isBeingDecrypted()) { if (originalEvent && originalEvent.isBeingDecrypted && originalEvent.isBeingDecrypted()) {
@ -355,6 +359,8 @@ export const useMessage = (
return { return {
event, event,
isIncoming,
attachment,
thread, thread,
validEvent, validEvent,

View file

@ -1,59 +0,0 @@
<template>
<message-incoming class="messageIn-thread" v-bind="{ ...$props, ...$attrs }">
<component v-for="item in items" :is="componentFn(item.event)" v-bind="{ ...$props, ...$attrs }" :originalEvent="item.event" :key="item.event.getId()"
ref="exportedEvent" />
</message-incoming>
</template>
<script>
import MessageIncoming from "../MessageIncoming.vue";
import messageMixin from "./../messageMixin";
import util from "../../../plugins/utils";
export default {
extends: MessageIncoming,
components: { MessageIncoming },
mixins: [messageMixin],
data() {
return {
items: [],
}
},
mounted() {
this.thread = this.timelineSet.relations.getChildEventsForEvent(this.event.getId(), util.threadMessageType(), "m.room.message");
if (!this.thread) {
this.event.on("Event.relationsCreated", this.onRelationsCreated);
}
},
beforeUnmount() {
this.event.off("Event.relationsCreated", this.onRelationsCreated);
},
methods: {
onRelationsCreated() {
this.thread = this.timelineSet.relations.getChildEventsForEvent(this.event.getId(), util.threadMessageType(), "m.room.message");
this.event.off("Event.relationsCreated", this.onRelationsCreated);
},
processThread() {
this.items = this.timelineSet.relations.getAllChildEventsForEvent(this.event.getId())
.filter(e => util.downloadableTypes().includes(e.getContent().msgtype))
.map(e => {
let ret = {
event: e,
src: null,
};
return ret;
});
},
}
};
</script>
<style lang="scss">
@use "@/assets/css/chat.scss" as *;
</style>
<style lang="scss" scoped>
.bubble {
width: 100%;
}
</style>

View file

@ -1,58 +0,0 @@
<template>
<message-outgoing class="messageOut-thread" v-bind="{ ...$props, ...$attrs }">
<component v-for="item in items" :is="componentFn(item.event)" v-bind="{ ...$props, ...$attrs }" :originalEvent="item.event" :key="item.event.getId()" ref="exportedEvent" />
</message-outgoing>
</template>
<script>
import MessageOutgoing from "../MessageOutgoing.vue";
import messageMixin from "./../messageMixin";
import util from "../../../plugins/utils";
export default {
extends: MessageOutgoing,
components: { MessageOutgoing },
mixins: [messageMixin],
data() {
return {
items: [],
}
},
mounted() {
this.thread = this.timelineSet.relations.getChildEventsForEvent(this.event.getId(), util.threadMessageType(), "m.room.message");
if (!this.thread) {
this.event.on("Event.relationsCreated", this.onRelationsCreated);
}
},
beforeUnmount() {
this.event.off("Event.relationsCreated", this.onRelationsCreated);
},
methods: {
onRelationsCreated() {
this.thread = this.timelineSet.relations.getChildEventsForEvent(this.event.getId(), util.threadMessageType(), "m.room.message");
this.event.off("Event.relationsCreated", this.onRelationsCreated);
},
processThread() {
this.items = this.timelineSet.relations.getAllChildEventsForEvent(this.event.getId())
.filter(e => util.downloadableTypes().includes(e.getContent().msgtype))
.map(e => {
let ret = {
event: e,
src: null,
};
return ret;
});
},
}
};
</script>
<style lang="scss">
@use "@/assets/css/chat.scss" as *;
</style>
<style lang="scss" scoped>
.bubble {
width: 100%;
}
</style>

View file

@ -1,5 +1,5 @@
import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk"; import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk";
import { EventAttachment, KeanuEventExtension } from "./eventAttachment"; import { EventAttachment, EventAttachmentLoadSrcOptions, 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";
@ -113,44 +113,68 @@ export class AttachmentManager {
return attachment; return attachment;
} }
public getEventAttachment(event: MatrixEvent & KeanuEventExtension): Reactive<EventAttachment> { private getFileName(event: KeanuEvent) {
const content = event.getContent();
return (content.body || content.filename || "").toLowerCase();
}
private getSrcFileSize(event: KeanuEvent) {
const content = event.getContent();
if (content.info) {
return content.info.size;
}
return 0;
}
public getEventAttachment(event: KeanuEvent): Reactive<EventAttachment> {
let entry = this.cache.get(event.getId()); let entry = this.cache.get(event.getId());
if (entry !== undefined) { if (entry !== undefined) {
return entry; return entry;
} }
const fileSize = this.getSrcFileSize(event);
const attachment: Reactive<EventAttachment> = reactive({ const attachment: Reactive<EventAttachment> = reactive({
event: event, event: event,
name: this.getFileName(event),
srcSize: fileSize,
srcProgress: -1, srcProgress: -1,
thumbnailProgress: -1, thumbnailProgress: -1,
autoDownloadable: fileSize <= this.maxSizeAutoDownloads,
loadSrc: () => Promise.reject("Not implemented"), loadSrc: () => Promise.reject("Not implemented"),
loadThumbnail: () => Promise.reject("Not implemented"), loadThumbnail: () => Promise.reject("Not implemented"),
release: () => Promise.reject("Not implemented"), release: () => Promise.reject("Not implemented"),
}); });
attachment.loadSrc = () => { attachment.loadSrc = (options?: EventAttachmentLoadSrcOptions) => {
if (attachment.src) { if (attachment.src && !options?.asBlob) {
return Promise.resolve(attachment.src); return Promise.resolve({data: attachment.src, type: "src"});
} else if (attachment.srcPromise) { } else if (attachment.srcPromise) {
return attachment.srcPromise; return attachment.srcPromise;
} }
attachment.srcPromise = this._loadEventAttachmentOrThumbnail(event, false, (percent) => { attachment.srcPromise = this._loadEventAttachmentOrThumbnail(event, false, !!options?.asBlob, (percent) => {
attachment.srcProgress = percent; attachment.srcProgress = percent;
}).then((src) => { }).then((res) => {
attachment.src = src; attachment.src = res.data as string;
return src; return res;
}); });
return attachment.srcPromise; return attachment.srcPromise;
}; };
attachment.loadThumbnail = () => { attachment.loadThumbnail = () => {
if (attachment.thumbnail) { if (attachment.thumbnail) {
return Promise.resolve(attachment.thumbnail); return Promise.resolve({data: attachment.thumbnail, type: "thumbnail"});
} else if (attachment.thumbnailPromise) { } else if (attachment.thumbnailPromise) {
return attachment.thumbnailPromise; return attachment.thumbnailPromise;
} }
attachment.thumbnailPromise = this._loadEventAttachmentOrThumbnail(event, true, (percent) => { attachment.thumbnailPromise = this._loadEventAttachmentOrThumbnail(event, true, false, (percent) => {
attachment.thumbnailProgress = percent; attachment.thumbnailProgress = percent;
}).then((thummbnail) => { }).then((res) => {
attachment.thumbnail = thummbnail; attachment.thumbnail = res.data as string;
return thummbnail; if (res.type == "src") {
// Downloaded the src as thumb, so set "src" as well!
attachment.src = res.data as string;
}
return res;
}); });
return attachment.thumbnailPromise; return attachment.thumbnailPromise;
}; };
@ -174,10 +198,13 @@ export class AttachmentManager {
private async _loadEventAttachmentOrThumbnail( private async _loadEventAttachmentOrThumbnail(
event: MatrixEvent & KeanuEventExtension, event: MatrixEvent & KeanuEventExtension,
thumbnail: boolean, thumbnail: boolean,
asBlob: boolean,
progress?: (percent: number) => void progress?: (percent: number) => void
): Promise<string> { ): Promise<{data: string | Blob, type: EventAttachmentUrlType}> {
await this.matrixClient.decryptEventIfNeeded(event); await this.matrixClient.decryptEventIfNeeded(event);
let urltype: EventAttachmentUrlType = thumbnail ? "thumbnail" : "src";
const content = event.getContent(); const content = event.getContent();
var url = null; var url = null;
var mime = "image/png"; var mime = "image/png";
@ -227,7 +254,7 @@ export class AttachmentManager {
content.file && content.file &&
content.file.url && content.file.url &&
event.getContent()?.info?.size > 0 && event.getContent()?.info?.size > 0 &&
event.getContent()?.info?.size < this.maxSizeAutoDownloads (!thumbnail || event.getContent()?.info?.size < this.maxSizeAutoDownloads)
) { ) {
// No thumb, use real url // No thumb, use real url
file = content.file; file = content.file;
@ -241,6 +268,7 @@ export class AttachmentManager {
this.useAuthedMedia this.useAuthedMedia
); );
mime = file.mimetype; mime = file.mimetype;
urltype = "src";
} }
if (url == null) { if (url == null) {
@ -263,7 +291,8 @@ 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 };
return URL.createObjectURL(new Blob([bytes.buffer], { type: mime })); const blob = new Blob([bytes.buffer], { type: mime });
return {data: asBlob ? blob : URL.createObjectURL(blob), type: urltype};
} }
private b64toBuffer(val: any) { private b64toBuffer(val: any) {

View file

@ -8,16 +8,24 @@ export type KeanuEventExtension = {
replyEvent?: MatrixEvent & KeanuEventExtension; replyEvent?: MatrixEvent & KeanuEventExtension;
} }
export type EventAttachmentUrlType = "src" | "thumbnail";
export type EventAttachmentLoadSrcOptions = {
asBlob?: boolean;
}
export type EventAttachment = { export type EventAttachment = {
event: MatrixEvent & KeanuEventExtension; event: MatrixEvent & KeanuEventExtension;
name: string;
src?: string; src?: string;
thumbnail?: string; srcSize: number;
srcPromise?: Promise<string>;
thumbnailPromise?: Promise<string>;
srcProgress: number; srcProgress: number;
srcPromise?: Promise<{data: string | Blob, type: EventAttachmentUrlType}>;
thumbnail?: string;
thumbnailProgress: number; thumbnailProgress: number;
loadSrc: () => void; thumbnailPromise?: Promise<{data: string | Blob, type: EventAttachmentUrlType}>;
loadThumbnail: () => Promise<string>; autoDownloadable: boolean;
loadSrc: (options?: EventAttachmentLoadSrcOptions) => Promise<{data: string | Blob, type: EventAttachmentUrlType}>;
loadThumbnail: () => Promise<{data: string | Blob, type: EventAttachmentUrlType}>;
release: (src: boolean, thumbnail: boolean) => void; release: (src: boolean, thumbnail: boolean) => void;
}; };