Messages.svelte 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  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.log('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=" ">Loading...</div>
  50. </div>
  51. </Loader>
  52. {:else if !thread}
  53. <div
  54. class="px-5
  55. {($settings?.widescreenMode ?? null) ? 'max-w-full' : 'max-w-5xl'} mx-auto"
  56. >
  57. {#if channel}
  58. <div class="flex flex-col gap-1.5 pb-5 pt-10">
  59. <div class="text-2xl font-medium capitalize">{channel.name}</div>
  60. <div class=" text-gray-500">
  61. This channel was created on {dayjs(channel.created_at / 1000000).format(
  62. 'MMMM D, YYYY'
  63. )}. This is the very beginning of the {channel.name}
  64. channel.
  65. </div>
  66. </div>
  67. {:else}
  68. <div class="flex justify-center text-xs items-center gap-2 py-5">
  69. <div class=" ">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. showUserProfile={messageIdx === 0 ||
  82. messageList.at(messageIdx - 1)?.user_id !== message.user_id}
  83. onDelete={() => {
  84. messages = messages.filter((m) => m.id !== message.id);
  85. const res = deleteMessage(localStorage.token, message.channel_id, message.id).catch(
  86. (error) => {
  87. toast.error(error);
  88. return null;
  89. }
  90. );
  91. }}
  92. onEdit={(content) => {
  93. messages = messages.map((m) => {
  94. if (m.id === message.id) {
  95. m.content = content;
  96. }
  97. return m;
  98. });
  99. const res = updateMessage(localStorage.token, message.channel_id, message.id, {
  100. content: content
  101. }).catch((error) => {
  102. toast.error(error);
  103. return null;
  104. });
  105. }}
  106. onThread={(id) => {
  107. onThread(id);
  108. }}
  109. onReaction={(name) => {
  110. if (
  111. (message?.reactions ?? [])
  112. .find((reaction) => reaction.name === name)
  113. ?.user_ids?.includes($user.id) ??
  114. false
  115. ) {
  116. messages = messages.map((m) => {
  117. if (m.id === message.id) {
  118. const reaction = m.reactions.find((reaction) => reaction.name === name);
  119. if (reaction) {
  120. reaction.user_ids = reaction.user_ids.filter((id) => id !== $user.id);
  121. reaction.count = reaction.user_ids.length;
  122. if (reaction.count === 0) {
  123. m.reactions = m.reactions.filter((r) => r.name !== name);
  124. }
  125. }
  126. }
  127. return m;
  128. });
  129. const res = removeReaction(
  130. localStorage.token,
  131. message.channel_id,
  132. message.id,
  133. name
  134. ).catch((error) => {
  135. toast.error(error);
  136. return null;
  137. });
  138. } else {
  139. messages = messages.map((m) => {
  140. if (m.id === message.id) {
  141. if (m.reactions) {
  142. const reaction = m.reactions.find((reaction) => reaction.name === name);
  143. if (reaction) {
  144. reaction.user_ids.push($user.id);
  145. reaction.count = reaction.user_ids.length;
  146. } else {
  147. m.reactions.push({
  148. name: name,
  149. user_ids: [$user.id],
  150. count: 1
  151. });
  152. }
  153. }
  154. }
  155. return m;
  156. });
  157. const res = addReaction(localStorage.token, message.channel_id, message.id, name).catch(
  158. (error) => {
  159. toast.error(error);
  160. return null;
  161. }
  162. );
  163. }
  164. }}
  165. />
  166. {/each}
  167. <div class="pb-6" />
  168. </div>
  169. {/if}