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:
DioCrafts
2026-06-20 00:23:54 +02:00
parent 3f887089ae
commit b3bde0d896
5 changed files with 254 additions and 27 deletions
+25
View File
@@ -0,0 +1,25 @@
/**
* Shared avatar helpers — initials + a deterministic colour bucket — so the
* user vignette (shares, recipients) and the app-shell account button render
* identically. Mirrors the original `userVignette` `_initials` / `_colorIndex`.
*/
/** Up-to-two-letter initials from a display label (name or email). */
export function userInitials(label: string | null | undefined): string {
const base = (label ?? '').trim();
if (!base) return '?';
const parts = base.split(/\s+/);
if (parts.length >= 2) return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
return base.slice(0, 2).toUpperCase();
}
/**
* Deterministic colour bucket 0–4 from an id, so the same user always gets the
* same avatar colour (matches the original `userVignette._colorIndex`).
*/
export function avatarColorIndex(id: string | null | undefined): number {
let hash = 0;
const s = id ?? '';
for (let i = 0; i < s.length; i++) hash = (hash * 31 + s.charCodeAt(i)) | 0;
return Math.abs(hash) % 5;
}