123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302 |
- <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-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>
|