open-webui/src/lib/components/channel/Messages.svelte
Timothy Jaeryang Baek c9185aaf44 refac
2025-11-29 14:18:22 -05:00

254 lines
6.7 KiB
Svelte

<script lang="ts">
import { toast } from 'svelte-sonner';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import isToday from 'dayjs/plugin/isToday';
import isYesterday from 'dayjs/plugin/isYesterday';
dayjs.extend(relativeTime);
dayjs.extend(isToday);
dayjs.extend(isYesterday);
import { tick, getContext, onMount, createEventDispatcher } from 'svelte';
import { settings, user } from '$lib/stores';
import Message from './Messages/Message.svelte';
import Loader from '../common/Loader.svelte';
import Spinner from '../common/Spinner.svelte';
import {
addReaction,
deleteMessage,
pinMessage,
removeReaction,
updateMessage
} from '$lib/apis/channels';
import { WEBUI_API_BASE_URL } from '$lib/constants';
const i18n = getContext('i18n');
export let id = null;
export let channel = null;
export let messages = [];
export let replyToMessage = null;
export let top = false;
export let thread = false;
export let onLoad: Function = () => {};
export let onReply: Function = () => {};
export let onThread: Function = () => {};
let messagesLoading = false;
const loadMoreMessages = async () => {
// scroll slightly down to disable continuous loading
const element = document.getElementById('messages-container');
element.scrollTop = element.scrollTop + 100;
messagesLoading = true;
await onLoad();
await tick();
messagesLoading = false;
};
</script>
{#if messages}
{@const messageList = messages.slice().reverse()}
<div>
{#if !top}
<Loader
on:visible={(e) => {
console.info('visible');
if (!messagesLoading) {
loadMoreMessages();
}
}}
>
<div class="w-full flex justify-center py-1 text-xs animate-pulse items-center gap-2">
<Spinner className=" size-4" />
<div class=" ">{$i18n.t('Loading...')}</div>
</div>
</Loader>
{:else if !thread}
<div class="px-5 max-w-full mx-auto">
{#if channel}
<div class="flex flex-col gap-1.5 pb-5 pt-10">
{#if channel?.type === 'dm'}
<div class="flex ml-[1px] mr-0.5">
{#each channel.users.filter((u) => u.id !== $user?.id).slice(0, 2) as u, index}
<img
src={`${WEBUI_API_BASE_URL}/users/${u.id}/profile/image`}
alt={u.name}
class=" size-7.5 rounded-full border-2 border-white dark:border-gray-900 {index ===
1
? '-ml-2.5'
: ''}"
/>
{/each}
</div>
{/if}
<div class="text-2xl font-medium capitalize">
{#if channel?.name}
{channel.name}
{:else}
{channel?.users
?.filter((u) => u.id !== $user?.id)
.map((u) => u.name)
.join(', ')}
{/if}
</div>
<div class=" text-gray-500">
{$i18n.t(
'This channel was created on {{createdAt}}. This is the very beginning of the {{channelName}} channel.',
{
createdAt: dayjs(channel.created_at / 1000000).format('MMMM D, YYYY'),
channelName: channel.name
}
)}
</div>
</div>
{:else}
<div class="flex justify-center text-xs items-center gap-2 py-5">
<div class=" ">{$i18n.t('Start of the channel')}</div>
</div>
{/if}
{#if messageList.length > 0}
<hr class=" border-gray-50 dark:border-gray-700/20 py-2.5 w-full" />
{/if}
</div>
{/if}
{#each messageList as message, messageIdx (id ? `${id}-${message.id}` : message.id)}
<Message
{message}
{thread}
replyToMessage={replyToMessage?.id === message.id}
disabled={!channel?.write_access || message?.temp_id}
pending={!!message?.temp_id}
showUserProfile={messageIdx === 0 ||
messageList.at(messageIdx - 1)?.user_id !== message.user_id ||
messageList.at(messageIdx - 1)?.meta?.model_id !== message?.meta?.model_id ||
message?.reply_to_message !== null}
onDelete={() => {
messages = messages.filter((m) => m.id !== message.id);
const res = deleteMessage(localStorage.token, message.channel_id, message.id).catch(
(error) => {
toast.error(`${error}`);
return null;
}
);
}}
onEdit={(content) => {
messages = messages.map((m) => {
if (m.id === message.id) {
m.content = content;
}
return m;
});
const res = updateMessage(localStorage.token, message.channel_id, message.id, {
content: content
}).catch((error) => {
toast.error(`${error}`);
return null;
});
}}
onReply={(message) => {
onReply(message);
}}
onPin={async (message) => {
messages = messages.map((m) => {
if (m.id === message.id) {
m.is_pinned = !m.is_pinned;
m.pinned_by = !m.is_pinned ? null : $user?.id;
m.pinned_at = !m.is_pinned ? null : Date.now() * 1000000;
}
return m;
});
const updatedMessage = await pinMessage(
localStorage.token,
message.channel_id,
message.id,
message.is_pinned
).catch((error) => {
toast.error(`${error}`);
return null;
});
}}
onThread={(id) => {
onThread(id);
}}
onReaction={(name) => {
if (
(message?.reactions ?? [])
.find((reaction) => reaction.name === name)
?.users?.some((u) => u.id === $user?.id) ??
false
) {
messages = messages.map((m) => {
if (m.id === message.id) {
const reaction = m.reactions.find((reaction) => reaction.name === name);
if (reaction) {
reaction.users = reaction.users.filter((u) => u.id !== $user?.id);
reaction.count = reaction.users.length;
if (reaction.count === 0) {
m.reactions = m.reactions.filter((r) => r.name !== name);
}
}
}
return m;
});
const res = removeReaction(
localStorage.token,
message.channel_id,
message.id,
name
).catch((error) => {
toast.error(`${error}`);
return null;
});
} else {
messages = messages.map((m) => {
if (m.id === message.id) {
if (m.reactions) {
const reaction = m.reactions.find((reaction) => reaction.name === name);
if (reaction) {
reaction.users.push({ id: $user?.id, name: $user?.name });
reaction.count = reaction.users.length;
} else {
m.reactions.push({
name: name,
users: [{ id: $user?.id, name: $user?.name }],
count: 1
});
}
}
}
return m;
});
const res = addReaction(localStorage.token, message.channel_id, message.id, name).catch(
(error) => {
toast.error(`${error}`);
return null;
}
);
}
}}
/>
{/each}
<div class="pb-6" />
</div>
{/if}