fix(desktop): let channel members add members and bots without admin (#815)

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
Wes
2026-06-02 09:55:09 -07:00
committed by GitHub
co-authored by Brain
parent a25ca5d1bf
commit 41a3fc1589
4 changed files with 80 additions and 2 deletions
@@ -35,12 +35,14 @@ function formatSearchUserSecondary(user: UserSearchResult) {
}
export function ChannelMemberInviteCard({
canAssignElevatedRoles,
existingMembers,
isPending,
onSubmit,
open,
requestErrorMessage,
}: {
canAssignElevatedRoles: boolean;
existingMembers: ChannelMember[];
isPending: boolean;
onSubmit: (input: {
@@ -63,6 +65,28 @@ export function ChannelMemberInviteCard({
AddChannelMembersResult["errors"]
>([]);
// Only owners/admins may grant the elevated "admin" role — the relay rejects
// it for everyone else. Hide it from the dropdown so we never offer a role
// the server will refuse. "member", "guest", and "bot" are non-elevated and
// any channel member may grant them.
const availableRoles = React.useMemo<
Exclude<ChannelMember["role"], "owner">[]
>(
() =>
canAssignElevatedRoles
? ["member", "admin", "guest", "bot"]
: ["member", "guest", "bot"],
[canAssignElevatedRoles],
);
// Guard against a stale elevated selection if the caller's permissions change
// while the card is mounted (e.g. the member gets demoted).
React.useEffect(() => {
if (!availableRoles.includes(inviteRole)) {
setInviteRole("member");
}
}, [availableRoles, inviteRole]);
const deferredInviteQuery = React.useDeferredValue(inviteQuery.trim());
const selectedInviteePubkeys = React.useMemo(
() =>
@@ -329,7 +353,7 @@ export function ChannelMemberInviteCard({
}
value={inviteRole}
>
{["member", "admin", "guest", "bot"].map((role) => (
{availableRoles.map((role) => (
<option key={role} value={role}>
{role}
</option>
@@ -228,9 +228,10 @@ export function MembersSidebar({
</SheetHeader>
<div className="flex-1 space-y-6 overflow-y-auto px-6 py-6">
{(canManageMembers || channel.visibility === "open") &&
{(selfMember !== null || channel.visibility === "open") &&
channel.channelType !== "dm" ? (
<ChannelMemberInviteCard
canAssignElevatedRoles={canManageMembers}
existingMembers={rawMembers}
isPending={addMembersMutation.isPending}
onSubmit={(input) => addMembersMutation.mutateAsync(input)}
+25
View File
@@ -1055,6 +1055,31 @@ const mockChannels: MockChannel[] = [
createMockMember(MOCK_IDENTITY_PUBKEY, "guest", 700),
],
}),
createMockChannel({
id: "3c2d9f0a-1b44-5e77-9a21-6f8b0c4d2e91",
name: "secret-projects",
channel_type: "stream",
visibility: "private",
description: "Private project room",
topic: "Skunkworks",
purpose: "Coordinate confidential project work.",
last_message_at: null,
archived_at: null,
created_by: ALICE_PUBKEY,
topic_set_by: ALICE_PUBKEY,
topic_set_at: isoMinutesAgo(120),
purpose_set_by: ALICE_PUBKEY,
purpose_set_at: isoMinutesAgo(130),
topic_required: false,
max_members: null,
nip29_group_id: null,
created_minutes_ago: 600,
updated_minutes_ago: 120,
members: [
createMockMember(ALICE_PUBKEY, "owner", 600),
createMockMember(MOCK_IDENTITY_PUBKEY, "member", 540),
],
}),
createMockChannel({
id: "f48efb06-0c93-5025-aac9-2e646bb6bfa8",
name: "alice-tyler",
+28
View File
@@ -872,6 +872,34 @@ test("open-channel members can add agents from the header", async ({
await expect(page.getByTestId("add-channel-bot-dialog-footer")).toBeVisible();
});
test("private-channel members can add members and bots without admin", async ({
page,
}) => {
await page.goto("/");
// secret-projects is a private (non-DM) channel where the current user is a
// plain member, not owner/admin. They should still be able to add members
// and bots — only granting elevated roles is reserved for owners/admins.
await openMembersSidebar(page, "secret-projects");
// The invite card is shown to any member, not just owners/admins.
await expect(
page.getByTestId("channel-management-search-users"),
).toBeVisible();
await expect(
page.getByTestId("channel-management-add-members"),
).toBeVisible();
// The role dropdown hides the elevated "admin" option for non-admins — the
// relay rejects it anyway — while keeping the non-elevated roles a member
// may grant (member, guest, bot).
const roleSelect = page.getByTestId("channel-management-add-role");
await expect(roleSelect.locator("option")).toHaveText([
"member",
"guest",
"bot",
]);
});
test("removing a channel-scoped agent preserves the managed agent record", async ({
page,
}) => {