Thread.svelte 4.5 KB

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