Use new listConvoRequests endpoint (#10755)
Co-authored-by: Samuel Newman <mozzius@protonmail.com>
This commit is contained in:
@@ -1773,16 +1773,6 @@
|
|||||||
"count": 7
|
"count": 7
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"src/state/queries/messages/accept-conversation.ts": {
|
|
||||||
"@typescript-eslint/no-floating-promises": {
|
|
||||||
"count": 2
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"src/state/queries/messages/update-all-read.ts": {
|
|
||||||
"@typescript-eslint/no-floating-promises": {
|
|
||||||
"count": 3
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"src/state/queries/my-lists.ts": {
|
"src/state/queries/my-lists.ts": {
|
||||||
"@typescript-eslint/no-floating-promises": {
|
"@typescript-eslint/no-floating-promises": {
|
||||||
"count": 2
|
"count": 2
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export function AvatarBubbles({
|
|||||||
moderationOpts,
|
moderationOpts,
|
||||||
}: {
|
}: {
|
||||||
animate?: boolean
|
animate?: boolean
|
||||||
profiles: bsky.profile.AnyProfileView[]
|
profiles: (bsky.profile.AnyProfileView | undefined)[]
|
||||||
/**
|
/**
|
||||||
* By default, when there are more than 2 profiles, the current user is
|
* By default, when there are more than 2 profiles, the current user is
|
||||||
* filtered out (so you don't see yourself among your own group's members).
|
* filtered out (so you don't see yourself among your own group's members).
|
||||||
@@ -50,12 +50,12 @@ export function AvatarBubbles({
|
|||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
const profiles =
|
const profiles =
|
||||||
!self && allProfiles.length > 2
|
!self && allProfiles.length > 2
|
||||||
? allProfiles.filter(p => p?.did != null && p.did !== currentAccount?.did)
|
? allProfiles.filter(p => !p || p.did !== currentAccount?.did)
|
||||||
: allProfiles
|
: allProfiles
|
||||||
const moderations = useMemo(() => {
|
const moderations = useMemo(() => {
|
||||||
if (!moderationOpts) return []
|
if (!moderationOpts) return []
|
||||||
return profiles.map(p => {
|
return profiles.map(p => {
|
||||||
return moderateProfile(p, moderationOpts)
|
return p && moderateProfile(p, moderationOpts)
|
||||||
})
|
})
|
||||||
}, [profiles, moderationOpts])
|
}, [profiles, moderationOpts])
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import {useCallback, useMemo, useState} from 'react'
|
import {useCallback, useMemo, useState} from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {
|
import {
|
||||||
type ChatBskyConvoDefs,
|
ChatBskyConvoDefs,
|
||||||
type ChatBskyConvoListConvos,
|
type ChatBskyConvoListConvoRequests,
|
||||||
|
ChatBskyGroupDefs,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
import {Trans, useLingui} from '@lingui/react/macro'
|
import {Trans, useLingui} from '@lingui/react/macro'
|
||||||
import {useFocusEffect, useNavigation} from '@react-navigation/native'
|
import {useFocusEffect, useNavigation} from '@react-navigation/native'
|
||||||
@@ -23,7 +24,7 @@ import {logger} from '#/logger'
|
|||||||
import {MESSAGE_SCREEN_POLL_INTERVAL} from '#/state/messages/convo/const'
|
import {MESSAGE_SCREEN_POLL_INTERVAL} from '#/state/messages/convo/const'
|
||||||
import {useMessagesEventBus} from '#/state/messages/events'
|
import {useMessagesEventBus} from '#/state/messages/events'
|
||||||
import {useLeftConvos} from '#/state/queries/messages/leave-conversation'
|
import {useLeftConvos} from '#/state/queries/messages/leave-conversation'
|
||||||
import {useListConvosQuery} from '#/state/queries/messages/list-conversations'
|
import {useListConvoRequests} from '#/state/queries/messages/list-conversation-requests'
|
||||||
import {useUpdateAllRead} from '#/state/queries/messages/update-all-read'
|
import {useUpdateAllRead} from '#/state/queries/messages/update-all-read'
|
||||||
import {EmptyState} from '#/view/com/util/EmptyState'
|
import {EmptyState} from '#/view/com/util/EmptyState'
|
||||||
import {List} from '#/view/com/util/List'
|
import {List} from '#/view/com/util/List'
|
||||||
@@ -43,11 +44,16 @@ import {ListFooter} from '#/components/Lists'
|
|||||||
import * as Toast from '#/components/Toast'
|
import * as Toast from '#/components/Toast'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {IS_NATIVE} from '#/env'
|
import {IS_NATIVE} from '#/env'
|
||||||
import {RequestListItem} from './components/RequestListItem'
|
import {IncomingRequestListItem} from './components/IncomingRequestListItem'
|
||||||
|
import {OutgoingRequestListItem} from './components/OutgoingRequestListItem'
|
||||||
import {useIsWithinSplitView} from './components/splitView/context'
|
import {useIsWithinSplitView} from './components/splitView/context'
|
||||||
|
|
||||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'MessagesInbox'>
|
type Props = NativeStackScreenProps<CommonNavigatorParams, 'MessagesInbox'>
|
||||||
|
|
||||||
|
type RequestItem =
|
||||||
|
| {type: 'incoming'; view: ChatBskyConvoDefs.ConvoView}
|
||||||
|
| {type: 'outgoing'; view: ChatBskyGroupDefs.JoinRequestConvoView}
|
||||||
|
|
||||||
export function MessagesInboxScreen(props: Props) {
|
export function MessagesInboxScreen(props: Props) {
|
||||||
const {t: l} = useLingui()
|
const {t: l} = useLingui()
|
||||||
const aaCopy = useAgeAssuranceCopy()
|
const aaCopy = useAgeAssuranceCopy()
|
||||||
@@ -61,29 +67,36 @@ export function MessagesInboxScreen(props: Props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function MessagesInboxScreenInner({}: Props) {
|
export function MessagesInboxScreenInner({}: Props) {
|
||||||
const listConvosQuery = useListConvosQuery({status: 'request'})
|
const listConvosQuery = useListConvoRequests()
|
||||||
const {data} = listConvosQuery
|
const {data} = listConvosQuery
|
||||||
|
|
||||||
const leftConvos = useLeftConvos()
|
const leftConvos = useLeftConvos()
|
||||||
|
|
||||||
const conversations = useMemo(() => {
|
const conversations = useMemo<RequestItem[]>(() => {
|
||||||
if (data?.pages) {
|
if (!data?.pages) return []
|
||||||
const convos = data.pages
|
const items: RequestItem[] = []
|
||||||
.flatMap(page => page.convos)
|
for (const page of data.pages) {
|
||||||
// filter out convos that are actively being left
|
for (const item of page.requests) {
|
||||||
.filter(convo => !leftConvos.includes(convo.id))
|
if (ChatBskyConvoDefs.isConvoView(item)) {
|
||||||
|
// filter out convos that are actively being left
|
||||||
return convos
|
if (leftConvos.includes(item.id)) continue
|
||||||
|
items.push({type: 'incoming', view: item})
|
||||||
|
} else if (ChatBskyGroupDefs.isJoinRequestConvoView(item)) {
|
||||||
|
items.push({type: 'outgoing', view: item})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return []
|
return items
|
||||||
}, [data, leftConvos])
|
}, [data, leftConvos])
|
||||||
|
|
||||||
const hasUnreadConvos = useMemo(() => {
|
const hasUnreadConvos = useMemo(() => {
|
||||||
return conversations.some(
|
return conversations.some(
|
||||||
conversation =>
|
item =>
|
||||||
conversation.members.every(
|
item.type === 'incoming' &&
|
||||||
|
item.view.members.every(
|
||||||
member => member.handle !== 'missing.invalid',
|
member => member.handle !== 'missing.invalid',
|
||||||
) && conversation.unreadCount > 0,
|
) &&
|
||||||
|
item.view.unreadCount > 0,
|
||||||
)
|
)
|
||||||
}, [conversations])
|
}, [conversations])
|
||||||
|
|
||||||
@@ -111,10 +124,10 @@ function RequestList({
|
|||||||
conversations,
|
conversations,
|
||||||
}: {
|
}: {
|
||||||
listConvosQuery: UseInfiniteQueryResult<
|
listConvosQuery: UseInfiniteQueryResult<
|
||||||
InfiniteData<ChatBskyConvoListConvos.OutputSchema>,
|
InfiniteData<ChatBskyConvoListConvoRequests.OutputSchema>,
|
||||||
Error
|
Error
|
||||||
>
|
>
|
||||||
conversations: ChatBskyConvoDefs.ConvoView[]
|
conversations: RequestItem[]
|
||||||
}) {
|
}) {
|
||||||
const {t: l} = useLingui()
|
const {t: l} = useLingui()
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
@@ -274,17 +287,21 @@ function RequestList({
|
|||||||
windowSize={11}
|
windowSize={11}
|
||||||
desktopFixedHeight
|
desktopFixedHeight
|
||||||
sideBorders={false}
|
sideBorders={false}
|
||||||
|
contentContainerStyle={[web(a.py_sm)]}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function keyExtractor(item: ChatBskyConvoDefs.ConvoView) {
|
function keyExtractor(item: RequestItem) {
|
||||||
return item.id
|
return item.type === 'incoming' ? item.view.id : item.view.convoId
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderItem({item}: {item: ChatBskyConvoDefs.ConvoView}) {
|
function renderItem({item}: {item: RequestItem}) {
|
||||||
return <RequestListItem convo={item} />
|
if (item.type === 'incoming') {
|
||||||
|
return <IncomingRequestListItem convo={item.view} />
|
||||||
|
}
|
||||||
|
return <OutgoingRequestListItem convo={item.view} />
|
||||||
}
|
}
|
||||||
|
|
||||||
function MarkAsReadHeaderButton() {
|
function MarkAsReadHeaderButton() {
|
||||||
|
|||||||
+1
-1
@@ -11,7 +11,7 @@ import {Text} from '#/components/Typography'
|
|||||||
import {ChatListItem, ChatListItemPortal} from './ChatListItem'
|
import {ChatListItem, ChatListItemPortal} from './ChatListItem'
|
||||||
import {AcceptChatButton, DeleteChatButton, RejectMenu} from './RequestButtons'
|
import {AcceptChatButton, DeleteChatButton, RejectMenu} from './RequestButtons'
|
||||||
|
|
||||||
export function RequestListItem({
|
export function IncomingRequestListItem({
|
||||||
convo: convoView,
|
convo: convoView,
|
||||||
}: {
|
}: {
|
||||||
convo: ChatBskyConvoDefs.ConvoView
|
convo: ChatBskyConvoDefs.ConvoView
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
import {View} from 'react-native'
|
||||||
|
import {
|
||||||
|
type ChatBskyGroupDefs,
|
||||||
|
ChatBskyGroupWithdrawJoinRequest,
|
||||||
|
} from '@atproto/api'
|
||||||
|
import {Trans, useLingui} from '@lingui/react/macro'
|
||||||
|
|
||||||
|
import {isNetworkError} from '#/lib/strings/errors'
|
||||||
|
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||||
|
import {useWithdrawJoinGroupChatRequest} from '#/state/queries/messages/withdraw-join-group-chat'
|
||||||
|
import {TimeElapsed} from '#/view/com/util/TimeElapsed'
|
||||||
|
import {atoms as a, useTheme, web} from '#/alf'
|
||||||
|
import {AvatarBubbles} from '#/components/AvatarBubbles'
|
||||||
|
import {createStaticClick, Link} from '#/components/Link'
|
||||||
|
import * as Prompt from '#/components/Prompt'
|
||||||
|
import * as Toast from '#/components/Toast'
|
||||||
|
import {Text} from '#/components/Typography'
|
||||||
|
|
||||||
|
export function OutgoingRequestListItem({
|
||||||
|
convo: convoView,
|
||||||
|
}: {
|
||||||
|
convo: ChatBskyGroupDefs.JoinRequestConvoView
|
||||||
|
}) {
|
||||||
|
const t = useTheme()
|
||||||
|
const {t: l} = useLingui()
|
||||||
|
|
||||||
|
const prompt = Prompt.usePromptControl()
|
||||||
|
|
||||||
|
const moderationOpts = useModerationOpts()
|
||||||
|
|
||||||
|
const {mutate: withdrawRequest, isPending: isWithdrawPending} =
|
||||||
|
useWithdrawJoinGroupChatRequest({
|
||||||
|
onSuccess: () => {
|
||||||
|
Toast.show(l`Join request rescinded.`)
|
||||||
|
},
|
||||||
|
onError: error => {
|
||||||
|
let errorMessage = l`Failed to rescind your request. Please try again.`
|
||||||
|
if (isNetworkError(error)) {
|
||||||
|
errorMessage = l`There was a problem with your internet connection, please try again`
|
||||||
|
} else if (
|
||||||
|
error instanceof
|
||||||
|
ChatBskyGroupWithdrawJoinRequest.InvalidJoinRequestError
|
||||||
|
) {
|
||||||
|
errorMessage = l`Invalid rescind request.`
|
||||||
|
}
|
||||||
|
Toast.show(errorMessage)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Link
|
||||||
|
label={l`Rescind request to join group chat`}
|
||||||
|
{...createStaticClick(() => {
|
||||||
|
prompt.open()
|
||||||
|
})}>
|
||||||
|
{({hovered, pressed, focused}) => (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.flex_row,
|
||||||
|
a.align_center,
|
||||||
|
a.flex_1,
|
||||||
|
a.px_lg,
|
||||||
|
a.py_md,
|
||||||
|
a.gap_md,
|
||||||
|
(hovered || pressed || focused) && t.atoms.bg_contrast_25,
|
||||||
|
]}>
|
||||||
|
<AvatarBubbles
|
||||||
|
profiles={[
|
||||||
|
convoView?.owner ?? undefined,
|
||||||
|
...Array(
|
||||||
|
Math.min(3, Math.max(0, convoView.memberCount - 1)),
|
||||||
|
).fill(undefined),
|
||||||
|
]}
|
||||||
|
size={48}
|
||||||
|
moderationOpts={moderationOpts}
|
||||||
|
/>
|
||||||
|
<View style={[a.flex_1]}>
|
||||||
|
<View style={[a.w_full, a.flex_row, a.align_center, a.pb_2xs]}>
|
||||||
|
<View style={[a.flex_shrink]}>
|
||||||
|
<Text
|
||||||
|
emoji
|
||||||
|
numberOfLines={1}
|
||||||
|
style={[a.text_md, a.font_semi_bold]}>
|
||||||
|
{convoView.name}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[a.pl_xs]}>
|
||||||
|
<TimeElapsed timestamp={convoView.requestedAt}>
|
||||||
|
{({timeElapsed}) => (
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
a.text_sm,
|
||||||
|
t.atoms.text_contrast_medium,
|
||||||
|
web({whiteSpace: 'preserve nowrap'}),
|
||||||
|
]}>
|
||||||
|
{timeElapsed}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</TimeElapsed>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<Text
|
||||||
|
numberOfLines={1}
|
||||||
|
style={[a.text_sm, t.atoms.text_contrast_high]}>
|
||||||
|
<Trans comment="Displayed when the user has requested to join a group chat.">
|
||||||
|
You requested to join
|
||||||
|
</Trans>
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
<Prompt.Basic
|
||||||
|
control={prompt}
|
||||||
|
title={l`Rescind request`}
|
||||||
|
description={l`Are you sure you want to rescind your request to join ${convoView.name}?`}
|
||||||
|
confirmButtonCta={l`Rescind request`}
|
||||||
|
onConfirm={() => {
|
||||||
|
prompt.close(() => {
|
||||||
|
if (isWithdrawPending) return
|
||||||
|
withdrawRequest({convoId: convoView.convoId})
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
Vendored
+2
@@ -11,6 +11,7 @@ import {findAllProfilesInQueryData as findAllProfilesInContactMatchesQueryData}
|
|||||||
import {findAllProfilesInQueryData as findAllProfilesInKnownFollowersQueryData} from '#/state/queries/known-followers'
|
import {findAllProfilesInQueryData as findAllProfilesInKnownFollowersQueryData} from '#/state/queries/known-followers'
|
||||||
import {findAllProfilesInQueryData as findAllProfilesInListMembersQueryData} from '#/state/queries/list-members'
|
import {findAllProfilesInQueryData as findAllProfilesInListMembersQueryData} from '#/state/queries/list-members'
|
||||||
import {findAllProfilesInQueryData as findAllProfilesInGetConvoQueryData} from '#/state/queries/messages/conversation'
|
import {findAllProfilesInQueryData as findAllProfilesInGetConvoQueryData} from '#/state/queries/messages/conversation'
|
||||||
|
import {findAllProfilesInQueryData as findAllProfilesInListConvoRequestsQueryData} from '#/state/queries/messages/list-conversation-requests'
|
||||||
import {findAllProfilesInQueryData as findAllProfilesInListConvosQueryData} from '#/state/queries/messages/list-conversations'
|
import {findAllProfilesInQueryData as findAllProfilesInListConvosQueryData} from '#/state/queries/messages/list-conversations'
|
||||||
import {findAllProfilesInQueryData as findAllProfilesInMessagesQueryData} from '#/state/queries/messages/list-convo-members'
|
import {findAllProfilesInQueryData as findAllProfilesInMessagesQueryData} from '#/state/queries/messages/list-convo-members'
|
||||||
import {findAllProfilesInQueryData as findAllProfilesInMyBlockedAccountsQueryData} from '#/state/queries/my-blocked-accounts'
|
import {findAllProfilesInQueryData as findAllProfilesInMyBlockedAccountsQueryData} from '#/state/queries/my-blocked-accounts'
|
||||||
@@ -259,6 +260,7 @@ function* findProfilesInCache(
|
|||||||
yield* findAllProfilesInSuggestedFollowsQueryData(queryClient, did)
|
yield* findAllProfilesInSuggestedFollowsQueryData(queryClient, did)
|
||||||
yield* findAllProfilesInActorSearchQueryData(queryClient, did)
|
yield* findAllProfilesInActorSearchQueryData(queryClient, did)
|
||||||
yield* findAllProfilesInListConvosQueryData(queryClient, did)
|
yield* findAllProfilesInListConvosQueryData(queryClient, did)
|
||||||
|
yield* findAllProfilesInListConvoRequestsQueryData(queryClient, did)
|
||||||
yield* findAllProfilesInFeedsQueryData(queryClient, did)
|
yield* findAllProfilesInFeedsQueryData(queryClient, did)
|
||||||
yield* findAllProfilesInPostThreadV2QueryData(queryClient, did)
|
yield* findAllProfilesInPostThreadV2QueryData(queryClient, did)
|
||||||
yield* findAllProfilesInKnownFollowersQueryData(queryClient, did)
|
yield* findAllProfilesInKnownFollowersQueryData(queryClient, did)
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ import {useMutation, useQueryClient} from '@tanstack/react-query'
|
|||||||
import {DM_SERVICE_HEADERS} from '#/lib/constants'
|
import {DM_SERVICE_HEADERS} from '#/lib/constants'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {useAgent} from '#/state/session'
|
import {useAgent} from '#/state/session'
|
||||||
|
import {
|
||||||
|
type ConvoRequestListQueryData,
|
||||||
|
optimisticDelete as optimisticDeleteRequest,
|
||||||
|
RQKEY_ROOT as REQUESTS_RQKEY_ROOT,
|
||||||
|
} from './list-conversation-requests'
|
||||||
import {
|
import {
|
||||||
RQKEY as CONVO_LIST_KEY,
|
RQKEY as CONVO_LIST_KEY,
|
||||||
RQKEY_ROOT as CONVO_LIST_ROOT_KEY,
|
RQKEY_ROOT as CONVO_LIST_ROOT_KEY,
|
||||||
@@ -96,11 +101,20 @@ export function useAcceptConversation(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
const prevRequestsQueries =
|
||||||
|
queryClient.getQueriesData<ConvoRequestListQueryData>({
|
||||||
|
queryKey: [REQUESTS_RQKEY_ROOT],
|
||||||
|
})
|
||||||
|
queryClient.setQueriesData<ConvoRequestListQueryData>(
|
||||||
|
{queryKey: [REQUESTS_RQKEY_ROOT]},
|
||||||
|
old => optimisticDeleteRequest(convoId, old),
|
||||||
|
)
|
||||||
onMutate?.()
|
onMutate?.()
|
||||||
return {prevAcceptedPages, prevInboxPages}
|
return {prevAcceptedPages, prevInboxPages, prevRequestsQueries}
|
||||||
},
|
},
|
||||||
onSuccess: data => {
|
onSuccess: data => {
|
||||||
queryClient.invalidateQueries({queryKey: [CONVO_LIST_KEY]})
|
void queryClient.invalidateQueries({queryKey: [CONVO_LIST_KEY]})
|
||||||
|
void queryClient.invalidateQueries({queryKey: [REQUESTS_RQKEY_ROOT]})
|
||||||
onSuccess?.(data)
|
onSuccess?.(data)
|
||||||
},
|
},
|
||||||
onError: (error, _, context) => {
|
onError: (error, _, context) => {
|
||||||
@@ -131,7 +145,13 @@ export function useAcceptConversation(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
queryClient.invalidateQueries({queryKey: [CONVO_LIST_ROOT_KEY]})
|
if (context?.prevRequestsQueries) {
|
||||||
|
for (const [queryKey, prevData] of context.prevRequestsQueries) {
|
||||||
|
queryClient.setQueryData(queryKey, prevData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void queryClient.invalidateQueries({queryKey: [CONVO_LIST_ROOT_KEY]})
|
||||||
|
void queryClient.invalidateQueries({queryKey: [REQUESTS_RQKEY_ROOT]})
|
||||||
onError?.(error)
|
onError?.(error)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,165 @@
|
|||||||
|
import {
|
||||||
|
ChatBskyConvoDefs,
|
||||||
|
type ChatBskyConvoListConvoRequests,
|
||||||
|
ChatBskyGroupDefs,
|
||||||
|
} from '@atproto/api'
|
||||||
|
import {
|
||||||
|
type InfiniteData,
|
||||||
|
type QueryClient,
|
||||||
|
useInfiniteQuery,
|
||||||
|
} from '@tanstack/react-query'
|
||||||
|
|
||||||
|
import {DM_SERVICE_HEADERS} from '#/lib/constants'
|
||||||
|
import {useAgent} from '#/state/session'
|
||||||
|
|
||||||
|
const DEFAULT_LIMIT = 10
|
||||||
|
|
||||||
|
export const RQKEY_ROOT = 'convo-request-list'
|
||||||
|
export const RQKEY = (limit: number = DEFAULT_LIMIT) => [RQKEY_ROOT, limit]
|
||||||
|
|
||||||
|
type RQPageParam = string | undefined
|
||||||
|
|
||||||
|
export function useListConvoRequests({
|
||||||
|
enabled = true,
|
||||||
|
limit = DEFAULT_LIMIT,
|
||||||
|
}: {
|
||||||
|
enabled?: boolean
|
||||||
|
limit?: number
|
||||||
|
} = {}) {
|
||||||
|
const agent = useAgent()
|
||||||
|
|
||||||
|
return useInfiniteQuery({
|
||||||
|
enabled,
|
||||||
|
queryKey: RQKEY(limit),
|
||||||
|
queryFn: async ({pageParam}) => {
|
||||||
|
const {data} = await agent.chat.bsky.convo.listConvoRequests(
|
||||||
|
{limit, cursor: pageParam},
|
||||||
|
{headers: DM_SERVICE_HEADERS},
|
||||||
|
)
|
||||||
|
return data
|
||||||
|
},
|
||||||
|
initialPageParam: undefined as RQPageParam,
|
||||||
|
getNextPageParam: lastPage => lastPage.cursor,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ConvoRequestListQueryData = {
|
||||||
|
pageParams: Array<string | undefined>
|
||||||
|
pages: Array<ChatBskyConvoListConvoRequests.OutputSchema>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ConvoRequestItem =
|
||||||
|
ChatBskyConvoListConvoRequests.OutputSchema['requests'][number]
|
||||||
|
|
||||||
|
export function optimisticUpdate(
|
||||||
|
chatId: string,
|
||||||
|
old: ConvoRequestListQueryData | undefined,
|
||||||
|
updateFn: (convo: ChatBskyConvoDefs.ConvoView) => ChatBskyConvoDefs.ConvoView,
|
||||||
|
): ConvoRequestListQueryData | undefined {
|
||||||
|
if (!old) return old
|
||||||
|
|
||||||
|
return {
|
||||||
|
...old,
|
||||||
|
pages: old.pages.map(page => ({
|
||||||
|
...page,
|
||||||
|
requests: page.requests.map((item): ConvoRequestItem => {
|
||||||
|
if (ChatBskyConvoDefs.isConvoView(item) && item.id === chatId) {
|
||||||
|
return {
|
||||||
|
...updateFn(item),
|
||||||
|
$type: 'chat.bsky.convo.defs#convoView',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return item
|
||||||
|
}),
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function optimisticDelete(
|
||||||
|
chatId: string,
|
||||||
|
old: ConvoRequestListQueryData | undefined,
|
||||||
|
) {
|
||||||
|
if (!old) return old
|
||||||
|
|
||||||
|
return {
|
||||||
|
...old,
|
||||||
|
pages: old.pages.map(page => ({
|
||||||
|
...page,
|
||||||
|
requests: page.requests.filter(
|
||||||
|
item => !ChatBskyConvoDefs.isConvoView(item) || item.id !== chatId,
|
||||||
|
),
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function markAllRead(
|
||||||
|
old: ConvoRequestListQueryData | undefined,
|
||||||
|
): ConvoRequestListQueryData | undefined {
|
||||||
|
if (!old) return old
|
||||||
|
|
||||||
|
return {
|
||||||
|
...old,
|
||||||
|
pages: old.pages.map(page => ({
|
||||||
|
...page,
|
||||||
|
requests: page.requests.map((item): ConvoRequestItem => {
|
||||||
|
if (ChatBskyConvoDefs.isConvoView(item)) {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
$type: 'chat.bsky.convo.defs#convoView',
|
||||||
|
unreadCount: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return item
|
||||||
|
}),
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function optimisticDeleteJoinRequest(
|
||||||
|
convoId: string,
|
||||||
|
old: ConvoRequestListQueryData | undefined,
|
||||||
|
) {
|
||||||
|
if (!old) return old
|
||||||
|
|
||||||
|
return {
|
||||||
|
...old,
|
||||||
|
pages: old.pages.map(page => ({
|
||||||
|
...page,
|
||||||
|
requests: page.requests.filter(
|
||||||
|
item =>
|
||||||
|
!ChatBskyGroupDefs.isJoinRequestConvoView(item) ||
|
||||||
|
item.convoId !== convoId,
|
||||||
|
),
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function* findAllProfilesInQueryData(
|
||||||
|
queryClient: QueryClient,
|
||||||
|
did: string,
|
||||||
|
) {
|
||||||
|
const queryDatas = queryClient.getQueriesData<
|
||||||
|
InfiniteData<ChatBskyConvoListConvoRequests.OutputSchema>
|
||||||
|
>({
|
||||||
|
queryKey: [RQKEY_ROOT],
|
||||||
|
})
|
||||||
|
for (const [_queryKey, queryData] of queryDatas) {
|
||||||
|
if (!queryData?.pages) continue
|
||||||
|
|
||||||
|
for (const page of queryData.pages) {
|
||||||
|
for (const item of page.requests) {
|
||||||
|
if (ChatBskyConvoDefs.isConvoView(item)) {
|
||||||
|
for (const member of item.members) {
|
||||||
|
if (member.did === did) {
|
||||||
|
yield member
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (ChatBskyGroupDefs.isJoinRequestConvoView(item)) {
|
||||||
|
if (item.owner.did === did) {
|
||||||
|
yield item.owner
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,6 +23,13 @@ import {parseConvoView} from '#/components/dms/util'
|
|||||||
import * as bsky from '#/types/bsky'
|
import * as bsky from '#/types/bsky'
|
||||||
import {RQKEY as CONVO_KEY} from './conversation'
|
import {RQKEY as CONVO_KEY} from './conversation'
|
||||||
import {useLeftConvos} from './leave-conversation'
|
import {useLeftConvos} from './leave-conversation'
|
||||||
|
import {
|
||||||
|
type ConvoRequestListQueryData,
|
||||||
|
optimisticDelete as optimisticDeleteRequest,
|
||||||
|
optimisticDeleteJoinRequest,
|
||||||
|
optimisticUpdate as optimisticUpdateRequest,
|
||||||
|
RQKEY_ROOT as REQUESTS_RQKEY_ROOT,
|
||||||
|
} from './list-conversation-requests'
|
||||||
import {listConvoMembersQueryKey} from './list-convo-members'
|
import {listConvoMembersQueryKey} from './list-convo-members'
|
||||||
|
|
||||||
const DEFAULT_LIMIT = 10
|
const DEFAULT_LIMIT = 10
|
||||||
@@ -130,6 +137,7 @@ export function ListConvosProviderInner({
|
|||||||
const refetchAndInvalidate = () => {
|
const refetchAndInvalidate = () => {
|
||||||
void refetch()
|
void refetch()
|
||||||
void queryClient.invalidateQueries({queryKey: [RQKEY_ROOT]})
|
void queryClient.invalidateQueries({queryKey: [RQKEY_ROOT]})
|
||||||
|
void queryClient.invalidateQueries({queryKey: [REQUESTS_RQKEY_ROOT]})
|
||||||
}
|
}
|
||||||
return throttle(refetchAndInvalidate, 500, {
|
return throttle(refetchAndInvalidate, 500, {
|
||||||
leading: true,
|
leading: true,
|
||||||
@@ -157,6 +165,22 @@ export function ListConvosProviderInner({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateConvoInAllLists(
|
||||||
|
convoId: string,
|
||||||
|
fn: (
|
||||||
|
convo: ChatBskyConvoDefs.ConvoView,
|
||||||
|
) => ChatBskyConvoDefs.ConvoView,
|
||||||
|
) {
|
||||||
|
queryClient.setQueriesData<ConvoListQueryData>(
|
||||||
|
{queryKey: [RQKEY_ROOT]},
|
||||||
|
old => optimisticUpdate(convoId, old, fn),
|
||||||
|
)
|
||||||
|
queryClient.setQueriesData<ConvoRequestListQueryData>(
|
||||||
|
{queryKey: [REQUESTS_RQKEY_ROOT]},
|
||||||
|
old => optimisticUpdateRequest(convoId, old, fn),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function mutateConvoView(
|
function mutateConvoView(
|
||||||
convoId: string,
|
convoId: string,
|
||||||
fn: (
|
fn: (
|
||||||
@@ -167,9 +191,17 @@ export function ListConvosProviderInner({
|
|||||||
CONVO_KEY(convoId),
|
CONVO_KEY(convoId),
|
||||||
old => (old ? fn(old) : old),
|
old => (old ? fn(old) : old),
|
||||||
)
|
)
|
||||||
|
updateConvoInAllLists(convoId, fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteConvoFromAllLists(convoId: string) {
|
||||||
queryClient.setQueriesData<ConvoListQueryData>(
|
queryClient.setQueriesData<ConvoListQueryData>(
|
||||||
{queryKey: [RQKEY_ROOT]},
|
{queryKey: [RQKEY_ROOT]},
|
||||||
old => optimisticUpdate(convoId, old, fn),
|
old => optimisticDelete(convoId, old),
|
||||||
|
)
|
||||||
|
queryClient.setQueriesData<ConvoRequestListQueryData>(
|
||||||
|
{queryKey: [REQUESTS_RQKEY_ROOT]},
|
||||||
|
old => optimisticDeleteRequest(convoId, old),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,35 +252,26 @@ export function ListConvosProviderInner({
|
|||||||
if (ChatBskyConvoDefs.isLogBeginConvo(log)) {
|
if (ChatBskyConvoDefs.isLogBeginConvo(log)) {
|
||||||
debouncedRefetch()
|
debouncedRefetch()
|
||||||
} else if (ChatBskyConvoDefs.isLogLeaveConvo(log)) {
|
} else if (ChatBskyConvoDefs.isLogLeaveConvo(log)) {
|
||||||
queryClient.setQueriesData(
|
deleteConvoFromAllLists(log.convoId)
|
||||||
{queryKey: [RQKEY_ROOT]},
|
|
||||||
(old?: ConvoListQueryData) => optimisticDelete(log.convoId, old),
|
|
||||||
)
|
|
||||||
} else if (ChatBskyConvoDefs.isLogDeleteMessage(log)) {
|
} else if (ChatBskyConvoDefs.isLogDeleteMessage(log)) {
|
||||||
queryClient.setQueriesData(
|
updateConvoInAllLists(log.convoId, convo => {
|
||||||
{queryKey: [RQKEY_ROOT]},
|
if (
|
||||||
(old?: ConvoListQueryData) =>
|
(ChatBskyConvoDefs.isDeletedMessageView(log.message) ||
|
||||||
optimisticUpdate(log.convoId, old, convo => {
|
ChatBskyConvoDefs.isMessageView(log.message)) &&
|
||||||
if (
|
(ChatBskyConvoDefs.isDeletedMessageView(convo.lastMessage) ||
|
||||||
(ChatBskyConvoDefs.isDeletedMessageView(log.message) ||
|
ChatBskyConvoDefs.isMessageView(convo.lastMessage))
|
||||||
ChatBskyConvoDefs.isMessageView(log.message)) &&
|
) {
|
||||||
(ChatBskyConvoDefs.isDeletedMessageView(
|
return log.message.id === convo.lastMessage.id
|
||||||
convo.lastMessage,
|
? {
|
||||||
) ||
|
...convo,
|
||||||
ChatBskyConvoDefs.isMessageView(convo.lastMessage))
|
rev: log.rev,
|
||||||
) {
|
lastMessage: log.message,
|
||||||
return log.message.id === convo.lastMessage.id
|
}
|
||||||
? {
|
: convo
|
||||||
...convo,
|
} else {
|
||||||
rev: log.rev,
|
return convo
|
||||||
lastMessage: log.message,
|
}
|
||||||
}
|
})
|
||||||
: convo
|
|
||||||
} else {
|
|
||||||
return convo
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
} else if (ChatBskyConvoDefs.isLogCreateMessage(log)) {
|
} else if (ChatBskyConvoDefs.isLogCreateMessage(log)) {
|
||||||
// Store in a new var to avoid TS errors due to closures.
|
// Store in a new var to avoid TS errors due to closures.
|
||||||
const logRef: ChatBskyConvoDefs.LogCreateMessage = log
|
const logRef: ChatBskyConvoDefs.LogCreateMessage = log
|
||||||
@@ -335,27 +358,24 @@ export function ListConvosProviderInner({
|
|||||||
)
|
)
|
||||||
} else if (updatedConvo.status === 'request') {
|
} else if (updatedConvo.status === 'request') {
|
||||||
queryClient.setQueriesData({queryKey: RQKEY('request')}, updateFn)
|
queryClient.setQueriesData({queryKey: RQKEY('request')}, updateFn)
|
||||||
|
// also move-to-top in the new requests cache
|
||||||
|
queryClient.setQueriesData<ConvoRequestListQueryData>(
|
||||||
|
{queryKey: [REQUESTS_RQKEY_ROOT]},
|
||||||
|
old => moveConvoToTopInRequests(updatedConvo, old),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} else if (ChatBskyConvoDefs.isLogReadMessage(log)) {
|
} else if (ChatBskyConvoDefs.isLogReadMessage(log)) {
|
||||||
queryClient.setQueriesData(
|
updateConvoInAllLists(log.convoId, convo => ({
|
||||||
{queryKey: [RQKEY_ROOT]},
|
...convo,
|
||||||
(old?: ConvoListQueryData) =>
|
unreadCount: 0,
|
||||||
optimisticUpdate(log.convoId, old, convo => ({
|
rev: log.rev,
|
||||||
...convo,
|
}))
|
||||||
unreadCount: 0,
|
|
||||||
rev: log.rev,
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
} else if (ChatBskyConvoDefs.isLogReadConvo(log)) {
|
} else if (ChatBskyConvoDefs.isLogReadConvo(log)) {
|
||||||
queryClient.setQueriesData(
|
updateConvoInAllLists(log.convoId, convo => ({
|
||||||
{queryKey: [RQKEY_ROOT]},
|
...convo,
|
||||||
(old?: ConvoListQueryData) =>
|
unreadCount: 0,
|
||||||
optimisticUpdate(log.convoId, old, convo => ({
|
rev: log.rev,
|
||||||
...convo,
|
}))
|
||||||
unreadCount: 0,
|
|
||||||
rev: log.rev,
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
} else if (ChatBskyConvoDefs.isLogAcceptConvo(log)) {
|
} else if (ChatBskyConvoDefs.isLogAcceptConvo(log)) {
|
||||||
const requests = queryClient.getQueryData<ConvoListQueryData>(
|
const requests = queryClient.getQueryData<ConvoListQueryData>(
|
||||||
RQKEY('request'),
|
RQKEY('request'),
|
||||||
@@ -373,6 +393,11 @@ export function ListConvosProviderInner({
|
|||||||
RQKEY('request'),
|
RQKEY('request'),
|
||||||
(old?: ConvoListQueryData) => optimisticDelete(log.convoId, old),
|
(old?: ConvoListQueryData) => optimisticDelete(log.convoId, old),
|
||||||
)
|
)
|
||||||
|
// also remove from the new requests cache
|
||||||
|
queryClient.setQueriesData<ConvoRequestListQueryData>(
|
||||||
|
{queryKey: [REQUESTS_RQKEY_ROOT]},
|
||||||
|
old => optimisticDeleteRequest(log.convoId, old),
|
||||||
|
)
|
||||||
queryClient.setQueriesData(
|
queryClient.setQueriesData(
|
||||||
{queryKey: RQKEY('accepted')},
|
{queryKey: RQKEY('accepted')},
|
||||||
(old?: ConvoListQueryData) => {
|
(old?: ConvoListQueryData) => {
|
||||||
@@ -398,55 +423,50 @@ export function ListConvosProviderInner({
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
} else if (ChatBskyConvoDefs.isLogMuteConvo(log)) {
|
} else if (ChatBskyConvoDefs.isLogMuteConvo(log)) {
|
||||||
queryClient.setQueriesData(
|
updateConvoInAllLists(log.convoId, convo => ({
|
||||||
{queryKey: [RQKEY_ROOT]},
|
...convo,
|
||||||
(old?: ConvoListQueryData) =>
|
muted: true,
|
||||||
optimisticUpdate(log.convoId, old, convo => ({
|
rev: log.rev,
|
||||||
...convo,
|
}))
|
||||||
muted: true,
|
|
||||||
rev: log.rev,
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
} else if (ChatBskyConvoDefs.isLogUnmuteConvo(log)) {
|
} else if (ChatBskyConvoDefs.isLogUnmuteConvo(log)) {
|
||||||
queryClient.setQueriesData(
|
updateConvoInAllLists(log.convoId, convo => ({
|
||||||
{queryKey: [RQKEY_ROOT]},
|
...convo,
|
||||||
(old?: ConvoListQueryData) =>
|
muted: false,
|
||||||
optimisticUpdate(log.convoId, old, convo => ({
|
rev: log.rev,
|
||||||
...convo,
|
}))
|
||||||
muted: false,
|
|
||||||
rev: log.rev,
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
} else if (ChatBskyConvoDefs.isLogLockConvo(log)) {
|
} else if (ChatBskyConvoDefs.isLogLockConvo(log)) {
|
||||||
mutateConvoView(log.convoId, convo =>
|
mutateConvoView(log.convoId, convo => {
|
||||||
ChatBskyConvoDefs.isGroupConvo(convo.kind)
|
if (ChatBskyConvoDefs.isGroupConvo(convo.kind)) {
|
||||||
? {
|
return {
|
||||||
...convo,
|
...convo,
|
||||||
kind: {...convo.kind, lockStatus: 'locked'},
|
kind: {...convo.kind, lockStatus: 'locked'},
|
||||||
rev: log.rev,
|
rev: log.rev,
|
||||||
}
|
}
|
||||||
: {...convo, rev: log.rev},
|
}
|
||||||
)
|
return {...convo, rev: log.rev}
|
||||||
|
})
|
||||||
} else if (ChatBskyConvoDefs.isLogUnlockConvo(log)) {
|
} else if (ChatBskyConvoDefs.isLogUnlockConvo(log)) {
|
||||||
mutateConvoView(log.convoId, convo =>
|
mutateConvoView(log.convoId, convo => {
|
||||||
ChatBskyConvoDefs.isGroupConvo(convo.kind)
|
if (ChatBskyConvoDefs.isGroupConvo(convo.kind)) {
|
||||||
? {
|
return {
|
||||||
...convo,
|
...convo,
|
||||||
kind: {...convo.kind, lockStatus: 'unlocked'},
|
kind: {...convo.kind, lockStatus: 'unlocked'},
|
||||||
rev: log.rev,
|
rev: log.rev,
|
||||||
}
|
}
|
||||||
: {...convo, rev: log.rev},
|
}
|
||||||
)
|
return {...convo, rev: log.rev}
|
||||||
|
})
|
||||||
} else if (ChatBskyConvoDefs.isLogLockConvoPermanently(log)) {
|
} else if (ChatBskyConvoDefs.isLogLockConvoPermanently(log)) {
|
||||||
mutateConvoView(log.convoId, convo =>
|
mutateConvoView(log.convoId, convo => {
|
||||||
ChatBskyConvoDefs.isGroupConvo(convo.kind)
|
if (ChatBskyConvoDefs.isGroupConvo(convo.kind)) {
|
||||||
? {
|
return {
|
||||||
...convo,
|
...convo,
|
||||||
kind: {...convo.kind, lockStatus: 'locked-permanently'},
|
kind: {...convo.kind, lockStatus: 'locked-permanently'},
|
||||||
rev: log.rev,
|
rev: log.rev,
|
||||||
}
|
}
|
||||||
: {...convo, rev: log.rev},
|
}
|
||||||
)
|
return {...convo, rev: log.rev}
|
||||||
|
})
|
||||||
} else if (
|
} else if (
|
||||||
ChatBskyConvoDefs.isLogCreateJoinLink(log) ||
|
ChatBskyConvoDefs.isLogCreateJoinLink(log) ||
|
||||||
ChatBskyConvoDefs.isLogEditJoinLink(log) ||
|
ChatBskyConvoDefs.isLogEditJoinLink(log) ||
|
||||||
@@ -455,37 +475,68 @@ export function ListConvosProviderInner({
|
|||||||
) {
|
) {
|
||||||
// Join link data not included in the log event, trigger refetch to get it
|
// Join link data not included in the log event, trigger refetch to get it
|
||||||
debouncedRefetch()
|
debouncedRefetch()
|
||||||
|
} else if (ChatBskyConvoDefs.isLogEditGroup(log)) {
|
||||||
|
// Updated group details (name etc.) aren't included in the log
|
||||||
|
// event, so refetch to pick them up.
|
||||||
|
debouncedRefetch()
|
||||||
} else if (
|
} else if (
|
||||||
ChatBskyConvoDefs.isLogApproveJoinRequest(log) ||
|
ChatBskyConvoDefs.isLogApproveJoinRequest(log) ||
|
||||||
ChatBskyConvoDefs.isLogRejectJoinRequest(log)
|
ChatBskyConvoDefs.isLogRejectJoinRequest(log)
|
||||||
) {
|
) {
|
||||||
queryClient.setQueriesData(
|
// Route through mutateConvoView (not updateConvoInAllLists) so the
|
||||||
{queryKey: [RQKEY_ROOT]},
|
// single-convo cache updates too, keeping the in-convo requests
|
||||||
(old?: ConvoListQueryData) =>
|
// banner in sync.
|
||||||
updateGroupConvoJoinRequestCount(log, old, -1),
|
mutateConvoView(log.convoId, convo =>
|
||||||
|
applyJoinRequestCountDelta(convo, log.rev, -1),
|
||||||
)
|
)
|
||||||
} else if (ChatBskyConvoDefs.isLogIncomingJoinRequest(log)) {
|
} else if (ChatBskyConvoDefs.isLogIncomingJoinRequest(log)) {
|
||||||
queryClient.setQueriesData(
|
// Route through mutateConvoView (not updateConvoInAllLists) so the
|
||||||
{queryKey: [RQKEY_ROOT]},
|
// single-convo cache updates too, letting the in-convo requests
|
||||||
(old?: ConvoListQueryData) =>
|
// banner appear live.
|
||||||
updateGroupConvoJoinRequestCount(log, old, 1),
|
mutateConvoView(log.convoId, convo =>
|
||||||
|
applyJoinRequestCountDelta(convo, log.rev, 1),
|
||||||
)
|
)
|
||||||
|
} else if (ChatBskyConvoDefs.isLogReadJoinRequests(log)) {
|
||||||
|
// The owner marked join requests as read (possibly on another
|
||||||
|
// device). Zero the unread count but keep the total, mirroring the
|
||||||
|
// useMarkJoinRequestsRead mutation.
|
||||||
|
mutateConvoView(log.convoId, convo => {
|
||||||
|
if (!ChatBskyConvoDefs.isGroupConvo(convo.kind)) {
|
||||||
|
return {...convo, rev: log.rev}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...convo,
|
||||||
|
kind: {...convo.kind, unreadJoinRequestCount: 0},
|
||||||
|
rev: log.rev,
|
||||||
|
}
|
||||||
|
})
|
||||||
} else if (ChatBskyConvoDefs.isLogOutgoingJoinRequest(log)) {
|
} else if (ChatBskyConvoDefs.isLogOutgoingJoinRequest(log)) {
|
||||||
// Viewer isn't in the chat yet, no need to do anything
|
// Viewer isn't in the chat yet, but the inbox surfaces outgoing
|
||||||
} else if (ChatBskyConvoDefs.isLogAddReaction(log)) {
|
// requests, so refetch to pick up the new entry.
|
||||||
queryClient.setQueriesData(
|
debouncedRefetch()
|
||||||
{queryKey: [RQKEY_ROOT]},
|
} else if (ChatBskyConvoDefs.isLogWithdrawIncomingJoinRequest(log)) {
|
||||||
(old?: ConvoListQueryData) =>
|
// A requester rescinded their request to a group the viewer owns.
|
||||||
optimisticUpdate(log.convoId, old, convo => ({
|
// Mirror of isLogIncomingJoinRequest: decrement the counts.
|
||||||
...convo,
|
mutateConvoView(log.convoId, convo =>
|
||||||
lastReaction: {
|
applyJoinRequestCountDelta(convo, log.rev, -1),
|
||||||
$type: 'chat.bsky.convo.defs#messageAndReactionView',
|
|
||||||
reaction: log.reaction,
|
|
||||||
message: log.message,
|
|
||||||
},
|
|
||||||
rev: log.rev,
|
|
||||||
})),
|
|
||||||
)
|
)
|
||||||
|
} else if (ChatBskyConvoDefs.isLogWithdrawOutgoingJoinRequest(log)) {
|
||||||
|
// The viewer rescinded their own outgoing join request (possibly on
|
||||||
|
// another device). Remove it from the requests inbox cache.
|
||||||
|
queryClient.setQueriesData<ConvoRequestListQueryData>(
|
||||||
|
{queryKey: [REQUESTS_RQKEY_ROOT]},
|
||||||
|
old => optimisticDeleteJoinRequest(log.convoId, old),
|
||||||
|
)
|
||||||
|
} else if (ChatBskyConvoDefs.isLogAddReaction(log)) {
|
||||||
|
updateConvoInAllLists(log.convoId, convo => ({
|
||||||
|
...convo,
|
||||||
|
lastReaction: {
|
||||||
|
$type: 'chat.bsky.convo.defs#messageAndReactionView',
|
||||||
|
reaction: log.reaction,
|
||||||
|
message: log.message,
|
||||||
|
},
|
||||||
|
rev: log.rev,
|
||||||
|
}))
|
||||||
} else if (ChatBskyConvoDefs.isLogAddMember(log)) {
|
} else if (ChatBskyConvoDefs.isLogAddMember(log)) {
|
||||||
const data = log.message.data
|
const data = log.message.data
|
||||||
if (
|
if (
|
||||||
@@ -739,27 +790,58 @@ function optimisticUpdate(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateGroupConvoJoinRequestCount(
|
function applyJoinRequestCountDelta(
|
||||||
log: {convoId: string; rev: string},
|
convo: ChatBskyConvoDefs.ConvoView,
|
||||||
old: ConvoListQueryData | undefined,
|
rev: string,
|
||||||
delta: 1 | -1,
|
delta: 1 | -1,
|
||||||
) {
|
): ChatBskyConvoDefs.ConvoView {
|
||||||
return optimisticUpdate(log.convoId, old, convo => {
|
// Join requests are only meaningful for group convos.
|
||||||
// Join requests are only meaningful for group convos.
|
if (!ChatBskyConvoDefs.isGroupConvo(convo.kind)) {
|
||||||
if (!ChatBskyConvoDefs.isGroupConvo(convo.kind)) {
|
return {...convo, rev}
|
||||||
return {...convo, rev: log.rev}
|
}
|
||||||
|
// Bump the total and unread counts together. Both are clamped at 0 and
|
||||||
|
// collapse to undefined when empty, matching the server's shape.
|
||||||
|
const bump = (current: number | undefined) => {
|
||||||
|
const next = Math.max(0, (current ?? 0) + delta)
|
||||||
|
return next === 0 ? undefined : next
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...convo,
|
||||||
|
kind: {
|
||||||
|
...convo.kind,
|
||||||
|
joinRequestCount: bump(convo.kind.joinRequestCount),
|
||||||
|
unreadJoinRequestCount: bump(convo.kind.unreadJoinRequestCount),
|
||||||
|
},
|
||||||
|
rev,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveConvoToTopInRequests(
|
||||||
|
updatedConvo: ChatBskyConvoDefs.ConvoView,
|
||||||
|
old: ConvoRequestListQueryData | undefined,
|
||||||
|
): ConvoRequestListQueryData | undefined {
|
||||||
|
if (!old) return old
|
||||||
|
const typedConvo: ConvoRequestListQueryData['pages'][number]['requests'][number] =
|
||||||
|
{
|
||||||
|
$type: 'chat.bsky.convo.defs#convoView',
|
||||||
|
...updatedConvo,
|
||||||
}
|
}
|
||||||
const current = convo.kind.joinRequestCount ?? 0
|
return {
|
||||||
const next = Math.max(0, current + delta)
|
...old,
|
||||||
return {
|
pages: old.pages.map((page, i) => {
|
||||||
...convo,
|
const filtered = page.requests.filter(
|
||||||
kind: {
|
item =>
|
||||||
...convo.kind,
|
!ChatBskyConvoDefs.isConvoView(item) || item.id !== updatedConvo.id,
|
||||||
joinRequestCount: next === 0 ? undefined : next,
|
)
|
||||||
},
|
if (i === 0) {
|
||||||
rev: log.rev,
|
return {
|
||||||
}
|
...page,
|
||||||
})
|
requests: [typedConvo, ...filtered],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {...page, requests: filtered}
|
||||||
|
}),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeMemberFromConvoView(
|
function removeMemberFromConvoView(
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import {type ChatBskyGroupRequestJoin} from '@atproto/api'
|
import {type ChatBskyGroupRequestJoin} from '@atproto/api'
|
||||||
import {useMutation} from '@tanstack/react-query'
|
import {useMutation, useQueryClient} from '@tanstack/react-query'
|
||||||
|
|
||||||
import {DM_SERVICE_HEADERS} from '#/lib/constants'
|
import {DM_SERVICE_HEADERS} from '#/lib/constants'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {useAgent, useSession} from '#/state/session'
|
import {useAgent, useSession} from '#/state/session'
|
||||||
|
import {RQKEY_ROOT as REQUESTS_RQKEY_ROOT} from './list-conversation-requests'
|
||||||
|
|
||||||
export function useRequestJoinGroupChat({
|
export function useRequestJoinGroupChat({
|
||||||
onSuccess,
|
onSuccess,
|
||||||
@@ -13,6 +14,7 @@ export function useRequestJoinGroupChat({
|
|||||||
onError?: (error: Error) => void
|
onError?: (error: Error) => void
|
||||||
} = {}) {
|
} = {}) {
|
||||||
const agent = useAgent()
|
const agent = useAgent()
|
||||||
|
const queryClient = useQueryClient()
|
||||||
const {hasSession} = useSession()
|
const {hasSession} = useSession()
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
@@ -27,6 +29,7 @@ export function useRequestJoinGroupChat({
|
|||||||
return res.data
|
return res.data
|
||||||
},
|
},
|
||||||
onSuccess: data => {
|
onSuccess: data => {
|
||||||
|
void queryClient.invalidateQueries({queryKey: [REQUESTS_RQKEY_ROOT]})
|
||||||
onSuccess?.(data)
|
onSuccess?.(data)
|
||||||
},
|
},
|
||||||
onError: error => {
|
onError: error => {
|
||||||
|
|||||||
@@ -4,6 +4,11 @@ import {useMutation, useQueryClient} from '@tanstack/react-query'
|
|||||||
import {DM_SERVICE_HEADERS} from '#/lib/constants'
|
import {DM_SERVICE_HEADERS} from '#/lib/constants'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {useAgent} from '#/state/session'
|
import {useAgent} from '#/state/session'
|
||||||
|
import {
|
||||||
|
type ConvoRequestListQueryData,
|
||||||
|
markAllRead as markAllRequestsRead,
|
||||||
|
RQKEY_ROOT as REQUESTS_RQKEY_ROOT,
|
||||||
|
} from './list-conversation-requests'
|
||||||
import {RQKEY as CONVO_LIST_KEY} from './list-conversations'
|
import {RQKEY as CONVO_LIST_KEY} from './list-conversations'
|
||||||
|
|
||||||
export function useUpdateAllRead(
|
export function useUpdateAllRead(
|
||||||
@@ -32,6 +37,9 @@ export function useUpdateAllRead(
|
|||||||
},
|
},
|
||||||
onMutate: () => {
|
onMutate: () => {
|
||||||
let prevPages: ChatBskyConvoListConvos.OutputSchema[] = []
|
let prevPages: ChatBskyConvoListConvos.OutputSchema[] = []
|
||||||
|
let prevRequestsQueries: Array<
|
||||||
|
[readonly unknown[], ConvoRequestListQueryData | undefined]
|
||||||
|
> = []
|
||||||
queryClient.setQueryData(
|
queryClient.setQueryData(
|
||||||
CONVO_LIST_KEY(status),
|
CONVO_LIST_KEY(status),
|
||||||
(old?: {
|
(old?: {
|
||||||
@@ -75,11 +83,24 @@ export function useUpdateAllRead(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
if (status === 'request') {
|
||||||
|
prevRequestsQueries =
|
||||||
|
queryClient.getQueriesData<ConvoRequestListQueryData>({
|
||||||
|
queryKey: [REQUESTS_RQKEY_ROOT],
|
||||||
|
})
|
||||||
|
queryClient.setQueriesData<ConvoRequestListQueryData>(
|
||||||
|
{queryKey: [REQUESTS_RQKEY_ROOT]},
|
||||||
|
markAllRequestsRead,
|
||||||
|
)
|
||||||
|
}
|
||||||
onMutate?.()
|
onMutate?.()
|
||||||
return {prevPages}
|
return {prevPages, prevRequestsQueries}
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({queryKey: CONVO_LIST_KEY(status)})
|
void queryClient.invalidateQueries({queryKey: CONVO_LIST_KEY(status)})
|
||||||
|
if (status === 'request') {
|
||||||
|
void queryClient.invalidateQueries({queryKey: [REQUESTS_RQKEY_ROOT]})
|
||||||
|
}
|
||||||
onSuccess?.()
|
onSuccess?.()
|
||||||
},
|
},
|
||||||
onError: (error, _, context) => {
|
onError: (error, _, context) => {
|
||||||
@@ -97,8 +118,18 @@ export function useUpdateAllRead(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
queryClient.invalidateQueries({queryKey: CONVO_LIST_KEY(status)})
|
if (status === 'request' && context?.prevRequestsQueries) {
|
||||||
queryClient.invalidateQueries({queryKey: CONVO_LIST_KEY('all', 'unread')})
|
for (const [queryKey, prevData] of context.prevRequestsQueries) {
|
||||||
|
queryClient.setQueryData(queryKey, prevData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void queryClient.invalidateQueries({queryKey: CONVO_LIST_KEY(status)})
|
||||||
|
void queryClient.invalidateQueries({
|
||||||
|
queryKey: CONVO_LIST_KEY('all', 'unread'),
|
||||||
|
})
|
||||||
|
if (status === 'request') {
|
||||||
|
void queryClient.invalidateQueries({queryKey: [REQUESTS_RQKEY_ROOT]})
|
||||||
|
}
|
||||||
onError?.(error)
|
onError?.(error)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
import {type ChatBskyGroupWithdrawJoinRequest} from '@atproto/api'
|
import {type ChatBskyGroupWithdrawJoinRequest} from '@atproto/api'
|
||||||
import {useMutation} from '@tanstack/react-query'
|
import {useMutation, useQueryClient} from '@tanstack/react-query'
|
||||||
|
|
||||||
import {DM_SERVICE_HEADERS} from '#/lib/constants'
|
import {DM_SERVICE_HEADERS} from '#/lib/constants'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {useAgent, useSession} from '#/state/session'
|
import {useAgent, useSession} from '#/state/session'
|
||||||
|
import {
|
||||||
|
type ConvoRequestListQueryData,
|
||||||
|
optimisticDeleteJoinRequest,
|
||||||
|
RQKEY_ROOT as REQUESTS_RQKEY_ROOT,
|
||||||
|
} from './list-conversation-requests'
|
||||||
|
|
||||||
export function useWithdrawJoinGroupChatRequest({
|
export function useWithdrawJoinGroupChatRequest({
|
||||||
onSuccess,
|
onSuccess,
|
||||||
@@ -13,6 +18,7 @@ export function useWithdrawJoinGroupChatRequest({
|
|||||||
onError?: (error: Error) => void
|
onError?: (error: Error) => void
|
||||||
} = {}) {
|
} = {}) {
|
||||||
const agent = useAgent()
|
const agent = useAgent()
|
||||||
|
const queryClient = useQueryClient()
|
||||||
const {hasSession} = useSession()
|
const {hasSession} = useSession()
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
@@ -27,7 +33,11 @@ export function useWithdrawJoinGroupChatRequest({
|
|||||||
)
|
)
|
||||||
return res.data
|
return res.data
|
||||||
},
|
},
|
||||||
onSuccess: data => {
|
onSuccess: (data, {convoId}) => {
|
||||||
|
queryClient.setQueriesData<ConvoRequestListQueryData>(
|
||||||
|
{queryKey: [REQUESTS_RQKEY_ROOT]},
|
||||||
|
old => optimisticDeleteJoinRequest(convoId, old),
|
||||||
|
)
|
||||||
onSuccess?.(data)
|
onSuccess?.(data)
|
||||||
},
|
},
|
||||||
onError: error => {
|
onError: error => {
|
||||||
|
|||||||
Reference in New Issue
Block a user