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
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">
|
||||
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 QuickReactions from "../QuickReactions.vue";
|
||||
import QuickReactionsChannel from "../channel/QuickReactionsChannel.vue";
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
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 QuickReactions from "../QuickReactions.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>
|
||||
<MessageIncoming
|
||||
<component
|
||||
:is="rootComponent"
|
||||
ref="root"
|
||||
v-bind="{ ...$props, ...$attrs }"
|
||||
v-if="showMultiview"
|
||||
v-intersect="onIntersect"
|
||||
>
|
||||
<div class="bubble">
|
||||
<div class="original-message" v-if="inReplyToText">
|
||||
|
|
@ -25,7 +25,7 @@
|
|||
</v-row>
|
||||
</v-container>
|
||||
<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)
|
||||
? $t("message.incoming_message_deleted_text")
|
||||
|
|
@ -45,10 +45,10 @@
|
|||
v-if="!!showItem"
|
||||
v-on:close="showItem = undefined"
|
||||
/>
|
||||
</MessageIncoming>
|
||||
</component>
|
||||
<component
|
||||
v-else-if="items.length == 1"
|
||||
:is="componentFn(items[0].event)"
|
||||
:is="$props.componentFn(items[0].event, false)"
|
||||
v-bind="{ ...$props, ...$attrs }"
|
||||
:originalEvent="items[0].event"
|
||||
/>
|
||||
|
|
@ -56,26 +56,29 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
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 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 { 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";
|
||||
import { useLazyLoad } from "./useLazyLoad";
|
||||
|
||||
const { t } = useI18n()
|
||||
const $matrix: any = inject('globalMatrix');
|
||||
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 items: Ref<EventAttachment[]> = ref([]);
|
||||
const showItem: Ref<EventAttachment | undefined> = ref(undefined);
|
||||
const isVisible: Ref<boolean> = ref(false);
|
||||
|
||||
const props = defineProps<MessageProps>();
|
||||
|
||||
|
|
@ -83,13 +86,19 @@ const { room } = props;
|
|||
|
||||
const processThread = () => {
|
||||
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 {
|
||||
event,
|
||||
thread,
|
||||
isIncoming,
|
||||
senderIsAdminOrModerator,
|
||||
inReplyToSender,
|
||||
inReplyToText,
|
||||
|
|
@ -98,6 +107,10 @@ const {
|
|||
linkify,
|
||||
} = useMessage($matrix, t, props, emits, processThread);
|
||||
|
||||
const rootComponent = computed(() => {
|
||||
return isIncoming.value ? MessageIncoming : MessageOutgoing;
|
||||
})
|
||||
|
||||
const onRelationsCreated = () => {
|
||||
if (event.value) {
|
||||
thread.value = props.timelineSet.relations.getChildEventsForEvent(
|
||||
|
|
@ -129,13 +142,19 @@ onBeforeUnmount(() => {
|
|||
});
|
||||
|
||||
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 ||
|
||||
(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
|
||||
});
|
||||
|
||||
watch(isVisible, (visible) => {
|
||||
if (showMultiview.value && visible) {
|
||||
items.value.forEach((a) => a.loadThumbnail());
|
||||
}
|
||||
});
|
||||
|
||||
const onItemClick = (event: any) => {
|
||||
showItem.value = event.item;
|
||||
};
|
||||
|
|
@ -187,13 +206,6 @@ const layoutedItems = computed(() => {
|
|||
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 *;
|
||||
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 { EventTimelineSet, Relations, RelationsEvent } from "matrix-js-sdk";
|
||||
import { KeanuEvent, KeanuRoom } from "../../../models/eventAttachment";
|
||||
import { EventAttachment, KeanuEvent, KeanuRoom } from "../../../models/eventAttachment";
|
||||
|
||||
export interface MessageProps {
|
||||
room: KeanuRoom;
|
||||
originalEvent: KeanuEvent;
|
||||
nextEvent: KeanuEvent | null | undefined;
|
||||
timelineSet: EventTimelineSet;
|
||||
componentFn: (event: KeanuEvent) => any;
|
||||
componentFn: (event: KeanuEvent, forExport: boolean) => any;
|
||||
}
|
||||
|
||||
export type MessageEmits = {
|
||||
|
|
@ -33,8 +33,11 @@ export const useMessage = (
|
|||
processThread?: () => void
|
||||
) => {
|
||||
const event: Ref<KeanuEvent | undefined> = ref(undefined);
|
||||
const attachment: Ref<EventAttachment | undefined> = ref(undefined);
|
||||
const thread: Ref<Relations | undefined> = ref(undefined);
|
||||
|
||||
const isIncoming = ref(props.originalEvent.getSender() != $matrix.currentUserId);
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
thread.value = undefined;
|
||||
});
|
||||
|
|
@ -43,6 +46,7 @@ export const useMessage = (
|
|||
props.originalEvent,
|
||||
(originalEvent) => {
|
||||
event.value = originalEvent;
|
||||
attachment.value = $matrix.attachmentManager.getEventAttachment(event.value);
|
||||
|
||||
// Check not null and not {}
|
||||
if (originalEvent && originalEvent.isBeingDecrypted && originalEvent.isBeingDecrypted()) {
|
||||
|
|
@ -355,6 +359,8 @@ export const useMessage = (
|
|||
|
||||
return {
|
||||
event,
|
||||
isIncoming,
|
||||
attachment,
|
||||
thread,
|
||||
|
||||
validEvent,
|
||||
Loading…
Add table
Add a link
Reference in a new issue