Spaces:
Sleeping
Sleeping
<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 && (data?.files ?? []).length === 0) { | |
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] transition-width duration-200 ease-in-out {$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-full' : '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-xs"> | |
<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> | |