diff --git a/desktop/src-tauri/src/commands/channels.rs b/desktop/src-tauri/src/commands/channels.rs
index 0a2ab77bee5..2844fd1f565 100644
--- a/desktop/src-tauri/src/commands/channels.rs
+++ b/desktop/src-tauri/src/commands/channels.rs
@@ -195,6 +195,26 @@ pub async fn remove_channel_member(
Ok(())
}
+#[tauri::command]
+pub async fn change_channel_member_role(
+ channel_id: String,
+ pubkey: String,
+ role: String,
+ state: State<'_, AppState>,
+) -> Result<(), String> {
+ let uuid = parse_channel_uuid(&channel_id)?;
+ // Only allow permission-tier roles for humans and bot/guest for bots.
+ // Owner changes require a dedicated transfer-ownership flow.
+ let role_str = match role.as_str() {
+ "admin" | "member" | "guest" | "bot" => role.as_str(),
+ "owner" => return Err("cannot assign owner role — use transfer ownership".into()),
+ other => return Err(format!("invalid role: {other}")),
+ };
+ let builder = events::build_add_member(uuid, &pubkey, Some(role_str))?;
+ submit_event(builder, &state).await?;
+ Ok(())
+}
+
#[tauri::command]
pub async fn join_channel(channel_id: String, state: State<'_, AppState>) -> Result<(), String> {
let uuid = parse_channel_uuid(&channel_id)?;
diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs
index f26ab2d81e4..095824c2d48 100644
--- a/desktop/src-tauri/src/lib.rs
+++ b/desktop/src-tauri/src/lib.rs
@@ -437,6 +437,7 @@ pub fn run() {
delete_channel,
add_channel_members,
remove_channel_member,
+ change_channel_member_role,
join_channel,
leave_channel,
get_canvas,
diff --git a/desktop/src/features/channels/ui/MembersSidebar.tsx b/desktop/src/features/channels/ui/MembersSidebar.tsx
index 06c41953024..a1255f127ac 100644
--- a/desktop/src/features/channels/ui/MembersSidebar.tsx
+++ b/desktop/src/features/channels/ui/MembersSidebar.tsx
@@ -1,4 +1,5 @@
import * as React from "react";
+import { useMutation, useQueryClient } from "@tanstack/react-query";
import {
useAddChannelMembersMutation,
useChannelMembersQuery,
@@ -7,6 +8,7 @@ import { useClassifiedMembers } from "@/features/channels/lib/useClassifiedMembe
import { formatMemberName } from "@/features/channels/lib/memberUtils";
import { useUsersBatchQuery } from "@/features/profile/hooks";
import { usePresenceQuery } from "@/features/presence/hooks";
+import { changeChannelMemberRole } from "@/shared/api/tauri";
import type { Channel, ChannelMember } from "@/shared/api/types";
import { normalizePubkey } from "@/shared/lib/pubkey";
import {
@@ -35,8 +37,24 @@ export function MembersSidebar({
onOpenChange,
}: MembersSidebarProps) {
const channelId = channel?.id ?? null;
+ const queryClient = useQueryClient();
const membersQuery = useChannelMembersQuery(channelId, open);
const addMembersMutation = useAddChannelMembersMutation(channelId);
+ const changeRoleMutation = useMutation({
+ mutationFn: async ({ pubkey, role }: { pubkey: string; role: string }) => {
+ if (!channelId) throw new Error("No channel selected.");
+ await changeChannelMemberRole(channelId, pubkey, role);
+ },
+ onSettled: async () => {
+ await queryClient.invalidateQueries({
+ queryKey: ["channels", channelId],
+ });
+ },
+ });
+ const changeRoleError =
+ changeRoleMutation.error instanceof Error
+ ? changeRoleMutation.error.message
+ : null;
const rawMembers = membersQuery.data ?? [];
const { people, bots, isBot, isMyBot, managedAgentsQuery } =
@@ -127,8 +145,9 @@ export function MembersSidebar({
function renderMemberCard(member: ChannelMember, memberIsBot: boolean) {
return (
- {actionErrorMessage} + {actionErrorMessage ?? changeRoleError}
) : null} diff --git a/desktop/src/features/channels/ui/MembersSidebarMemberCard.tsx b/desktop/src/features/channels/ui/MembersSidebarMemberCard.tsx index 8cac47e4893..8b5625ea077 100644 --- a/desktop/src/features/channels/ui/MembersSidebarMemberCard.tsx +++ b/desktop/src/features/channels/ui/MembersSidebarMemberCard.tsx @@ -1,4 +1,11 @@ -import { Ellipsis, Play, RotateCcw, Square, Trash2 } from "lucide-react"; +import { + Ellipsis, + Play, + RotateCcw, + Shield, + Square, + Trash2, +} from "lucide-react"; import { getManagedAgentPrimaryActionLabel, @@ -17,10 +24,14 @@ import { DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, DropdownMenuTrigger, } from "@/shared/ui/dropdown-menu"; type MembersSidebarMemberCardProps = { + canChangeRole: boolean; canRemoveMember: boolean; isActionPending: boolean; isArchived: boolean; @@ -28,6 +39,7 @@ type MembersSidebarMemberCardProps = { member: ChannelMember; memberIsBot: boolean; memberLabel: string; + onChangeRole: (member: ChannelMember, role: string) => void; onManagedAgentAction: (agent: ManagedAgent) => void; onRemoveMember: (member: ChannelMember) => void; presenceStatus?: PresenceStatus | null; @@ -56,6 +68,7 @@ function formatManagedAgentStatus(agent: ManagedAgent) { } export function MembersSidebarMemberCard({ + canChangeRole, canRemoveMember, isActionPending, isArchived, @@ -63,6 +76,7 @@ export function MembersSidebarMemberCard({ member, memberIsBot, memberLabel, + onChangeRole, onManagedAgentAction, onRemoveMember, presenceStatus, @@ -72,7 +86,7 @@ export function MembersSidebarMemberCard({ const disabled = isActionPending || isArchived; const hasActions = memberIsBot ? Boolean(managedAgent) || canRemoveMember - : canRemoveMember; + : canRemoveMember || canChangeRole; return (