From bcf7b1396f61322b918686fcca2f508be573c47f Mon Sep 17 00:00:00 2001 From: Wes Date: Tue, 23 Jun 2026 09:28:42 -0600 Subject: [PATCH 1/2] fix(desktop): show NIP-OA owners in profile pane Signed-off-by: Wes Co-authored-by: Pinky <44b8e82baa6e0e254e0208d68f335c283c94e7b78dd1fa10d5a49d3f13dd0435@sprout-oss.stage.blox.sqprod.co> --- desktop/src-tauri/src/commands/profile.rs | 1 + desktop/src-tauri/src/models.rs | 3 ++ desktop/src-tauri/src/nostr_convert.rs | 43 ++++++++++++++----- .../src/nostr_convert/user_search.rs | 12 +++++- .../src/features/channels/ui/ChannelPane.tsx | 1 + .../features/channels/ui/MembersSidebar.tsx | 3 ++ desktop/src/features/profile/hooks.ts | 1 + desktop/src/features/profile/lib/identity.ts | 2 + .../profile/lib/userCandidateSearch.test.mjs | 1 + .../features/profile/ui/UserProfilePanel.tsx | 39 +++++++++++++++-- .../profile/ui/UserProfilePanelSections.tsx | 36 +++++++++++++++- desktop/src/features/pulse/ui/PulseScreen.tsx | 1 + .../sidebar/ui/NewDirectMessageDialog.tsx | 3 ++ desktop/src/shared/api/tauri.ts | 17 +++----- desktop/src/shared/api/types.ts | 3 ++ desktop/src/testing/e2eBridge.ts | 37 +++++++++++++--- 16 files changed, 169 insertions(+), 34 deletions(-) diff --git a/desktop/src-tauri/src/commands/profile.rs b/desktop/src-tauri/src/commands/profile.rs index e218ca36e7..64cc6256d4 100644 --- a/desktop/src-tauri/src/commands/profile.rs +++ b/desktop/src-tauri/src/commands/profile.rs @@ -296,5 +296,6 @@ fn empty_profile_info(pubkey: &str) -> ProfileInfo { avatar_url: None, about: None, nip05_handle: None, + owner_pubkey: None, } } diff --git a/desktop/src-tauri/src/models.rs b/desktop/src-tauri/src/models.rs index 99007d0c4d..dc6f03ca8f 100644 --- a/desktop/src-tauri/src/models.rs +++ b/desktop/src-tauri/src/models.rs @@ -15,6 +15,7 @@ pub struct ProfileInfo { pub avatar_url: Option, pub about: Option, pub nip05_handle: Option, + pub owner_pubkey: Option, } #[derive(Serialize, Deserialize)] @@ -22,6 +23,7 @@ pub struct UserProfileSummaryInfo { pub display_name: Option, pub avatar_url: Option, pub nip05_handle: Option, + pub owner_pubkey: Option, #[serde(default)] pub is_agent: bool, } @@ -38,6 +40,7 @@ pub struct UserSearchResultInfo { pub display_name: Option, pub avatar_url: Option, pub nip05_handle: Option, + pub owner_pubkey: Option, #[serde(default)] pub is_agent: bool, } diff --git a/desktop/src-tauri/src/nostr_convert.rs b/desktop/src-tauri/src/nostr_convert.rs index aeb7f8c598..0ef035f48f 100644 --- a/desktop/src-tauri/src/nostr_convert.rs +++ b/desktop/src-tauri/src/nostr_convert.rs @@ -55,16 +55,16 @@ fn tags_named<'a>(event: &'a Event, name: &'a str) -> impl Iterator bool { +pub(crate) fn profile_valid_oa_owner_pubkey(event: &Event) -> Option { let target_hex = event.pubkey.to_hex(); let Ok(target_pubkey) = nostr::PublicKey::from_hex(&target_hex) else { - return false; + return None; }; for tag in event.tags.iter() { @@ -75,12 +75,16 @@ pub(crate) fn profile_has_valid_oa_owner(event: &Event) -> bool { let Ok(json) = serde_json::to_string(slice) else { continue; }; - if buzz_sdk_pkg::nip_oa::verify_auth_tag(&json, &target_pubkey).is_ok() { - return true; + if let Ok(owner_pubkey) = buzz_sdk_pkg::nip_oa::verify_auth_tag(&json, &target_pubkey) { + return Some(owner_pubkey.to_hex()); } } - false + None +} + +pub(crate) fn profile_has_valid_oa_owner(event: &Event) -> bool { + profile_valid_oa_owner_pubkey(event).is_some() } // ── kind:39000 / 39002 (NIP-29) ───────────────────────────────────────────── @@ -299,6 +303,7 @@ pub fn profile_info_from_event(event: &Event) -> Result { avatar_url, about, nip05_handle, + owner_pubkey: profile_valid_oa_owner_pubkey(event), }) } @@ -326,6 +331,7 @@ pub fn users_batch_from_events( let mut profiles = HashMap::new(); for (pk, ev) in &latest { let v: Value = serde_json::from_str(&ev.content).unwrap_or(Value::Null); + let owner_pubkey = profile_valid_oa_owner_pubkey(ev); let summary = UserProfileSummaryInfo { display_name: v .get("display_name") @@ -334,7 +340,8 @@ pub fn users_batch_from_events( .map(str::to_string), avatar_url: v.get("picture").and_then(Value::as_str).map(str::to_string), nip05_handle: v.get("nip05").and_then(Value::as_str).map(str::to_string), - is_agent: profile_has_valid_oa_owner(ev), + is_agent: owner_pubkey.is_some(), + owner_pubkey, }; profiles.insert(pk.clone(), summary); } @@ -586,7 +593,7 @@ mod tests { } /// Build a kind:0 profile with a valid NIP-OA auth tag. - fn oa_profile_event(content: &str) -> Event { + fn oa_profile_event(content: &str) -> (Event, String) { let agent_keys = Keys::generate(); let owner_keys = Keys::generate(); let agent_pubkey = agent_keys.public_key(); @@ -595,10 +602,11 @@ mod tests { let tag_values: Vec = serde_json::from_str(&tag_json).expect("parse auth tag json"); let auth_tag = Tag::parse(tag_values).expect("parse auth tag"); - EventBuilder::new(Kind::Metadata, content) + let event = EventBuilder::new(Kind::Metadata, content) .tags(vec![auth_tag]) .sign_with_keys(&agent_keys) - .expect("sign") + .expect("sign"); + (event, owner_keys.public_key().to_hex()) } #[test] @@ -760,6 +768,15 @@ mod tests { assert_eq!(p.about.as_deref(), Some("hi")); assert_eq!(p.nip05_handle.as_deref(), Some("alice@x")); assert_eq!(p.pubkey, e.pubkey.to_hex()); + assert!(p.owner_pubkey.is_none()); + } + + #[test] + fn profile_info_extracts_valid_nip_oa_owner() { + let (event, owner_pubkey) = oa_profile_event(r#"{"display_name":"Mira"}"#); + let p = profile_info_from_event(&event).unwrap(); + + assert_eq!(p.owner_pubkey.as_deref(), Some(owner_pubkey.as_str())); } #[test] @@ -803,12 +820,16 @@ mod tests { #[test] fn users_batch_marks_valid_nip_oa_profiles_as_agents() { - let agent = oa_profile_event(r#"{"display_name":"Mira"}"#); + let (agent, owner_pubkey) = oa_profile_event(r#"{"display_name":"Mira"}"#); let pubkey = agent.pubkey.to_hex(); let resp = users_batch_from_events(std::slice::from_ref(&agent), std::slice::from_ref(&pubkey)); assert!(resp.profiles[&pubkey].is_agent); + assert_eq!( + resp.profiles[&pubkey].owner_pubkey.as_deref(), + Some(owner_pubkey.as_str()) + ); } #[test] diff --git a/desktop/src-tauri/src/nostr_convert/user_search.rs b/desktop/src-tauri/src/nostr_convert/user_search.rs index 85a68df146..c170d895c0 100644 --- a/desktop/src-tauri/src/nostr_convert/user_search.rs +++ b/desktop/src-tauri/src/nostr_convert/user_search.rs @@ -5,11 +5,12 @@ use serde_json::Value; use crate::models::{SearchUsersResponse, UserSearchResultInfo}; -use super::profile_has_valid_oa_owner; +use super::profile_valid_oa_owner_pubkey; /// Convert a single kind:0 event to a [`UserSearchResultInfo`]. pub fn user_search_result_from_event(ev: &Event) -> UserSearchResultInfo { let v: Value = serde_json::from_str(&ev.content).unwrap_or(Value::Null); + let owner_pubkey = profile_valid_oa_owner_pubkey(ev); UserSearchResultInfo { pubkey: ev.pubkey.to_hex(), display_name: v @@ -19,7 +20,8 @@ pub fn user_search_result_from_event(ev: &Event) -> UserSearchResultInfo { .map(str::to_string), avatar_url: v.get("picture").and_then(Value::as_str).map(str::to_string), nip05_handle: v.get("nip05").and_then(Value::as_str).map(str::to_string), - is_agent: profile_has_valid_oa_owner(ev), + is_agent: owner_pubkey.is_some(), + owner_pubkey, } } @@ -224,9 +226,15 @@ mod tests { #[test] fn user_search_result_marks_valid_nip_oa_profile_as_agent() { let event = oa_profile_event(r#"{"display_name":"Mira"}"#); + let owner_pubkey = event + .tags + .iter() + .find_map(|tag| tag.as_slice().get(1).cloned()) + .expect("owner pubkey"); let result = user_search_result_from_event(&event); assert_eq!(result.display_name.as_deref(), Some("Mira")); + assert_eq!(result.owner_pubkey.as_deref(), Some(owner_pubkey.as_str())); assert!(result.is_agent); } diff --git a/desktop/src/features/channels/ui/ChannelPane.tsx b/desktop/src/features/channels/ui/ChannelPane.tsx index 79c5e79394..5fd7641fea 100644 --- a/desktop/src/features/channels/ui/ChannelPane.tsx +++ b/desktop/src/features/channels/ui/ChannelPane.tsx @@ -948,6 +948,7 @@ export const ChannelPane = React.memo(function ChannelPane({ layout={useSplitAuxiliaryPane ? "split" : "standalone"} onClose={onCloseProfilePanel} onOpenDm={onOpenDm} + onOpenProfile={onOpenProfilePanel} onViewChange={onProfilePanelViewChange} pubkey={profilePanelPubkey} splitPaneClamp diff --git a/desktop/src/features/channels/ui/MembersSidebar.tsx b/desktop/src/features/channels/ui/MembersSidebar.tsx index b29f19e75f..37741cb927 100644 --- a/desktop/src/features/channels/ui/MembersSidebar.tsx +++ b/desktop/src/features/channels/ui/MembersSidebar.tsx @@ -247,6 +247,7 @@ export function MembersSidebar({ ? currentName : (currentName ?? candidateName), nip05Handle: current.nip05Handle ?? candidate.nip05Handle ?? null, + ownerPubkey: current.ownerPubkey ?? candidate.ownerPubkey ?? null, isAgent: current.isAgent || candidate.isAgent, }); }; @@ -265,6 +266,7 @@ export function MembersSidebar({ displayName: agent.name, avatarUrl: null, nip05Handle: null, + ownerPubkey: null, isAgent: true, }); } @@ -275,6 +277,7 @@ export function MembersSidebar({ displayName: agent.name, avatarUrl: null, nip05Handle: null, + ownerPubkey: null, isAgent: true, }); } diff --git a/desktop/src/features/profile/hooks.ts b/desktop/src/features/profile/hooks.ts index 4c4f86143b..9ba5273bd1 100644 --- a/desktop/src/features/profile/hooks.ts +++ b/desktop/src/features/profile/hooks.ts @@ -104,6 +104,7 @@ export function useProfileQuery(enabled = true) { avatarUrl: cached.avatarUrl, about: null, nip05Handle: null, + ownerPubkey: null, } satisfies Profile) : undefined, [cached, pubkey], diff --git a/desktop/src/features/profile/lib/identity.ts b/desktop/src/features/profile/lib/identity.ts index 69c849651c..491883a0af 100644 --- a/desktop/src/features/profile/lib/identity.ts +++ b/desktop/src/features/profile/lib/identity.ts @@ -36,6 +36,8 @@ export function mergeCurrentProfileIntoLookup( avatarUrl: currentProfile.avatarUrl, nip05Handle: currentProfile.nip05Handle, isAgent: profiles?.[normalizePubkey(currentProfile.pubkey)]?.isAgent, + ownerPubkey: + profiles?.[normalizePubkey(currentProfile.pubkey)]?.ownerPubkey ?? null, }, }; } diff --git a/desktop/src/features/profile/lib/userCandidateSearch.test.mjs b/desktop/src/features/profile/lib/userCandidateSearch.test.mjs index d8b1a29904..210e92758b 100644 --- a/desktop/src/features/profile/lib/userCandidateSearch.test.mjs +++ b/desktop/src/features/profile/lib/userCandidateSearch.test.mjs @@ -13,6 +13,7 @@ function makeUser(overrides = {}) { displayName: null, isAgent: false, nip05Handle: null, + ownerPubkey: null, pubkey: "abcdef1234567890", ...overrides, }; diff --git a/desktop/src/features/profile/ui/UserProfilePanel.tsx b/desktop/src/features/profile/ui/UserProfilePanel.tsx index 930e73a949..4d9f8348a5 100644 --- a/desktop/src/features/profile/ui/UserProfilePanel.tsx +++ b/desktop/src/features/profile/ui/UserProfilePanel.tsx @@ -56,6 +56,7 @@ type UserProfilePanelProps = { layout?: "standalone" | "split"; onClose: () => void; onOpenDm?: (pubkeys: string[]) => void; + onOpenProfile?: (pubkey: string) => void; onResetWidth?: () => void; onResizeStart?: (event: React.PointerEvent) => void; onViewChange: ( @@ -136,6 +137,7 @@ export function UserProfilePanel({ layout = "standalone", onClose, onOpenDm, + onOpenProfile, onResetWidth, onResizeStart, onViewChange, @@ -172,6 +174,8 @@ export function UserProfilePanel({ const { goChannel } = useAppNavigation(); const profile = profileQuery.data; + const ownerPubkey = profile?.ownerPubkey ?? null; + const ownerProfileQuery = useUserProfileQuery(ownerPubkey ?? undefined); const pubkeyLower = pubkey.toLowerCase(); const presenceStatus = presenceQuery.data?.[pubkeyLower]; const userStatus = userStatusQuery.data?.[pubkeyLower]; @@ -252,7 +256,16 @@ export function UserProfilePanel({ const displayName = profile?.displayName ?? truncatePubkey(pubkey); const ownerHandle = React.useMemo(() => { - if (currentPubkey === undefined) { + if (ownerPubkey) { + const ownerProfile = ownerProfileQuery.data; + return ( + ownerProfile?.nip05Handle?.trim() || + ownerProfile?.displayName?.trim() || + truncatePubkey(ownerPubkey) + ); + } + + if (currentPubkey === undefined || isOwner !== true) { return null; } @@ -262,8 +275,22 @@ export function UserProfilePanel({ currentProfile?.displayName?.trim() || truncatePubkey(currentPubkey) ); - }, [currentProfileQuery.data, currentPubkey]); - const ownerDisplayName = ownerHandle ? `${ownerHandle} (you)` : null; + }, [ + currentProfileQuery.data, + currentPubkey, + isOwner, + ownerProfileQuery.data, + ownerPubkey, + ]); + const isCurrentUserOwner = + currentPubkey !== undefined && + ownerPubkey !== null && + ownerPubkey.toLowerCase() === currentPubkey.toLowerCase(); + const ownerDisplayName = ownerHandle + ? isCurrentUserOwner || (!ownerPubkey && isOwner === true) + ? `${ownerHandle} (you)` + : ownerHandle + : null; const panelTitle = VIEW_TITLES[view]; const memoryCount = memoryQuery.data ? (memoryQuery.data.core ? 1 : 0) + memoryQuery.data.memories.length @@ -335,7 +362,13 @@ export function UserProfilePanel({ memoryCount={memoryCount} ownerDisplayName={ownerDisplayName} ownerHandle={ownerHandle} + ownerPubkey={ownerPubkey} onOpenChannels={() => onViewChange("channels")} + onOpenOwner={ + ownerPubkey && onOpenProfile + ? () => onOpenProfile(ownerPubkey) + : undefined + } onOpenMemories={() => onViewChange("memories")} onOpenDm={onOpenDm} presenceLoaded={presenceQuery.isSuccess} diff --git a/desktop/src/features/profile/ui/UserProfilePanelSections.tsx b/desktop/src/features/profile/ui/UserProfilePanelSections.tsx index 3baa46ea2e..e9feb6a671 100644 --- a/desktop/src/features/profile/ui/UserProfilePanelSections.tsx +++ b/desktop/src/features/profile/ui/UserProfilePanelSections.tsx @@ -81,7 +81,9 @@ export type ProfileSummaryViewProps = { memoryCount: number | undefined; ownerDisplayName: string | null; ownerHandle: string | null; + ownerPubkey: string | null; onOpenChannels: () => void; + onOpenOwner?: () => void; onOpenMemories: () => void; onOpenDm?: (pubkeys: string[]) => void; presenceLoaded: boolean; @@ -113,7 +115,9 @@ export function ProfileSummaryView({ memoryCount, ownerDisplayName, ownerHandle, + ownerPubkey, onOpenChannels, + onOpenOwner, onOpenMemories, onOpenDm, presenceLoaded, @@ -134,11 +138,14 @@ export function ProfileSummaryView({ relayAgent, isBot, }), - ...(isOwner === true + ...(ownerDisplayName || isOwner === true ? buildOwnerFields({ + includeOperationalFields: isOwner === true, managedAgent, ownerDisplayName, ownerHandle, + ownerPubkey, + onOpenOwner, presenceLoaded, presenceStatus, relayAgent, @@ -594,16 +601,22 @@ function buildPublicFields({ } function buildOwnerFields({ + includeOperationalFields, managedAgent, ownerDisplayName, ownerHandle, + ownerPubkey, + onOpenOwner, presenceLoaded, presenceStatus, relayAgent, }: { + includeOperationalFields: boolean; managedAgent: ManagedAgent | undefined; ownerDisplayName: string | null; ownerHandle: string | null; + ownerPubkey: string | null; + onOpenOwner?: () => void; presenceLoaded: boolean; presenceStatus: "online" | "away" | "offline" | undefined; relayAgent: RelayAgent | undefined; @@ -612,14 +625,33 @@ function buildOwnerFields({ if (ownerDisplayName) { fields.push({ - copyValue: ownerHandle ?? undefined, + copyValue: onOpenOwner + ? undefined + : (ownerPubkey ?? ownerHandle ?? undefined), displayValue: ownerDisplayName, + displayNode: onOpenOwner ? ( + + ) : undefined, icon: UserRound, label: "Owned by", testId: "user-profile-owned-by", }); } + if (!includeOperationalFields) { + return fields; + } + if (managedAgent?.agentCommand) { fields.push({ copyValue: managedAgent.agentCommand, diff --git a/desktop/src/features/pulse/ui/PulseScreen.tsx b/desktop/src/features/pulse/ui/PulseScreen.tsx index 6a0563ec85..1baef3b142 100644 --- a/desktop/src/features/pulse/ui/PulseScreen.tsx +++ b/desktop/src/features/pulse/ui/PulseScreen.tsx @@ -59,6 +59,7 @@ export function PulseScreen() { currentPubkey={identityQuery.data?.pubkey} onClose={handleCloseProfilePanel} onOpenDm={handleOpenDm} + onOpenProfile={handleOpenProfilePanel} onResetWidth={threadPanelWidth.onResetWidth} onResizeStart={threadPanelWidth.onResizeStart} onViewChange={handleProfilePanelViewChange} diff --git a/desktop/src/features/sidebar/ui/NewDirectMessageDialog.tsx b/desktop/src/features/sidebar/ui/NewDirectMessageDialog.tsx index 3430ffdc27..64b65fa3f3 100644 --- a/desktop/src/features/sidebar/ui/NewDirectMessageDialog.tsx +++ b/desktop/src/features/sidebar/ui/NewDirectMessageDialog.tsx @@ -316,6 +316,7 @@ export function NewDirectMessageDialog({ ? currentName : (currentName ?? candidateName), nip05Handle: current.nip05Handle ?? candidate.nip05Handle ?? null, + ownerPubkey: current.ownerPubkey ?? candidate.ownerPubkey ?? null, isAgent: current.isAgent || candidate.isAgent, }); }; @@ -334,6 +335,7 @@ export function NewDirectMessageDialog({ displayName: agent.name, avatarUrl: null, nip05Handle: null, + ownerPubkey: null, isAgent: true, }); } @@ -344,6 +346,7 @@ export function NewDirectMessageDialog({ displayName: agent.name, avatarUrl: null, nip05Handle: null, + ownerPubkey: null, isAgent: true, }); } diff --git a/desktop/src/shared/api/tauri.ts b/desktop/src/shared/api/tauri.ts index 11e25da4d3..6afa9d8f39 100644 --- a/desktop/src/shared/api/tauri.ts +++ b/desktop/src/shared/api/tauri.ts @@ -56,12 +56,10 @@ type RawProfile = { avatar_url: string | null; about: string | null; nip05_handle: string | null; + owner_pubkey: string | null; }; -type RawUserProfileSummary = { - display_name: string | null; - avatar_url: string | null; - nip05_handle: string | null; +type RawUserProfileSummary = Omit & { is_agent?: boolean; }; @@ -70,13 +68,7 @@ type RawUsersBatchResponse = { missing: string[]; }; -type RawUserSearchResult = { - pubkey: string; - display_name: string | null; - avatar_url: string | null; - nip05_handle: string | null; - is_agent?: boolean; -}; +type RawUserSearchResult = RawUserProfileSummary & { pubkey: string }; type RawSearchUsersResponse = { users: RawUserSearchResult[]; @@ -426,6 +418,7 @@ function fromRawProfile(profile: RawProfile): Profile { avatarUrl: profile.avatar_url, about: profile.about, nip05Handle: profile.nip05_handle, + ownerPubkey: profile.owner_pubkey, }; } @@ -436,6 +429,7 @@ function fromRawUserProfileSummary( displayName: profile.display_name, avatarUrl: profile.avatar_url, nip05Handle: profile.nip05_handle, + ownerPubkey: profile.owner_pubkey, isAgent: profile.is_agent ?? false, }; } @@ -446,6 +440,7 @@ function fromRawUserSearchResult(user: RawUserSearchResult): UserSearchResult { displayName: user.display_name, avatarUrl: user.avatar_url, nip05Handle: user.nip05_handle, + ownerPubkey: user.owner_pubkey, isAgent: user.is_agent ?? false, }; } diff --git a/desktop/src/shared/api/types.ts b/desktop/src/shared/api/types.ts index 653a1be992..1ca74eca34 100644 --- a/desktop/src/shared/api/types.ts +++ b/desktop/src/shared/api/types.ts @@ -114,12 +114,14 @@ export type Profile = { avatarUrl: string | null; about: string | null; nip05Handle: string | null; + ownerPubkey: string | null; }; export type UserProfileSummary = { displayName: string | null; avatarUrl: string | null; nip05Handle: string | null; + ownerPubkey: string | null; isAgent?: boolean; }; @@ -133,6 +135,7 @@ export type UserSearchResult = { displayName: string | null; avatarUrl: string | null; nip05Handle: string | null; + ownerPubkey: string | null; isAgent: boolean; }; diff --git a/desktop/src/testing/e2eBridge.ts b/desktop/src/testing/e2eBridge.ts index 5fa2c20951..04f8bfe987 100644 --- a/desktop/src/testing/e2eBridge.ts +++ b/desktop/src/testing/e2eBridge.ts @@ -52,6 +52,7 @@ type MockSearchProfileSeed = { avatarUrl?: string | null; nip05Handle?: string | null; about?: string | null; + ownerPubkey?: string | null; isAgent?: boolean; }; @@ -136,6 +137,7 @@ type RawProfile = { avatar_url: string | null; about: string | null; nip05_handle: string | null; + owner_pubkey: string | null; is_agent?: boolean; }; @@ -143,6 +145,7 @@ type RawUserProfileSummary = { display_name: string | null; avatar_url: string | null; nip05_handle: string | null; + owner_pubkey: string | null; is_agent?: boolean; }; @@ -156,6 +159,7 @@ type RawUserSearchResult = { display_name: string | null; avatar_url: string | null; nip05_handle: string | null; + owner_pubkey: string | null; is_agent?: boolean; }; @@ -1118,6 +1122,7 @@ function seedMockSearchProfiles(config?: E2eConfig) { avatar_url: seed.avatarUrl ?? null, about: seed.about ?? null, nip05_handle: seed.nip05Handle ?? null, + owner_pubkey: seed.ownerPubkey ?? null, is_agent: seed.isAgent ?? false, }; mockProfiles.set(pubkey, profile); @@ -1145,6 +1150,7 @@ function getMockProfileByPubkey(pubkey: string): RawProfile | null { avatar_url: null, about: null, nip05_handle: null, + owner_pubkey: null, is_agent: mockAgentPubkeys.has(normalizedPubkey), }; } @@ -1811,6 +1817,7 @@ const mockProfiles = new Map([ avatar_url: null, about: null, nip05_handle: null, + owner_pubkey: null, is_agent: false, }, ], @@ -1822,6 +1829,7 @@ const mockProfiles = new Map([ avatar_url: null, about: null, nip05_handle: null, + owner_pubkey: MOCK_IDENTITY_PUBKEY, is_agent: true, }, ], @@ -1947,6 +1955,7 @@ function importMockIdentity(nsec: string) { avatar_url: null, about: null, nip05_handle: null, + owner_pubkey: null, }); } @@ -1993,6 +2002,7 @@ function ensureMockProfile(config: E2eConfig | undefined): RawProfile { avatar_url: null, about: null, nip05_handle: null, + owner_pubkey: null, }; mockProfiles.set(pubkey, profile); return profile; @@ -3078,7 +3088,8 @@ async function handleGetProfile(config: E2eConfig | undefined) { display_name: null, about: null, avatar_url: null, - nip05: null, + nip05_handle: null, + owner_pubkey: null, }; } const content = JSON.parse(events[0].content ?? "{}"); @@ -3087,7 +3098,8 @@ async function handleGetProfile(config: E2eConfig | undefined) { display_name: content.display_name ?? content.name ?? null, about: content.about ?? null, avatar_url: content.picture ?? null, - nip05: content.nip05 ?? null, + nip05_handle: content.nip05 ?? null, + owner_pubkey: null, }; } @@ -3170,7 +3182,8 @@ async function handleUpdateProfile( display_name: updated.display_name ?? null, about: updated.about ?? null, avatar_url: updated.picture ?? null, - nip05: updated.nip05 ?? null, + nip05_handle: updated.nip05 ?? null, + owner_pubkey: null, }; } @@ -3201,7 +3214,8 @@ async function handleGetUserProfile( display_name: null, about: null, avatar_url: null, - nip05: null, + nip05_handle: null, + owner_pubkey: null, }; } const content = JSON.parse(events[0].content ?? "{}"); @@ -3210,7 +3224,8 @@ async function handleGetUserProfile( display_name: content.display_name ?? content.name ?? null, about: content.about ?? null, avatar_url: content.picture ?? null, - nip05: content.nip05 ?? null, + nip05_handle: content.nip05 ?? null, + owner_pubkey: null, }; } @@ -3238,6 +3253,7 @@ async function handleGetUsersBatch( display_name: profile.display_name, avatar_url: profile.avatar_url, nip05_handle: profile.nip05_handle, + owner_pubkey: profile.owner_pubkey, is_agent: profile.is_agent ?? false, }; } @@ -3261,6 +3277,10 @@ async function handleGetUsersBatch( display_name: content.display_name ?? content.name ?? null, avatar_url: content.picture ?? null, nip05_handle: content.nip05 ?? null, + owner_pubkey: + ((ev.tags ?? []) as string[][]).find( + (tag) => Array.isArray(tag) && tag[0] === "auth" && tag.length === 4, + )?.[1] ?? null, is_agent: Array.isArray(ev.tags) ? ev.tags.some( (tag) => @@ -3285,6 +3305,7 @@ async function handleGetUsersBatch( display_name: profile.display_name, avatar_url: profile.avatar_url, nip05_handle: profile.nip05_handle, + owner_pubkey: profile.owner_pubkey, is_agent: profile.is_agent ?? false, }; } @@ -3337,6 +3358,7 @@ async function handleSearchUsers( display_name: profile.display_name, avatar_url: profile.avatar_url, nip05_handle: profile.nip05_handle, + owner_pubkey: profile.owner_pubkey, is_agent: profile.is_agent ?? false, })); @@ -3360,6 +3382,10 @@ async function handleSearchUsers( display_name: content.display_name ?? content.name ?? null, avatar_url: content.picture ?? null, nip05_handle: content.nip05 ?? null, + owner_pubkey: + ((ev.tags ?? []) as string[][]).find( + (tag) => Array.isArray(tag) && tag[0] === "auth" && tag.length === 4, + )?.[1] ?? null, is_agent: Array.isArray(ev.tags) ? ev.tags.some( (tag) => @@ -5053,6 +5079,7 @@ async function handleCreateManagedAgent( avatar_url: avatarUrl, about: args.input.systemPrompt?.trim() || null, nip05_handle: null, + owner_pubkey: MOCK_IDENTITY_PUBKEY, is_agent: true, }); syncMockRelayAgentsFromManagedAgents(); From 99e285b3dc709081b42870342595b51d6b10f627 Mon Sep 17 00:00:00 2001 From: Wes Date: Tue, 23 Jun 2026 09:53:15 -0600 Subject: [PATCH 2/2] fix(desktop): show owner avatar in profile pane Signed-off-by: Wes Co-authored-by: Pinky <44b8e82baa6e0e254e0208d68f335c283c94e7b78dd1fa10d5a49d3f13dd0435@sprout-oss.stage.blox.sqprod.co> --- .../features/profile/ui/UserProfilePanel.tsx | 1 + .../profile/ui/UserProfilePanelSections.tsx | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/desktop/src/features/profile/ui/UserProfilePanel.tsx b/desktop/src/features/profile/ui/UserProfilePanel.tsx index 4d9f8348a5..e84dba7ad7 100644 --- a/desktop/src/features/profile/ui/UserProfilePanel.tsx +++ b/desktop/src/features/profile/ui/UserProfilePanel.tsx @@ -361,6 +361,7 @@ export function UserProfilePanel({ memoriesLoading={memoryQuery.isLoading} memoryCount={memoryCount} ownerDisplayName={ownerDisplayName} + ownerAvatarUrl={ownerProfileQuery.data?.avatarUrl ?? null} ownerHandle={ownerHandle} ownerPubkey={ownerPubkey} onOpenChannels={() => onViewChange("channels")} diff --git a/desktop/src/features/profile/ui/UserProfilePanelSections.tsx b/desktop/src/features/profile/ui/UserProfilePanelSections.tsx index e9feb6a671..b00a986733 100644 --- a/desktop/src/features/profile/ui/UserProfilePanelSections.tsx +++ b/desktop/src/features/profile/ui/UserProfilePanelSections.tsx @@ -42,6 +42,7 @@ import { useFeatureEnabled } from "@/shared/features"; import { cn } from "@/shared/lib/cn"; import { useNow } from "@/shared/lib/useNow"; import { Badge } from "@/shared/ui/badge"; +import { UserAvatar } from "@/shared/ui/UserAvatar"; const RUNTIME_LABELS: Record = { goose: "Goose", @@ -80,6 +81,7 @@ export type ProfileSummaryViewProps = { memoriesLoading: boolean; memoryCount: number | undefined; ownerDisplayName: string | null; + ownerAvatarUrl: string | null; ownerHandle: string | null; ownerPubkey: string | null; onOpenChannels: () => void; @@ -114,6 +116,7 @@ export function ProfileSummaryView({ memoriesLoading, memoryCount, ownerDisplayName, + ownerAvatarUrl, ownerHandle, ownerPubkey, onOpenChannels, @@ -143,6 +146,7 @@ export function ProfileSummaryView({ includeOperationalFields: isOwner === true, managedAgent, ownerDisplayName, + ownerAvatarUrl, ownerHandle, ownerPubkey, onOpenOwner, @@ -604,6 +608,7 @@ function buildOwnerFields({ includeOperationalFields, managedAgent, ownerDisplayName, + ownerAvatarUrl, ownerHandle, ownerPubkey, onOpenOwner, @@ -614,6 +619,7 @@ function buildOwnerFields({ includeOperationalFields: boolean; managedAgent: ManagedAgent | undefined; ownerDisplayName: string | null; + ownerAvatarUrl: string | null; ownerHandle: string | null; ownerPubkey: string | null; onOpenOwner?: () => void; @@ -631,7 +637,7 @@ function buildOwnerFields({ displayValue: ownerDisplayName, displayNode: onOpenOwner ? ( ) : undefined, icon: UserRound,