2025-06-10 13:35:51 +02:00
|
|
|
<template>
|
2025-06-27 16:10:25 +02:00
|
|
|
<component
|
|
|
|
|
:is="rootComponent"
|
2025-06-10 13:35:51 +02:00
|
|
|
ref="root"
|
|
|
|
|
v-bind="{ ...$props, ...$attrs }"
|
|
|
|
|
v-if="showMultiview"
|
|
|
|
|
>
|
|
|
|
|
<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">
|
2025-06-27 16:10:25 +02:00
|
|
|
<v-icon :color="isIncoming && senderIsAdminOrModerator(event) ? 'white' : ''" size="small">block</v-icon>
|
2025-06-10 13:35:51 +02:00
|
|
|
{{
|
|
|
|
|
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"
|
|
|
|
|
/>
|
2025-06-27 16:10:25 +02:00
|
|
|
</component>
|
2025-06-10 13:35:51 +02:00
|
|
|
<component
|
|
|
|
|
v-else-if="items.length == 1"
|
2025-06-27 16:10:25 +02:00
|
|
|
:is="$props.componentFn(items[0].event, false)"
|
2025-06-10 13:35:51 +02:00
|
|
|
v-bind="{ ...$props, ...$attrs }"
|
|
|
|
|
:originalEvent="items[0].event"
|
|
|
|
|
/>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
import MessageIncoming from "./MessageIncoming.vue";
|
2025-06-27 16:10:25 +02:00
|
|
|
import MessageOutgoing from "./MessageOutgoing.vue";
|
|
|
|
|
import { MessageEmits, MessageProps, useMessage } from "./useMessage";
|
2025-06-10 13:35:51 +02:00
|
|
|
import util, { ROOM_TYPE_CHANNEL, ROOM_TYPE_FILE_MODE } from "@/plugins/utils";
|
|
|
|
|
import GalleryItemsView from "../../file_mode/GalleryItemsView.vue";
|
|
|
|
|
import ThumbnailView from "../../file_mode/ThumbnailView.vue";
|
|
|
|
|
import SwipeableThumbnailsView from "../channel/SwipeableThumbnailsView.vue";
|
2025-06-27 16:10:25 +02:00
|
|
|
import { computed, inject, onBeforeUnmount, ref, Ref, useTemplateRef, watch } from "vue";
|
2025-06-10 13:35:51 +02:00
|
|
|
import { EventAttachment } from "../../../models/eventAttachment";
|
|
|
|
|
import { useI18n } from 'vue-i18n'
|
|
|
|
|
import { MatrixEvent, MatrixEventEvent } from "matrix-js-sdk";
|
2025-06-27 16:10:25 +02:00
|
|
|
import { useLazyLoad } from "./useLazyLoad";
|
2025-06-10 13:35:51 +02:00
|
|
|
|
|
|
|
|
const { t } = useI18n()
|
|
|
|
|
const $matrix: any = inject('globalMatrix');
|
|
|
|
|
const $$sanitize: any = inject('globalSanitize');
|
|
|
|
|
|
2025-06-27 16:10:25 +02:00
|
|
|
type RootType = InstanceType<typeof MessageOutgoing | typeof MessageIncoming>
|
|
|
|
|
const rootRef = useTemplateRef<RootType>("root");
|
|
|
|
|
|
2025-06-10 13:35:51 +02:00
|
|
|
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 props = defineProps<MessageProps>();
|
|
|
|
|
|
|
|
|
|
const { room } = props;
|
|
|
|
|
|
|
|
|
|
const processThread = () => {
|
|
|
|
|
if (!event.value?.isRedacted()) {
|
2025-06-27 16:10:25 +02:00
|
|
|
const el = rootRef.value?.$el;
|
|
|
|
|
emits("layout-change", {element: el, action: _processThread});
|
2025-06-10 13:35:51 +02:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-06-27 16:10:25 +02:00
|
|
|
const {
|
|
|
|
|
isVisible
|
|
|
|
|
} = useLazyLoad({ root: rootRef });
|
|
|
|
|
|
2025-06-10 13:35:51 +02:00
|
|
|
const {
|
|
|
|
|
event,
|
|
|
|
|
thread,
|
2025-06-27 16:10:25 +02:00
|
|
|
isIncoming,
|
2025-06-10 13:35:51 +02:00
|
|
|
senderIsAdminOrModerator,
|
|
|
|
|
inReplyToSender,
|
|
|
|
|
inReplyToText,
|
|
|
|
|
messageText,
|
|
|
|
|
redactedBySomeoneElse,
|
|
|
|
|
linkify,
|
|
|
|
|
} = useMessage($matrix, t, props, emits, processThread);
|
|
|
|
|
|
2025-06-27 16:10:25 +02:00
|
|
|
const rootComponent = computed(() => {
|
|
|
|
|
return isIncoming.value ? MessageIncoming : MessageOutgoing;
|
|
|
|
|
})
|
|
|
|
|
|
2025-06-19 10:58:11 +02:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-06-10 13:35:51 +02:00
|
|
|
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 => {
|
2025-06-27 16:10:25 +02:00
|
|
|
return (isIncoming.value && props.room.displayType == ROOM_TYPE_FILE_MODE) ||
|
2025-06-10 13:35:51 +02:00
|
|
|
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
|
|
|
|
|
});
|
|
|
|
|
|
2025-06-27 16:10:25 +02:00
|
|
|
watch(isVisible, (visible) => {
|
|
|
|
|
if (showMultiview.value && visible) {
|
|
|
|
|
items.value.forEach((a) => a.loadThumbnail());
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2025-06-10 13:35:51 +02:00
|
|
|
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);
|
2025-06-19 10:58:11 +02:00
|
|
|
if (showMultiview.value && isVisible.value) {
|
2025-06-10 13:35:51 +02:00
|
|
|
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;
|
|
|
|
|
});
|
2025-06-19 10:58:11 +02:00
|
|
|
|
2025-06-10 13:35:51 +02:00
|
|
|
</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>
|