Messages.svelte 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. <script lang="ts">
  2. import { toast } from 'svelte-sonner';
  3. import dayjs from 'dayjs';
  4. import relativeTime from 'dayjs/plugin/relativeTime';
  5. import isToday from 'dayjs/plugin/isToday';
  6. import isYesterday from 'dayjs/plugin/isYesterday';
  7. dayjs.extend(relativeTime);
  8. dayjs.extend(isToday);
  9. dayjs.extend(isYesterday);
  10. import { tick, getContext, onMount, createEventDispatcher } from 'svelte';
  11. import { settings, user } from '$lib/stores';
  12. import Message from './Messages/Message.svelte';
  13. import Loader from '../common/Loader.svelte';
  14. import Spinner from '../common/Spinner.svelte';
  15. import { addReaction, deleteMessage, removeReaction, updateMessage } from '$lib/apis/channels';
  16. const i18n = getContext('i18n');
  17. export let id = null;
  18. export let channel = null;
  19. export let messages = [];
  20. export let top = false;
  21. export let thread = false;
  22. export let onLoad: Function = () => {};
  23. export let onThread: Function = () => {};
  24. let messagesLoading = false;
  25. const loadMoreMessages = async () => {
  26. // scroll slightly down to disable continuous loading
  27. const element = document.getElementById('messages-container');
  28. element.scrollTop = element.scrollTop + 100;
  29. messagesLoading = true;
  30. await onLoad();
  31. await tick();
  32. messagesLoading = false;
  33. };
  34. </script>
  35. {#if messages}
  36. {@const messageList = messages.slice().reverse()}
  37. <div>
  38. {#if !top}
  39. <Loader
  40. on:visible={(e) => {
  41. console.info('visible');
  42. if (!messagesLoading) {
  43. loadMoreMessages();
  44. }
  45. }}
  46. >
  47. <div class="w-full flex justify-center py-1 text-xs animate-pulse items-center gap-2">
  48. <Spinner className=" size-4" />
  49. <div class=" ">{$i18n.t('Loading...')}</div>
  50. </div>
  51. </Loader>
  52. {:else if !thread}
  53. <div class="px-5 max-w-full mx-auto">
  54. {#if channel}
  55. <div class="flex flex-col gap-1.5 pb-5 pt-10">
  56. <div class="text-2xl font-medium capitalize">{channel.name}</div>
  57. <div class=" text-gray-500">
  58. {$i18n.t(
  59. 'This channel was created on {{createdAt}}. This is the very beginning of the {{channelName}} channel.',
  60. {
  61. createdAt: dayjs(channel.created_at / 1000000).format('MMMM D, YYYY'),
  62. channelName: channel.name
  63. }
  64. )}
  65. </div>
  66. </div>
  67. {:else}
  68. <div class="flex justify-center text-xs items-center gap-2 py-5">
  69. <div class=" ">{$i18n.t('Start of the channel')}</div>
  70. </div>
  71. {/if}
  72. {#if messageList.length > 0}
  73. <hr class=" border-gray-50 dark:border-gray-700/20 py-2.5 w-full" />
  74. {/if}
  75. </div>
  76. {/if}
  77. {#each messageList as message, messageIdx (id ? `${id}-${message.id}` : message.id)}
  78. <Message
  79. {message}
  80. {thread}
  81. disabled={!channel?.write_access}
  82. showUserProfile={messageIdx === 0 ||
  83. messageList.at(messageIdx - 1)?.user_id !== message.user_id ||
  84. messageList.at(messageIdx - 1)?.meta?.model_id !== message?.meta?.model_id}
  85. onDelete={() => {
  86. messages = messages.filter((m) => m.id !== message.id);
  87. const res = deleteMessage(localStorage.token, message.channel_id, message.id).catch(
  88. (error) => {
  89. toast.error(`${error}`);
  90. return null;
  91. }
  92. );
  93. }}
  94. onEdit={(content) => {
  95. messages = messages.map((m) => {
  96. if (m.id === message.id) {
  97. m.content = content;
  98. }
  99. return m;
  100. });
  101. const res = updateMessage(localStorage.token, message.channel_id, message.id, {
  102. content: content
  103. }).catch((error) => {
  104. toast.error(`${error}`);
  105. return null;
  106. });
  107. }}
  108. onThread={(id) => {
  109. onThread(id);
  110. }}
  111. onReaction={(name) => {
  112. if (
  113. (message?.reactions ?? [])
  114. .find((reaction) => reaction.name === name)
  115. ?.user_ids?.includes($user?.id) ??
  116. false
  117. ) {
  118. messages = messages.map((m) => {
  119. if (m.id === message.id) {
  120. const reaction = m.reactions.find((reaction) => reaction.name === name);
  121. if (reaction) {
  122. reaction.user_ids = reaction.user_ids.filter((id) => id !== $user?.id);
  123. reaction.count = reaction.user_ids.length;
  124. if (reaction.count === 0) {
  125. m.reactions = m.reactions.filter((r) => r.name !== name);
  126. }
  127. }
  128. }
  129. return m;
  130. });
  131. const res = removeReaction(
  132. localStorage.token,
  133. message.channel_id,
  134. message.id,
  135. name
  136. ).catch((error) => {
  137. toast.error(`${error}`);
  138. return null;
  139. });
  140. } else {
  141. messages = messages.map((m) => {
  142. if (m.id === message.id) {
  143. if (m.reactions) {
  144. const reaction = m.reactions.find((reaction) => reaction.name === name);
  145. if (reaction) {
  146. reaction.user_ids.push($user?.id);
  147. reaction.count = reaction.user_ids.length;
  148. } else {
  149. m.reactions.push({
  150. name: name,
  151. user_ids: [$user?.id],
  152. count: 1
  153. });
  154. }
  155. }
  156. }
  157. return m;
  158. });
  159. const res = addReaction(localStorage.token, message.channel_id, message.id, name).catch(
  160. (error) => {
  161. toast.error(`${error}`);
  162. return null;
  163. }
  164. );
  165. }
  166. }}
  167. />
  168. {/each}
  169. <div class="pb-6" />
  170. </div>
  171. {/if}