fix(identity): clarify verified profile labels

This commit is contained in:
Franco Sola
2026-07-24 17:03:34 -06:00
parent b0e9f79968
commit 15e6db22e1
5 changed files with 70 additions and 31 deletions
@@ -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)",
);
});
+20 -13
View File
@@ -5,6 +5,20 @@ export type UserProfileLookup = Record<string, UserProfileSummary>;
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);
@@ -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}
</div>
{profileAlias ? (
<p className="mt-0.5 truncate text-xs leading-4 text-muted-foreground">
{profileAlias}
</p>
) : null}
{isBotProfile && ownerLabel ? (
<p
className="mt-0.5 truncate text-xs leading-4 text-muted-foreground"
@@ -47,6 +47,7 @@ import { CreateChannelDialog } from "@/features/sidebar/ui/CreateChannelDialog";
import { SidebarProfileCard } from "@/features/sidebar/ui/SidebarProfileCard";
import { SidebarRelayConnectionCard } from "@/features/sidebar/ui/SidebarRelayConnectionCard";
import type { useSidebarRelayConnectionCard } from "@/features/sidebar/ui/useSidebarRelayConnectionCard";
import { formatVerifiedUserLabel } from "@/features/profile/lib/identity";
import {
SidebarLoadingContent,
useSidebarLoadingShape,
@@ -472,13 +473,17 @@ export function AppSidebar({
immediate: isSelectedDirectMessage,
timeoutMs: 400,
});
const resolvedProfileDisplayName = formatVerifiedUserLabel(
profile?.displayName,
profile?.verifiedName,
);
const { dmChannelLabels, dmParticipantsByChannelId, dmPresenceByChannelId } =
useDmSidebarMetadata({
currentPubkey,
directMessages,
enabled: shouldLoadDmMetadata,
fallbackDisplayName,
profileDisplayName: profile?.verifiedName ?? profile?.displayName,
profileDisplayName: resolvedProfileDisplayName,
});
const sortedDirectMessages = React.useMemo(
() =>
@@ -498,8 +503,7 @@ export function AppSidebar({
streamChannels,
});
const resolvedDisplayName =
profile?.verifiedName?.trim() ||
profile?.displayName?.trim() ||
resolvedProfileDisplayName ||
fallbackDisplayName?.trim() ||
"Current identity";
const {
+6 -2
View File
@@ -8,10 +8,14 @@ export function VerifiedBadge({ verifiedName }: { verifiedName: string }) {
<TooltipTrigger asChild>
<span
aria-label={`Verified corporate identity: ${verifiedName}`}
className="inline-flex shrink-0 items-center text-blue-500"
className="inline-flex shrink-0 items-center"
data-testid="verified-corporate-identity"
>
<BadgeCheck aria-hidden="true" className="h-4 w-4" fill="currentColor" />
<BadgeCheck
aria-hidden="true"
className="h-4 w-4 fill-blue-500 text-white"
strokeWidth={3}
/>
</span>
</TooltipTrigger>
<TooltipContent side="top">