Skip to content
13 changes: 10 additions & 3 deletions src/app/shell/SearchResults.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { workflowLabel } from "../../features/relay/workflow-attribution";
import { useIdentityNames } from "../../features/identity-names/react";
import {
useIdentityNames,
useNamesRevision,
} from "../../features/identity-names/react";
import { npubEncode } from "nostr-tools/nip19";
import {
useEffect,
Expand Down Expand Up @@ -84,6 +87,7 @@ function conversationName(

/** The most rows the People and Agents groups each show. */
const PERSON_ROWS = 6;
const noMembers: readonly string[] = [];

export function SearchResults({
session,
Expand Down Expand Up @@ -321,6 +325,8 @@ export function SearchResults({
peopleActive,
);
useMentionArchives(session, showAuthorPicker || peopleActive);
// People labels come from one names scope built each render.
useNamesRevision(session.names, peopleActive);
// The viewer's own agents open a new chat, not a direct message: the
// same agents the new-chat composer accepts. Their earlier chats
// still show as conversations.
Expand Down Expand Up @@ -588,13 +594,14 @@ export function SearchResults({
name: person.name,
...(person.isAgent ? { isAgent: true } : {}),
}));
const named = session.names?.scope(candidates, displayFacts);
for (const { pubkey, source, agent } of eligible) {
const dm = relationships.dm(pubkey);
const agentName =
messageableAgents.get(pubkey)?.name ?? sessionAgents.get(pubkey)?.name;
people.push({
pubkey,
label: resolveName(pubkey, source.name, candidates, displayFacts),
label: named?.(pubkey)?.name ?? source.name,
username: source.username,
agentName: agentName !== source.name ? agentName : undefined,
agent,
Expand Down Expand Up @@ -949,7 +956,7 @@ export function SearchResults({
displayId,
profiles.get(displayId)?.name ?? displayId.slice(0, 10),
list.channels.find((channel) => channel.id === message.channelId)
?.members ?? [],
?.members ?? noMembers,
);
return {
key: message.id,
Expand Down
21 changes: 14 additions & 7 deletions src/bundled/channels/useSoloShare.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
useEffect,
useMemo,
useRef,
useState,
useSyncExternalStore,
Expand Down Expand Up @@ -94,14 +95,17 @@ export function useSoloShare({
const chosen = list.channels.find(
(item) => item.id === destination && item.id !== channelId,
);
const destinationMembers =
const readyMembers =
chosen &&
destinationRoster?.id === chosen.id &&
destinationRoster.status === "ready" &&
!chosen.cached &&
chosen.members
? [...new Set(chosen.members)]
!chosen.cached
? chosen.members
: undefined;
const destinationMembers = useMemo(
() => (readyMembers ? [...new Set(readyMembers)] : undefined),
[readyMembers],
);
useEffect(() => {
void rosterReload;
if (!enabled) return;
Expand Down Expand Up @@ -199,9 +203,12 @@ export function useSoloShare({
.catch(() => {});
}, [session, destinationProfileKeys]);
const resolveName = useIdentityNames(session.names);
const displayKeys = [
...new Set([...(source?.members ?? []), ...(destinationMembers ?? [])]),
];
const displayKeys = useMemo(
() => [
...new Set([...(source?.members ?? []), ...(destinationMembers ?? [])]),
],
[source?.members, destinationMembers],
);
const labels = new Map(
displayKeys.map((key) => [
key,
Expand Down
30 changes: 30 additions & 0 deletions src/bundled/mentions/team-mentions.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -675,3 +675,33 @@ it("directory pages rebuild the name index only for people they add", async () =
view.unmount();
h.library.dispose();
});

it("an open chooser relabels choices when only names change", async () => {
const h = fixture();
let name = "Before";
let revision = 0;
const listeners = new Set<() => void>();
Object.assign(h.session, {
names: {
subscribe: (listener: () => void) => {
listeners.add(listener);
return () => listeners.delete(listener);
},
snapshot: () => revision,
scope: () => () => ({ name, source: "agent-directory" }),
},
});
function Chooser() {
const model = useMentionChoices(h.session, "parent", false, "", "open");
return <>{model.candidates.map((choice) => choice.label).join(",")}</>;
}
render(<Chooser />);
await waitFor(() => expect(screen.getByText("Before")).toBeInTheDocument());
act(() => {
name = "After";
revision++;
for (const listener of listeners) listener();
});
expect(screen.getByText("After")).toBeInTheDocument();
h.library.dispose();
});
9 changes: 5 additions & 4 deletions src/bundled/mentions/use-mention-choices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ import type { MentionRecipient } from "../../features/messages/mention-draft";
import type { Agent } from "../../features/agents/service";
import { useAgentChoices } from "../../features/agents/use-choices";
import { AgentsContext, useAgentsSnapshot } from "../../features/agents/react";
import { useIdentityNames } from "../../features/identity-names/react";
import { DraftMentionRoster } from "../../features/messages/draft-mention-roster";
import { SelectedMentionContext } from "../../features/messages/selected-mention-context";
import { archivedMention } from "../../features/messages/mention-admission";
import { mentionHistory } from "../../features/messages/mention-history";
import { directoryChanges, mentionCandidates } from "./mention-candidates";
import { useListedChannel } from "../../features/relay/listed-channel";
import { useNamesRevision } from "../../features/identity-names/react";
import {
exactMention,
matchingMentions,
Expand Down Expand Up @@ -223,7 +223,8 @@ export function useMentionChoices(
lifetime,
);
const archives = useMentionArchives(session, open);
const resolve = useIdentityNames(session.names);
// Labels come from names scopes directly: watch every name change while open.
const names = useNamesRevision(session.names, open);
const [installed, install] = useState<{
session: ComposerSession;
key: string;
Expand Down Expand Up @@ -278,7 +279,7 @@ export function useMentionChoices(
void channel;
void agents;
void archives;
void resolve;
void names;
void revision;
// A closed or disabled chooser shows nothing, so it names nothing.
if (!open)
Expand All @@ -296,7 +297,7 @@ export function useMentionChoices(
channel,
agents,
archives,
resolve,
names,
revision,
]);
// Install membership first; agent discovery must not hide known members.
Expand Down
5 changes: 4 additions & 1 deletion src/bundled/relay-notes/NotesPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,10 @@ export function NotesCommunity({
}: { session: RelaySession } & NotesNavigation) {
const choices = useAgentChoices(session);
const resolveName = useIdentityNames(session.names);
const candidates = choices.identities.map((agent) => agent.pubkey);
const candidates = useMemo(
() => choices.identities.map((agent) => agent.pubkey),
[choices.identities],
);
const agents = choices.identities.map((agent) => ({
pubkey: agent.pubkey,
picture: agent.avatar,
Expand Down
3 changes: 2 additions & 1 deletion src/bundled/todos/TodosPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ type Draft = {
base: string | null;
input: string;
};
const noMembers: readonly string[] = [];
const empty: Draft = { content: "", original: "", base: null, input: "" };
const statuses: [Status, string][] = [
["todo", "To do"],
Expand Down Expand Up @@ -145,7 +146,7 @@ export function TodosPanel({
(channel) => channel.id === context.channelId,
)?.members;
const resolveName = (pubkey: string, fallback: string) =>
resolveIdentity(pubkey, fallback, members ?? []);
resolveIdentity(pubkey, fallback, members ?? noMembers);
const key = `todos-draft-v1:${context.channelId}`;
const [saved] = useState(() => readDraft(context.scope, key));
const savedDirty = !!saved && saved.content !== saved.original;
Expand Down
8 changes: 6 additions & 2 deletions src/features/identity-names/consumers.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ stubAvatarBrowserApis();
import "@testing-library/jest-dom/vitest";
import { act, cleanup, render, screen } from "@testing-library/react";
import { afterEach, beforeEach, expect, it, vi } from "vitest";
import { createRef } from "react";
import { createRef, useSyncExternalStore } from "react";
import { Context } from "@deepseek-ai/cordis";
import userEvent from "@testing-library/user-event";
import { PluginRuntime } from "../../plugins/runtime";
Expand Down Expand Up @@ -385,8 +385,12 @@ it.each(["live", "reconnect", "clear-cache"] as const)(
it("uses channel scope in link previews and activity, and participant scope in sidebar DMs", async () => {
const f = fixture();
function Sidebar() {
const list = useSyncExternalStore(
f.session.channels.subscribeList,
f.session.channels.list,
);
const labels = useChannelLabels(
f.session.channels.list().channels,
list.channels,
f.session.profiles,
f.session.names,
);
Expand Down
Loading
Loading