From 43c24ec16bbf059b01a67c4a35aa8b4950416b04 Mon Sep 17 00:00:00 2001 From: Ole-Morten Duesund Date: Mon, 25 May 2026 14:00:39 +0200 Subject: [PATCH] fix(profile): stop falling back to email when display_name is empty MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The owner attribution helper used to fall back from display_name to the part of the email before "@" when no display name was set. That defeats the point of letting users pick a name: anyone who hadn't explicitly chosen one had their email prefix surfaced publicly. New fallback chain, applied uniformly: - display_name (the user's chosen name) — if set, use it - username (also chosen by the user as a URL slug) — if set, use it - null — render nothing; the client hides the attribution line Wire type ActivityPublic.owner_display is now `string | null`. ActivityRow renders the "Lagt til av X" line only when display is non-null. Same idea applied to the user's own surfaces (nav + greeting): - Nav button shows "Profil" (a label, not a name) when display_name is empty, instead of falling back to the email. - Home greeting drops "Velkommen, ." entirely when the user has no display name, leaving just "Her er aktivitetene dine ...". The feedback list (moderator view) and admin user table keep showing the email — moderators and admins legitimately need it to identify users for triage and role management. --- frontend/src/App.svelte | 2 +- frontend/src/components/ActivityRow.svelte | 2 +- frontend/src/components/Home.svelte | 7 +++++-- server/activities.ts | 23 +++++++++++++++++----- server/users.ts | 4 +++- shared/types.ts | 8 +++++++- 6 files changed, 35 insertions(+), 11 deletions(-) diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte index efb3a5a..5e43b31 100644 --- a/frontend/src/App.svelte +++ b/frontend/src/App.svelte @@ -168,7 +168,7 @@ {:else if view !== 'login' && view !== 'signup' && view !== 'recovery'} diff --git a/frontend/src/components/ActivityRow.svelte b/frontend/src/components/ActivityRow.svelte index 3bae7e8..81673fe 100644 --- a/frontend/src/components/ActivityRow.svelte +++ b/frontend/src/components/ActivityRow.svelte @@ -211,7 +211,7 @@ {@render locationLine(activity.loc_label, activity.loc_lat, activity.loc_lng)} {/if} {#if activity.scheduled_at}

🕒 {formatDate(activity.scheduled_at)}

{/if} - {#if activity.visibility === 'public'} + {#if activity.visibility === 'public' && activity.owner_display}

Lagt til av {#if activity.owner_username} diff --git a/frontend/src/components/Home.svelte b/frontend/src/components/Home.svelte index fe75953..7d0b4e2 100644 --- a/frontend/src/components/Home.svelte +++ b/frontend/src/components/Home.svelte @@ -117,8 +117,11 @@

{:else if session.user}

- Velkommen, {session.user.display_name?.trim() || session.user.email}. - Her er aktivitetene dine for vinteren. + {#if session.user.display_name?.trim()} + Velkommen, {session.user.display_name}. Her er aktivitetene dine for vinteren. + {:else} + Her er aktivitetene dine for vinteren. + {/if}

{#if !showForm && !editing} diff --git a/server/activities.ts b/server/activities.ts index a9bac9b..2dc5a3b 100644 --- a/server/activities.ts +++ b/server/activities.ts @@ -66,16 +66,29 @@ function heartsFor(activityId: string, viewerId: string | null): { count: number return { count, hearted }; } -function ownerAttribution(ownerId: string): { display: string; username: string | null } { +/** + * Build the public-facing attribution for an owner. Prefer the user's chosen + * `display_name`; fall back to their `username` slug if set (also user-chosen); + * otherwise return null so the client hides attribution entirely. We + * deliberately do NOT fall back to the email or any prefix of it — that's a + * contact identifier the user didn't elect to surface. + * + * `username` (the link target) is returned independently and is only non-null + * when the owner has opted into a public list — drives whether the + * attribution renders as a link to //list. + */ +function ownerAttribution(ownerId: string): { display: string | null; username: string | null } { const row = getDb() - .prepare('SELECT display_name, email, username, public_list_enabled FROM users WHERE id = ?') + .prepare('SELECT display_name, username, public_list_enabled FROM users WHERE id = ?') .get(ownerId) as - | { display_name: string | null; email: string; username: string | null; public_list_enabled: number | null } + | { display_name: string | null; username: string | null; public_list_enabled: number | null } | null; - if (!row) return { display: 'ukjent', username: null }; + if (!row) return { display: null, username: null }; const display = (row.display_name && row.display_name.trim()) ? row.display_name - : (row.email.indexOf('@') > 0 ? row.email.slice(0, row.email.indexOf('@')) : row.email); + : (row.username && row.username.trim()) + ? row.username + : null; const username = row.public_list_enabled === 1 ? row.username : null; return { display, username }; } diff --git a/server/users.ts b/server/users.ts index d2f8e2b..0e31cf0 100644 --- a/server/users.ts +++ b/server/users.ts @@ -57,7 +57,9 @@ usersRoutes.get('/:username/list', (c) => { id: r.id, visibility: 'public', owner_id: r.owner_id, - owner_display: user.display_name?.trim() || username, + // Prefer the display name; fall back to the username slug (which is + // what the URL already shows). Never falls through to email/id. + owner_display: (user.display_name && user.display_name.trim()) || username, // The list itself is at //list, so we already know the slug. // Surfacing it on each row keeps ActivityRow's rendering uniform. owner_username: username, diff --git a/shared/types.ts b/shared/types.ts index bec887e..546b371 100644 --- a/shared/types.ts +++ b/shared/types.ts @@ -173,7 +173,13 @@ export interface ActivityPublic { id: string; visibility: 'public'; owner_id: string; // serialized for public - owner_display: string; // display_name OR derived handle (email prefix) + /** + * Public-facing handle: display_name OR (when only the slug is set) the + * username. NULL when the owner has set neither — in that case the client + * hides attribution entirely rather than falling back to the email or to + * an identifier the user didn't choose. + */ + owner_display: string | null; // Owner's URL slug, if they've opted into a public list. When non-null, the // client renders the owner attribution as a link to //list. owner_username: string | null;