Channel.svelte 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. <script lang="ts">
  2. import { toast } from 'svelte-sonner';
  3. import { onDestroy, onMount, tick } from 'svelte';
  4. import { goto } from '$app/navigation';
  5. import { chatId, showSidebar, socket, user } from '$lib/stores';
  6. import { getChannelById, getChannelMessages, sendMessage } from '$lib/apis/channels';
  7. import Messages from './Messages.svelte';
  8. import MessageInput from './MessageInput.svelte';
  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 === 'message:reaction') {
  68. console.log('message:reaction', data);
  69. const idx = messages.findIndex((message) => message.id === data.id);
  70. if (idx !== -1) {
  71. messages[idx] = data;
  72. }
  73. } else if (type === 'typing') {
  74. if (event.user.id === $user.id) {
  75. return;
  76. }
  77. typingUsers = data.typing
  78. ? [
  79. ...typingUsers,
  80. ...(typingUsers.find((user) => user.id === event.user.id)
  81. ? []
  82. : [
  83. {
  84. id: event.user.id,
  85. name: event.user.name
  86. }
  87. ])
  88. ]
  89. : typingUsers.filter((user) => user.id !== event.user.id);
  90. if (typingUsersTimeout[event.user.id]) {
  91. clearTimeout(typingUsersTimeout[event.user.id]);
  92. }
  93. typingUsersTimeout[event.user.id] = setTimeout(() => {
  94. typingUsers = typingUsers.filter((user) => user.id !== event.user.id);
  95. }, 5000);
  96. }
  97. }
  98. };
  99. const submitHandler = async ({ content, data }) => {
  100. if (!content) {
  101. return;
  102. }
  103. const res = await sendMessage(localStorage.token, id, { content: content, data: data }).catch(
  104. (error) => {
  105. toast.error(error);
  106. return null;
  107. }
  108. );
  109. if (res) {
  110. messagesContainerElement.scrollTop = messagesContainerElement.scrollHeight;
  111. }
  112. };
  113. const onChange = async () => {
  114. $socket?.emit('channel-events', {
  115. channel_id: id,
  116. data: {
  117. type: 'typing',
  118. data: {
  119. typing: true
  120. }
  121. }
  122. });
  123. };
  124. onMount(() => {
  125. if ($chatId) {
  126. chatId.set('');
  127. }
  128. $socket?.on('channel-events', channelEventHandler);
  129. });
  130. onDestroy(() => {
  131. // $socket?.off('channel-events', channelEventHandler);
  132. });
  133. </script>
  134. <svelte:head>
  135. <title>#{channel?.name ?? 'Channel'} | Open WebUI</title>
  136. </svelte:head>
  137. <div
  138. class="h-screen max-h-[100dvh] {$showSidebar
  139. ? 'md:max-w-[calc(100%-260px)]'
  140. : ''} w-full max-w-full flex flex-col"
  141. id="channel-container"
  142. >
  143. <Navbar {channel} />
  144. <div class="flex-1 overflow-y-auto">
  145. {#if channel}
  146. <div
  147. 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"
  148. id="messages-container"
  149. bind:this={messagesContainerElement}
  150. on:scroll={(e) => {
  151. scrollEnd = Math.abs(messagesContainerElement.scrollTop) <= 50;
  152. }}
  153. >
  154. {#key id}
  155. <Messages
  156. {channel}
  157. {messages}
  158. {top}
  159. onLoad={async () => {
  160. const newMessages = await getChannelMessages(localStorage.token, id, messages.length);
  161. messages = [...messages, ...newMessages];
  162. if (newMessages.length < 50) {
  163. top = true;
  164. return;
  165. }
  166. }}
  167. />
  168. {/key}
  169. </div>
  170. {/if}
  171. </div>
  172. <div class=" pb-[1rem]">
  173. <MessageInput {typingUsers} {onChange} onSubmit={submitHandler} {scrollToBottom} {scrollEnd} />
  174. </div>
  175. </div>