Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 50 additions & 1 deletion src/app/shell/BestieNavigation.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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<string, Profile> = new Map();
const profileListeners = new Set<() => void>();
const ensured = vi.fn<ProfileQueries["ensure"]>(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,
Expand Down Expand Up @@ -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<RelaySnapshot>) {
snapshot = { ...snapshot, ...patch };
for (const listener of listeners) listener();
Expand Down Expand Up @@ -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(
<BestieNavigationProvider pages={[page]} relay={f.relay}>
<PageNavigation
pages={[page]}
selected={page.key}
onSelect={() => {}}
placement="dock"
/>
</BestieNavigationProvider>,
);
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);

Expand Down
30 changes: 26 additions & 4 deletions src/app/shell/BestieNavigation.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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,
Expand Down
Loading