diff --git a/src/screens/Messages/ConversationSettings.tsx b/src/screens/Messages/ConversationSettings.tsx index 892e2e2c45..c926afb62d 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 {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' import {useLeaveConvo} from '#/state/queries/messages/leave-conversation' @@ -717,11 +718,26 @@ function SettingsHeader({ const convoState = useConvo() const {currentAccount} = useSession() + const groupName = convoState.getGroupInfo?.()?.name ?? '' + const [newGroupName, setNewGroupName] = useState(groupName) + + const [isLocked, setIsLocked] = useState(false) + const isOwner = currentAccount?.did == null ? false : convoState.getPrimaryMember?.()?.did === currentAccount.did + const {mutate: editGroupName} = useEditGroupName(convo.id, { + onError: e => { + setNewGroupName(groupName) + logger.error('Failed to edit group chat name', {message: e}) + Toast.show(l`Failed to edit group chat name`, { + type: 'error', + }) + }, + }) + const {mutate: muteConvo} = useMuteConvo(convo.id, { onSuccess: data => { if (data.convo.muted) { @@ -755,13 +771,6 @@ function SettingsHeader({ const lockChatPrompt = Prompt.usePromptControl() const leaveChatPrompt = Prompt.usePromptControl() - const [groupName, setGroupName] = useState( - convoState.getGroupInfo?.()?.name ?? '', - ) - const [newGroupName, setNewGroupName] = useState(groupName) - - const [isLocked, setIsLocked] = useState(false) - const handleToggleMute = () => { muteConvo({mute: !convo?.muted}) } @@ -777,7 +786,7 @@ function SettingsHeader({ } const handleEditName = () => { - setGroupName(newGroupName) + editGroupName({name: newGroupName}) editNamePrompt.close() } @@ -1045,6 +1054,7 @@ function EditNamePrompt({ autoCapitalize="none" autoComplete="off" autoCorrect={false} + autoFocus onSubmitEditing={onConfirm} /> diff --git a/src/state/messages/convo/agent.ts b/src/state/messages/convo/agent.ts index 9f0693c4a3..f89155bec4 100644 --- a/src/state/messages/convo/agent.ts +++ b/src/state/messages/convo/agent.ts @@ -116,6 +116,7 @@ export class Convo { this.isGroup = this.isGroup.bind(this) this.getGroupInfo = this.getGroupInfo.bind(this) this.getPrimaryMember = this.getPrimaryMember.bind(this) + this.updateGroupName = this.updateGroupName.bind(this) } private commit() { @@ -655,7 +656,7 @@ export class Convo { const nextCursor = this.oldestRev // for TS const response = await networkRetry(2, () => { - return this.agent.api.chat.bsky.convo.getMessages( + return this.agent.chat.bsky.convo.getMessages( { cursor: nextCursor, convoId: this.convoId, @@ -889,6 +890,25 @@ export class Convo { this.commit() } + updateGroupName(name: string) { + if ( + this.convo && + bsky.dangerousIsType( + this.convo.kind, + ChatBskyConvoDefs.isGroupConvo, + ) + ) { + this.convo = { + ...this.convo, + kind: { + ...this.convo.kind, + name, + }, + } + } + this.commit() + } + async processPendingMessages() { logger.debug( `processing messages (${this.pendingMessages.size} remaining)`, @@ -1388,14 +1408,14 @@ export class Convo { getPrimaryMember(): ChatBskyActorDefs.ProfileViewBasic | undefined { if (this.isGroup()) { - return this.recipients?.find(r => { + return this.convo?.members.find(m => { if ( bsky.dangerousIsType( - r.kind, + m.kind, ChatBskyActorDefs.isGroupConvoMember, ) ) { - return r.kind.role === 'owner' + return m.kind.role === 'owner' } else { throw new Error( 'Expected a GroupConvoMember, got an unknown kind of member', diff --git a/src/state/messages/convo/index.tsx b/src/state/messages/convo/index.tsx index 4117aef2ed..5461301fb3 100644 --- a/src/state/messages/convo/index.tsx +++ b/src/state/messages/convo/index.tsx @@ -6,7 +6,7 @@ import { useState, useSyncExternalStore, } from 'react' -import {type ChatBskyConvoDefs} from '@atproto/api' +import {ChatBskyConvoDefs} from '@atproto/api' import {useFocusEffect} from '@react-navigation/native' import {useQueryClient} from '@tanstack/react-query' @@ -130,6 +130,15 @@ export function ConvoProvider({ if (data && convo.convo && data.muted !== convo.convo.muted) { convo.updateMuted(data.muted) } + if ( + data && + convo.convo && + ChatBskyConvoDefs.isGroupConvo(data.kind) && + ChatBskyConvoDefs.isGroupConvo(convo.convo.kind) && + data.kind.name !== convo.convo.kind.name + ) { + convo.updateGroupName(data.kind.name) + } } }) }, [convo, convoId, queryClient]) diff --git a/src/state/queries/messages/edit-group-name.ts b/src/state/queries/messages/edit-group-name.ts new file mode 100644 index 0000000000..cbff0331ed --- /dev/null +++ b/src/state/queries/messages/edit-group-name.ts @@ -0,0 +1,108 @@ +import { + ChatBskyConvoDefs, + type ChatBskyConvoListConvos, + type ChatBskyGroupEditGroup, +} 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 useEditGroupName( + convoId: string | undefined, + { + onSuccess, + onError, + }: { + onSuccess?: (data: ChatBskyGroupEditGroup.OutputSchema) => void + onError?: (error: Error) => void + }, +) { + const queryClient = useQueryClient() + const agent = useAgent() + + return useMutation({ + mutationFn: async ({name: groupName}: {name: string}) => { + if (!convoId) throw new Error('No convoId provided') + const {data} = await agent.chat.bsky.group.editGroup( + {convoId, name: groupName}, + {headers: DM_SERVICE_HEADERS, encoding: 'application/json'}, + ) + return data + }, + onMutate: ({name: groupName}) => { + if (!convoId) return + + const prevConvo = queryClient.getQueryData( + CONVO_KEY(convoId), + ) + const prevListEntries = queryClient.getQueriesData< + InfiniteData + >({queryKey: [CONVO_LIST_KEY]}) + + // Update for a single chat thread + queryClient.setQueryData( + CONVO_KEY(convoId), + prev => { + if (!prev) return + if (!ChatBskyConvoDefs.isGroupConvo(prev.kind)) return prev + return { + ...prev, + kind: { + ...prev.kind, + name: groupName, + }, + } + }, + ) + + // Update for the chat list + 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 + if (!ChatBskyConvoDefs.isGroupConvo(convo.kind)) return convo + return { + ...convo, + kind: { + ...convo.kind, + name: groupName, + }, + } + }), + })), + } + }) + + 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) + }, + }) +}