This commit is contained in:
Timothy Jaeryang Baek 2026-10-09 12:07:57 +04:00
parent a53e453a26
commit 538f9c9090
5 changed files with 97 additions and 15 deletions

View file

@ -310,6 +310,8 @@ class UserInfoResponse(UserStatus):
email: str
role: str
bio: str | None = None
last_active_at: int | None = None
timezone: str | None = None
groups: list | None = []
is_active: bool = False

View file

@ -1,18 +1,61 @@
<script lang="ts">
import { getContext } from 'svelte';
import { getContext, onMount } from 'svelte';
const i18n = getContext('i18n');
const i18n = getContext<typeof import('$lib/i18n').default>('i18n');
import { user as _user } from '$lib/stores';
import { WEBUI_API_BASE_URL } from '$lib/constants';
import { getDMChannelByUserId } from '$lib/apis/channels';
import ChatBubbleOval from '$lib/components/icons/ChatBubbleOval.svelte';
import ClockRotateRight from '$lib/components/icons/ClockRotateRight.svelte';
import GlobeAlt from '$lib/components/icons/GlobeAlt.svelte';
import { goto } from '$app/navigation';
import Emoji from '$lib/components/common/Emoji.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte';
export let user = null;
export let user: {
id: string;
name: string;
is_active?: boolean;
last_active_at?: number | null;
timezone?: string | null;
status_emoji?: string | null;
status_message?: string | null;
bio?: string | null;
groups?: { name: string }[] | null;
} | null = null;
let now = new Date();
let localTime = '';
$: lastSeen = user?.last_active_at
? new Date(user.last_active_at * 1000).toLocaleString($i18n.language, {
dateStyle: 'medium',
timeStyle: 'short'
})
: '';
$: {
localTime = '';
if (user?.timezone) {
try {
localTime = now.toLocaleTimeString($i18n.language, {
timeZone: user.timezone,
hour: 'numeric',
minute: '2-digit',
timeZoneName: 'short'
});
} catch {
// Older profiles may contain a timezone the browser cannot resolve.
}
}
}
onMount(() => {
const interval = setInterval(() => (now = new Date()), 30_000);
return () => clearInterval(interval);
});
const directMessageHandler = async () => {
if (!user) {
@ -62,7 +105,7 @@
{#if user.status_emoji || user.status_message}
<div class="mt-2.5">
<Tooltip content={user.status_message}>
<Tooltip content={user.status_message ?? undefined}>
<div class="flex min-w-0 items-start gap-1.5 text-gray-700 dark:text-gray-300">
{#if user.status_emoji}
<div class="flex h-4 shrink-0 items-center">
@ -100,16 +143,47 @@
{/if}
</div>
{#if $_user?.id !== user.id}
{#if lastSeen || localTime || $_user?.id !== user.id}
<div class="border-t border-gray-50/60 p-1.5 dark:border-gray-800/30">
<button
class="flex w-full items-center gap-2 rounded-lg px-3 py-1.5 text-left text-xs text-gray-700 transition-colors hover:bg-gray-50/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400 dark:text-gray-300 dark:hover:bg-gray-800/60"
type="button"
on:click={directMessageHandler}
>
<ChatBubbleOval className="size-3.5 shrink-0" />
<span>{$i18n.t('Message')}</span>
</button>
{#if lastSeen || localTime}
<dl class="text-[0.625rem] leading-[0.875rem] text-gray-500 dark:text-gray-400">
{#if localTime}
<div class="flex items-start gap-1.5 px-1.5 py-0.5">
<dt class="shrink-0 pt-px">
<Tooltip content={$i18n.t('Local time')}>
<GlobeAlt className="size-3" />
<span class="sr-only">{$i18n.t('Local time')}</span>
</Tooltip>
</dt>
<dd title={user.timezone}>{localTime}</dd>
</div>
{/if}
{#if lastSeen}
<div class="flex items-start gap-1.5 px-1.5 py-0.5">
<dt class="shrink-0 pt-px">
<Tooltip content={$i18n.t('Last seen')}>
<ClockRotateRight className="size-3" />
<span class="sr-only">{$i18n.t('Last seen')}</span>
</Tooltip>
</dt>
<dd>{lastSeen}</dd>
</div>
{/if}
</dl>
{/if}
{#if $_user?.id !== user.id}
{#if lastSeen || localTime}
<div class="-mx-1.5 my-1 border-t border-gray-50/60 dark:border-gray-800/30"></div>
{/if}
<button
class="flex w-full items-center gap-2 rounded-lg px-3 py-1.5 text-left text-xs text-gray-700 transition-colors hover:bg-gray-50/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400 dark:text-gray-300 dark:hover:bg-gray-800/60"
type="button"
on:click={directMessageHandler}
>
<ChatBubbleOval className="size-3.5 shrink-0" />
<span>{$i18n.t('Message')}</span>
</button>
{/if}
</div>
{/if}
</div>

View file

@ -19,6 +19,7 @@
const loadUser = async (userId: string) => {
requestedUserId = userId;
user = null;
const loadedUser = await getUserInfoById(localStorage.token, userId).catch((error) => {
if (requestedUserId === userId) {
@ -35,6 +36,8 @@
$: if (openPreview && id && id !== requestedUserId) {
loadUser(id);
} else if (!openPreview) {
requestedUserId = null;
}
</script>

View file

@ -18,6 +18,7 @@
let idType = null;
let id = '';
let openPreview = false;
$: if (token && $i18n && $models && $channels) {
init();
@ -81,7 +82,7 @@
};
</script>
<LinkPreview.Root openDelay={0} closeDelay={0}>
<LinkPreview.Root openDelay={0} closeDelay={0} bind:open={openPreview}>
<LinkPreview.Trigger class=" cursor-pointer no-underline! font-normal! ">
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
@ -117,6 +118,6 @@
</LinkPreview.Trigger>
{#if triggerChar === '@' && idType === 'U'}
<UserStatusLinkPreview {id} />
<UserStatusLinkPreview {id} {openPreview} />
{/if}
</LinkPreview.Root>

View file

@ -1328,6 +1328,7 @@
"Last 7 days": "",
"Last 90 days": "",
"Last Active": "",
"Last seen": "",
"Last Modified": "",
"Last reply": "",
"Last run": "",
@ -1376,6 +1377,7 @@
"Loading release notes...": "",
"Loading tools...": "",
"Loading...": "",
"Local time": "",
"local": "",
"Local": "",
"Location": "",