feat(user): show if user is online
This commit is contained in:
@@ -16,15 +16,23 @@ export interface ResolvedUser {
|
||||
email: string;
|
||||
image: string | null;
|
||||
isExternal: boolean;
|
||||
/** Presence — TRUE when the server observed a request on any of this
|
||||
* user's non-revoked sessions within the last 5 min (backend
|
||||
* `PublicUserDto.is_online`). Drives the presence dot overlay on
|
||||
* `<UserAvatar>` / `<UserVignette>`. `false` when the caller's
|
||||
* source didn't compute presence (a bare `resolveUser(id)` from
|
||||
* pre-3-layer callers, an older backend build) — dot stays dark. */
|
||||
isOnline: boolean;
|
||||
}
|
||||
|
||||
/** Subset of the backend `UserDto` we consume here. */
|
||||
interface UserDtoShape {
|
||||
/** Subset of the backend `PublicUserDto` we consume here. */
|
||||
interface PublicUserShape {
|
||||
id: string;
|
||||
username?: string | null;
|
||||
email?: string | null;
|
||||
image?: string | null;
|
||||
is_external: boolean;
|
||||
is_online?: boolean;
|
||||
}
|
||||
|
||||
// id → in-flight/resolved lookup (the Promise is cached so concurrent callers
|
||||
@@ -41,13 +49,14 @@ export function resolveUser(id: string): Promise<ResolvedUser | null> {
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
if (!res.ok) return null;
|
||||
const u = (await res.json()) as UserDtoShape;
|
||||
const u = (await res.json()) as PublicUserShape;
|
||||
return {
|
||||
id: u.id,
|
||||
name: u.username?.trim() || u.email || u.id,
|
||||
email: u.email ?? '',
|
||||
image: u.image ?? null,
|
||||
isExternal: u.is_external
|
||||
isExternal: u.is_external,
|
||||
isOnline: u.is_online ?? false
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
@@ -78,6 +87,7 @@ export function seedUser(u: {
|
||||
email: string;
|
||||
image?: string | null;
|
||||
is_external: boolean;
|
||||
is_online?: boolean;
|
||||
}): void {
|
||||
if (cache.has(u.id)) return;
|
||||
const resolved: ResolvedUser = {
|
||||
@@ -85,7 +95,8 @@ export function seedUser(u: {
|
||||
name: u.username?.trim() || u.email || u.id,
|
||||
email: u.email,
|
||||
image: u.image ?? null,
|
||||
isExternal: u.is_external
|
||||
isExternal: u.is_external,
|
||||
isOnline: u.is_online ?? false
|
||||
};
|
||||
cache.set(u.id, Promise.resolve(resolved));
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
const label = $derived(resolved?.name ?? fallbackLabel ?? userId);
|
||||
const email = $derived(resolved?.email || fallbackSublabel || '');
|
||||
const isExternal = $derived(resolved?.isExternal ?? false);
|
||||
const isOnline = $derived(resolved?.isOnline ?? false);
|
||||
const image = $derived(resolved?.image ?? null);
|
||||
const colorIndex = $derived(avatarColorIndex(userId));
|
||||
const initials = $derived(userInitials(label));
|
||||
@@ -50,6 +51,22 @@
|
||||
<Icon name="building-circle-xmark" />
|
||||
</span>
|
||||
{/if}
|
||||
{#if isOnline}
|
||||
<!-- Presence dot — top-right corner so it doesn't collide with the
|
||||
external badge at bottom-right. Only rendered when true (absent
|
||||
= offline reads cleanly on an avatar; no grey placeholder). Uses
|
||||
`--color-success-alt` (same green as `.badge--active` in the
|
||||
admin sessions panel + `.presence-dot--online` in the sessions
|
||||
status column) so the presence signal reads consistently across
|
||||
every surface. The 2px surface-coloured border visually detaches
|
||||
the dot from the avatar's own background — matches the pattern
|
||||
Slack/Teams/Discord use. -->
|
||||
<span
|
||||
class="uv__presence"
|
||||
title={t('share.userOnline', 'Online')}
|
||||
aria-label={t('share.userOnline', 'Online')}
|
||||
></span>
|
||||
{/if}
|
||||
</span>
|
||||
<span class="uv__text">
|
||||
<span class="uv__name">{label}</span>
|
||||
@@ -128,6 +145,27 @@
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
/* Presence dot — top-right, symmetric with `.uv__badge` at
|
||||
bottom-right so the two corners don't collide. Slightly smaller
|
||||
(10x10 vs the badge's 16x16) because it's a pure signal — no
|
||||
icon, no text. The 2px `--color-bg-surface` border creates a
|
||||
visual gap between dot and avatar so the green pops out cleanly
|
||||
regardless of avatar palette (photo, dark initials, light
|
||||
initials). `box-sizing: border-box` keeps the inner circle's
|
||||
green footprint at 6x6 — same visual weight the sessions-table
|
||||
dot has. See `docs/plan/sessions.md` § UI. */
|
||||
.uv__presence {
|
||||
position: absolute;
|
||||
right: -2px;
|
||||
top: -2px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-success-alt);
|
||||
border: 2px solid var(--color-bg-surface);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.uv__text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
Reference in New Issue
Block a user