/
Netrunners
/
Millenium_UI
Обзор
Документация
Войти
/
Netrunners
/
Millenium_UI
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
src/lib/components/channel/Messages/Message.svelte
343 строки
11 KB
Валерий Конюхов
first_commit
08 июл 2025, 22:20
08 июл 2025, 22:20
a4e8dc9
Код
Авторство
О чём код?
<script lang="ts"> import dayjs from 'dayjs'; import relativeTime from 'dayjs/plugin/relativeTime'; import isToday from 'dayjs/plugin/isToday'; import isYesterday from 'dayjs/plugin/isYesterday'; import localizedFormat from 'dayjs/plugin/localizedFormat'; dayjs.extend(relativeTime); dayjs.extend(isToday); dayjs.extend(isYesterday); dayjs.extend(localizedFormat); import { getContext, onMount } from 'svelte'; const i18n = getContext<Writable<i18nType>>('i18n'); import { settings, user, shortCodesToEmojis } from '$lib/stores'; import { WEBUI_BASE_URL } from '$lib/constants'; import Markdown from '$lib/components/chat/Messages/Markdown.svelte'; import ProfileImage from '$lib/components/chat/Messages/ProfileImage.svelte'; import Name from '$lib/components/chat/Messages/Name.svelte'; import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte'; import GarbageBin from '$lib/components/icons/GarbageBin.svelte'; import Pencil from '$lib/components/icons/Pencil.svelte'; import Tooltip from '$lib/components/common/Tooltip.svelte'; import Textarea from '$lib/components/common/Textarea.svelte'; import Image from '$lib/components/common/Image.svelte'; import FileItem from '$lib/components/common/FileItem.svelte'; import ProfilePreview from './Message/ProfilePreview.svelte'; import ChatBubbleOvalEllipsis from '$lib/components/icons/ChatBubbleOvalEllipsis.svelte'; import FaceSmile from '$lib/components/icons/FaceSmile.svelte'; import ReactionPicker from './Message/ReactionPicker.svelte'; import ChevronRight from '$lib/components/icons/ChevronRight.svelte'; import { formatDate } from '$lib/utils'; export let message; export let showUserProfile = true; export let thread = false; export let onDelete: Function = () => {}; export let onEdit: Function = () => {}; export let onThread: Function = () => {}; export let onReaction: Function = () => {}; let showButtons = false; let edit = false; let editedContent = null; let showDeleteConfirmDialog = false; </script> <ConfirmDialog bind:show={showDeleteConfirmDialog} title={$i18n.t('Delete Message')} message={$i18n.t('Are you sure you want to delete this message?')} onConfirm={async () => { await onDelete(); }} /> {#if message} <div class="flex flex-col justify-between px-5 {showUserProfile ? 'pt-1.5 pb-0.5' : ''} w-full {($settings?.widescreenMode ?? null) ? 'max-w-full' : 'max-w-5xl'} mx-auto group hover:bg-gray-300/5 dark:hover:bg-gray-700/5 transition relative" > {#if !edit} <div class=" absolute {showButtons ? '' : 'invisible group-hover:visible'} right-1 -top-2 z-10" > <div class="flex gap-1 rounded-lg bg-white dark:bg-gray-850 shadow-md p-0.5 border border-gray-100 dark:border-gray-850" > <ReactionPicker onClose={() => (showButtons = false)} onSubmit={(name) => { showButtons = false; onReaction(name); }} > <Tooltip content={$i18n.t('Add Reaction')}> <button class="hover:bg-gray-100 dark:hover:bg-gray-800 transition rounded-lg p-1" on:click={() => { showButtons = true; }} > <FaceSmile /> </button> </Tooltip> </ReactionPicker> {#if !thread} <Tooltip content={$i18n.t('Reply in Thread')}> <button class="hover:bg-gray-100 dark:hover:bg-gray-800 transition rounded-lg p-1" on:click={() => { onThread(message.id); }} > <ChatBubbleOvalEllipsis /> </button> </Tooltip> {/if} {#if message.user_id === $user.id || $user.role === 'admin'} <Tooltip content={$i18n.t('Edit')}> <button class="hover:bg-gray-100 dark:hover:bg-gray-800 transition rounded-lg p-1" on:click={() => { edit = true; editedContent = message.content; }} > <Pencil /> </button> </Tooltip> <Tooltip content={$i18n.t('Delete')}> <button class="hover:bg-gray-100 dark:hover:bg-gray-800 transition rounded-lg p-1" on:click={() => (showDeleteConfirmDialog = true)} > <GarbageBin /> </button> </Tooltip> {/if} </div> </div> {/if} <div class=" flex w-full message-{message.id}" id="message-{message.id}" dir={$settings.chatDirection} > <div class={`shrink-0 ${($settings?.chatDirection ?? 'LTR') === 'LTR' ? 'mr-3' : 'ml-3'} w-9`} > {#if showUserProfile} <ProfilePreview user={message.user}> <ProfileImage src={message.user?.profile_image_url ?? ($i18n.language === 'dg-DG' ? `/doge.png` : `${WEBUI_BASE_URL}/static/favicon.png`)} className={'size-8 translate-y-1 ml-0.5'} /> </ProfilePreview> {:else} <!-- <div class="w-7 h-7 rounded-full bg-transparent" /> --> {#if message.created_at} <div class="mt-1.5 flex shrink-0 items-center text-xs self-center invisible group-hover:visible text-gray-500 font-medium first-letter:capitalize" > <Tooltip content={dayjs(message.created_at / 1000000).format('LLLL')}> {dayjs(message.created_at / 1000000).format('HH:mm')} </Tooltip> </div> {/if} {/if} </div> <div class="flex-auto w-0 pl-1"> {#if showUserProfile} <Name> <div class=" self-end text-base shrink-0 font-medium truncate"> {message?.user?.name} </div> {#if message.created_at} <div class=" self-center text-xs invisible group-hover:visible text-gray-400 font-medium first-letter:capitalize ml-0.5 translate-y-[1px]" > <Tooltip content={dayjs(message.created_at / 1000000).format('LLLL')}> <span class="line-clamp-1">{formatDate(message.created_at / 1000000)}</span> </Tooltip> </div> {/if} </Name> {/if} {#if (message?.data?.files ?? []).length > 0} <div class="my-2.5 w-full flex overflow-x-auto gap-2 flex-wrap"> {#each message?.data?.files as file} <div> {#if file.type === 'image'} <Image src={file.url} alt={file.name} imageClassName=" max-h-96 rounded-lg" /> {:else} <FileItem item={file} url={file.url} name={file.name} type={file.type} size={file?.size} colorClassName="bg-white dark:bg-gray-850 " /> {/if} </div> {/each} </div> {/if} {#if edit} <div class="py-2"> <Textarea className=" bg-transparent outline-hidden w-full resize-none" bind:value={editedContent} onKeydown={(e) => { if (e.key === 'Escape') { document.getElementById('close-edit-message-button')?.click(); } const isCmdOrCtrlPressed = e.metaKey || e.ctrlKey; const isEnterPressed = e.key === 'Enter'; if (isCmdOrCtrlPressed && isEnterPressed) { document.getElementById('confirm-edit-message-button')?.click(); } }} /> <div class=" mt-2 mb-1 flex justify-end text-sm font-medium"> <div class="flex space-x-1.5"> <button id="close-edit-message-button" class="px-4 py-2 bg-white dark:bg-gray-900 hover:bg-gray-100 text-gray-800 dark:text-gray-100 transition rounded-3xl" on:click={() => { edit = false; editedContent = null; }} > {$i18n.t('Cancel')} </button> <button id="confirm-edit-message-button" class=" px-4 py-2 bg-gray-900 dark:bg-white hover:bg-gray-850 text-gray-100 dark:text-gray-800 transition rounded-3xl" on:click={async () => { onEdit(editedContent); edit = false; editedContent = null; }} > {$i18n.t('Save')} </button> </div> </div> </div> {:else} <div class=" min-w-full markdown-prose"> <Markdown id={message.id} content={message.content} />{#if message.created_at !== message.updated_at}<span class="text-gray-500 text-[10px]" >(edited)</span >{/if} </div> {#if (message?.reactions ?? []).length > 0} <div> <div class="flex items-center flex-wrap gap-y-1.5 gap-1 mt-1 mb-2"> {#each message.reactions as reaction} <Tooltip content={`:${reaction.name}:`}> <button class="flex items-center gap-1.5 transition rounded-xl px-2 py-1 cursor-pointer {reaction.user_ids.includes( $user.id ) ? ' bg-blue-300/10 outline outline-blue-500/50 outline-1' : 'bg-gray-300/10 dark:bg-gray-500/10 hover:outline hover:outline-gray-700/30 dark:hover:outline-gray-300/30 hover:outline-1'}" on:click={() => { onReaction(reaction.name); }} > {#if $shortCodesToEmojis[reaction.name]} <img src="/assets/emojis/{$shortCodesToEmojis[ reaction.name ].toLowerCase()}.svg" alt={reaction.name} class=" size-4" loading="lazy" /> {:else} <div> {reaction.name} </div> {/if} {#if reaction.user_ids.length > 0} <div class="text-xs font-medium text-gray-500 dark:text-gray-400"> {reaction.user_ids?.length} </div> {/if} </button> </Tooltip> {/each} <ReactionPicker onSubmit={(name) => { onReaction(name); }} > <Tooltip content={$i18n.t('Add Reaction')}> <div class="flex items-center gap-1.5 bg-gray-500/10 hover:outline hover:outline-gray-700/30 dark:hover:outline-gray-300/30 hover:outline-1 transition rounded-xl px-1 py-1 cursor-pointer text-gray-500 dark:text-gray-400" > <FaceSmile /> </div> </Tooltip> </ReactionPicker> </div> </div> {/if} {#if !thread && message.reply_count > 0} <div class="flex items-center gap-1.5 -mt-0.5 mb-1.5"> <button class="flex items-center text-xs py-1 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 transition" on:click={() => { onThread(message.id); }} > <span class="font-medium mr-1"> {$i18n.t('{{COUNT}} Replies', { COUNT: message.reply_count })}</span ><span> {' - '}{$i18n.t('Last reply')} {dayjs.unix(message.latest_reply_at / 1000000000).fromNow()}</span > <span class="ml-1"> <ChevronRight className="size-2.5" strokeWidth="3" /> </span> <!-- {$i18n.t('View Replies')} --> </button> </div> {/if} {/if} </div> </div> </div> {/if}