Skip to content
Merged
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
34 changes: 15 additions & 19 deletions src/features/messages/MessageComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
allowsOutsideMentions,
archivedMention,
mentionAdmission,
pastedMentionRecipient,
} from "./mention-admission";
import { rememberMention } from "./mention-history";
import {
Expand Down Expand Up @@ -1392,25 +1393,20 @@ function Composer({
}}
onFormatsChange={setActiveFormats}
onEditLink={setLinkEdit}
// Pasted identity links notify the people the picker would offer,
// under their current names; edits never add recipients. In a
// stream or forum, a cached profile outside the channel counts as
// a picked directory row, so Send asks before adding them. The
// pasted label is never the name: it could disguise the invitee.
acceptRecipient={(pubkey) => {
if (editing.target) return null;
const name = session.profiles.snapshot().get(pubkey)?.name;
return (
mentionCandidates(
session,
channelId,
agentChoices,
mentionRoster,
name ? [{ pubkey, name }] : [],
).find((c) => c.recipient.pubkey === pubkey)?.recipient ??
null
);
}}
// Pasted identity links notify admitted recipients under their
// current names; edits never add recipients. Send still asks
// before adding someone outside the channel.
acceptRecipient={(pubkey) =>
editing.target
? null
: pastedMentionRecipient(
session,
channelId,
pubkey,
agentChoices,
mentionRoster,
)
}
data-single-emoji={largeEmojiDraft || undefined}
maxLength={16000}
aria-label={label}
Expand Down
27 changes: 26 additions & 1 deletion src/features/messages/mention-admission.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { expect, it } from "vitest";
import type { RelaySession } from "../relay/session";
import { mentionAdmission } from "./mention-admission";
import { mentionAdmission, pastedMentionRecipient } from "./mention-admission";

const member = "a".repeat(64);
const outside = "b".repeat(64);
Expand Down Expand Up @@ -84,3 +84,28 @@ it("refuses invalid keys, archived identities and closed destinations", () => {
)(member),
).toBe(false);
});

it("names a pasted recipient from current state, and leaves an unknown outsider display-only", () => {
const base = session({ channelType: "stream" });
const profiles = new Map([[outside, { name: "Jane" }]]);
const withProfiles = {
...base,
profiles: { snapshot: () => profiles },
} as unknown as RelaySession;
expect(pastedMentionRecipient(withProfiles, "channel", outside)).toEqual({
pubkey: outside,
name: "Jane",
});
expect(pastedMentionRecipient(withProfiles, "channel", member)).toEqual({
pubkey: member,
name: member.slice(0, 12),
});
expect(pastedMentionRecipient(withProfiles, "channel", library)).toEqual({
pubkey: library,
name: "Honey",
});
expect(pastedMentionRecipient(withProfiles, "channel", "f".repeat(64))).toBe(
null,
);
expect(pastedMentionRecipient(withProfiles, "channel", archived)).toBe(null);
});
32 changes: 32 additions & 0 deletions src/features/messages/mention-admission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,35 @@ export function mentionAdmission(
!archivedMention(session, pubkey) &&
(outside || allowed.has(pubkey));
}

/**
* The recipient a pasted identity link adds, under its current name, or null.
* The pasted label is never the name: it could disguise the invitee. A key
* with no known name is accepted only if it is a member; otherwise the link
* stays display-only, as the chooser would not offer it.
*/
export function pastedMentionRecipient(
session: RelaySession,
channelId: string,
pubkey: string,
inviteAgents = false,
roster?: readonly MentionRecipient[],
): MentionRecipient | null {
if (!mentionAdmission(session, channelId, inviteAgents, roster)(pubkey))
return null;
const listed = roster?.find((person) => person.pubkey === pubkey);
const name =
session.profiles.snapshot().get(pubkey)?.name ||
listed?.name ||
session.agentChoices
.snapshot()
.identities.find((agent) => agent.pubkey === pubkey)?.name;
if (name) return { pubkey, name };
const member = roster
? !!listed
: !!session.channels
.list()
.channels.find((c) => c.id === channelId)
?.members?.includes(pubkey);
return member ? { pubkey, name: pubkey.slice(0, 12) } : null;
}
Loading