Make images and video smaller in chat
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 55s

This commit is contained in:
2026-03-17 00:53:37 +00:00
parent c1ce3c31ba
commit a0215f7810

View File

@@ -13,136 +13,129 @@ const messagesContainer = ref(null);
const fileInput = ref(null); const fileInput = ref(null);
function scrollToBottom() { function scrollToBottom() {
nextTick(() => { nextTick(() => {
if (messagesContainer.value) { if (messagesContainer.value) {
messagesContainer.value.scrollTop = messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight;
messagesContainer.value.scrollHeight; }
} });
});
} }
watch(messages, scrollToBottom, { deep: true }); watch(messages, scrollToBottom, { deep: true });
function sendMessage() { function sendMessage() {
const text = messageInput.value.trim(); const text = messageInput.value.trim();
if (!text) return; if (!text) return;
messagesStore.sendMessage(text); messagesStore.sendMessage(text);
messageInput.value = ""; messageInput.value = "";
} }
async function onFileSelected(e) { async function onFileSelected(e) {
const file = e.target.files[0]; const file = e.target.files[0];
if (!file) return; if (!file) return;
await messagesStore.uploadAndSendFile(file); await messagesStore.uploadAndSendFile(file);
fileInput.value.value = ""; fileInput.value.value = "";
} }
function isImageUrl(url) { function isImageUrl(url) {
return /\.(jpg|jpeg|png|gif|webp)$/i.test(url); return /\.(jpg|jpeg|png|gif|webp)$/i.test(url);
} }
function isVideoUrl(url) { function isVideoUrl(url) {
return /\.(mp4|webm|ogg|mov)$/i.test(url); return /\.(mp4|webm|ogg|mov)$/i.test(url);
} }
function isSafeFileUrl(url) { function isSafeFileUrl(url) {
return typeof url === "string" && url.startsWith("/uploads/"); return typeof url === "string" && url.startsWith("/uploads/");
} }
const urlRegex = /(https?:\/\/[^\s<]+)/g; const urlRegex = /(https?:\/\/[^\s<]+)/g;
function parseMessageParts(text) { function parseMessageParts(text) {
const parts = []; const parts = [];
let lastIndex = 0; let lastIndex = 0;
let match; let match;
while ((match = urlRegex.exec(text)) !== null) { while ((match = urlRegex.exec(text)) !== null) {
if (match.index > lastIndex) { if (match.index > lastIndex) {
parts.push({ parts.push({
type: "text", type: "text",
value: text.slice(lastIndex, match.index), value: text.slice(lastIndex, match.index),
}); });
}
parts.push({ type: "link", value: match[1] });
lastIndex = urlRegex.lastIndex;
} }
if (lastIndex < text.length) { parts.push({ type: "link", value: match[1] });
parts.push({ type: "text", value: text.slice(lastIndex) }); lastIndex = urlRegex.lastIndex;
} }
return parts; if (lastIndex < text.length) {
parts.push({ type: "text", value: text.slice(lastIndex) });
}
return parts;
} }
onMounted(() => { onMounted(() => {
messagesStore.connect(); messagesStore.connect();
}); });
onUnmounted(() => { onUnmounted(() => {
messagesStore.disconnect(); messagesStore.disconnect();
}); });
</script> </script>
<template> <template>
<div class="flex-col flex"> <div class="flex-col flex">
<Header>Chat</Header> <Header>Chat</Header>
<div ref="messagesContainer" class="flex flex-col overflow-y-auto p-2"> <div ref="messagesContainer" class="flex flex-col overflow-y-auto p-2">
<p v-for="message in messages" :key="message.id"> <p v-for="message in messages" :key="message.id">
<span class="text-tertiary">{{ message.authorId }}:</span> <span class="text-tertiary">{{ message.authorId }}:</span>
<template <template
v-for="(part, i) in parseMessageParts(message.text || '')" v-for="(part, i) in parseMessageParts(message.text || '')"
:key="i" :key="i"
> >
<a <a
v-if="part.type === 'link'" v-if="part.type === 'link'"
:href="part.value" :href="part.value"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
class="text-primary underline" class="text-primary underline"
>{{ part.value }}</a >{{ part.value }}</a
> >
<span v-else>{{ part.value }}</span> <span v-else>{{ part.value }}</span>
</template> </template>
<template <template v-if="message.fileUrl && isSafeFileUrl(message.fileUrl)">
v-if="message.fileUrl && isSafeFileUrl(message.fileUrl)" <img
> v-if="isImageUrl(message.fileUrl)"
<img :src="message.fileUrl"
v-if="isImageUrl(message.fileUrl)" class="w-full max-h-48 rounded"
:src="message.fileUrl" @load="scrollToBottom"
class="max-w-xs max-h-48 rounded" />
@load="scrollToBottom" <video
/> v-else-if="isVideoUrl(message.fileUrl)"
<video :src="message.fileUrl"
v-else-if="isVideoUrl(message.fileUrl)" controls
:src="message.fileUrl" class="w-full max-h-48 rounded"
controls @loadedmetadata="scrollToBottom"
class="max-w-xs max-h-48 rounded" />
@loadedmetadata="scrollToBottom" <a v-else :href="message.fileUrl" target="_blank" class="underline">{{
/> message.fileUrl.split("/").pop()
<a }}</a>
v-else </template>
:href="message.fileUrl" </p>
target="_blank"
class="underline"
>{{ message.fileUrl.split("/").pop() }}</a
>
</template>
</p>
</div>
<div>
<input v-model="messageInput" @keyup.enter="sendMessage" />
<input
ref="fileInput"
type="file"
class="hidden"
@change="onFileSelected"
/>
<div class="flex gap-2">
<Button class="flex-1" @click="sendMessage">Send</Button>
<Button
v-if="authStore.user.admin"
class="flex-1"
@click="fileInput.click()"
>Attach</Button
>
<Button class="flex-1" @click="scrollToBottom">Bottom</Button>
</div>
</div>
</div> </div>
<div>
<input v-model="messageInput" @keyup.enter="sendMessage" />
<input
ref="fileInput"
type="file"
class="hidden"
@change="onFileSelected"
/>
<div class="flex gap-2">
<Button class="flex-1" @click="sendMessage">Send</Button>
<Button
v-if="authStore.user.admin"
class="flex-1"
@click="fileInput.click()"
>Attach</Button
>
<Button class="flex-1" @click="scrollToBottom">Bottom</Button>
</div>
</div>
</div>
</template> </template>