diff --git a/src/app/shell/BestieNavigation.test.tsx b/src/app/shell/BestieNavigation.test.tsx index f7afd25a6e..bd9a02d1c6 100644 --- a/src/app/shell/BestieNavigation.test.tsx +++ b/src/app/shell/BestieNavigation.test.tsx @@ -17,6 +17,8 @@ import { createAgentLibrary } from "../../features/agents/library"; import { localAgentsFixture } from "../../features/agents/local-testing"; import type { RegisteredPage } from "../../features/pages/service"; import type { RelayData, RelaySnapshot } from "../../features/relay/service"; +import type { ProfileQueries } from "../../features/relay/profile-directory"; +import type { Profile } from "../../features/relay/contracts"; import { createRelaySession } from "../../features/relay/session"; import { createSidebarPreferencesStore } from "../../features/relay/sidebar-preferences-store"; import { @@ -112,7 +114,20 @@ async function fixture(firstAgent = false) { signal: lifetime.signal, }); await choices.refresh(false); - const session = { ...owner.session, viewer, agentChoices: choices }; + let profileMap: ReadonlyMap = new Map(); + const profileListeners = new Set<() => void>(); + const ensured = vi.fn(async () => {}); + const profiles: ProfileQueries = { + snapshot: () => profileMap, + subscribe(listener) { + profileListeners.add(listener); + return () => { + profileListeners.delete(listener); + }; + }, + ensure: ensured, + }; + const session = { ...owner.session, viewer, agentChoices: choices, profiles }; let snapshot: RelaySnapshot = { status: "ready", generation: 1, @@ -143,6 +158,11 @@ async function fixture(firstAgent = false) { relay, native, libraryRead, + ensured, + publishProfile(pubkey: string, profile: Profile) { + profileMap = new Map([...profileMap, [pubkey, profile]]); + for (const listener of profileListeners) listener(); + }, update(patch: Partial) { snapshot = { ...snapshot, ...patch }; for (const listener of listeners) listener(); @@ -179,6 +199,35 @@ it("shows the creation invitation in the dock, then follows the configured Besti expect(fizzTab).toHaveTextContent("F"); }); +it("shows the Bestie's published profile picture over its agent definition art", async () => { + const f = await fixture(); + const published = agentAvatars[1]; + if (!published) throw new Error("Expected a second bundled avatar"); + setDefaultAgent(scope, pollen); + render( + + {}} + placement="dock" + /> + , + ); + const tab = await screen.findByRole("tab", { name: "Pollen" }); + // The definition's art covers the wait for the profile it asked for. + expect(tab.querySelector("img")).toHaveAttribute("src", portrait.preview); + await waitFor(() => + expect(f.ensured).toHaveBeenCalledWith([pollen], "background"), + ); + act(() => + f.publishProfile(pollen, { name: "Pollen", picture: published.url }), + ); + expect( + screen.getByRole("tab", { name: "Pollen" }).querySelector("img"), + ).toHaveAttribute("src", published.preview); +}); + it("offers first-agent creation, then shows the exact Bestie in navigation and search", async () => { const f = await fixture(true); diff --git a/src/app/shell/BestieNavigation.tsx b/src/app/shell/BestieNavigation.tsx index 27293469ce..1f0bcc2d00 100644 --- a/src/app/shell/BestieNavigation.tsx +++ b/src/app/shell/BestieNavigation.tsx @@ -1,11 +1,19 @@ -import { useContext, useSyncExternalStore, type ReactNode } from "react"; +import { + useContext, + useEffect, + useSyncExternalStore, + type ReactNode, +} from "react"; import { agentAvatars } from "../../features/agents/avatar-packs"; import { bestieChat } from "../../bundled/bestie/chat"; import { UnreadBadge } from "../../bundled/channels/UnreadBadge"; import channelStyles from "../../bundled/channels/Channels.module.css"; import { useAgentChoices } from "../../features/agents/use-choices"; import type { RegisteredPage } from "../../features/pages/service"; -import { useRelayConnection } from "../../features/relay/react"; +import { + useProfileGeneration, + useRelayConnection, +} from "../../features/relay/react"; import type { RelayData } from "../../features/relay/service"; import { useSavedAgentState } from "../../features/solo/default-agent"; import { pagePresentation } from "./presentation"; @@ -56,9 +64,23 @@ export function BestieNavigationProvider({ session.channels.list, ); const channel = bestieChat(channels.channels, viewer, promoted); - const pack = agentAvatars.find((item) => item.url === agent?.avatar); + // The published profile picture wins, as in DM rows and message avatars; the + // agent definition's art covers the wait for that profile, or a profile + // without a picture (as ChannelMemberAvatars does). + const bestie = agent?.pubkey; + const generation = useProfileGeneration(session.profiles); + useEffect(() => { + void generation; + if (bestie) + void session.profiles.ensure([bestie], "background").catch(() => {}); + }, [session, bestie, generation]); + const profilePicture = useSyncExternalStore(session.profiles.subscribe, () => + bestie ? session.profiles.snapshot().get(bestie)?.picture : undefined, + ); + const picture = profilePicture ?? agent?.avatar; + const pack = agentAvatars.find((item) => item.url === picture); const preview = useAvatarPreview( - pack ? "" : (agent?.avatar ?? ""), + pack ? "" : (picture ?? ""), connection.origin, "small", session.media,