Group Clops Feature Branch (#10360)
Co-authored-by: DS Boyce <260543580+ds-boyce@users.noreply.github.com> Co-authored-by: Samuel Newman <mozzius@protonmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
import {
|
||||
type ChatBskyActorDefs,
|
||||
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 {useProfileQuery} from '#/state/queries/profile'
|
||||
import {useAgent, useSession} from '#/state/session'
|
||||
import type * as bsky from '#/types/bsky'
|
||||
import {RQKEY as CONVO_KEY} from './conversation'
|
||||
import {RQKEY_ROOT as CONVO_LIST_KEY} from './list-conversations'
|
||||
import {listConvoMembersQueryKey} from './list-convo-members'
|
||||
|
||||
export function useAddGroupMembers(
|
||||
convoId: string | undefined,
|
||||
{
|
||||
onSuccess,
|
||||
onError,
|
||||
}: {
|
||||
onSuccess?: (data: ChatBskyGroupAddMembers.OutputSchema) => void
|
||||
onError?: (error: Error) => void
|
||||
},
|
||||
) {
|
||||
const queryClient = useQueryClient()
|
||||
const agent = useAgent()
|
||||
const {currentAccount} = useSession()
|
||||
const {data: myProfile} = useProfileQuery({did: currentAccount?.did})
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
members,
|
||||
}: {
|
||||
members: string[]
|
||||
profiles: bsky.profile.AnyProfileView[]
|
||||
}) => {
|
||||
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: ({profiles}) => {
|
||||
if (!convoId) return
|
||||
|
||||
const prevConvo = queryClient.getQueryData<ChatBskyConvoDefs.ConvoView>(
|
||||
CONVO_KEY(convoId),
|
||||
)
|
||||
const prevListEntries = queryClient.getQueriesData<
|
||||
InfiniteData<ChatBskyConvoListConvos.OutputSchema>
|
||||
>({queryKey: [CONVO_LIST_KEY]})
|
||||
const prevMemberList = queryClient.getQueryData<
|
||||
ChatBskyActorDefs.ProfileViewBasic[]
|
||||
>(listConvoMembersQueryKey(convoId))
|
||||
|
||||
const addedBy: ChatBskyActorDefs.ProfileViewBasic | undefined = myProfile
|
||||
? {
|
||||
...myProfile,
|
||||
$type: 'chat.bsky.actor.defs#profileViewBasic',
|
||||
}
|
||||
: undefined
|
||||
|
||||
const optimisticMembers: ChatBskyActorDefs.ProfileViewBasic[] =
|
||||
profiles.map(profile => ({
|
||||
...profile,
|
||||
$type: 'chat.bsky.actor.defs#profileViewBasic',
|
||||
kind: {
|
||||
$type: 'chat.bsky.actor.defs#groupConvoMember',
|
||||
role: 'standard',
|
||||
addedBy,
|
||||
},
|
||||
}))
|
||||
|
||||
queryClient.setQueryData<ChatBskyConvoDefs.ConvoView>(
|
||||
CONVO_KEY(convoId),
|
||||
prev => {
|
||||
if (!prev) return
|
||||
if (!ChatBskyConvoDefs.isGroupConvo(prev.kind)) return prev
|
||||
return {
|
||||
...prev,
|
||||
members: [...prev.members, ...optimisticMembers],
|
||||
kind: {
|
||||
...prev.kind,
|
||||
memberCount: prev.kind.memberCount + optimisticMembers.length,
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
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 => {
|
||||
if (convo.id !== convoId) return convo
|
||||
if (!ChatBskyConvoDefs.isGroupConvo(convo.kind)) return convo
|
||||
return {
|
||||
...convo,
|
||||
members: [...convo.members, ...optimisticMembers],
|
||||
kind: {
|
||||
...convo.kind,
|
||||
memberCount:
|
||||
convo.kind.memberCount + optimisticMembers.length,
|
||||
},
|
||||
}
|
||||
}),
|
||||
})),
|
||||
}
|
||||
})
|
||||
|
||||
queryClient.setQueryData<ChatBskyActorDefs.ProfileViewBasic[]>(
|
||||
listConvoMembersQueryKey(convoId),
|
||||
prev => {
|
||||
if (!prev) return
|
||||
return [...prev, ...optimisticMembers]
|
||||
},
|
||||
)
|
||||
|
||||
return {prevConvo, prevListEntries, prevMemberList}
|
||||
},
|
||||
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)
|
||||
}
|
||||
}
|
||||
if (context?.prevMemberList && convoId) {
|
||||
queryClient.setQueryData(
|
||||
listConvoMembersQueryKey(convoId),
|
||||
context.prevMemberList,
|
||||
)
|
||||
}
|
||||
onError?.(e)
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
RQKEY_ROOT as LIST_CONVOS_KEY,
|
||||
} from './list-conversations'
|
||||
|
||||
const RQKEY_ROOT = 'convo'
|
||||
export const RQKEY_ROOT = 'convo'
|
||||
export const RQKEY = (convoId: string) => [RQKEY_ROOT, convoId]
|
||||
|
||||
export function useConvoQuery({convoId}: {convoId: string}) {
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import {
|
||||
ChatBskyConvoDefs,
|
||||
type ChatBskyGroupCreateJoinLink,
|
||||
type ChatBskyGroupDefs,
|
||||
} from '@atproto/api'
|
||||
import {useMutation, useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {DM_SERVICE_HEADERS} from '#/lib/constants'
|
||||
import {logger} from '#/logger'
|
||||
import {useAgent} from '#/state/session'
|
||||
import {
|
||||
rollbackConvoOptimistic,
|
||||
updateConvoOptimistic,
|
||||
} from './utils/convo-cache'
|
||||
|
||||
export function useCreateJoinLink(
|
||||
convoId: string | undefined,
|
||||
{
|
||||
onSuccess,
|
||||
onError,
|
||||
}: {
|
||||
onSuccess?: (data: ChatBskyGroupCreateJoinLink.OutputSchema) => void
|
||||
onError?: (error: Error) => void
|
||||
},
|
||||
) {
|
||||
const queryClient = useQueryClient()
|
||||
const agent = useAgent()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
joinRule,
|
||||
requireApproval,
|
||||
}: {
|
||||
joinRule: ChatBskyGroupDefs.JoinRule
|
||||
requireApproval: boolean
|
||||
}) => {
|
||||
if (!convoId) throw new Error('No convoId provided')
|
||||
const {data} = await agent.chat.bsky.group.createJoinLink(
|
||||
{convoId, joinRule, requireApproval},
|
||||
{headers: DM_SERVICE_HEADERS, encoding: 'application/json'},
|
||||
)
|
||||
return data
|
||||
},
|
||||
onMutate: ({joinRule, requireApproval}) => {
|
||||
if (!convoId) return
|
||||
return updateConvoOptimistic(queryClient, convoId, prev => {
|
||||
if (!ChatBskyConvoDefs.isGroupConvo(prev.kind)) return undefined
|
||||
return {
|
||||
...prev,
|
||||
kind: {
|
||||
...prev.kind,
|
||||
joinLink: {
|
||||
$type: 'chat.bsky.group.defs#joinLinkView',
|
||||
code: '',
|
||||
enabledStatus: 'enabled',
|
||||
joinRule,
|
||||
requireApproval,
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
},
|
||||
onSuccess: data => {
|
||||
if (convoId) {
|
||||
updateConvoOptimistic(queryClient, convoId, prev => {
|
||||
if (!ChatBskyConvoDefs.isGroupConvo(prev.kind)) return undefined
|
||||
return {
|
||||
...prev,
|
||||
kind: {...prev.kind, joinLink: data.joinLink},
|
||||
}
|
||||
})
|
||||
}
|
||||
onSuccess?.(data)
|
||||
},
|
||||
onError: (e, _variables, context) => {
|
||||
logger.error(e)
|
||||
if (convoId && context) {
|
||||
rollbackConvoOptimistic(queryClient, convoId, context)
|
||||
}
|
||||
onError?.(e)
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import {
|
||||
ChatBskyConvoDefs,
|
||||
type ChatBskyGroupDisableJoinLink,
|
||||
} from '@atproto/api'
|
||||
import {useMutation, useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {DM_SERVICE_HEADERS} from '#/lib/constants'
|
||||
import {logger} from '#/logger'
|
||||
import {useAgent} from '#/state/session'
|
||||
import {
|
||||
rollbackConvoOptimistic,
|
||||
updateConvoOptimistic,
|
||||
} from './utils/convo-cache'
|
||||
|
||||
export function useDisableJoinLink(
|
||||
convoId: string | undefined,
|
||||
{
|
||||
onSuccess,
|
||||
onError,
|
||||
}: {
|
||||
onSuccess?: (data: ChatBskyGroupDisableJoinLink.OutputSchema) => void
|
||||
onError?: (error: Error) => void
|
||||
},
|
||||
) {
|
||||
const queryClient = useQueryClient()
|
||||
const agent = useAgent()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async () => {
|
||||
if (!convoId) throw new Error('No convoId provided')
|
||||
const {data} = await agent.chat.bsky.group.disableJoinLink(
|
||||
{convoId},
|
||||
{headers: DM_SERVICE_HEADERS, encoding: 'application/json'},
|
||||
)
|
||||
return data
|
||||
},
|
||||
onMutate: () => {
|
||||
if (!convoId) return
|
||||
return updateConvoOptimistic(queryClient, convoId, prev => {
|
||||
if (!ChatBskyConvoDefs.isGroupConvo(prev.kind) || !prev.kind.joinLink) {
|
||||
return undefined
|
||||
}
|
||||
return {
|
||||
...prev,
|
||||
kind: {
|
||||
...prev.kind,
|
||||
joinLink: {...prev.kind.joinLink, enabledStatus: 'disabled'},
|
||||
},
|
||||
}
|
||||
})
|
||||
},
|
||||
onSuccess: data => {
|
||||
if (convoId) {
|
||||
updateConvoOptimistic(queryClient, convoId, prev => {
|
||||
if (!ChatBskyConvoDefs.isGroupConvo(prev.kind)) return undefined
|
||||
return {
|
||||
...prev,
|
||||
kind: {...prev.kind, joinLink: data.joinLink},
|
||||
}
|
||||
})
|
||||
}
|
||||
onSuccess?.(data)
|
||||
},
|
||||
onError: (e, _variables, context) => {
|
||||
logger.error(e)
|
||||
if (convoId && context) {
|
||||
rollbackConvoOptimistic(queryClient, convoId, context)
|
||||
}
|
||||
onError?.(e)
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import {ChatBskyConvoDefs, type ChatBskyGroupEditGroup} from '@atproto/api'
|
||||
import {useMutation, useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {DM_SERVICE_HEADERS} from '#/lib/constants'
|
||||
import {logger} from '#/logger'
|
||||
import {useAgent} from '#/state/session'
|
||||
import {
|
||||
rollbackConvoOptimistic,
|
||||
updateConvoOptimistic,
|
||||
} from './utils/convo-cache'
|
||||
|
||||
export function useEditGroupChatName(
|
||||
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
|
||||
return updateConvoOptimistic(queryClient, convoId, prev => {
|
||||
if (!ChatBskyConvoDefs.isGroupConvo(prev.kind)) return undefined
|
||||
return {
|
||||
...prev,
|
||||
kind: {...prev.kind, name: groupName},
|
||||
}
|
||||
})
|
||||
},
|
||||
onSuccess: data => {
|
||||
onSuccess?.(data)
|
||||
},
|
||||
onError: (e, _variables, context) => {
|
||||
logger.error(e)
|
||||
if (convoId && context) {
|
||||
rollbackConvoOptimistic(queryClient, convoId, context)
|
||||
}
|
||||
onError?.(e)
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import {
|
||||
ChatBskyConvoDefs,
|
||||
type ChatBskyGroupDefs,
|
||||
type ChatBskyGroupEditJoinLink,
|
||||
} from '@atproto/api'
|
||||
import {useMutation, useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {DM_SERVICE_HEADERS} from '#/lib/constants'
|
||||
import {logger} from '#/logger'
|
||||
import {useAgent} from '#/state/session'
|
||||
import {
|
||||
rollbackConvoOptimistic,
|
||||
updateConvoOptimistic,
|
||||
} from './utils/convo-cache'
|
||||
|
||||
export function useEditJoinLink(
|
||||
convoId: string | undefined,
|
||||
{
|
||||
onSuccess,
|
||||
onError,
|
||||
}: {
|
||||
onSuccess?: (data: ChatBskyGroupEditJoinLink.OutputSchema) => void
|
||||
onError?: (error: Error) => void
|
||||
},
|
||||
) {
|
||||
const queryClient = useQueryClient()
|
||||
const agent = useAgent()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
joinRule,
|
||||
requireApproval,
|
||||
}: {
|
||||
joinRule: ChatBskyGroupDefs.JoinRule
|
||||
requireApproval: boolean
|
||||
}) => {
|
||||
if (!convoId) throw new Error('No convoId provided')
|
||||
const {data} = await agent.chat.bsky.group.editJoinLink(
|
||||
{convoId, joinRule, requireApproval},
|
||||
{headers: DM_SERVICE_HEADERS, encoding: 'application/json'},
|
||||
)
|
||||
return data
|
||||
},
|
||||
onMutate: ({joinRule, requireApproval}) => {
|
||||
if (!convoId) return
|
||||
return updateConvoOptimistic(queryClient, convoId, prev => {
|
||||
if (!ChatBskyConvoDefs.isGroupConvo(prev.kind) || !prev.kind.joinLink) {
|
||||
return undefined
|
||||
}
|
||||
return {
|
||||
...prev,
|
||||
kind: {
|
||||
...prev.kind,
|
||||
joinLink: {...prev.kind.joinLink, joinRule, requireApproval},
|
||||
},
|
||||
}
|
||||
})
|
||||
},
|
||||
onSuccess: data => {
|
||||
if (convoId) {
|
||||
updateConvoOptimistic(queryClient, convoId, prev => {
|
||||
if (!ChatBskyConvoDefs.isGroupConvo(prev.kind)) return undefined
|
||||
return {
|
||||
...prev,
|
||||
kind: {...prev.kind, joinLink: data.joinLink},
|
||||
}
|
||||
})
|
||||
}
|
||||
onSuccess?.(data)
|
||||
},
|
||||
onError: (e, _variables, context) => {
|
||||
logger.error(e)
|
||||
if (convoId && context) {
|
||||
rollbackConvoOptimistic(queryClient, convoId, context)
|
||||
}
|
||||
onError?.(e)
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import {ChatBskyConvoDefs, type ChatBskyGroupEnableJoinLink} from '@atproto/api'
|
||||
import {useMutation, useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {DM_SERVICE_HEADERS} from '#/lib/constants'
|
||||
import {logger} from '#/logger'
|
||||
import {useAgent} from '#/state/session'
|
||||
import {
|
||||
rollbackConvoOptimistic,
|
||||
updateConvoOptimistic,
|
||||
} from './utils/convo-cache'
|
||||
|
||||
export function useEnableJoinLink(
|
||||
convoId: string | undefined,
|
||||
{
|
||||
onSuccess,
|
||||
onError,
|
||||
}: {
|
||||
onSuccess?: (data: ChatBskyGroupEnableJoinLink.OutputSchema) => void
|
||||
onError?: (error: Error) => void
|
||||
},
|
||||
) {
|
||||
const queryClient = useQueryClient()
|
||||
const agent = useAgent()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async () => {
|
||||
if (!convoId) throw new Error('No convoId provided')
|
||||
const {data} = await agent.chat.bsky.group.enableJoinLink(
|
||||
{convoId},
|
||||
{headers: DM_SERVICE_HEADERS, encoding: 'application/json'},
|
||||
)
|
||||
return data
|
||||
},
|
||||
onMutate: () => {
|
||||
if (!convoId) return
|
||||
return updateConvoOptimistic(queryClient, convoId, prev => {
|
||||
if (!ChatBskyConvoDefs.isGroupConvo(prev.kind) || !prev.kind.joinLink) {
|
||||
return undefined
|
||||
}
|
||||
return {
|
||||
...prev,
|
||||
kind: {
|
||||
...prev.kind,
|
||||
joinLink: {...prev.kind.joinLink, enabledStatus: 'enabled'},
|
||||
},
|
||||
}
|
||||
})
|
||||
},
|
||||
onSuccess: data => {
|
||||
if (convoId) {
|
||||
updateConvoOptimistic(queryClient, convoId, prev => {
|
||||
if (!ChatBskyConvoDefs.isGroupConvo(prev.kind)) return undefined
|
||||
return {
|
||||
...prev,
|
||||
kind: {...prev.kind, joinLink: data.joinLink},
|
||||
}
|
||||
})
|
||||
}
|
||||
onSuccess?.(data)
|
||||
},
|
||||
onError: (e, _variables, context) => {
|
||||
logger.error(e)
|
||||
if (convoId && context) {
|
||||
rollbackConvoOptimistic(queryClient, convoId, context)
|
||||
}
|
||||
onError?.(e)
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -7,7 +7,10 @@ import {STALE} from '..'
|
||||
const RQKEY_ROOT = 'convo-availability'
|
||||
export const RQKEY = (did: string) => [RQKEY_ROOT, did]
|
||||
|
||||
export function useGetConvoAvailabilityQuery(did: string) {
|
||||
export function useGetConvoAvailabilityQuery(
|
||||
did: string,
|
||||
{enabled = true}: {enabled?: boolean} = {},
|
||||
) {
|
||||
const agent = useAgent()
|
||||
|
||||
return useQuery({
|
||||
@@ -21,5 +24,6 @@ export function useGetConvoAvailabilityQuery(did: string) {
|
||||
return data
|
||||
},
|
||||
staleTime: STALE.INFINITY,
|
||||
enabled,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ export function useLeaveConvo(
|
||||
return {prevPages}
|
||||
},
|
||||
onSuccess: data => {
|
||||
queryClient.invalidateQueries({queryKey: [CONVO_LIST_KEY]})
|
||||
void queryClient.invalidateQueries({queryKey: [CONVO_LIST_KEY]})
|
||||
onSuccess?.(data)
|
||||
},
|
||||
onError: (error, _, context) => {
|
||||
@@ -89,7 +89,7 @@ export function useLeaveConvo(
|
||||
}
|
||||
},
|
||||
)
|
||||
queryClient.invalidateQueries({queryKey: [CONVO_LIST_KEY]})
|
||||
void queryClient.invalidateQueries({queryKey: [CONVO_LIST_KEY]})
|
||||
onError?.(error)
|
||||
},
|
||||
})
|
||||
|
||||
@@ -105,8 +105,8 @@ export function ListConvosProviderInner({
|
||||
|
||||
const debouncedRefetch = useMemo(() => {
|
||||
const refetchAndInvalidate = () => {
|
||||
refetch()
|
||||
queryClient.invalidateQueries({queryKey: [RQKEY_ROOT]})
|
||||
void refetch()
|
||||
void queryClient.invalidateQueries({queryKey: [RQKEY_ROOT]})
|
||||
}
|
||||
return throttle(refetchAndInvalidate, 500, {
|
||||
leading: true,
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
import {useEffect} from 'react'
|
||||
import {type ChatBskyActorDefs, ChatBskyConvoDefs} from '@atproto/api'
|
||||
import {type QueryClient, useQuery, useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {DM_SERVICE_HEADERS} from '#/lib/constants'
|
||||
import {useMessagesEventBus} from '#/state/messages/events'
|
||||
import {STALE} from '#/state/queries'
|
||||
import {createQueryKey} from '#/state/queries/util'
|
||||
import {useAgent} from '#/state/session'
|
||||
import * as bsky from '#/types/bsky'
|
||||
|
||||
const RQKEY_ROOT = 'listConvoMembers'
|
||||
export const listConvoMembersQueryKey = (convoId: string) =>
|
||||
createQueryKey(RQKEY_ROOT, {convoId})
|
||||
|
||||
// group chat size is 50, so should fetch the whole list in one go
|
||||
const LIMIT = 50
|
||||
|
||||
export function useListConvoMembersQuery({
|
||||
convoId,
|
||||
placeholderData,
|
||||
}: {
|
||||
convoId: string
|
||||
placeholderData?: ChatBskyActorDefs.ProfileViewBasic[]
|
||||
}) {
|
||||
const agent = useAgent()
|
||||
const queryClient = useQueryClient()
|
||||
const messagesBus = useMessagesEventBus()
|
||||
|
||||
useEffect(() => {
|
||||
const unsub = messagesBus.on(
|
||||
ev => {
|
||||
if (ev.type !== 'logs') return
|
||||
|
||||
function mutateList(
|
||||
fn: (
|
||||
update: ChatBskyActorDefs.ProfileViewBasic[],
|
||||
) => ChatBskyActorDefs.ProfileViewBasic[],
|
||||
) {
|
||||
queryClient.setQueryData<ChatBskyActorDefs.ProfileViewBasic[]>(
|
||||
listConvoMembersQueryKey(convoId),
|
||||
old => {
|
||||
if (!old) return // query doesn't exist yet, skip
|
||||
return fn(old)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
for (const log of ev.logs) {
|
||||
if (ChatBskyConvoDefs.isLogAddMember(log)) {
|
||||
const data = log.message.data
|
||||
if (
|
||||
bsky.dangerousIsType<ChatBskyConvoDefs.SystemMessageDataAddMember>(
|
||||
data,
|
||||
ChatBskyConvoDefs.isSystemMessageDataAddMember,
|
||||
)
|
||||
) {
|
||||
const newMember = log.relatedProfiles.find(
|
||||
r => r.did === data.member.did,
|
||||
)
|
||||
if (newMember) {
|
||||
mutateList(list => list.concat(newMember))
|
||||
}
|
||||
}
|
||||
} else if (ChatBskyConvoDefs.isLogRemoveMember(log)) {
|
||||
const data = log.message.data
|
||||
if (
|
||||
bsky.dangerousIsType<ChatBskyConvoDefs.SystemMessageDataRemoveMember>(
|
||||
data,
|
||||
ChatBskyConvoDefs.isSystemMessageDataRemoveMember,
|
||||
)
|
||||
) {
|
||||
mutateList(list => list.filter(m => m.did !== data.member.did))
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{convoId},
|
||||
)
|
||||
return () => unsub()
|
||||
}, [convoId, messagesBus, queryClient])
|
||||
|
||||
return useQuery({
|
||||
queryKey: listConvoMembersQueryKey(convoId),
|
||||
queryFn: async () => {
|
||||
const members = []
|
||||
let cursor
|
||||
|
||||
do {
|
||||
const {data} = await agent.chat.bsky.convo.getConvoMembers(
|
||||
{convoId, cursor, limit: LIMIT},
|
||||
{headers: DM_SERVICE_HEADERS},
|
||||
)
|
||||
members.push(...data.members)
|
||||
cursor = data.cursor
|
||||
} while (cursor)
|
||||
|
||||
return members
|
||||
},
|
||||
staleTime: STALE.MINUTES.THIRTY,
|
||||
placeholderData,
|
||||
})
|
||||
}
|
||||
|
||||
export function* findAllProfilesInQueryData(
|
||||
queryClient: QueryClient,
|
||||
did: string,
|
||||
): Generator<ChatBskyActorDefs.ProfileViewBasic, void> {
|
||||
const queryDatas = queryClient.getQueriesData<
|
||||
ChatBskyActorDefs.ProfileViewBasic[]
|
||||
>({
|
||||
queryKey: [RQKEY_ROOT],
|
||||
})
|
||||
for (const [_queryKey, queryData] of queryDatas) {
|
||||
if (!queryData) continue
|
||||
for (const member of queryData) {
|
||||
if (member.did === did) {
|
||||
yield member
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import {ChatBskyConvoDefs, type ChatBskyConvoLockConvo} from '@atproto/api'
|
||||
import {useMutation, useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {DM_SERVICE_HEADERS} from '#/lib/constants'
|
||||
import {useAgent} from '#/state/session'
|
||||
import {
|
||||
rollbackConvoOptimistic,
|
||||
updateConvoOptimistic,
|
||||
} from './utils/convo-cache'
|
||||
|
||||
export function useLockConvo(
|
||||
convoId: string | undefined,
|
||||
{
|
||||
onSuccess,
|
||||
onError,
|
||||
}: {
|
||||
onSuccess?: (data: ChatBskyConvoLockConvo.OutputSchema) => void
|
||||
onError?: (error: Error, variables: {lock: boolean}) => void
|
||||
},
|
||||
) {
|
||||
const queryClient = useQueryClient()
|
||||
const agent = useAgent()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({lock}: {lock: boolean}) => {
|
||||
if (!convoId) throw new Error('No convoId provided')
|
||||
if (lock) {
|
||||
const {data} = await agent.chat.bsky.convo.lockConvo(
|
||||
{convoId},
|
||||
{headers: DM_SERVICE_HEADERS, encoding: 'application/json'},
|
||||
)
|
||||
return data
|
||||
} else {
|
||||
const {data} = await agent.chat.bsky.convo.unlockConvo(
|
||||
{convoId},
|
||||
{headers: DM_SERVICE_HEADERS, encoding: 'application/json'},
|
||||
)
|
||||
return data
|
||||
}
|
||||
},
|
||||
onMutate: ({lock}) => {
|
||||
if (!convoId) return
|
||||
return updateConvoOptimistic(queryClient, convoId, prev => {
|
||||
if (!ChatBskyConvoDefs.isGroupConvo(prev.kind)) return undefined
|
||||
return {
|
||||
...prev,
|
||||
kind: {
|
||||
...prev.kind,
|
||||
lockStatus: lock ? 'locked' : 'unlocked',
|
||||
},
|
||||
}
|
||||
})
|
||||
},
|
||||
onSuccess: data => {
|
||||
onSuccess?.(data)
|
||||
},
|
||||
onError: (e, variables, context) => {
|
||||
if (convoId && context) {
|
||||
rollbackConvoOptimistic(queryClient, convoId, context)
|
||||
}
|
||||
onError?.(e, variables)
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -1,18 +1,12 @@
|
||||
import {
|
||||
type ChatBskyConvoDefs,
|
||||
type ChatBskyConvoListConvos,
|
||||
type ChatBskyConvoMuteConvo,
|
||||
} from '@atproto/api'
|
||||
import {
|
||||
type InfiniteData,
|
||||
useMutation,
|
||||
useQueryClient,
|
||||
} from '@tanstack/react-query'
|
||||
import {type ChatBskyConvoMuteConvo} from '@atproto/api'
|
||||
import {useMutation, useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {DM_SERVICE_HEADERS} from '#/lib/constants'
|
||||
import {useAgent} from '#/state/session'
|
||||
import {RQKEY as CONVO_KEY} from './conversation'
|
||||
import {RQKEY_ROOT as CONVO_LIST_KEY} from './list-conversations'
|
||||
import {
|
||||
rollbackConvoOptimistic,
|
||||
updateConvoOptimistic,
|
||||
} from './utils/convo-cache'
|
||||
|
||||
export function useMuteConvo(
|
||||
convoId: string | undefined,
|
||||
@@ -46,59 +40,17 @@ export function useMuteConvo(
|
||||
},
|
||||
onMutate: ({mute}) => {
|
||||
if (!convoId) return
|
||||
|
||||
const prevConvo = queryClient.getQueryData<ChatBskyConvoDefs.ConvoView>(
|
||||
CONVO_KEY(convoId),
|
||||
)
|
||||
const prevListEntries = queryClient.getQueriesData<
|
||||
InfiniteData<ChatBskyConvoListConvos.OutputSchema>
|
||||
>({queryKey: [CONVO_LIST_KEY]})
|
||||
|
||||
// Update for a single chat thread
|
||||
queryClient.setQueryData<ChatBskyConvoDefs.ConvoView>(
|
||||
CONVO_KEY(convoId),
|
||||
prev => {
|
||||
if (!prev) return
|
||||
return {
|
||||
...prev,
|
||||
muted: mute,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// Update for the chat list
|
||||
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 => {
|
||||
if (convo.id !== convoId) return convo
|
||||
return {
|
||||
...convo,
|
||||
muted: mute,
|
||||
}
|
||||
}),
|
||||
})),
|
||||
}
|
||||
})
|
||||
|
||||
return {prevConvo, prevListEntries}
|
||||
return updateConvoOptimistic(queryClient, convoId, prev => ({
|
||||
...prev,
|
||||
muted: mute,
|
||||
}))
|
||||
},
|
||||
onSuccess: data => {
|
||||
onSuccess?.(data)
|
||||
},
|
||||
onError: (e, _variables, context) => {
|
||||
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)
|
||||
}
|
||||
if (convoId && context) {
|
||||
rollbackConvoOptimistic(queryClient, convoId, context)
|
||||
}
|
||||
onError?.(e)
|
||||
},
|
||||
|
||||
+30
-21
@@ -1,7 +1,8 @@
|
||||
import {
|
||||
ChatBskyConvoDefs,
|
||||
type ChatBskyActorDefs,
|
||||
type ChatBskyConvoDefs,
|
||||
type ChatBskyConvoListConvos,
|
||||
type ChatBskyGroupEditGroup,
|
||||
type ChatBskyGroupRemoveMembers,
|
||||
} from '@atproto/api'
|
||||
import {
|
||||
type InfiniteData,
|
||||
@@ -14,14 +15,15 @@ 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'
|
||||
import {listConvoMembersQueryKey} from './list-convo-members'
|
||||
|
||||
export function useEditGroupName(
|
||||
export function useRemoveFromGroupChat(
|
||||
convoId: string | undefined,
|
||||
{
|
||||
onSuccess,
|
||||
onError,
|
||||
}: {
|
||||
onSuccess?: (data: ChatBskyGroupEditGroup.OutputSchema) => void
|
||||
onSuccess?: (data: ChatBskyGroupRemoveMembers.OutputSchema) => void
|
||||
onError?: (error: Error) => void
|
||||
},
|
||||
) {
|
||||
@@ -29,15 +31,15 @@ export function useEditGroupName(
|
||||
const agent = useAgent()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({name: groupName}: {name: string}) => {
|
||||
mutationFn: async ({members}: {members: string[]}) => {
|
||||
if (!convoId) throw new Error('No convoId provided')
|
||||
const {data} = await agent.chat.bsky.group.editGroup(
|
||||
{convoId, name: groupName},
|
||||
const {data} = await agent.chat.bsky.group.removeMembers(
|
||||
{convoId, members},
|
||||
{headers: DM_SERVICE_HEADERS, encoding: 'application/json'},
|
||||
)
|
||||
return data
|
||||
},
|
||||
onMutate: ({name: groupName}) => {
|
||||
onMutate: ({members}) => {
|
||||
if (!convoId) return
|
||||
|
||||
const prevConvo = queryClient.getQueryData<ChatBskyConvoDefs.ConvoView>(
|
||||
@@ -46,24 +48,21 @@ export function useEditGroupName(
|
||||
const prevListEntries = queryClient.getQueriesData<
|
||||
InfiniteData<ChatBskyConvoListConvos.OutputSchema>
|
||||
>({queryKey: [CONVO_LIST_KEY]})
|
||||
const prevMemberList = queryClient.getQueryData<
|
||||
ChatBskyActorDefs.ProfileViewBasic[]
|
||||
>(listConvoMembersQueryKey(convoId))
|
||||
|
||||
// Update for a single chat thread
|
||||
queryClient.setQueryData<ChatBskyConvoDefs.ConvoView>(
|
||||
CONVO_KEY(convoId),
|
||||
prev => {
|
||||
if (!prev) return
|
||||
if (!ChatBskyConvoDefs.isGroupConvo(prev.kind)) return prev
|
||||
return {
|
||||
...prev,
|
||||
kind: {
|
||||
...prev.kind,
|
||||
name: groupName,
|
||||
},
|
||||
members: prev.members.filter(m => !members.includes(m.did)),
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// Update for the chat list
|
||||
queryClient.setQueriesData<
|
||||
InfiniteData<ChatBskyConvoListConvos.OutputSchema>
|
||||
>({queryKey: [CONVO_LIST_KEY]}, prev => {
|
||||
@@ -74,20 +73,24 @@ export function useEditGroupName(
|
||||
...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,
|
||||
},
|
||||
members: convo.members.filter(m => !members.includes(m.did)),
|
||||
}
|
||||
}),
|
||||
})),
|
||||
}
|
||||
})
|
||||
|
||||
return {prevConvo, prevListEntries}
|
||||
queryClient.setQueryData<ChatBskyActorDefs.ProfileViewBasic[]>(
|
||||
listConvoMembersQueryKey(convoId),
|
||||
prev => {
|
||||
if (!prev) return
|
||||
return prev.filter(m => !members.includes(m.did))
|
||||
},
|
||||
)
|
||||
|
||||
return {prevConvo, prevListEntries, prevMemberList}
|
||||
},
|
||||
onSuccess: data => {
|
||||
onSuccess?.(data)
|
||||
@@ -102,6 +105,12 @@ export function useEditGroupName(
|
||||
queryClient.setQueryData(key, data)
|
||||
}
|
||||
}
|
||||
if (context?.prevMemberList && convoId) {
|
||||
queryClient.setQueryData(
|
||||
listConvoMembersQueryKey(convoId),
|
||||
context.prevMemberList,
|
||||
)
|
||||
}
|
||||
onError?.(e)
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,87 @@
|
||||
import {
|
||||
type ChatBskyConvoDefs,
|
||||
type ChatBskyConvoListConvos,
|
||||
} from '@atproto/api'
|
||||
import {
|
||||
type InfiniteData,
|
||||
type QueryClient,
|
||||
type QueryKey,
|
||||
} from '@tanstack/react-query'
|
||||
|
||||
import {RQKEY as CONVO_KEY} from '../conversation'
|
||||
import {RQKEY_ROOT as CONVO_LIST_KEY} from '../list-conversations'
|
||||
|
||||
type ConvoUpdater = (
|
||||
prev: ChatBskyConvoDefs.ConvoView,
|
||||
) => ChatBskyConvoDefs.ConvoView | undefined
|
||||
|
||||
export type ConvoCacheSnapshot = {
|
||||
prevConvo: ChatBskyConvoDefs.ConvoView | undefined
|
||||
prevListEntries: Array<
|
||||
[QueryKey, InfiniteData<ChatBskyConvoListConvos.OutputSchema> | undefined]
|
||||
>
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes an optimistic update to a convo across both the single-convo and
|
||||
* convo-list caches. The updater receives the current ConvoView and returns
|
||||
* the next one - return undefined to bail out (e.g. when the convo's kind
|
||||
* doesn't match what the mutation requires). Returns a snapshot that can be
|
||||
* passed to `rollbackConvoOptimistic`.
|
||||
*/
|
||||
export function updateConvoOptimistic(
|
||||
queryClient: QueryClient,
|
||||
convoId: string,
|
||||
updater: ConvoUpdater,
|
||||
): ConvoCacheSnapshot {
|
||||
const prevConvo = queryClient.getQueryData<ChatBskyConvoDefs.ConvoView>(
|
||||
CONVO_KEY(convoId),
|
||||
)
|
||||
const prevListEntries = queryClient.getQueriesData<
|
||||
InfiniteData<ChatBskyConvoListConvos.OutputSchema>
|
||||
>({queryKey: [CONVO_LIST_KEY]})
|
||||
|
||||
queryClient.setQueryData<ChatBskyConvoDefs.ConvoView>(
|
||||
CONVO_KEY(convoId),
|
||||
prev => {
|
||||
if (!prev) return
|
||||
const next = updater(prev)
|
||||
return next ?? prev
|
||||
},
|
||||
)
|
||||
|
||||
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 => {
|
||||
if (convo.id !== convoId) return convo
|
||||
const next = updater(convo)
|
||||
return next ?? convo
|
||||
}),
|
||||
})),
|
||||
}
|
||||
})
|
||||
|
||||
return {prevConvo, prevListEntries}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores the caches to the state captured by `updateConvoOptimistic`.
|
||||
*/
|
||||
export function rollbackConvoOptimistic(
|
||||
queryClient: QueryClient,
|
||||
convoId: string,
|
||||
snapshot: ConvoCacheSnapshot,
|
||||
) {
|
||||
if (snapshot.prevConvo) {
|
||||
queryClient.setQueryData(CONVO_KEY(convoId), snapshot.prevConvo)
|
||||
}
|
||||
for (const [key, data] of snapshot.prevListEntries) {
|
||||
queryClient.setQueryData(key, data)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user