feat(shares): show external users with avatar, email and a badge (#500)
Two related parity gaps from the VanillaJS → Svelte migration (issue #500): internal-vs-external users weren't badged, and external users in a share's member list rendered as a bare UUID with a static icon — no avatar, no email. Both share one root cause: there was no shared user vignette and no resolver for non-directory (external) users (the system address book lists internal users only, and ShareDialog hardcoded isExternal=false). - lib/api/endpoints/users.ts: resolveUser(id) — cached GET /api/users/{id} (the authenticated per-user profile lookup) → {name, email, image, isExternal}; returns null when the profile isn't visible so callers keep their fallback label. - lib/components/UserVignette.svelte: reusable identity chip — avatar (photo or coloured initials), name, email, and a building-circle-xmark badge for external users; resolves lazily and falls back to a caller-supplied label. - lib/utils/avatar.ts: userInitials() + avatarColorIndex() extracted from AppShell (now shared by both — no duplicated logic) so vignette and account button render identically. - ShareDialog: user member rows now render <UserVignette>; groups keep their icon+label. Drops the dead hardcoded isExternal. Backend already exposes everything (UserDto.email/image/is_external via GET /api/users/{id}); no backend change. Frontend gate green (svelte-check 0/0, eslint, stylelint, prettier) + 47 Vitest. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
import CommandPalette from '$lib/components/CommandPalette.svelte';
|
||||
import Icon from '$lib/icons/Icon.svelte';
|
||||
import { iconNameFromClass } from '$lib/utils/display';
|
||||
import { userInitials, avatarColorIndex } from '$lib/utils/avatar';
|
||||
import { i18n, LANGUAGES, setLocale, t, type Locale } from '$lib/i18n/index.svelte';
|
||||
import { apiFetch } from '$lib/api/client';
|
||||
import { session } from '$lib/stores/session.svelte';
|
||||
@@ -171,28 +172,13 @@
|
||||
: 0
|
||||
);
|
||||
|
||||
const initials = $derived.by(() => {
|
||||
const u = session.user;
|
||||
if (!u) return '?';
|
||||
const base = u.username || u.email || '?';
|
||||
const parts = base.trim().split(/\s+/);
|
||||
if (parts.length >= 2) return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
|
||||
return base.slice(0, 2).toUpperCase();
|
||||
});
|
||||
const initials = $derived(userInitials(session.user?.username || session.user?.email));
|
||||
|
||||
/** Uploaded avatar photo URL, if any. */
|
||||
const avatarPhoto = $derived(session.user?.image ?? null);
|
||||
|
||||
/**
|
||||
* Deterministic colour bucket 0–4 from the user id (matches the original
|
||||
* userVignette `_colorIndex`) so the same user always gets the same colour.
|
||||
*/
|
||||
const avatarColor = $derived.by(() => {
|
||||
const id = session.user?.id ?? '';
|
||||
let hash = 0;
|
||||
for (let i = 0; i < id.length; i++) hash = (hash * 31 + id.charCodeAt(i)) | 0;
|
||||
return Math.abs(hash) % 5;
|
||||
});
|
||||
/** Deterministic colour bucket 0–4 from the user id (shared with UserVignette). */
|
||||
const avatarColor = $derived(avatarColorIndex(session.user?.id));
|
||||
|
||||
function closeMenus() {
|
||||
notifOpen = false;
|
||||
|
||||
Reference in New Issue
Block a user