Lots of fixes to "media threads"

This commit is contained in:
N Pex 2023-11-06 15:28:26 +00:00
parent fe081edc62
commit 8bcceafcff
23 changed files with 867 additions and 333 deletions

View file

@ -8,7 +8,7 @@
</div>
</div>
<v-avatar class="avatar" ref="avatar" size="32" color="#ededed" @click.stop="otherAvatarClicked($refs.avatar.$el)">
<img v-if="messageEventAvatar(event)" :src="messageEventAvatar(event)" />
<img v-if="messageEventAvatar(event)" :src="messageEventAvatar(event)" onerror="this.style.display='none'" />
<span v-else class="white--text headline">{{
eventSenderDisplayName(event).substring(0, 1).toUpperCase()
}}</span>
@ -20,7 +20,7 @@
<v-icon>more_vert</v-icon>
</v-btn>
</div>
<QuickReactions :event="event" :timelineSet="timelineSet" v-on="$listeners"/>
<QuickReactions :event="eventForReactions" :timelineSet="timelineSet" v-on="$listeners"/>
<SeenBy :room="room" :event="event"/>
</div>
</template>

View file

@ -2,14 +2,13 @@
<message-incoming v-bind="{...$props, ...$attrs}" v-on="$listeners">
<div class="bubble">
<div class="original-message" v-if="inReplyToText">
<div class="original-message-sender">
{{ $t('message.user_said', {user: inReplyToSender || "Someone"}) }}
</div>
<div class="original-message-sender">{{ inReplyToSender }}</div>
<div
class="original-message-text"
v-html="linkify($sanitize(inReplyToText))"
/>
</div>
<div class="message">
<span>{{ $t('message.file_prefix') }}</span>
<span

View file

@ -2,14 +2,13 @@
<message-incoming v-bind="{...$props, ...$attrs}" v-on="$listeners">
<div class="bubble">
<div class="original-message" v-if="inReplyToText">
<div class="original-message-sender">
{{ $t('message.user_said', {user: inReplyToSender || "Someone"}) }}
</div>
<div class="original-message-sender">{{ inReplyToSender }}</div>
<div
class="original-message-text"
v-html="linkify($sanitize(inReplyToText))"
/>
</div>
<div class="message">
<i v-if="event.isRedacted()" class="deleted-text">
<v-icon :color="this.senderIsAdminOrModerator(this.event)?'white':''" size="small">block</v-icon>

View file

@ -2,14 +2,13 @@
<message-incoming v-bind="{ ...$props, ...$attrs }" v-on="$listeners" v-if="items.length > 1">
<div class="bubble">
<div class="original-message" v-if="inReplyToText">
<div class="original-message-sender">
{{ $t('message.user_said', {user: inReplyToSender || "Someone"}) }}
</div>
<div class="original-message-sender">{{ inReplyToSender }}</div>
<div
class="original-message-text"
v-html="linkify($sanitize(inReplyToText))"
/>
</div>
<div class="message">
<v-container fluid class="imageCollection">
<v-row wrap>
@ -51,54 +50,46 @@ export default {
return {
items: [],
showItem: null,
thread: null,
}
},
mounted() {
this.thread = this.timelineSet.relations.getChildEventsForEvent(this.event.getId(), "m.thread", "m.room.message");
this.processThread();
},
beforeDestroy() {
this.thread = null;
},
watch: {
thread: {
handler(newValue, oldValue) {
if (oldValue) {
oldValue.off('Relations.add', this.onAddRelation);
}
if (newValue) {
newValue.on('Relations.add', this.onAddRelation);
}
this.processThread();
},
immediate: true
if (!this.thread) {
this.event.on("Event.relationsCreated", this.onRelationsCreated);
}
},
beforeDestroy() {
this.event.off("Event.relationsCreated", this.onRelationsCreated);
},
methods: {
onAddRelation() {
this.processThread();
onRelationsCreated() {
this.thread = this.timelineSet.relations.getChildEventsForEvent(this.event.getId(), "m.thread", "m.room.message");
this.event.off("Event.relationsCreated", this.onRelationsCreated);
},
onItemClick(event) {
this.showItem = event.item;
},
processThread() {
this.items = this.timelineSet.relations.getAllChildEventsForEvent(this.event.getId()).map(e => {
let ret = {
event: e,
src: null,
};
ret.promise =
util
.getThumbnail(this.$matrix.matrixClient, e, 100, 100)
.then((url) => {
ret.src = url;
})
.catch((err) => {
console.log("Failed to fetch thumbnail: ", err);
});
return ret;
});
this.$emit('layout-change', () => {
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,
};
ret.promise =
util
.getThumbnail(this.$matrix.matrixClient, e, 100, 100)
.then((url) => {
ret.src = url;
})
.catch((err) => {
console.log("Failed to fetch thumbnail: ", err);
});
return ret;
});
}, this.$el);
},
layoutedItems() {
if (!this.items || this.items.length == 0) { return [] }

View file

@ -24,7 +24,7 @@
<img v-if="userAvatar" :src="userAvatar" />
<span v-else class="white--text headline">{{ userAvatarLetter }}</span>
</v-avatar>
<QuickReactions :event="event" :timelineSet="timelineSet" v-on="$listeners"/>
<QuickReactions :event="eventForReactions" :timelineSet="timelineSet" v-on="$listeners"/>
<SeenBy :room="room" :event="event"/>
</div>
</template>

View file

@ -2,15 +2,14 @@
<message-outgoing v-bind="{ ...$props, ...$attrs }" v-on="$listeners">
<div class="bubble">
<div class="original-message" v-if="inReplyToText">
<div class="original-message-sender">
{{ $t('message.user_said', {user: inReplyToSender || "Someone"}) }}
</div>
<div class="original-message-sender">{{ inReplyToSender }}</div>
<div
class="original-message-text"
v-html="linkify($sanitize(inReplyToText))"
/>
</div>
<div class="message">
<span>{{ $t('message.file_prefix') }}</span>
<span

View file

@ -2,9 +2,7 @@
<message-outgoing v-bind="{ ...$props, ...$attrs }" v-on="$listeners">
<div class="bubble">
<div class="original-message" v-if="inReplyToText">
<div class="original-message-sender">
{{ $t('message.user_said', {user: inReplyToSender || "Someone"}) }}
</div>
<div class="original-message-sender">{{ inReplyToSender }}</div>
<div
class="original-message-text"
v-html="linkify($sanitize(inReplyToText))"

View file

@ -2,12 +2,14 @@
<message-outgoing v-bind="{ ...$props, ...$attrs }" v-on="$listeners" v-if="items.length > 1">
<div class="bubble">
<div class="original-message" v-if="inReplyToText">
<div class="original-message-sender">
{{ $t('message.user_said', { user: inReplyToSender || "Someone" }) }}
</div>
<div class="original-message-text" v-html="linkify($sanitize(inReplyToText))" />
<div class="original-message-sender">{{ inReplyToSender }}</div>
<div
class="original-message-text"
v-html="linkify($sanitize(inReplyToText))"
/>
</div>
<div class="message">
<v-container fluid class="imageCollection">
<v-row wrap>
@ -49,54 +51,46 @@ export default {
return {
items: [],
showItem: null,
thread: null,
}
},
mounted() {
this.thread = this.timelineSet.relations.getChildEventsForEvent(this.event.getId(), "m.thread", "m.room.message");
this.processThread();
},
beforeDestroy() {
this.thread = null;
},
watch: {
thread: {
handler(newValue, oldValue) {
if (oldValue) {
oldValue.off('Relations.add', this.onAddRelation);
}
if (newValue) {
newValue.on('Relations.add', this.onAddRelation);
}
this.processThread();
},
immediate: true
if (!this.thread) {
this.event.on("Event.relationsCreated", this.onRelationsCreated);
}
},
beforeDestroy() {
this.event.off("Event.relationsCreated", this.onRelationsCreated);
},
methods: {
onAddRelation() {
this.processThread();
onRelationsCreated() {
this.thread = this.timelineSet.relations.getChildEventsForEvent(this.event.getId(), "m.thread", "m.room.message");
this.event.off("Event.relationsCreated", this.onRelationsCreated);
},
onItemClick(event) {
this.showItem = event.item;
},
processThread() {
this.items = this.timelineSet.relations.getAllChildEventsForEvent(this.event.getId()).map(e => {
let ret = {
event: e,
src: null,
};
ret.promise =
util
.getThumbnail(this.$matrix.matrixClient, e, 100, 100)
.then((url) => {
ret.src = url;
})
.catch((err) => {
console.log("Failed to fetch thumbnail: ", err);
});
return ret;
});
this.$emit('layout-change', () => {
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,
};
ret.promise =
util
.getThumbnail(this.$matrix.matrixClient, e, 100, 100)
.then((url) => {
ret.src = url;
})
.catch((err) => {
console.log("Failed to fetch thumbnail: ", err);
});
return ret;
});
}, this.$el);
},
layoutedItems() {
if (!this.items || this.items.length == 0) { return [] }

View file

@ -0,0 +1,59 @@
<template>
<message-incoming class="messageIn-thread" v-bind="{ ...$props, ...$attrs }" v-on="$listeners">
<component v-for="item in items" :is="componentFn(item.event)" :originalEvent="item.event" :key="item.event.getId()"
v-bind="{ ...$props }" v-on="$listeners" 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(), "m.thread", "m.room.message");
if (!this.thread) {
this.event.on("Event.relationsCreated", this.onRelationsCreated);
}
},
beforeDestroy() {
this.event.off("Event.relationsCreated", this.onRelationsCreated);
},
methods: {
onRelationsCreated() {
this.thread = this.timelineSet.relations.getChildEventsForEvent(this.event.getId(), "m.thread", "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">
@import "@/assets/css/chat.scss";
</style>
<style lang="scss" scoped>
.bubble {
width: 100%;
}
</style>

View file

@ -0,0 +1,59 @@
<template>
<message-outgoing class="messageOut-thread" v-bind="{ ...$props, ...$attrs }" v-on="$listeners">
<component v-for="item in items" :is="componentFn(item.event)" :originalEvent="item.event" :key="item.event.getId()"
v-bind="{ ...$props }" v-on="$listeners" 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(), "m.thread", "m.room.message");
if (!this.thread) {
this.event.on("Event.relationsCreated", this.onRelationsCreated);
}
},
beforeDestroy() {
this.event.off("Event.relationsCreated", this.onRelationsCreated);
},
methods: {
onRelationsCreated() {
this.thread = this.timelineSet.relations.getChildEventsForEvent(this.event.getId(), "m.thread", "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">
@import "@/assets/css/chat.scss";
</style>
<style lang="scss" scoped>
.bubble {
width: 100%;
}
</style>

View file

@ -2,7 +2,7 @@ import QuickReactions from "./QuickReactions.vue";
import * as linkify from 'linkifyjs';
import linkifyHtml from 'linkify-html';
import utils from "../../plugins/utils"
import Vue from "vue";
import util from "../../plugins/utils";
linkify.options.defaults.className = "link";
linkify.options.defaults.target = { url: "_blank" };
@ -40,51 +40,22 @@ export default {
type: Function,
default: function () {
return () => {};
}
},
},
},
data() {
return {
event: {},
inReplyToEvent: null,
inReplyToSender: null,
utils
thread: null,
utils,
};
},
mounted() {
const relatesTo = this.validEvent && this.event.getWireContent()["m.relates_to"];
if (relatesTo && relatesTo["m.in_reply_to"]) {
// Can we find the original message?
const originalEventId = relatesTo["m.in_reply_to"].event_id;
if (originalEventId && this.timelineSet) {
const originalEvent = this.timelineSet.findEventById(originalEventId);
if (originalEvent) {
this.inReplyToEvent = originalEvent;
this.inReplyToSender = this.eventSenderDisplayName(originalEvent);
}
}
}
},
beforeUnmount() {
if (this.validEvent) {
this.event.off("Event.relationsCreated", this.onRelationsCreated);
}
beforeDestroy() {
this.thread = null;
},
watch: {
event: {
immediate: true,
handler(newValue, oldValue) {
if (oldValue && oldValue.getId) {
oldValue.off("Event.relationsCreated", this.onRelationsCreated);
}
if (newValue && newValue.getId) {
newValue.on("Event.relationsCreated", this.onRelationsCreated);
if (newValue.isThreadRoot) {
Vue.set(newValue, "isThread", true);
}
}
}
},
originalEvent: {
immediate: true,
handler(originalEvent, ignoredOldValue) {
@ -96,7 +67,19 @@ export default {
});
}
},
}
},
thread: {
handler(newValue, oldValue) {
if (oldValue) {
oldValue.off("Relations.add", this.onAddRelation);
}
if (newValue) {
newValue.on("Relations.add", this.onAddRelation);
}
this.processThread();
},
immediate: true,
},
},
computed: {
/**
@ -107,6 +90,16 @@ export default {
return this.event && Object.keys(this.event).length !== 0;
},
/**
* If this is a thread event, we return the root here, so all reactions will land on the root event.
*/
eventForReactions() {
if (this.event.parentThread) {
return this.event.parentThread;
}
return this.event;
},
incoming() {
return this.event && this.event.getSender() != this.$matrix.currentUserId;
},
@ -123,11 +116,34 @@ export default {
return true;
},
inReplyToSender() {
const originalEvent = this.validEvent && this.event.replyEvent;
if (originalEvent) {
const sender = this.eventSenderDisplayName(originalEvent);
if (originalEvent.isThreadRoot || originalEvent.isMxThread) {
return sender || this.$t("message.someone");
} else {
return this.$t("message.user_said", { user: sender || this.$t("message.someone") });
}
}
return null;
},
inReplyToEvent() {
return this.validEvent && this.event.replyEvent;
},
inReplyToText() {
const relatesTo = this.event.getWireContent()["m.relates_to"];
if (relatesTo && relatesTo["m.in_reply_to"]) {
if (this.inReplyToEvent && (this.inReplyToEvent.isThreadRoot || this.inReplyToEvent.isMxThread)) {
const children = this.timelineSet.relations
.getAllChildEventsForEvent(this.inReplyToEvent.getId())
.filter((e) => util.downloadableTypes().includes(e.getContent().msgtype));
return this.$t("message.sent_media", { count: children.length });
}
const content = this.event.getContent();
if ('body' in content) {
if ("body" in content) {
const lines = content.body.split("\n").reverse() || [];
while (lines.length && !lines[0].startsWith("> ")) lines.shift();
// Reply fallback has a blank line after it, so remove it to prevent leading newline
@ -137,12 +153,10 @@ export default {
return text;
}
}
if (this.inReplyToEvent) {
var c = this.inReplyToEvent.getContent();
return c.body;
}
// We don't have the original text (at the moment at least)
return this.$t("fallbacks.original_text");
}
@ -153,7 +167,7 @@ export default {
const relatesTo = this.event.getWireContent()["m.relates_to"];
if (relatesTo && relatesTo["m.in_reply_to"]) {
const content = this.event.getContent();
if ('body' in content) {
if ("body" in content) {
// Remove the new text and strip "> " from the old original text
const lines = content.body.split("\n");
while (lines.length && lines[0].startsWith("> ")) lines.shift();
@ -190,10 +204,9 @@ export default {
},
},
methods: {
onRelationsCreated(relationType, ignoredEventType) {
if (relationType === "m.thread") {
Vue.set(this.event, "isThread", true);
}
onAddRelation() {
console.error("onAddRelation");
this.processThread();
},
ownAvatarClicked() {
this.$emit("own-avatar-clicked", { event: this.event });
@ -308,5 +321,10 @@ export default {
linkify(text) {
return linkifyHtml(text);
},
/**
* Override this to handle updates to (the) message thread.
*/
processThread() {},
},
};

View file

@ -1,3 +1,4 @@
import util from "../../plugins/utils";
export default {
computed: {
@ -5,8 +6,12 @@ export default {
return !this.incoming && this.event.getContent().msgtype == "m.text";
},
isDownloadable() {
if ((this.event.isThreadRoot || this.event.isMxThread) && this.timelineSet) {
const children = this.timelineSet.relations.getAllChildEventsForEvent(this.event.getId()).filter(e => util.downloadableTypes().includes(e.getContent().msgtype));
return children.length > 0;
}
const msgtype = this.event.getContent().msgtype;
return ['m.video','m.audio','m.image','m.file'].includes(msgtype);
return util.downloadableTypes().includes(msgtype);
},
isRedactable() {
const room = this.$matrix.matrixClient.getRoom(this.event.getRoomId());