Thread.svelte 4.1 KB

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