open-webui/src/lib/components/channel/Channel.svelte

325 lines
7.6 KiB
Svelte
Raw Normal View History

2024-12-22 11:49:24 +00:00
<script lang="ts">
2024-12-23 02:40:01 +00:00
import { toast } from 'svelte-sonner';
2024-12-31 07:48:55 +00:00
import { Pane, PaneGroup, PaneResizer } from 'paneforge';
2024-12-23 02:40:01 +00:00
import { onDestroy, onMount, tick } from 'svelte';
2024-12-31 07:06:34 +00:00
import { goto } from '$app/navigation';
2024-12-23 02:40:01 +00:00
2024-12-27 05:51:09 +00:00
import { chatId, showSidebar, socket, user } from '$lib/stores';
2024-12-23 05:20:24 +00:00
import { getChannelById, getChannelMessages, sendMessage } from '$lib/apis/channels';
import Messages from './Messages.svelte';
import MessageInput from './MessageInput.svelte';
2024-12-23 05:33:13 +00:00
import Navbar from './Navbar.svelte';
2024-12-31 07:48:55 +00:00
import Drawer from '../common/Drawer.svelte';
import EllipsisVertical from '../icons/EllipsisVertical.svelte';
2024-12-31 08:51:43 +00:00
import Thread from './Thread.svelte';
2025-09-24 15:09:59 +00:00
import i18n from '$lib/i18n';
2024-12-23 05:20:24 +00:00
2024-12-22 11:49:24 +00:00
export let id = '';
2024-12-23 02:40:01 +00:00
let scrollEnd = true;
let messagesContainerElement = null;
2025-09-27 09:05:12 +00:00
let chatInputElement = null;
2024-12-23 02:40:01 +00:00
let top = false;
2024-12-23 05:20:24 +00:00
let channel = null;
2024-12-23 02:40:01 +00:00
let messages = null;
2025-09-27 09:05:12 +00:00
let replyToMessage = null;
2024-12-31 07:48:55 +00:00
let threadId = null;
2024-12-27 05:51:09 +00:00
let typingUsers = [];
let typingUsersTimeout = {};
2024-12-23 02:40:01 +00:00
$: if (id) {
initHandler();
}
2024-12-23 04:56:51 +00:00
const scrollToBottom = () => {
2024-12-31 11:45:54 +00:00
if (messagesContainerElement) {
messagesContainerElement.scrollTop = messagesContainerElement.scrollHeight;
}
2024-12-23 04:56:51 +00:00
};
2024-12-23 02:40:01 +00:00
const initHandler = async () => {
top = false;
messages = null;
2024-12-23 05:20:24 +00:00
channel = null;
2024-12-31 07:55:19 +00:00
threadId = null;
typingUsers = [];
typingUsersTimeout = {};
2024-12-23 05:20:24 +00:00
channel = await getChannelById(localStorage.token, id).catch((error) => {
return null;
});
2024-12-23 02:40:01 +00:00
2024-12-23 05:20:24 +00:00
if (channel) {
2024-12-23 07:31:33 +00:00
messages = await getChannelMessages(localStorage.token, id, 0);
2024-12-23 02:40:01 +00:00
2024-12-23 05:20:24 +00:00
if (messages) {
2024-12-31 11:45:54 +00:00
scrollToBottom();
2024-12-23 03:28:15 +00:00
2024-12-23 05:20:24 +00:00
if (messages.length < 50) {
top = true;
}
2024-12-23 03:28:15 +00:00
}
2024-12-23 05:20:24 +00:00
} else {
goto('/');
2024-12-23 02:40:01 +00:00
}
};
2024-12-23 03:28:15 +00:00
const channelEventHandler = async (event) => {
if (event.channel_id === id) {
const type = event?.data?.type ?? null;
const data = event?.data?.data ?? null;
if (type === 'message') {
2024-12-31 10:05:11 +00:00
if ((data?.parent_id ?? null) === null) {
messages = [data, ...messages];
2024-12-27 05:55:21 +00:00
2024-12-31 10:05:11 +00:00
if (typingUsers.find((user) => user.id === event.user.id)) {
typingUsers = typingUsers.filter((user) => user.id !== event.user.id);
}
2024-12-27 05:55:21 +00:00
2024-12-31 10:05:11 +00:00
await tick();
if (scrollEnd) {
2024-12-31 11:45:54 +00:00
scrollToBottom();
2024-12-31 10:05:11 +00:00
}
2024-12-23 03:28:15 +00:00
}
2024-12-23 08:12:55 +00:00
} else if (type === 'message:update') {
const idx = messages.findIndex((message) => message.id === data.id);
if (idx !== -1) {
messages[idx] = data;
}
} else if (type === 'message:delete') {
messages = messages.filter((message) => message.id !== data.id);
2024-12-31 10:16:07 +00:00
} else if (type === 'message:reply') {
const idx = messages.findIndex((message) => message.id === data.id);
if (idx !== -1) {
messages[idx] = data;
}
2024-12-31 10:05:11 +00:00
} else if (type.includes('message:reaction')) {
2024-12-31 07:06:34 +00:00
const idx = messages.findIndex((message) => message.id === data.id);
if (idx !== -1) {
messages[idx] = data;
}
2024-12-31 10:16:07 +00:00
} else if (type === 'typing' && event.message_id === null) {
2025-04-01 03:32:12 +00:00
if (event.user.id === $user?.id) {
2024-12-27 05:51:09 +00:00
return;
}
typingUsers = data.typing
? [
...typingUsers,
...(typingUsers.find((user) => user.id === event.user.id)
? []
: [
{
id: event.user.id,
name: event.user.name
}
])
]
: typingUsers.filter((user) => user.id !== event.user.id);
if (typingUsersTimeout[event.user.id]) {
clearTimeout(typingUsersTimeout[event.user.id]);
}
typingUsersTimeout[event.user.id] = setTimeout(() => {
typingUsers = typingUsers.filter((user) => user.id !== event.user.id);
}, 5000);
2024-12-23 03:28:15 +00:00
}
}
2024-12-23 02:40:01 +00:00
};
2024-12-23 21:43:58 +00:00
const submitHandler = async ({ content, data }) => {
2025-01-01 03:42:49 +00:00
if (!content && (data?.files ?? []).length === 0) {
2024-12-23 02:40:01 +00:00
return;
}
2025-09-27 09:05:12 +00:00
const res = await sendMessage(localStorage.token, id, {
content: content,
data: data,
reply_to_id: replyToMessage?.id ?? null
}).catch((error) => {
toast.error(`${error}`);
return null;
});
2024-12-23 02:40:01 +00:00
if (res) {
messagesContainerElement.scrollTop = messagesContainerElement.scrollHeight;
}
2025-09-27 09:05:12 +00:00
replyToMessage = null;
2024-12-23 02:40:01 +00:00
};
2024-12-27 05:51:09 +00:00
const onChange = async () => {
$socket?.emit('channel-events', {
channel_id: id,
2024-12-31 08:51:43 +00:00
message_id: null,
2024-12-27 05:51:09 +00:00
data: {
type: 'typing',
data: {
typing: true
}
}
});
};
2024-12-31 07:48:55 +00:00
let mediaQuery;
let largeScreen = false;
2024-12-23 02:40:01 +00:00
onMount(() => {
2024-12-25 05:04:43 +00:00
if ($chatId) {
chatId.set('');
}
2024-12-23 02:40:01 +00:00
$socket?.on('channel-events', channelEventHandler);
2024-12-31 07:48:55 +00:00
mediaQuery = window.matchMedia('(min-width: 1024px)');
const handleMediaQuery = async (e) => {
if (e.matches) {
largeScreen = true;
} else {
largeScreen = false;
}
};
mediaQuery.addEventListener('change', handleMediaQuery);
handleMediaQuery(mediaQuery);
2024-12-23 02:40:01 +00:00
});
onDestroy(() => {
2024-12-31 11:54:43 +00:00
$socket?.off('channel-events', channelEventHandler);
2024-12-23 02:40:01 +00:00
});
2024-12-22 11:49:24 +00:00
</script>
2024-12-27 06:03:39 +00:00
<svelte:head>
2025-05-03 14:16:32 +00:00
<title>#{channel?.name ?? 'Channel'} • Open WebUI</title>
2024-12-27 06:03:39 +00:00
</svelte:head>
2024-12-23 05:33:13 +00:00
<div
2025-01-14 03:23:57 +00:00
class="h-screen max-h-[100dvh] transition-width duration-200 ease-in-out {$showSidebar
2024-12-23 05:33:13 +00:00
? 'md:max-w-[calc(100%-260px)]'
: ''} w-full max-w-full flex flex-col"
2024-12-23 21:43:58 +00:00
id="channel-container"
2024-12-23 05:33:13 +00:00
>
2024-12-31 07:48:55 +00:00
<PaneGroup direction="horizontal" class="w-full h-full">
<Pane defaultSize={50} minSize={50} class="h-full flex flex-col w-full relative">
<Navbar {channel} />
<div class="flex-1 overflow-y-auto">
{#if channel}
<div
class=" pb-2.5 max-w-full z-10 scrollbar-hidden w-full h-full pt-6 flex-1 flex flex-col-reverse overflow-auto"
id="messages-container"
bind:this={messagesContainerElement}
on:scroll={(e) => {
scrollEnd = Math.abs(messagesContainerElement.scrollTop) <= 50;
}}
>
{#key id}
<Messages
{channel}
{top}
2025-09-27 09:05:12 +00:00
{messages}
{replyToMessage}
onReply={async (message) => {
replyToMessage = message;
await tick();
chatInputElement?.focus();
}}
2024-12-31 07:48:55 +00:00
onThread={(id) => {
threadId = id;
}}
onLoad={async () => {
const newMessages = await getChannelMessages(
localStorage.token,
id,
messages.length
);
messages = [...messages, ...newMessages];
if (newMessages.length < 50) {
top = true;
return;
}
}}
/>
{/key}
</div>
{/if}
</div>
2025-07-07 15:26:12 +00:00
<div class=" pb-[1rem] px-2.5">
2024-12-31 07:48:55 +00:00
<MessageInput
2024-12-31 10:16:07 +00:00
id="root"
2025-09-27 09:05:12 +00:00
bind:chatInputElement
bind:replyToMessage
2024-12-31 07:48:55 +00:00
{typingUsers}
2025-09-17 02:41:47 +00:00
userSuggestions={true}
channelSuggestions={true}
2025-09-24 15:09:59 +00:00
disabled={!channel?.write_access}
placeholder={!channel?.write_access
? $i18n.t('You do not have permission to send messages in this channel.')
: $i18n.t('Type here...')}
2024-12-31 07:48:55 +00:00
{onChange}
onSubmit={submitHandler}
{scrollToBottom}
{scrollEnd}
/>
</div>
</Pane>
{#if !largeScreen}
{#if threadId !== null}
<Drawer
show={threadId !== null}
2025-04-18 08:26:42 +00:00
onClose={() => {
2024-12-31 07:48:55 +00:00
threadId = null;
}}
>
2025-01-14 05:13:16 +00:00
<div class=" {threadId !== null ? ' h-screen w-full' : 'px-6 py-4'} h-full">
2024-12-31 07:48:55 +00:00
<Thread
{threadId}
{channel}
onClose={() => {
threadId = null;
}}
/>
</div>
</Drawer>
{/if}
{:else if threadId !== null}
<PaneResizer
2025-09-15 19:08:07 +00:00
class="relative flex items-center justify-center group border-l border-gray-50 dark:border-gray-850 hover:border-gray-200 dark:hover:border-gray-800 transition z-20"
id="controls-resizer"
2024-12-23 08:35:03 +00:00
>
2025-09-15 19:08:07 +00:00
<div
class=" absolute -left-1.5 -right-1.5 -top-0 -bottom-0 z-20 cursor-col-resize bg-transparent"
/>
2024-12-31 07:48:55 +00:00
</PaneResizer>
2024-12-31 08:51:43 +00:00
<Pane defaultSize={50} minSize={30} class="h-full w-full">
2024-12-31 07:48:55 +00:00
<div class="h-full w-full shadow-xl">
<Thread
{threadId}
2024-12-23 08:35:03 +00:00
{channel}
2024-12-31 07:48:55 +00:00
onClose={() => {
threadId = null;
2024-12-23 08:35:03 +00:00
}}
/>
2024-12-31 07:48:55 +00:00
</div>
</Pane>
2024-12-23 08:35:03 +00:00
{/if}
2024-12-31 07:48:55 +00:00
</PaneGroup>
2024-12-23 02:40:01 +00:00
</div>