Channel.svelte 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. <script lang="ts">
  2. import { toast } from 'svelte-sonner';
  3. import { onDestroy, onMount, tick } from 'svelte';
  4. import { chatId, showSidebar, socket, user } from '$lib/stores';
  5. import { getChannelById, getChannelMessages, sendMessage } from '$lib/apis/channels';
  6. import Messages from './Messages.svelte';
  7. import MessageInput from './MessageInput.svelte';
  8. import { goto } from '$app/navigation';
  9. import Navbar from './Navbar.svelte';
  10. export let id = '';
  11. let scrollEnd = true;
  12. let messagesContainerElement = null;
  13. let top = false;
  14. let channel = null;
  15. let messages = null;
  16. let typingUsers = [];
  17. let typingUsersTimeout = {};
  18. $: if (id) {
  19. initHandler();
  20. }
  21. const scrollToBottom = () => {
  22. messagesContainerElement.scrollTop = messagesContainerElement.scrollHeight;
  23. };
  24. const initHandler = async () => {
  25. top = false;
  26. messages = null;
  27. channel = null;
  28. channel = await getChannelById(localStorage.token, id).catch((error) => {
  29. return null;
  30. });
  31. if (channel) {
  32. messages = await getChannelMessages(localStorage.token, id, 0);
  33. if (messages) {
  34. messagesContainerElement.scrollTop = messagesContainerElement.scrollHeight;
  35. if (messages.length < 50) {
  36. top = true;
  37. }
  38. }
  39. } else {
  40. goto('/');
  41. }
  42. };
  43. const channelEventHandler = async (event) => {
  44. console.log(event);
  45. if (event.channel_id === id) {
  46. const type = event?.data?.type ?? null;
  47. const data = event?.data?.data ?? null;
  48. if (type === 'message') {
  49. console.log('message', data);
  50. messages = [data, ...messages];
  51. if (typingUsers.find((user) => user.id === event.user.id)) {
  52. typingUsers = typingUsers.filter((user) => user.id !== event.user.id);
  53. }
  54. await tick();
  55. if (scrollEnd) {
  56. messagesContainerElement.scrollTop = messagesContainerElement.scrollHeight;
  57. }
  58. } else if (type === 'message:update') {
  59. console.log('message:update', data);
  60. const idx = messages.findIndex((message) => message.id === data.id);
  61. if (idx !== -1) {
  62. messages[idx] = data;
  63. }
  64. } else if (type === 'message:delete') {
  65. console.log('message:delete', data);
  66. messages = messages.filter((message) => message.id !== data.id);
  67. } else if (type === 'typing') {
  68. if (event.user.id === $user.id) {
  69. return;
  70. }
  71. typingUsers = data.typing
  72. ? [
  73. ...typingUsers,
  74. ...(typingUsers.find((user) => user.id === event.user.id)
  75. ? []
  76. : [
  77. {
  78. id: event.user.id,
  79. name: event.user.name
  80. }
  81. ])
  82. ]
  83. : typingUsers.filter((user) => user.id !== event.user.id);
  84. if (typingUsersTimeout[event.user.id]) {
  85. clearTimeout(typingUsersTimeout[event.user.id]);
  86. }
  87. typingUsersTimeout[event.user.id] = setTimeout(() => {
  88. typingUsers = typingUsers.filter((user) => user.id !== event.user.id);
  89. }, 5000);
  90. }
  91. }
  92. };
  93. const submitHandler = async ({ content, data }) => {
  94. if (!content) {
  95. return;
  96. }
  97. const res = await sendMessage(localStorage.token, id, { content: content, data: data }).catch(
  98. (error) => {
  99. toast.error(error);
  100. return null;
  101. }
  102. );
  103. if (res) {
  104. messagesContainerElement.scrollTop = messagesContainerElement.scrollHeight;
  105. }
  106. };
  107. const onChange = async () => {
  108. $socket?.emit('channel-events', {
  109. channel_id: id,
  110. data: {
  111. type: 'typing',
  112. data: {
  113. typing: true
  114. }
  115. }
  116. });
  117. };
  118. onMount(() => {
  119. if ($chatId) {
  120. chatId.set('');
  121. }
  122. $socket?.on('channel-events', channelEventHandler);
  123. });
  124. onDestroy(() => {
  125. // $socket?.off('channel-events', channelEventHandler);
  126. });
  127. </script>
  128. <svelte:head>
  129. <title>#{channel?.name ?? 'Channel'} | Open WebUI</title>
  130. </svelte:head>
  131. <div
  132. class="h-screen max-h-[100dvh] {$showSidebar
  133. ? 'md:max-w-[calc(100%-260px)]'
  134. : ''} w-full max-w-full flex flex-col"
  135. id="channel-container"
  136. >
  137. <Navbar {channel} />
  138. <div class="flex-1 overflow-y-auto">
  139. {#if channel}
  140. <div
  141. 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"
  142. id="messages-container"
  143. bind:this={messagesContainerElement}
  144. on:scroll={(e) => {
  145. scrollEnd = Math.abs(messagesContainerElement.scrollTop) <= 50;
  146. }}
  147. >
  148. {#key id}
  149. <Messages
  150. {channel}
  151. {messages}
  152. {top}
  153. onLoad={async () => {
  154. const newMessages = await getChannelMessages(localStorage.token, id, messages.length);
  155. messages = [...messages, ...newMessages];
  156. if (newMessages.length < 50) {
  157. top = true;
  158. return;
  159. }
  160. }}
  161. />
  162. {/key}
  163. </div>
  164. {/if}
  165. </div>
  166. <div class=" pb-[1rem]">
  167. <MessageInput {typingUsers} {onChange} onSubmit={submitHandler} {scrollToBottom} {scrollEnd} />
  168. </div>
  169. </div>