Work on export and moving to Vue composition API
This commit is contained in:
parent
b0fae3396d
commit
9a124c5ab9
22 changed files with 660 additions and 906 deletions
|
|
@ -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,15 +254,21 @@ 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)
|
||||||
|
.forEach((event) => {
|
||||||
|
const parentEvent =
|
||||||
|
this.timelineSet.findEventById(event.threadRootId) || this.room.findEventById(event.threadRootId);
|
||||||
if (parentEvent) {
|
if (parentEvent) {
|
||||||
parentEvent["isMxThread"] = true;
|
parentEvent["isMxThread"] = true;
|
||||||
event["parentThread"] = parentEvent;
|
event["parentThread"] = parentEvent;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.events.filter(event => (event.replyEventId && !event.replyEvent)).forEach(event => {
|
this.events
|
||||||
const parentEvent = this.timelineSet.findEventById(event.replyEventId) || this.room.findEventById(event.replyEventId);
|
.filter((event) => event.replyEventId && !event.replyEvent)
|
||||||
|
.forEach((event) => {
|
||||||
|
const parentEvent =
|
||||||
|
this.timelineSet.findEventById(event.replyEventId) || this.room.findEventById(event.replyEventId);
|
||||||
if (parentEvent) {
|
if (parentEvent) {
|
||||||
event["replyEvent"] = parentEvent;
|
event["replyEvent"] = parentEvent;
|
||||||
}
|
}
|
||||||
|
|
@ -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",
|
||||||
|
headers: this.$matrix.useAuthedMedia
|
||||||
|
? {
|
||||||
|
Authorization: `Bearer ${this.$matrix.matrixClient.getAccessToken()}`,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
})
|
})
|
||||||
.then(result => {
|
.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,23 +369,27 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let componentClass = comp.$options ? comp.$options.__file.split("/").reverse()[0].split(".")[0] : "invalid_component";
|
let componentClass = comp.$options
|
||||||
|
? comp.$options.__file.split("/").reverse()[0].split(".")[0]
|
||||||
|
: "invalid_component";
|
||||||
|
let attachment =
|
||||||
|
comp.event && comp.event.getId
|
||||||
|
? this.$matrix.attachmentManager.getEventAttachment(comp.event)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
if (attachment && (attachment.srcSize = 0 || currentMediaSize + attachment.srcSize <= maxMediaSize)) {
|
||||||
|
downloadPromises.push(
|
||||||
|
attachment
|
||||||
|
.loadSrc({ asBlob: true })
|
||||||
|
.then((res) => {
|
||||||
|
const blob = res.data;
|
||||||
|
if (currentMediaSize + blob.size <= maxMediaSize) {
|
||||||
|
currentMediaSize += blob.size;
|
||||||
|
|
||||||
switch (componentClass) {
|
switch (componentClass) {
|
||||||
case "MessageIncomingImageExport":
|
case "MessageIncomingImageExport":
|
||||||
case "MessageOutgoingImageExport":
|
case "MessageOutgoingImageExport":
|
||||||
// TODO - maybe consider what media to download based on the file size we already have?
|
{
|
||||||
// info = comp.event.getContent().info;
|
|
||||||
// if (info && info.size && currentMediaSize + info.size > maxMediaSize) {
|
|
||||||
// // No need to even download.
|
|
||||||
// console.log("Dont download!");
|
|
||||||
// continue;
|
|
||||||
// }
|
|
||||||
|
|
||||||
downloadPromises.push(
|
|
||||||
util
|
|
||||||
.getAttachment(this.$matrix.matrixClient, this.$matrix.useAuthedMedia, comp.event, null, true)
|
|
||||||
.then((blob) => {
|
|
||||||
return new Promise((resolve, ignoredReject) => {
|
|
||||||
let mime = blob.type;
|
let mime = blob.type;
|
||||||
var extension = ".png";
|
var extension = ".png";
|
||||||
switch (mime) {
|
switch (mime) {
|
||||||
|
|
@ -380,8 +400,6 @@ export default {
|
||||||
case "image/gif":
|
case "image/gif":
|
||||||
extension = ".gif";
|
extension = ".gif";
|
||||||
}
|
}
|
||||||
if (currentMediaSize + blob.size <= maxMediaSize) {
|
|
||||||
currentMediaSize += blob.size;
|
|
||||||
|
|
||||||
let fileName = comp.event.getId() + extension;
|
let fileName = comp.event.getId() + extension;
|
||||||
imageFolder.file(fileName, blob); // TODO calc bytes
|
imageFolder.file(fileName, blob); // TODO calc bytes
|
||||||
|
|
@ -391,28 +409,15 @@ export default {
|
||||||
for (let imageIndex = 0; imageIndex < images.length; imageIndex++) {
|
for (let imageIndex = 0; imageIndex < images.length; imageIndex++) {
|
||||||
const img = images[imageIndex];
|
const img = images[imageIndex];
|
||||||
img.removeAttribute("src");
|
img.removeAttribute("src");
|
||||||
img.setAttribute("data-exported-src", './images/' + fileName);
|
img.setAttribute("data-exported-src", "./images/" + fileName);
|
||||||
}
|
}
|
||||||
this.processedEvents += 1;
|
this.processedEvents += 1;
|
||||||
resolve(true);
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch((ignoredErr) => {
|
|
||||||
this.processedEvents += 1;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "MessageIncomingAudioExport":
|
case "MessageIncomingAudioExport":
|
||||||
case "MessageOutgoingAudioExport":
|
case "MessageOutgoingAudioExport":
|
||||||
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) => {
|
|
||||||
//let mime = blob.type;
|
|
||||||
var extension = ".webm";
|
var extension = ".webm";
|
||||||
let fileName = comp.event.getId() + extension;
|
let fileName = comp.event.getId() + extension;
|
||||||
audioFolder.file(fileName, blob); // TODO calc bytes
|
audioFolder.file(fileName, blob); // TODO calc bytes
|
||||||
|
|
@ -422,25 +427,12 @@ export default {
|
||||||
element.setAttribute("data-exported-src", "./audio/" + fileName);
|
element.setAttribute("data-exported-src", "./audio/" + fileName);
|
||||||
}
|
}
|
||||||
this.processedEvents += 1;
|
this.processedEvents += 1;
|
||||||
resolve(true);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
.catch((ignoredErr) => {
|
|
||||||
this.processedEvents += 1;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "MessageIncomingVideoExport":
|
case "MessageIncomingVideoExport":
|
||||||
case "MessageOutgoingVideoExport":
|
case "MessageOutgoingVideoExport":
|
||||||
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) => {
|
|
||||||
//let mime = blob.type;
|
|
||||||
var extension = ".mp4";
|
var extension = ".mp4";
|
||||||
let fileName = comp.event.getId() + extension;
|
let fileName = comp.event.getId() + extension;
|
||||||
videoFolder.file(fileName, blob); // TODO calc bytes
|
videoFolder.file(fileName, blob); // TODO calc bytes
|
||||||
|
|
@ -451,41 +443,33 @@ export default {
|
||||||
element.setAttribute("data-exported-src", "./video/" + fileName);
|
element.setAttribute("data-exported-src", "./video/" + fileName);
|
||||||
}
|
}
|
||||||
this.processedEvents += 1;
|
this.processedEvents += 1;
|
||||||
resolve(true);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
.catch((ignoredErr) => {
|
|
||||||
this.processedEvents += 1;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "MessageIncomingFileExport":
|
case "MessageIncomingFileExport":
|
||||||
case "MessageOutgoingFileExport":
|
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);
|
var extension = util.getFileExtension(comp.event);
|
||||||
let fileName = comp.event.getId() + extension;
|
let fileName = comp.event.getId() + extension;
|
||||||
filesFolder.file(fileName, blob);
|
filesFolder.file(fileName, blob);
|
||||||
comp.href = "./files/" + fileName;
|
comp.href = "./files/" + fileName;
|
||||||
this.processedEvents += 1;
|
this.processedEvents += 1;
|
||||||
resolve(true);
|
}
|
||||||
});
|
break;
|
||||||
|
}
|
||||||
|
this.processedEvents += 1;
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
this.processedEvents += 1;
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((ignoredErr) => {
|
.catch((ignoredErr) => {
|
||||||
this.processedEvents += 1;
|
this.processedEvents += 1;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
break;
|
} else {
|
||||||
default:
|
|
||||||
this.processedEvents += 1;
|
this.processedEvents += 1;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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>
|
|
||||||
55
src/components/messages/composition/MessageFile.vue
Normal file
55
src/components/messages/composition/MessageFile.vue
Normal 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>
|
||||||
97
src/components/messages/composition/MessageImage.vue
Normal file
97
src/components/messages/composition/MessageImage.vue
Normal 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>
|
||||||
|
|
@ -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";
|
||||||
|
|
|
||||||
|
|
@ -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";
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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 *;
|
||||||
115
src/components/messages/composition/MessageThreadExport.vue
Normal file
115
src/components/messages/composition/MessageThreadExport.vue
Normal 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>
|
||||||
73
src/components/messages/composition/MessageVideo.vue
Normal file
73
src/components/messages/composition/MessageVideo.vue
Normal 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>
|
||||||
39
src/components/messages/composition/useLazyLoad.ts
Normal file
39
src/components/messages/composition/useLazyLoad.ts
Normal 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,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
@ -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,
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue