This commit is contained in:
Timothy Jaeryang Baek 2026-10-11 16:12:59 +04:00
parent 78613d86f7
commit a165c83858
4 changed files with 145 additions and 119 deletions

View file

@ -12,18 +12,18 @@
<script lang="ts">
import { LinkPreview } from 'bits-ui';
import { getContext, onDestroy } from 'svelte';
import { user as sessionUser } from '$lib/stores';
const i18n = getContext('i18n');
import UserStatus from './UserStatus.svelte';
const i18n = getContext<typeof import('$lib/i18n').default>('i18n');
import UserStatusLinkPreview from './UserStatusLinkPreview.svelte';
export let user = null;
export let user: { id?: string | null; name?: string | null } | null = null;
export let align = 'center';
export let side = 'right';
export let sideOffset = 8;
let openPreview = false;
export let openPreview = false;
const closeProfilePreview = () => {
if (openPreview) {
@ -43,18 +43,25 @@
});
</script>
<LinkPreview.Root openDelay={0} closeDelay={200} bind:open={openPreview}>
<LinkPreview.Trigger class="flex shrink-0 items-center">
<button
type="button"
class=" cursor-pointer no-underline! font-normal!"
on:click={() => {
openPreview = !openPreview;
}}
{#if user?.id && $sessionUser}
<LinkPreview.Root openDelay={0} closeDelay={200} bind:open={openPreview}>
<LinkPreview.Trigger
class="flex shrink-0 items-center cursor-pointer no-underline! font-normal!"
>
<slot />
</button>
</LinkPreview.Trigger>
{#snippet child({ props })}
<button
{...props}
type="button"
aria-label={user.name || $i18n.t('User')}
onclick={() => (openPreview = !openPreview)}
>
<slot />
</button>
{/snippet}
</LinkPreview.Trigger>
<UserStatusLinkPreview id={user?.id} {openPreview} {side} {align} {sideOffset} />
</LinkPreview.Root>
<UserStatusLinkPreview id={user.id} {openPreview} {side} {align} {sideOffset} />
</LinkPreview.Root>
{:else}
<slot />
{/if}

View file

@ -15,6 +15,7 @@
import Name from './Name.svelte';
import ProfileImage from './ProfileImage.svelte';
import ProfilePreview from '$lib/components/channel/Messages/Message/ProfilePreview.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import FileItem from '$lib/components/common/FileItem.svelte';
import Markdown from './Markdown.svelte';
@ -158,12 +159,14 @@
<!-- LICENSE covers this Open WebUI fallback logo.
Do not alter, remove, obscure, or replace it except as LICENSE permits:
https://docs.openwebui.com/license. -->
<ProfileImage
src={authorId
? `${WEBUI_API_BASE_URL}/users/${authorId}/profile/image`
: `${WEBUI_BASE_URL}/static/favicon.png`}
className={'size-7 user-message-profile-image'}
/>
<ProfilePreview user={{ id: authorId, name: authorName }}>
<ProfileImage
src={authorId
? `${WEBUI_API_BASE_URL}/users/${authorId}/profile/image`
: `${WEBUI_BASE_URL}/static/favicon.png`}
className={'size-7 user-message-profile-image'}
/>
</ProfilePreview>
</div>
{/if}
<div

View file

@ -10,6 +10,7 @@
import { WEBUI_API_BASE_URL } from '$lib/constants';
import ChannelModal from './ChannelModal.svelte';
import ProfilePreview from '$lib/components/channel/Messages/Message/ProfilePreview.svelte';
import Emoji from '$lib/components/common/Emoji.svelte';
import HashtagIcon from '$lib/components/icons/Hashtag.svelte';
import LockIcon from '$lib/components/icons/Lock.svelte';
@ -87,6 +88,41 @@
? 'font-normal dark:text-white text-black'
: ' dark:text-gray-400 text-gray-600'} cursor-pointer select-none"
>
{#if channel?.type === 'dm' && channel?.users}
{@const channelMembers = channel.users.filter((u) => u.id !== $user?.id)}
<div class="self-center mr-1">
<div class="flex ml-[0.0625rem] mr-0.5 relative">
{#each channelMembers.slice(0, 2) as u, index}
<div class={index === 1 ? '-ml-2.5' : ''}>
<ProfilePreview user={u}>
<img
src={`${WEBUI_API_BASE_URL}/users/${u.id}/profile/image`}
alt={u.name}
class="size-5.5 rounded-full border-2 border-white dark:border-gray-900"
/>
</ProfilePreview>
</div>
{/each}
{#if channelMembers.length === 1}
<div class="absolute bottom-0 right-0 pointer-events-none">
<span class="relative flex size-2">
{#if channelMembers[0]?.is_active}
<span
class="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-400 opacity-75"
></span>
{/if}
<span
class="relative inline-flex size-2 rounded-full {channelMembers[0]?.is_active
? 'bg-green-500'
: 'bg-gray-300 dark:bg-gray-700'} border-[1.5px] border-white dark:border-gray-900"
></span>
</span>
</div>
{/if}
</div>
</div>
{/if}
<a
class="min-w-0 flex flex-1"
href="/channels/{channel.id}"
@ -111,52 +147,19 @@
draggable="false"
>
<div class="flex min-w-0 flex-1 items-center gap-1">
<div>
{#if channel?.type === 'dm'}
{#if channel?.users}
{@const channelMembers = channel.users.filter((u) => u.id !== $user?.id)}
<div class="flex ml-[0.0625rem] mr-0.5 relative">
{#each channelMembers.slice(0, 2) as u, index}
<img
src={`${WEBUI_API_BASE_URL}/users/${u.id}/profile/image`}
alt={u.name}
class=" size-5.5 rounded-full border-2 border-white dark:border-gray-900 {index ===
1
? '-ml-2.5'
: ''}"
/>
{/each}
{#if channelMembers.length === 1}
<div class="absolute bottom-0 right-0">
<span class="relative flex size-2">
{#if channelMembers[0]?.is_active}
<span
class="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-400 opacity-75"
></span>
{/if}
<span
class="relative inline-flex size-2 rounded-full {channelMembers[0]?.is_active
? 'bg-green-500'
: 'bg-gray-300 dark:bg-gray-700'} border-[1.5px] border-white dark:border-gray-900"
></span>
</span>
</div>
{/if}
</div>
{:else}
<UserIcon className="size-4 ml-1 mr-0.5" strokeWidth="1.5" />
{/if}
{:else}
<div class=" size-4 justify-center flex items-center ml-1">
{#if isPublicChannel(channel)}
<HashtagIcon className="size-3" strokeWidth="1.8" />
{:else}
<LockIcon className="size-3.5" strokeWidth="1.7" />
{/if}
</div>
{#if channel?.type === 'dm'}
{#if !channel?.users}
<UserIcon className="size-4 ml-1 mr-0.5" strokeWidth="1.5" />
{/if}
</div>
{:else}
<div class="size-4 shrink-0 justify-center flex items-center ml-1">
{#if isPublicChannel(channel)}
<HashtagIcon className="size-3" strokeWidth="1.8" />
{:else}
<LockIcon className="size-3.5" strokeWidth="1.7" />
{/if}
</div>
{/if}
<div class="text-left self-center min-w-0 flex-1 pr-1 flex items-center gap-1.5">
{#if channel?.name}

View file

@ -50,6 +50,7 @@
import DragGhost from '$lib/components/common/DragGhost.svelte';
import Spinner from '$lib/components/common/Spinner.svelte';
import ChatHoverPreview from './ChatHoverPreview.svelte';
import ProfilePreview from '$lib/components/channel/Messages/Message/ProfilePreview.svelte';
import ChatIcon from './icons/Chat.svelte';
import MoreHorizontalIcon from './icons/MoreHorizontal.svelte';
import SparklesIcon from './icons/Sparkles.svelte';
@ -107,6 +108,9 @@
let focusWithin = false;
let menuOpen = false;
let openPreview = false;
let openProfilePreview = false;
$: if (openProfilePreview) openPreview = false;
const closeHoverPreview = () => {
if (openPreview) {
@ -514,21 +518,6 @@
</script>
{#snippet chatItemContent()}
{#if ownerUserId}
<Tooltip content={ownerName || 'Unknown'} className="flex items-center self-center shrink-0">
<img
src="{WEBUI_API_BASE_URL}/users/{ownerUserId}/profile/image"
alt=""
class="size-3.5 rounded-full shrink-0 object-cover mr-2"
on:error={(e) => {
if (!e.currentTarget.src.endsWith('/static/favicon.png')) {
e.currentTarget.src = `${WEBUI_BASE_URL}/static/favicon.png`;
}
}}
/>
</Tooltip>
{/if}
<!-- Loading spinner for active chat (left side) -->
{#if active}
<div class="shrink-0 self-center pr-2">
@ -645,44 +634,68 @@
}}
/>
</div>
{:else if $mobile}
<a
id="sidebar-chat-item"
class={chatItemClass}
href="/c/{id}"
aria-current={id === $chatId ? 'page' : undefined}
on:click={selectChatHandler}
draggable="false"
>
{@render chatItemContent()}
</a>
{:else}
<LinkPreview.Root
openDelay={300}
closeDelay={0}
disabled={confirmEdit || dragged || !($settings?.chatHoverPreview ?? true)}
bind:open={openPreview}
>
<LinkPreview.Trigger
id="sidebar-chat-item"
class={chatItemClass}
href="/c/{id}"
aria-current={id === $chatId ? 'page' : undefined}
onclick={selectChatHandler}
ondblclick={renameChatFromDoubleClick}
draggable="false"
>
{@render chatItemContent()}
</LinkPreview.Trigger>
<div id="sidebar-chat-item" class={chatItemClass}>
{#if ownerUserId}
<div class="flex items-center self-center shrink-0 mr-2">
<ProfilePreview
user={{ id: ownerUserId, name: ownerName }}
bind:openPreview={openProfilePreview}
>
<img
src="{WEBUI_API_BASE_URL}/users/{ownerUserId}/profile/image"
alt=""
class="size-3.5 rounded-full shrink-0 object-cover"
on:error={(e) => {
if (!e.currentTarget.src.endsWith('/static/favicon.png')) {
e.currentTarget.src = `${WEBUI_BASE_URL}/static/favicon.png`;
}
}}
/>
</ProfilePreview>
</div>
{/if}
{#if $mobile}
<a
class="flex min-w-0 flex-1 items-center"
href="/c/{id}"
aria-current={id === $chatId ? 'page' : undefined}
on:click={selectChatHandler}
draggable="false"
>
{@render chatItemContent()}
</a>
{:else}
<LinkPreview.Root
openDelay={300}
closeDelay={0}
disabled={confirmEdit || dragged || !($settings?.chatHoverPreview ?? true)}
bind:open={openPreview}
onOpenChange={(open) => {
if (open) openProfilePreview = false;
}}
>
<LinkPreview.Trigger
class="flex min-w-0 flex-1 items-center"
href="/c/{id}"
aria-current={id === $chatId ? 'page' : undefined}
onclick={selectChatHandler}
ondblclick={renameChatFromDoubleClick}
draggable="false"
>
{@render chatItemContent()}
</LinkPreview.Trigger>
<ChatHoverPreview
chatId={id}
title={chatTitle || title}
{openPreview}
side="right"
align="center"
/>
</LinkPreview.Root>
<ChatHoverPreview
chatId={id}
title={chatTitle || title}
{openPreview}
side="right"
align="center"
/>
</LinkPreview.Root>
{/if}
</div>
{/if}
{#if !readonly}