mirror of
https://github.com/block/buzz.git
synced 2026-11-08 10:21:46 +00:00
fix(desktop): ask before mentioning a person outside a forum
A forum now signs only current members, so a person outside the forum gets the same Invite or Cancel prompt that relay agents already get. Invite adds people as members and agents as bots, then publishes. Cancel keeps the draft. Before this, the publish roster check refused the mention with no way to add the person. Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Signed-off-by: Logan Johnson <loganj@squareup.com>
This commit is contained in:
@@ -141,9 +141,10 @@ async function setup(options = {}) {
|
||||
},
|
||||
"@/features/channels/hooks": {
|
||||
useAddChannelMembersMutation: () => ({
|
||||
mutateAsync: async () => {
|
||||
calls.push(["add"]);
|
||||
mutateAsync: async (input) => {
|
||||
calls.push(["add", input]);
|
||||
if (control.add) await control.add.promise;
|
||||
if (control.onAdd) control.onAdd(input);
|
||||
return { errors: [] };
|
||||
},
|
||||
}),
|
||||
@@ -1215,3 +1216,52 @@ for (const main of [false, true])
|
||||
assert.ok(sends[0].flat(2).includes(HUMAN));
|
||||
assert.ok(sends[0].flat(2).includes(KEY));
|
||||
});
|
||||
|
||||
// A forum signs only current members, so a person outside the channel must
|
||||
// be invited first, as a member. Cancel keeps the draft and sends nothing.
|
||||
for (const choice of ["invite", "cancel"]) {
|
||||
test(`mounted forum asks before mentioning a person outside the channel: ${choice}`, async () => {
|
||||
const s = await setup({ realMentions: true });
|
||||
s.control.cachedMembers = [
|
||||
{ pubkey: HUMAN, role: "member", isAgent: false, displayName: "Pat" },
|
||||
];
|
||||
s.control.liveMembers = [PAT];
|
||||
s.navigate("b");
|
||||
await s.open();
|
||||
await s.choose("Pat");
|
||||
const text = s.text;
|
||||
// Pat leaves the cached roster too, so the composer knows to ask.
|
||||
s.control.cachedMembers = [];
|
||||
s.control.liveMembers = [];
|
||||
s.navigate("a");
|
||||
s.navigate("b");
|
||||
await act(async () => {});
|
||||
s.control.onAdd = ({ pubkeys }) => {
|
||||
s.control.liveMembers = pubkeys.includes(HUMAN) ? [PAT] : [];
|
||||
};
|
||||
await s.submit();
|
||||
assert.equal(s.prompt.open, true);
|
||||
assert.deepEqual([...s.prompt.names], ["Pat"]);
|
||||
if (choice === "invite") await s.invite();
|
||||
else await s.dismiss();
|
||||
await act(async () => {});
|
||||
const sends = s.calls.filter(([name]) => name === "send");
|
||||
if (choice === "cancel") {
|
||||
assert.equal(sends.length, 0);
|
||||
assert.equal(s.text.trim(), text.trim(), "the draft is kept");
|
||||
return;
|
||||
}
|
||||
assert.deepEqual(
|
||||
s.calls
|
||||
.filter(([name]) => name === "add")
|
||||
.map(([, { channelId, pubkeys, role }]) => ({
|
||||
channelId,
|
||||
pubkeys: [...pubkeys],
|
||||
role,
|
||||
})),
|
||||
[{ channelId: "forum", pubkeys: [HUMAN], role: "member" }],
|
||||
);
|
||||
assert.equal(sends.length, 1);
|
||||
assert.ok(sends[0].flat(2).includes(HUMAN), "the invited person is tagged");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ type PendingInvite = {
|
||||
pubkeys: string[];
|
||||
nonMemberPubkeys: string[];
|
||||
intendedAgentPubkeys: string[];
|
||||
agentPubkeys: ReadonlySet<string>;
|
||||
resolve: (invited: boolean) => void;
|
||||
};
|
||||
|
||||
@@ -72,15 +73,16 @@ export function useForumMentionPreparation(
|
||||
.map((ref) => ref.pubkey),
|
||||
];
|
||||
const agentPubkeys = new Set(intendedAgentPubkeys.map(normalizePubkey));
|
||||
// Local managed-agent lifecycle and channel-less/notes surfaces are not
|
||||
// part of this adapter. Relay-only agents use the same bot add as chat.
|
||||
// Publication signs only current members, so a mentioned nonmember must
|
||||
// be invited first. People join as members and relay-only agents use
|
||||
// the same bot add as chat. Local managed-agent lifecycle and
|
||||
// channel-less/notes surfaces are not part of this adapter.
|
||||
const nonMemberPubkeys =
|
||||
capturedChannelId &&
|
||||
channelType === "forum" &&
|
||||
mentions.hasResolvedMembers
|
||||
? [...new Set(pubkeys.map(normalizePubkey))].filter(
|
||||
(pubkey) =>
|
||||
agentPubkeys.has(pubkey) &&
|
||||
!mentions.isManagedAgentPubkey(pubkey) &&
|
||||
!mentions.memberPubkeys.has(pubkey),
|
||||
)
|
||||
@@ -92,6 +94,7 @@ export function useForumMentionPreparation(
|
||||
pubkeys,
|
||||
nonMemberPubkeys,
|
||||
intendedAgentPubkeys,
|
||||
agentPubkeys,
|
||||
resolve,
|
||||
};
|
||||
pendingRef.current = draft;
|
||||
@@ -140,15 +143,21 @@ export function useForumMentionPreparation(
|
||||
intendedAgentPubkeys: draft.intendedAgentPubkeys,
|
||||
});
|
||||
if (!isCurrent()) return;
|
||||
const result = await addMembers.mutateAsync({
|
||||
channelId: draft.channelId,
|
||||
pubkeys: draft.nonMemberPubkeys,
|
||||
role: "bot",
|
||||
});
|
||||
if (!isCurrent()) return;
|
||||
if (result.errors.length > 0) {
|
||||
setError(result.errors.map((failure) => failure.error).join("; "));
|
||||
return;
|
||||
for (const role of ["member", "bot"] as const) {
|
||||
const pubkeys = draft.nonMemberPubkeys.filter(
|
||||
(pubkey) => draft.agentPubkeys.has(pubkey) === (role === "bot"),
|
||||
);
|
||||
if (pubkeys.length === 0) continue;
|
||||
const result = await addMembers.mutateAsync({
|
||||
channelId: draft.channelId,
|
||||
pubkeys,
|
||||
role,
|
||||
});
|
||||
if (!isCurrent()) return;
|
||||
if (result.errors.length > 0) {
|
||||
setError(result.errors.map((failure) => failure.error).join("; "));
|
||||
return;
|
||||
}
|
||||
}
|
||||
pendingRef.current = null;
|
||||
setPending(null);
|
||||
|
||||
@@ -796,7 +796,11 @@ test("forum post and reply selection copies carry the mention", async ({
|
||||
await expect(input).toHaveText(FORUM_REPLY_BODY);
|
||||
await expect(input.locator(".mention-chip")).toHaveText("John Smith");
|
||||
|
||||
// John is not in the forum: a forum signs only members, so it asks first.
|
||||
await page.getByTestId("send-message").click();
|
||||
const dialog = page.getByRole("alertdialog");
|
||||
await expect(dialog).toContainText("John Smith");
|
||||
await dialog.getByRole("button", { name: "Invite", exact: true }).click();
|
||||
await expect(input).toHaveText("");
|
||||
await expect
|
||||
.poll(() => readSentMentionPubkeys(page, FORUM_REPLY_BODY))
|
||||
|
||||
Reference in New Issue
Block a user