feat(user-avatar): users can now edit there image (image is taken from OIDC picture)

This commit is contained in:
Edouard Vanbelle
2026-05-26 00:50:38 +02:00
parent 8b6c8e345b
commit b0c5e7827e
43 changed files with 1563 additions and 250 deletions
+74 -9
View File
@@ -4,9 +4,11 @@
* System-users convenience layer.
*
* Thin wrapper over `addressBook.listContacts(SYSTEM_BOOK_ID)` that
* provides a userId → display-name index. Used wherever a grant's
* `granted_by` UUID needs to be shown as a human-readable name
* (owner tooltips, share dialogs, etc.).
* provides a userId → display-name index, a userId → photo-url index,
* and a userId → primary-email index.
* Used wherever a grant's `granted_by` UUID needs to be shown as a
* human-readable name (owner tooltips, share dialogs, etc.), avatar
* image (userVignette, user menu), or email (suggestion dropdowns).
*
* Falls back gracefully when the system address book is disabled
* server-side (`OXICLOUD_EXPOSE_SYSTEM_USERS` not set): `isAvailable()`
@@ -20,6 +22,12 @@ import { addressBook, SYSTEM_BOOK_ID } from './addressBook.js';
/** @type {Map<string, string> | null} userId → display name, built lazily */
let _index = null;
/** @type {Map<string, string | null> | null} userId → photo URL (or null), built lazily */
let _photoIndex = null;
/** @type {Map<string, string | null> | null} userId → primary email (or null), built lazily */
let _emailIndex = null;
/**
* Derive the best human-readable name from a contact.
* Priority: "First Last" → full_name → primary email → shortened id.
@@ -36,7 +44,7 @@ function _nameFor(c) {
}
/**
* Ensure the index is built (idempotent).
* Ensure both indexes are built (idempotent).
* After loading contacts from the system address book, the current user
* (from localStorage) is injected so owner cells resolve correctly even
* when the server-side address book does not include the logged-in user.
@@ -46,15 +54,30 @@ async function _ensureIndex() {
if (_index !== null) return;
const contacts = await addressBook.listContacts(SYSTEM_BOOK_ID);
_index = new Map(contacts.map((c) => [c.id, _nameFor(c)]));
_photoIndex = new Map(contacts.map((c) => [c.id, c.photo_url ?? null]));
_emailIndex = new Map(
contacts.map((c) => {
const primary = c.email?.find((e) => e.is_primary)?.email ?? c.email?.[0]?.email ?? null;
return [c.id, primary];
})
);
// Inject the current user if they are not already in the index
try {
const raw = localStorage.getItem('oxicloud_user');
if (raw) {
const u = /** @type {{id?:string, display_name?:string, username?:string, email?:string}} */ (JSON.parse(raw));
if (u?.id && !_index.has(u.id)) {
const name = u.display_name || u.username || u.email || `${u.id.slice(0, 8)}…`;
_index.set(u.id, name);
const u = /** @type {{id?:string, display_name?:string, username?:string, email?:string, image?:string|null}} */ (JSON.parse(raw));
if (u?.id) {
if (!_index.has(u.id)) {
const name = u.display_name || u.username || u.email || `${u.id.slice(0, 8)}…`;
_index.set(u.id, name);
}
if (!_photoIndex.has(u.id)) {
_photoIndex.set(u.id, u.image ?? null);
}
if (!_emailIndex.has(u.id)) {
_emailIndex.set(u.id, u.email ?? null);
}
}
}
} catch {
@@ -85,6 +108,48 @@ async function getDisplayName(userId) {
return _index?.get(userId) ?? `${userId.slice(0, 8)}…`;
}
/**
* Resolve a user UUID to a photo URL (or null if none set).
* Awaits the first load if not yet cached; subsequent calls resolve instantly.
*
* @param {string} userId
* @returns {Promise<string | null>}
*/
async function getPhoto(userId) {
await _ensureIndex();
return _photoIndex?.get(userId) ?? null;
}
/**
* Resolve a user UUID to their primary email address (or null if unknown).
* Awaits the first load if not yet cached; subsequent calls resolve instantly.
*
* @param {string} userId
* @returns {Promise<string | null>}
*/
async function getEmail(userId) {
await _ensureIndex();
return _emailIndex?.get(userId) ?? null;
}
/**
* Force-refresh the current user's photo entry in the index from localStorage.
* Call this after saving a new avatar on the profile page so that existing
* vignettes can re-render without a full page reload.
*/
function refreshCurrentUserPhoto() {
try {
const raw = localStorage.getItem('oxicloud_user');
if (!raw || !_photoIndex) return;
const u = /** @type {{id?:string, image?:string|null}} */ (JSON.parse(raw));
if (u?.id) {
_photoIndex.set(u.id, u.image ?? null);
}
} catch {
// ignore
}
}
/**
* Returns `false` only after a confirmed 404 from the server (feature
* disabled). Returns `true` when status is unknown or the book loaded OK.
@@ -94,4 +159,4 @@ function isAvailable() {
return addressBook.isSystemAvailable();
}
export const systemUsers = { prefetch, getDisplayName, isAvailable };
export const systemUsers = { prefetch, getDisplayName, getPhoto, getEmail, refreshCurrentUserPhoto, isAvailable };