From c49e5a566c48e8e9292609327920f621b6b18c0f Mon Sep 17 00:00:00 2001 From: DS Boyce <260543580+ds-boyce@users.noreply.github.com> Date: Thu, 23 Apr 2026 12:09:43 -0700 Subject: [PATCH] Add ability to add members to an existing group clip clops (#10344) --- src/screens/Messages/ConversationSettings.tsx | 39 ++++---- .../components/MessagesListInfoPanel.tsx | 15 +++- src/state/messages/convo/agent.ts | 13 +++ src/state/messages/convo/index.tsx | 16 ++++ .../queries/messages/add-group-members.ts | 89 +++++++++++++++++++ 5 files changed, 155 insertions(+), 17 deletions(-) create mode 100644 src/state/queries/messages/add-group-members.ts diff --git a/src/screens/Messages/ConversationSettings.tsx b/src/screens/Messages/ConversationSettings.tsx index b5384b6c87..0f76696ee8 100644 --- a/src/screens/Messages/ConversationSettings.tsx +++ b/src/screens/Messages/ConversationSettings.tsx @@ -20,6 +20,7 @@ import {type Shadow} from '#/state/cache/types' 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 {useGetConvoAvailabilityQuery} from '#/state/queries/messages/get-convo-availability' import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members' @@ -82,7 +83,7 @@ type Item = | { type: 'CHAT_MEMBER' profile: Shadow - status: 'owner' | 'member' | 'invited' + status: 'owner' | 'standard' | 'invited' } type Props = NativeStackScreenProps< @@ -136,7 +137,6 @@ function SettingsInner({convoId}: {convoId: string}) { const isOwner = !!primaryMember && primaryMember.did === currentAccount?.did const data: bsky.profile.AnyProfileView[] = convo?.members ?? [] - const invites: string[] = [] const {data: joinRequestsData, hasNextPage: hasMoreRequests} = useListJoinRequestsQuery({ @@ -169,12 +169,7 @@ function SettingsInner({convoId}: {convoId: string}) { .map(profile => ({ type: 'CHAT_MEMBER', profile, - status: - primaryMember?.did === profile.did - ? 'owner' - : invites.includes(profile.did) - ? 'invited' - : 'member', + status: primaryMember?.did === profile.did ? 'owner' : 'standard', })), ] @@ -190,7 +185,7 @@ function SettingsInner({convoId}: {convoId: string}) { /> ) case 'ADD_MEMBERS_LINK': - return + return case 'CHAT_MEMBER': return ( { + logger.error('Failed to add group chat members', {message: e}) + Toast.show(l`Failed to add members`, {type: 'error'}) + }, + }) + if (!isOwner) { return null } @@ -371,8 +380,8 @@ function AddMembersLink({isOwner}: {isOwner: boolean}) { { - // TODO Add members here + onAddMembers={members => { + addGroupMembers({members}) addMembersControl.close() }} /> @@ -387,7 +396,7 @@ function Member({ isOwner, }: { profile: Shadow - status: 'owner' | 'member' | 'invited' + status: 'owner' | 'standard' | 'invited' isOwner: boolean }) { const navigation = useNavigation() @@ -533,7 +542,7 @@ function MemberMenu({ isOwner, }: { profile: Shadow - type: 'owner' | 'member' | 'invited' + type: 'owner' | 'standard' | 'invited' isOwner: boolean }) { const navigation = useNavigation() @@ -688,7 +697,7 @@ function MemberMenu({ - {type === 'owner' || type === 'member' ? ( + {type === 'owner' || type === 'standard' ? ( { + logger.error('Failed to add group chat members', {message: e}) + Toast.show(l`Failed to add members`, {type: 'error'}) + }, + }) + const isOwner = currentAccount?.did == null ? false @@ -124,8 +135,8 @@ export function MessagesListInfoPanel({convoState}: {convoState: ConvoState}) { { - // TODO Add members here + onAddMembers={(members: string[]) => { + addGroupMembers({members}) addMembersControl.close() }} /> diff --git a/src/state/messages/convo/agent.ts b/src/state/messages/convo/agent.ts index 3552de39e6..891564ff7e 100644 --- a/src/state/messages/convo/agent.ts +++ b/src/state/messages/convo/agent.ts @@ -145,6 +145,7 @@ export class Convo { this.getGroupInfo = this.getGroupInfo.bind(this) this.getPrimaryMember = this.getPrimaryMember.bind(this) this.updateGroupName = this.updateGroupName.bind(this) + this.updateGroupMembers = this.updateGroupMembers.bind(this) } private commit() { @@ -959,6 +960,18 @@ export class Convo { this.commit() } + updateGroupMembers(members: ChatBskyActorDefs.ProfileViewBasic[]) { + if (this.convo) { + this.convo = { + ...this.convo, + members, + } + this.sender = members.find(m => m.did === this.senderUserDid) + this.recipients = members.filter(m => m.did !== this.senderUserDid) + } + this.commit() + } + async processPendingMessages() { logger.debug( `processing messages (${this.pendingMessages.size} remaining)`, diff --git a/src/state/messages/convo/index.tsx b/src/state/messages/convo/index.tsx index 5461301fb3..458c1eb65b 100644 --- a/src/state/messages/convo/index.tsx +++ b/src/state/messages/convo/index.tsx @@ -32,6 +32,15 @@ import {useAgent} from '#/state/session' export * from '#/state/messages/convo/util' +function membersChanged( + a: ChatBskyConvoDefs.ConvoView['members'], + b: ChatBskyConvoDefs.ConvoView['members'], +) { + if (a.length !== b.length) return true + const aDids = new Set(a.map(m => m.did)) + return b.some(m => !aDids.has(m.did)) +} + const ChatContext = createContext(null) ChatContext.displayName = 'ChatContext' @@ -139,6 +148,13 @@ export function ConvoProvider({ ) { convo.updateGroupName(data.kind.name) } + if ( + data && + convo.convo && + membersChanged(data.members, convo.convo.members) + ) { + convo.updateGroupMembers(data.members) + } } }) }, [convo, convoId, queryClient]) diff --git a/src/state/queries/messages/add-group-members.ts b/src/state/queries/messages/add-group-members.ts new file mode 100644 index 0000000000..a1eb189040 --- /dev/null +++ b/src/state/queries/messages/add-group-members.ts @@ -0,0 +1,89 @@ +import { + type ChatBskyConvoDefs, + type ChatBskyConvoListConvos, + type ChatBskyGroupAddMembers, +} 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 useAddGroupMembers( + convoId: string | undefined, + { + onSuccess, + onError, + }: { + onSuccess?: (data: ChatBskyGroupAddMembers.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.addMembers( + {convoId, members}, + {headers: DM_SERVICE_HEADERS, encoding: 'application/json'}, + ) + return data + }, + onMutate: () => { + if (!convoId) return + + const prevConvo = queryClient.getQueryData( + CONVO_KEY(convoId), + ) + const prevListEntries = queryClient.getQueriesData< + InfiniteData + >({queryKey: [CONVO_LIST_KEY]}) + + return {prevConvo, prevListEntries} + }, + onSuccess: data => { + if (convoId) { + queryClient.setQueryData( + CONVO_KEY(convoId), + data.convo, + ) + + 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 => + convo.id === convoId ? data.convo : convo, + ), + })), + } + }) + } + 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) + }, + }) +}