From 5039d80a7eb805a093b73eb0f7161410128a5c40 Mon Sep 17 00:00:00 2001 From: DS Boyce <260543580+ds-boyce@users.noreply.github.com> Date: Thu, 23 Apr 2026 12:30:03 -0700 Subject: [PATCH] Add ability to remove members from a group clip clop (#10287) --- src/screens/Messages/ConversationSettings.tsx | 30 ++++-- src/state/messages/convo/index.tsx | 4 +- ...-group-name.ts => edit-group-chat-name.ts} | 2 +- .../queries/messages/remove-from-group.ts | 98 +++++++++++++++++++ 4 files changed, 125 insertions(+), 9 deletions(-) rename src/state/queries/messages/{edit-group-name.ts => edit-group-chat-name.ts} (98%) create mode 100644 src/state/queries/messages/remove-from-group.ts diff --git a/src/screens/Messages/ConversationSettings.tsx b/src/screens/Messages/ConversationSettings.tsx index 0f76696ee8..972a0f2543 100644 --- a/src/screens/Messages/ConversationSettings.tsx +++ b/src/screens/Messages/ConversationSettings.tsx @@ -21,12 +21,13 @@ import {ConvoProvider, useConvo} from '#/state/messages/convo' import {ConvoStatus} from '#/state/messages/convo/types' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useAddGroupMembers} from '#/state/queries/messages/add-group-members' -import {useEditGroupName} from '#/state/queries/messages/edit-group-name' +import {useEditGroupChatName} from '#/state/queries/messages/edit-group-chat-name' import {useGetConvoAvailabilityQuery} from '#/state/queries/messages/get-convo-availability' import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members' import {useLeaveConvo} from '#/state/queries/messages/leave-conversation' import {useListJoinRequestsQuery} from '#/state/queries/messages/list-join-requests' import {useMuteConvo} from '#/state/queries/messages/mute-conversation' +import {useRemoveFromGroupChat} from '#/state/queries/messages/remove-from-group' import {useProfileBlockMutationQueue} from '#/state/queries/profile' import {useSession} from '#/state/session' import {List} from '#/view/com/util/List' @@ -189,6 +190,7 @@ function SettingsInner({convoId}: {convoId: string}) { case 'CHAT_MEMBER': return ( status: 'owner' | 'standard' | 'invited' isOwner: boolean @@ -429,9 +433,14 @@ function Member({ break } } else { - statusBadge = ( - - ) + statusBadge = convo ? ( + + ) : null } return ( @@ -537,10 +546,12 @@ function StatusButton({ } function MemberMenu({ + convo, profile, type, isOwner, }: { + convo: ConvoWithDetails profile: Shadow type: 'owner' | 'standard' | 'invited' isOwner: boolean @@ -564,6 +575,13 @@ function MemberMenu({ Toast.show(l`Failed to create conversation`) }, }) + const convoId = convo?.view.id + const {mutate: removeMembers} = useRemoveFromGroupChat(convoId, { + onError: e => { + logger.error('Failed to remove group chat member', {message: e}) + Toast.show(l`Failed to remove group chat member`) + }, + }) const [queueBlock, queueUnblock] = useProfileBlockMutationQueue(profile) const messageMember = () => { @@ -714,7 +732,7 @@ function MemberMenu({ {isOwner ? ( {}}> + onPress={() => removeMembers({members: [profile.did]})}> Remove from chat @@ -759,7 +777,7 @@ function SettingsHeader({ const [isLocked, setIsLocked] = useState(false) - const {mutate: editGroupName} = useEditGroupName(convo.view.id, { + const {mutate: editGroupName} = useEditGroupChatName(convo.view.id, { onError: e => { setNewGroupName(groupName) logger.error('Failed to edit group chat name', {message: e}) diff --git a/src/state/messages/convo/index.tsx b/src/state/messages/convo/index.tsx index 458c1eb65b..5402ab787e 100644 --- a/src/state/messages/convo/index.tsx +++ b/src/state/messages/convo/index.tsx @@ -116,11 +116,11 @@ export function ConvoProvider({ switch (event.type) { case 'invalidate-block-state': { for (const did of event.accountDids) { - queryClient.invalidateQueries({ + void queryClient.invalidateQueries({ queryKey: createProfileQueryKey(did), }) } - queryClient.invalidateQueries({ + void queryClient.invalidateQueries({ queryKey: [ListConvosQueryKeyRoot], }) } diff --git a/src/state/queries/messages/edit-group-name.ts b/src/state/queries/messages/edit-group-chat-name.ts similarity index 98% rename from src/state/queries/messages/edit-group-name.ts rename to src/state/queries/messages/edit-group-chat-name.ts index cbff0331ed..b605c999cc 100644 --- a/src/state/queries/messages/edit-group-name.ts +++ b/src/state/queries/messages/edit-group-chat-name.ts @@ -15,7 +15,7 @@ import {useAgent} from '#/state/session' import {RQKEY as CONVO_KEY} from './conversation' import {RQKEY_ROOT as CONVO_LIST_KEY} from './list-conversations' -export function useEditGroupName( +export function useEditGroupChatName( convoId: string | undefined, { onSuccess, diff --git a/src/state/queries/messages/remove-from-group.ts b/src/state/queries/messages/remove-from-group.ts new file mode 100644 index 0000000000..0c047f0249 --- /dev/null +++ b/src/state/queries/messages/remove-from-group.ts @@ -0,0 +1,98 @@ +import { + type ChatBskyConvoDefs, + type ChatBskyConvoListConvos, + type ChatBskyGroupRemoveMembers, +} from '@atproto/api' +import { + type InfiniteData, + useMutation, + useQueryClient, +} from '@tanstack/react-query' + +import {DM_SERVICE_HEADERS} from '#/lib/constants' +import {logger} from '#/logger' +import {useAgent} from '#/state/session' +import {RQKEY as CONVO_KEY} from './conversation' +import {RQKEY_ROOT as CONVO_LIST_KEY} from './list-conversations' + +export function useRemoveFromGroupChat( + convoId: string | undefined, + { + onSuccess, + onError, + }: { + onSuccess?: (data: ChatBskyGroupRemoveMembers.OutputSchema) => void + onError?: (error: Error) => void + }, +) { + const queryClient = useQueryClient() + const agent = useAgent() + + return useMutation({ + mutationFn: async ({members}: {members: string[]}) => { + if (!convoId) throw new Error('No convoId provided') + const {data} = await agent.chat.bsky.group.removeMembers( + {convoId, members}, + {headers: DM_SERVICE_HEADERS, encoding: 'application/json'}, + ) + return data + }, + onMutate: ({members}) => { + if (!convoId) return + + const prevConvo = queryClient.getQueryData( + CONVO_KEY(convoId), + ) + const prevListEntries = queryClient.getQueriesData< + InfiniteData + >({queryKey: [CONVO_LIST_KEY]}) + + queryClient.setQueryData( + CONVO_KEY(convoId), + prev => { + if (!prev) return + return { + ...prev, + members: prev.members.filter(m => !members.includes(m.did)), + } + }, + ) + + queryClient.setQueriesData< + InfiniteData + >({queryKey: [CONVO_LIST_KEY]}, prev => { + if (!prev?.pages) return + return { + ...prev, + pages: prev.pages.map(page => ({ + ...page, + convos: page.convos.map(convo => { + if (convo.id !== convoId) return convo + return { + ...convo, + members: convo.members.filter(m => !members.includes(m.did)), + } + }), + })), + } + }) + + return {prevConvo, prevListEntries} + }, + onSuccess: data => { + onSuccess?.(data) + }, + onError: (e, _variables, context) => { + logger.error(e) + if (context?.prevConvo && convoId) { + queryClient.setQueryData(CONVO_KEY(convoId), context.prevConvo) + } + if (context?.prevListEntries) { + for (const [key, data] of context.prevListEntries) { + queryClient.setQueryData(key, data) + } + } + onError?.(e) + }, + }) +}