/
Netrunners
/
Millenium_UI
Обзор
Документация
Войти
/
Netrunners
/
Millenium_UI
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
src/lib/components/channel/Channel.svelte
302 строки
7 KB
Валерий Конюхов
first_commit
08 июл 2025, 22:20
08 июл 2025, 22:20
a4e8dc9
Код
Авторство
О чём код?
<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'} | Millennium UI</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>