mirror of
https://github.com/open-webui/open-webui
synced 2025-04-24 00:06:11 +00:00
303 lines
6.8 KiB
Svelte
303 lines
6.8 KiB
Svelte
<script lang="ts">
|
|
import { toast } from 'svelte-sonner';
|
|
import { Pane, PaneGroup, PaneResizer } from 'paneforge';
|
|
|
|
import { onDestroy, onMount, tick } from 'svelte';
|
|
import { goto } from '$app/navigation';
|
|
|
|
import { chatId, showSidebar, socket, user } from '$lib/stores';
|
|
import { getChannelById, getChannelMessages, sendMessage } from '$lib/apis/channels';
|
|
|
|
import Messages from './Messages.svelte';
|
|
import MessageInput from './MessageInput.svelte';
|
|
import Navbar from './Navbar.svelte';
|
|
import Drawer from '../common/Drawer.svelte';
|
|
import EllipsisVertical from '../icons/EllipsisVertical.svelte';
|
|
import Thread from './Thread.svelte';
|
|
|
|
export let id = '';
|
|
|
|
let scrollEnd = true;
|
|
let messagesContainerElement = null;
|
|
|
|
let top = false;
|
|
|
|
let channel = null;
|
|
let messages = null;
|
|
|
|
let threadId = null;
|
|
|
|
let typingUsers = [];
|
|
let typingUsersTimeout = {};
|
|
|
|
$: if (id) {
|
|
initHandler();
|
|
}
|
|
|
|
const scrollToBottom = () => {
|
|
if (messagesContainerElement) {
|
|
messagesContainerElement.scrollTop = messagesContainerElement.scrollHeight;
|
|
}
|
|
};
|
|
|
|
const initHandler = async () => {
|
|
top = false;
|
|
messages = null;
|
|
channel = null;
|
|
threadId = null;
|
|
|
|
typingUsers = [];
|
|
typingUsersTimeout = {};
|
|
|
|
channel = await getChannelById(localStorage.token, id).catch((error) => {
|
|
return null;
|
|
});
|
|
|
|
if (channel) {
|
|
messages = await getChannelMessages(localStorage.token, id, 0);
|
|
|
|
if (messages) {
|
|
scrollToBottom();
|
|
|
|
if (messages.length < 50) {
|
|
top = true;
|
|
}
|
|
}
|
|
} else {
|
|
goto('/');
|
|
}
|
|
};
|
|
|
|
const channelEventHandler = async (event) => {
|
|
if (event.channel_id === id) {
|
|
const type = event?.data?.type ?? null;
|
|
const data = event?.data?.data ?? null;
|
|
|
|
if (type === 'message') {
|
|
if ((data?.parent_id ?? null) === null) {
|
|
messages = [data, ...messages];
|
|
|
|
if (typingUsers.find((user) => user.id === event.user.id)) {
|
|
typingUsers = typingUsers.filter((user) => user.id !== event.user.id);
|
|
}
|
|
|
|
await tick();
|
|
if (scrollEnd) {
|
|
scrollToBottom();
|
|
}
|
|
}
|
|
} 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);
|
|
} else if (type === 'message:reply') {
|
|
const idx = messages.findIndex((message) => message.id === data.id);
|
|
|
|
if (idx !== -1) {
|
|
messages[idx] = data;
|
|
}
|
|
} else if (type.includes('message:reaction')) {
|
|
const idx = messages.findIndex((message) => message.id === data.id);
|
|
if (idx !== -1) {
|
|
messages[idx] = data;
|
|
}
|
|
} else if (type === 'typing' && event.message_id === null) {
|
|
if (event.user.id === $user.id) {
|
|
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);
|
|
}
|
|
}
|
|
};
|
|
|
|
const submitHandler = async ({ content, data }) => {
|
|
if (!content) {
|
|
return;
|
|
}
|
|
|
|
const res = await sendMessage(localStorage.token, id, { content: content, data: data }).catch(
|
|
(error) => {
|
|
toast.error(error);
|
|
return null;
|
|
}
|
|
);
|
|
|
|
if (res) {
|
|
messagesContainerElement.scrollTop = messagesContainerElement.scrollHeight;
|
|
}
|
|
};
|
|
|
|
const onChange = async () => {
|
|
$socket?.emit('channel-events', {
|
|
channel_id: id,
|
|
message_id: null,
|
|
data: {
|
|
type: 'typing',
|
|
data: {
|
|
typing: true
|
|
}
|
|
}
|
|
});
|
|
};
|
|
|
|
let mediaQuery;
|
|
let largeScreen = false;
|
|
|
|
onMount(() => {
|
|
if ($chatId) {
|
|
chatId.set('');
|
|
}
|
|
|
|
$socket?.on('channel-events', channelEventHandler);
|
|
|
|
mediaQuery = window.matchMedia('(min-width: 1024px)');
|
|
|
|
const handleMediaQuery = async (e) => {
|
|
if (e.matches) {
|
|
largeScreen = true;
|
|
} else {
|
|
largeScreen = false;
|
|
}
|
|
};
|
|
|
|
mediaQuery.addEventListener('change', handleMediaQuery);
|
|
handleMediaQuery(mediaQuery);
|
|
});
|
|
|
|
onDestroy(() => {
|
|
$socket?.off('channel-events', channelEventHandler);
|
|
});
|
|
</script>
|
|
|
|
<svelte:head>
|
|
<title>#{channel?.name ?? 'Channel'} | Open WebUI</title>
|
|
</svelte:head>
|
|
|
|
<div
|
|
class="h-screen max-h-[100dvh] {$showSidebar
|
|
? 'md:max-w-[calc(100%-260px)]'
|
|
: ''} w-full max-w-full flex flex-col"
|
|
id="channel-container"
|
|
>
|
|
<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}
|
|
{messages}
|
|
{top}
|
|
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>
|
|
|
|
<div class=" pb-[1rem]">
|
|
<MessageInput
|
|
id="root"
|
|
{typingUsers}
|
|
{onChange}
|
|
onSubmit={submitHandler}
|
|
{scrollToBottom}
|
|
{scrollEnd}
|
|
/>
|
|
</div>
|
|
</Pane>
|
|
|
|
{#if !largeScreen}
|
|
{#if threadId !== null}
|
|
<Drawer
|
|
show={threadId !== null}
|
|
on:close={() => {
|
|
threadId = null;
|
|
}}
|
|
>
|
|
<div class=" {threadId !== null ? ' h-screen w-screen' : 'px-6 py-4'} h-full">
|
|
<Thread
|
|
{threadId}
|
|
{channel}
|
|
onClose={() => {
|
|
threadId = null;
|
|
}}
|
|
/>
|
|
</div>
|
|
</Drawer>
|
|
{/if}
|
|
{:else if threadId !== null}
|
|
<PaneResizer
|
|
class="relative flex w-[3px] items-center justify-center bg-background group bg-gray-50 dark:bg-gray-850"
|
|
>
|
|
<div class="z-10 flex h-7 w-5 items-center justify-center rounded-sm">
|
|
<EllipsisVertical className="size-4 invisible group-hover:visible" />
|
|
</div>
|
|
</PaneResizer>
|
|
|
|
<Pane defaultSize={50} minSize={30} class="h-full w-full">
|
|
<div class="h-full w-full shadow-xl">
|
|
<Thread
|
|
{threadId}
|
|
{channel}
|
|
onClose={() => {
|
|
threadId = null;
|
|
}}
|
|
/>
|
|
</div>
|
|
</Pane>
|
|
{/if}
|
|
</PaneGroup>
|
|
</div>
|