| | <script lang="ts"> |
| | import { toast } from 'svelte-sonner'; |
| | |
| | import dayjs from 'dayjs'; |
| | import relativeTime from 'dayjs/plugin/relativeTime'; |
| | import isToday from 'dayjs/plugin/isToday'; |
| | import isYesterday from 'dayjs/plugin/isYesterday'; |
| | |
| | dayjs.extend(relativeTime); |
| | dayjs.extend(isToday); |
| | dayjs.extend(isYesterday); |
| | import { tick, getContext, onMount, createEventDispatcher } from 'svelte'; |
| | |
| | import { settings, user } from '$lib/stores'; |
| | |
| | import Message from './Messages/Message.svelte'; |
| | import Loader from '../common/Loader.svelte'; |
| | import Spinner from '../common/Spinner.svelte'; |
| | import { |
| | addReaction, |
| | deleteMessage, |
| | pinMessage, |
| | removeReaction, |
| | updateMessage |
| | } from '$lib/apis/channels'; |
| | import { WEBUI_API_BASE_URL } from '$lib/constants'; |
| | |
| | const i18n = getContext('i18n'); |
| | |
| | export let id = null; |
| | export let channel = null; |
| | export let messages = []; |
| | export let replyToMessage = null; |
| | export let top = false; |
| | export let thread = false; |
| | |
| | export let onLoad: Function = () => {}; |
| | export let onReply: Function = () => {}; |
| | export let onThread: Function = () => {}; |
| | |
| | let messagesLoading = false; |
| | |
| | const loadMoreMessages = async () => { |
| | |
| | const element = document.getElementById('messages-container'); |
| | element.scrollTop = element.scrollTop + 100; |
| | |
| | messagesLoading = true; |
| | |
| | await onLoad(); |
| | |
| | await tick(); |
| | messagesLoading = false; |
| | }; |
| | </script> |
| |
|
| | {#if messages} |
| | {@const messageList = messages.slice().reverse()} |
| | <div> |
| | {#if !top} |
| | <Loader |
| | on:visible={(e) => { |
| | console.info('visible'); |
| | if (!messagesLoading) { |
| | loadMoreMessages(); |
| | } |
| | }} |
| | > |
| | <div class="w-full flex justify-center py-1 text-xs animate-pulse items-center gap-2"> |
| | <Spinner className=" size-4" /> |
| | <div class=" ">{$i18n.t('Loading...')}</div> |
| | </div> |
| | </Loader> |
| | {:else if !thread} |
| | <div class="px-5 max-w-full mx-auto"> |
| | {#if channel} |
| | <div class="flex flex-col gap-1.5 pb-5 pt-10"> |
| | {#if channel?.type === 'dm'} |
| | <div class="flex ml-[1px] mr-0.5"> |
| | {#each channel.users.filter((u) => u.id !== $user?.id).slice(0, 2) as u, index} |
| | <img |
| | src={`${WEBUI_API_BASE_URL}/users/${u.id}/profile/image`} |
| | alt={u.name} |
| | class=" size-7.5 rounded-full border-2 border-white dark:border-gray-900 {index === |
| | 1 |
| | ? '-ml-2.5' |
| | : ''}" |
| | /> |
| | {/each} |
| | </div> |
| | {/if} |
| |
|
| | <div class="text-2xl font-medium capitalize"> |
| | {#if channel?.name} |
| | {channel.name} |
| | {:else} |
| | {channel?.users |
| | ?.filter((u) => u.id !== $user?.id) |
| | .map((u) => u.name) |
| | .join(', ')} |
| | {/if} |
| | </div> |
| |
|
| | <div class=" text-gray-500"> |
| | {$i18n.t( |
| | 'This channel was created on {{createdAt}}. This is the very beginning of the {{channelName}} channel.', |
| | { |
| | createdAt: dayjs(channel.created_at / 1000000).format('MMMM D, YYYY'), |
| | channelName: channel.name |
| | } |
| | )} |
| | </div> |
| | </div> |
| | {:else} |
| | <div class="flex justify-center text-xs items-center gap-2 py-5"> |
| | <div class=" ">{$i18n.t('Start of the channel')}</div> |
| | </div> |
| | {/if} |
| |
|
| | {#if messageList.length > 0} |
| | <hr class=" border-gray-50 dark:border-gray-700/20 py-2.5 w-full" /> |
| | {/if} |
| | </div> |
| | {/if} |
| |
|
| | {#each messageList as message, messageIdx (id ? `${id}-${message.id}` : message.id)} |
| | <Message |
| | {message} |
| | {channel} |
| | {thread} |
| | replyToMessage={replyToMessage?.id === message.id} |
| | disabled={!channel?.write_access || message?.temp_id} |
| | pending={!!message?.temp_id} |
| | showUserProfile={messageIdx === 0 || |
| | messageList.at(messageIdx - 1)?.user_id !== message.user_id || |
| | messageList.at(messageIdx - 1)?.user?.id !== message.user?.id || |
| | messageList.at(messageIdx - 1)?.meta?.model_id !== message?.meta?.model_id || |
| | message?.reply_to_message !== null} |
| | onDelete={() => { |
| | messages = messages.filter((m) => m.id !== message.id); |
| |
|
| | const res = deleteMessage(localStorage.token, message.channel_id, message.id).catch( |
| | (error) => { |
| | toast.error(`${error}`); |
| | return null; |
| | } |
| | ); |
| | }} |
| | onEdit={(content) => { |
| | messages = messages.map((m) => { |
| | if (m.id === message.id) { |
| | m.content = content; |
| | } |
| | return m; |
| | }); |
| |
|
| | const res = updateMessage(localStorage.token, message.channel_id, message.id, { |
| | content: content |
| | }).catch((error) => { |
| | toast.error(`${error}`); |
| | return null; |
| | }); |
| | }} |
| | onReply={(message) => { |
| | onReply(message); |
| | }} |
| | onPin={async (message) => { |
| | messages = messages.map((m) => { |
| | if (m.id === message.id) { |
| | m.is_pinned = !m.is_pinned; |
| | m.pinned_by = !m.is_pinned ? null : $user?.id; |
| | m.pinned_at = !m.is_pinned ? null : Date.now() * 1000000; |
| | } |
| | return m; |
| | }); |
| |
|
| | const updatedMessage = await pinMessage( |
| | localStorage.token, |
| | message.channel_id, |
| | message.id, |
| | message.is_pinned |
| | ).catch((error) => { |
| | toast.error(`${error}`); |
| | return null; |
| | }); |
| | }} |
| | onThread={(id) => { |
| | onThread(id); |
| | }} |
| | onReaction={(name) => { |
| | if ( |
| | (message?.reactions ?? []) |
| | .find((reaction) => reaction.name === name) |
| | ?.users?.some((u) => u.id === $user?.id) ?? |
| | false |
| | ) { |
| | messages = messages.map((m) => { |
| | if (m.id === message.id) { |
| | const reaction = m.reactions.find((reaction) => reaction.name === name); |
| |
|
| | if (reaction) { |
| | reaction.users = reaction.users.filter((u) => u.id !== $user?.id); |
| | reaction.count = reaction.users.length; |
| |
|
| | if (reaction.count === 0) { |
| | m.reactions = m.reactions.filter((r) => r.name !== name); |
| | } |
| | } |
| | } |
| | return m; |
| | }); |
| |
|
| | const res = removeReaction( |
| | localStorage.token, |
| | message.channel_id, |
| | message.id, |
| | name |
| | ).catch((error) => { |
| | toast.error(`${error}`); |
| | return null; |
| | }); |
| | } else { |
| | messages = messages.map((m) => { |
| | if (m.id === message.id) { |
| | if (m.reactions) { |
| | const reaction = m.reactions.find((reaction) => reaction.name === name); |
| |
|
| | if (reaction) { |
| | reaction.users.push({ id: $user?.id, name: $user?.name }); |
| | reaction.count = reaction.users.length; |
| | } else { |
| | m.reactions.push({ |
| | name: name, |
| | users: [{ id: $user?.id, name: $user?.name }], |
| | count: 1 |
| | }); |
| | } |
| | } |
| | } |
| | return m; |
| | }); |
| |
|
| | const res = addReaction(localStorage.token, message.channel_id, message.id, name).catch( |
| | (error) => { |
| | toast.error(`${error}`); |
| | return null; |
| | } |
| | ); |
| | } |
| | }} |
| | /> |
| | {/each} |
| |
|
| | <div class="pb-6" /> |
| | </div> |
| | {/if} |
| |
|