Add ability to add members to an existing group clip clops (#10344)

This commit is contained in:
DS Boyce
2026-04-23 12:09:43 -07:00
committed by Eric Bailey
parent 9762a6eef4
commit c49e5a566c
5 changed files with 155 additions and 17 deletions
+24 -15
View File
@@ -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<bsky.profile.AnyProfileView>
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 <AddMembersLink isOwner={isOwner} />
return <AddMembersLink convo={convo} isOwner={isOwner} />
case 'CHAT_MEMBER':
return (
<Member
@@ -292,12 +287,26 @@ function MembersAndRequests({
)
}
function AddMembersLink({isOwner}: {isOwner: boolean}) {
function AddMembersLink({
convo,
isOwner,
}: {
convo: ConvoWithDetails | null
isOwner: boolean
}) {
const t = useTheme()
const {t: l} = useLingui()
const addMembersControl = Dialog.useDialogControl()
const convoId = convo?.view.id
const {mutate: addGroupMembers} = useAddGroupMembers(convoId, {
onError: e => {
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}) {
<Dialog.Handle />
<AddMembersFlow
title={l`Add members`}
onAddMembers={(_dids: string[]) => {
// TODO Add members here
onAddMembers={members => {
addGroupMembers({members})
addMembersControl.close()
}}
/>
@@ -387,7 +396,7 @@ function Member({
isOwner,
}: {
profile: Shadow<bsky.profile.AnyProfileView>
status: 'owner' | 'member' | 'invited'
status: 'owner' | 'standard' | 'invited'
isOwner: boolean
}) {
const navigation = useNavigation<NavigationProp>()
@@ -533,7 +542,7 @@ function MemberMenu({
isOwner,
}: {
profile: Shadow<bsky.profile.AnyProfileView>
type: 'owner' | 'member' | 'invited'
type: 'owner' | 'standard' | 'invited'
isOwner: boolean
}) {
const navigation = useNavigation<NavigationProp>()
@@ -688,7 +697,7 @@ function MemberMenu({
</Menu.Group>
<Menu.Divider />
<Menu.Group>
{type === 'owner' || type === 'member' ? (
{type === 'owner' || type === 'standard' ? (
<Menu.Item
label={
profile.viewer?.blocking
@@ -1,7 +1,9 @@
import {View} from 'react-native'
import {Plural, Trans, useLingui} from '@lingui/react/macro'
import {logger} from '#/logger'
import {type ConvoState} from '#/state/messages/convo/types'
import {useAddGroupMembers} from '#/state/queries/messages/add-group-members'
import {useSession} from '#/state/session'
import {atoms as a, useTheme} from '#/alf'
import {AvatarBubbles} from '#/components/AvatarBubbles'
@@ -10,6 +12,7 @@ import * as Dialog from '#/components/Dialog'
import {AddMembersFlow} from '#/components/dms/AddMembersFlow'
import {ChainLink_Stroke2_Corner0_Rounded as ChainLinkIcon} from '#/components/icons/ChainLink'
import {PersonPlus_Stroke2_Corner0_Rounded as PersonPlusIcon} from '#/components/icons/Person'
import * as Toast from '#/components/Toast'
import {Text} from '#/components/Typography'
export function MessagesListInfoPanel({convoState}: {convoState: ConvoState}) {
@@ -20,6 +23,14 @@ export function MessagesListInfoPanel({convoState}: {convoState: ConvoState}) {
const {currentAccount} = useSession()
const convoId = convoState.convo?.id
const {mutate: addGroupMembers} = useAddGroupMembers(convoId, {
onError: e => {
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}) {
<Dialog.Handle />
<AddMembersFlow
title={l`Add people`}
onAddMembers={(_dids: string[]) => {
// TODO Add members here
onAddMembers={(members: string[]) => {
addGroupMembers({members})
addMembersControl.close()
}}
/>
+13
View File
@@ -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)`,
+16
View File
@@ -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<ConvoState | null>(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])
@@ -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<ChatBskyConvoDefs.ConvoView>(
CONVO_KEY(convoId),
)
const prevListEntries = queryClient.getQueriesData<
InfiniteData<ChatBskyConvoListConvos.OutputSchema>
>({queryKey: [CONVO_LIST_KEY]})
return {prevConvo, prevListEntries}
},
onSuccess: data => {
if (convoId) {
queryClient.setQueryData<ChatBskyConvoDefs.ConvoView>(
CONVO_KEY(convoId),
data.convo,
)
queryClient.setQueriesData<
InfiniteData<ChatBskyConvoListConvos.OutputSchema>
>({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)
},
})
}