diff --git a/desktop/src/features/profile/lib/identity.test.mjs b/desktop/src/features/profile/lib/identity.test.mjs index da0259a66..865290fb0 100644 --- a/desktop/src/features/profile/lib/identity.test.mjs +++ b/desktop/src/features/profile/lib/identity.test.mjs @@ -1,10 +1,16 @@ import assert from "node:assert/strict"; import test from "node:test"; -import { formatOwnerLabel, profileLookupsEqual } from "./identity.ts"; +import { + formatOwnerLabel, + formatVerifiedUserLabel, + profileLookupsEqual, + resolveUserLabel, +} from "./identity.ts"; const OWNER_PUBKEY = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; +const USER_PUBKEY = "11".repeat(32); const summary = (over = {}) => ({ displayName: "Ada", @@ -58,6 +64,7 @@ test("profileLookupsEqual: same count, different keys is not equal", () => { test("profileLookupsEqual: a changed field is not equal", () => { for (const field of [ "displayName", + "verifiedName", "avatarUrl", "nip05Handle", "ownerPubkey", @@ -120,3 +127,29 @@ test("stabiliser: a real profile change swaps the reference (re-render fires)", const held = stabilise({ p1: summary({ displayName: "Grace" }) }); assert.equal(held, changed, "must re-stabilise around the new value"); }); + +test("formats a chosen name followed by the authoritative display name", () => { + assert.equal( + formatVerifiedUserLabel("Franco", "fsola"), + "Franco (fsola)", + ); +}); + +test("does not duplicate equal chosen and authoritative names", () => { + assert.equal(formatVerifiedUserLabel("fsola", "fsola"), "fsola"); +}); + +test("resolved user labels keep the chosen name first", () => { + assert.equal( + resolveUserLabel({ + pubkey: USER_PUBKEY, + profiles: { + [USER_PUBKEY]: summary({ + displayName: "Franco", + verifiedName: "fsola", + }), + }, + }), + "Franco (fsola)", + ); +}); diff --git a/desktop/src/features/profile/lib/identity.ts b/desktop/src/features/profile/lib/identity.ts index 8294ae386..9bfad6a49 100644 --- a/desktop/src/features/profile/lib/identity.ts +++ b/desktop/src/features/profile/lib/identity.ts @@ -5,6 +5,20 @@ export type UserProfileLookup = Record; export { truncatePubkey }; +export function formatVerifiedUserLabel( + chosenName: string | null | undefined, + verifiedName: string | null | undefined, +): string | null { + const chosen = chosenName?.trim(); + const verified = verifiedName?.trim(); + + if (chosen && verified && chosen !== verified) { + return `${chosen} (${verified})`; + } + + return chosen || verified || null; +} + /** * Deep-equal two profile lookups by value. Used to stabilise the merged * `messageProfiles` reference at the ChannelScreen boundary: the underlying @@ -120,21 +134,14 @@ export function resolveUserLabel(input: { const profile = getResolvedProfile(pubkey, profiles); const verifiedName = profile?.verifiedName?.trim(); const displayName = profile?.displayName?.trim(); - if (verifiedName) { - return verifiedName; - } - if (displayName) { - return displayName; - } - const nip05Handle = profile?.nip05Handle?.trim(); - if (nip05Handle) { - return nip05Handle; - } - const safeFallback = fallbackName?.trim(); - if (safeFallback) { - return safeFallback; + const label = formatVerifiedUserLabel( + displayName || nip05Handle || safeFallback, + verifiedName, + ); + if (label) { + return label; } return truncatePubkey(pubkey); diff --git a/desktop/src/features/profile/ui/UserProfilePopover.tsx b/desktop/src/features/profile/ui/UserProfilePopover.tsx index ad1e8ee41..cac63a1e6 100644 --- a/desktop/src/features/profile/ui/UserProfilePopover.tsx +++ b/desktop/src/features/profile/ui/UserProfilePopover.tsx @@ -25,6 +25,7 @@ import { useIsManagedAgent } from "@/features/agent-memory/hooks"; import { useIdentityQuery } from "@/shared/api/hooks"; import { useAgentWorking } from "@/features/agents/agentWorkingSignal"; import { + formatVerifiedUserLabel, formatOwnerLabel, ownsAuthorAgent, } from "@/features/profile/lib/identity"; @@ -236,13 +237,8 @@ export function UserProfilePopover({ managedAgentsQuery.isPending || usersBatchQuery.isPending); const displayName = - profile?.verifiedName ?? profile?.displayName ?? truncatePubkey(pubkey); - const profileAlias = - profile?.verifiedName && - profile.displayName && - profile.verifiedName !== profile.displayName - ? profile.displayName - : null; + formatVerifiedUserLabel(profile?.displayName, profile?.verifiedName) ?? + truncatePubkey(pubkey); // Owner signal mirrors UserProfilePanel: a declared NIP-OA owner whose agent // runs elsewhere holds no local seckey, so key custody (`isOwner`) alone // wrongly hides the affordance from them — and gating on bot-ness alone shows @@ -539,11 +535,6 @@ export function UserProfilePopover({ /> ) : null} - {profileAlias ? ( -

- {profileAlias} -

- ) : null} {isBotProfile && ownerLabel ? (

@@ -498,8 +503,7 @@ export function AppSidebar({ streamChannels, }); const resolvedDisplayName = - profile?.verifiedName?.trim() || - profile?.displayName?.trim() || + resolvedProfileDisplayName || fallbackDisplayName?.trim() || "Current identity"; const { diff --git a/desktop/src/shared/ui/VerifiedBadge.tsx b/desktop/src/shared/ui/VerifiedBadge.tsx index e8996ffd0..b7653e3df 100644 --- a/desktop/src/shared/ui/VerifiedBadge.tsx +++ b/desktop/src/shared/ui/VerifiedBadge.tsx @@ -8,10 +8,14 @@ export function VerifiedBadge({ verifiedName }: { verifiedName: string }) { -