Bluesky app fork with some witchin' additions ๐Ÿ’ซ
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

[๐Ÿด] Mutate data instead of invalidating queries when muting or unmuting (#3946)

* mutate for mutes

* mutate data for mutes

* add initial data, `useConvoQuery` in `ConvoMenu`

* `useInitialData`

* don't use `identifier` for notifications, use `dates` instead

* better implementation

* simplify

* simplify

* fix types

authored by

Hailey and committed by
GitHub
f928e0a5 8f56f79c

+92 -97
+19 -24
src/components/dms/ConvoMenu.tsx
··· 2 2 import {Keyboard, Pressable, View} from 'react-native' 3 3 import {AppBskyActorDefs} from '@atproto/api' 4 4 import {ChatBskyConvoDefs} from '@atproto-labs/api' 5 + import {ConvoView} from '@atproto-labs/api/dist/client/types/chat/bsky/convo/defs' 5 6 import {msg, Trans} from '@lingui/macro' 6 7 import {useLingui} from '@lingui/react' 7 8 import {useNavigation} from '@react-navigation/native' 8 9 9 10 import {NavigationProp} from '#/lib/routes/types' 10 - import {useMarkAsReadMutation} from '#/state/queries/messages/conversation' 11 + import { 12 + useConvoQuery, 13 + useMarkAsReadMutation, 14 + } from '#/state/queries/messages/conversation' 11 15 import {useLeaveConvo} from '#/state/queries/messages/leave-conversation' 12 - import { 13 - useMuteConvo, 14 - useUnmuteConvo, 15 - } from '#/state/queries/messages/mute-conversation' 16 + import {useMuteConvo} from '#/state/queries/messages/mute-conversation' 16 17 import * as Toast from '#/view/com/util/Toast' 17 18 import {atoms as a, useTheme} from '#/alf' 18 19 import {ArrowBoxLeft_Stroke2_Corner0_Rounded as ArrowBoxLeft} from '#/components/icons/ArrowBoxLeft' ··· 28 29 import {Bubble_Stroke2_Corner2_Rounded as Bubble} from '../icons/Bubble' 29 30 30 31 let ConvoMenu = ({ 31 - convo, 32 + convo: initialConvo, 32 33 profile, 33 - onUpdateConvo, 34 34 control, 35 35 currentScreen, 36 36 showMarkAsRead, 37 37 hideTrigger, 38 38 triggerOpacity, 39 39 }: { 40 - convo: ChatBskyConvoDefs.ConvoView 40 + convo: ConvoView 41 41 profile: AppBskyActorDefs.ProfileViewBasic 42 42 onUpdateConvo?: (convo: ChatBskyConvoDefs.ConvoView) => void 43 43 control?: Menu.MenuControlProps ··· 52 52 const leaveConvoControl = Prompt.usePromptControl() 53 53 const {mutate: markAsRead} = useMarkAsReadMutation() 54 54 55 + const {data: convo} = useConvoQuery(initialConvo) 56 + 55 57 const onNavigateToProfile = useCallback(() => { 56 58 navigation.navigate('Profile', {name: profile.did}) 57 59 }, [navigation, profile.did]) 58 60 59 - const {mutate: muteConvo} = useMuteConvo(convo.id, { 61 + const {mutate: muteConvo} = useMuteConvo(convo?.id, { 60 62 onSuccess: data => { 61 - onUpdateConvo?.(data.convo) 62 - Toast.show(_(msg`Chat muted`)) 63 + if (data.convo.muted) { 64 + Toast.show(_(msg`Chat muted`)) 65 + } else { 66 + Toast.show(_(msg`Chat unmuted`)) 67 + } 63 68 }, 64 69 onError: () => { 65 70 Toast.show(_(msg`Could not mute chat`)) 66 71 }, 67 72 }) 68 73 69 - const {mutate: unmuteConvo} = useUnmuteConvo(convo.id, { 70 - onSuccess: data => { 71 - onUpdateConvo?.(data.convo) 72 - Toast.show(_(msg`Chat unmuted`)) 73 - }, 74 - onError: () => { 75 - Toast.show(_(msg`Could not unmute chat`)) 76 - }, 77 - }) 78 - 79 - const {mutate: leaveConvo} = useLeaveConvo(convo.id, { 74 + const {mutate: leaveConvo} = useLeaveConvo(convo?.id, { 80 75 onSuccess: () => { 81 76 if (currentScreen === 'conversation') { 82 77 navigation.replace('Messages') ··· 121 116 label={_(msg`Mark as read`)} 122 117 onPress={() => 123 118 markAsRead({ 124 - convoId: convo.id, 119 + convoId: convo?.id, 125 120 }) 126 121 }> 127 122 <Menu.ItemText> ··· 140 135 </Menu.Item> 141 136 <Menu.Item 142 137 label={_(msg`Mute notifications`)} 143 - onPress={() => (convo?.muted ? unmuteConvo() : muteConvo())}> 138 + onPress={() => muteConvo({mute: !convo?.muted})}> 144 139 <Menu.ItemText> 145 140 {convo?.muted ? ( 146 141 <Trans>Unmute notifications</Trans>
+11 -16
src/screens/Messages/Conversation/index.tsx
··· 56 56 57 57 function Inner() { 58 58 const t = useTheme() 59 - const convo = useConvo() 59 + const convoState = useConvo() 60 60 const {_} = useLingui() 61 61 62 62 const [hasInitiallyRendered, setHasInitiallyRendered] = React.useState(false) ··· 72 72 React.useEffect(() => { 73 73 if ( 74 74 !hasInitiallyRendered && 75 - convo.status === ConvoStatus.Ready && 76 - !convo.isFetchingHistory 75 + convoState.status === ConvoStatus.Ready && 76 + !convoState.isFetchingHistory 77 77 ) { 78 78 setTimeout(() => { 79 79 setHasInitiallyRendered(true) 80 80 }, 15) 81 81 } 82 - }, [convo.isFetchingHistory, convo.items, convo.status, hasInitiallyRendered]) 82 + }, [convoState.isFetchingHistory, convoState.status, hasInitiallyRendered]) 83 83 84 - if (convo.status === ConvoStatus.Error) { 84 + if (convoState.status === ConvoStatus.Error) { 85 85 return ( 86 86 <CenteredView style={a.flex_1} sideBorders> 87 87 <Header /> 88 88 <Error 89 89 title={_(msg`Something went wrong`)} 90 90 message={_(msg`We couldn't load this conversation`)} 91 - onRetry={() => convo.error.retry()} 91 + onRetry={() => convoState.error.retry()} 92 92 /> 93 93 </CenteredView> 94 94 ) ··· 106 106 behavior="padding" 107 107 contentContainerStyle={a.flex_1}> 108 108 <CenteredView style={a.flex_1} sideBorders> 109 - <Header profile={convo.recipients?.[0]} /> 109 + <Header profile={convoState.recipients?.[0]} /> 110 110 <View style={[a.flex_1]}> 111 - {convo.status !== ConvoStatus.Ready ? ( 111 + {convoState.status !== ConvoStatus.Ready ? ( 112 112 <ListMaybePlaceholder isLoading /> 113 113 ) : ( 114 114 <MessagesList /> ··· 145 145 const {_} = useLingui() 146 146 const {gtTablet} = useBreakpoints() 147 147 const navigation = useNavigation<NavigationProp>() 148 - const convo = useConvo() 148 + const convoState = useConvo() 149 149 150 150 const onPressBack = useCallback(() => { 151 151 if (isWeb) { ··· 154 154 navigation.goBack() 155 155 } 156 156 }, [navigation]) 157 - 158 - const onUpdateConvo = useCallback(() => { 159 - // TODO eric update muted state 160 - }, []) 161 157 162 158 return ( 163 159 <View ··· 234 230 </> 235 231 )} 236 232 </View> 237 - {convo.status === ConvoStatus.Ready && profile ? ( 233 + {convoState.status === ConvoStatus.Ready && profile ? ( 238 234 <ConvoMenu 239 - convo={convo.convo} 235 + convo={convoState.convo} 240 236 profile={profile} 241 - onUpdateConvo={onUpdateConvo} 242 237 currentScreen="conversation" 243 238 /> 244 239 ) : (
+9 -4
src/state/queries/messages/conversation.ts
··· 1 1 import {BskyAgent} from '@atproto-labs/api' 2 + import {ConvoView} from '@atproto-labs/api/dist/client/types/chat/bsky/convo/defs' 2 3 import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query' 3 4 4 5 import {useOnMarkAsRead} from '#/state/queries/messages/list-converations' ··· 9 10 const RQKEY_ROOT = 'convo' 10 11 export const RQKEY = (convoId: string) => [RQKEY_ROOT, convoId] 11 12 12 - export function useConvoQuery(convoId: string) { 13 + export function useConvoQuery(convo: ConvoView) { 13 14 const headers = useHeaders() 14 15 const {serviceUrl} = useDmServiceUrlStorage() 15 16 16 17 return useQuery({ 17 - queryKey: RQKEY(convoId), 18 + queryKey: RQKEY(convo.id), 18 19 queryFn: async () => { 19 20 const agent = new BskyAgent({service: serviceUrl}) 20 21 const {data} = await agent.api.chat.bsky.convo.getConvo( 21 - {convoId}, 22 + {convoId: convo.id}, 22 23 {headers}, 23 24 ) 24 25 return data.convo 25 26 }, 27 + initialData: convo, 26 28 }) 27 29 } 28 30 ··· 37 39 convoId, 38 40 messageId, 39 41 }: { 40 - convoId: string 42 + convoId?: string 41 43 messageId?: string 42 44 }) => { 45 + if (!convoId) throw new Error('No convoId provided') 46 + 43 47 const agent = new BskyAgent({service: serviceUrl}) 44 48 await agent.api.chat.bsky.convo.updateRead( 45 49 { ··· 53 57 ) 54 58 }, 55 59 onMutate({convoId}) { 60 + if (!convoId) throw new Error('No convoId provided') 56 61 optimisticUpdate(convoId) 57 62 }, 58 63 onSettled() {
+3 -2
src/state/queries/messages/leave-conversation.ts
··· 11 11 import {useHeaders} from './temp-headers' 12 12 13 13 export function useLeaveConvo( 14 - convoId: string, 14 + convoId: string | undefined, 15 15 { 16 16 onSuccess, 17 17 onError, ··· 26 26 27 27 return useMutation({ 28 28 mutationFn: async () => { 29 + if (!convoId) throw new Error('No convoId provided') 30 + 29 31 const agent = new BskyAgent({service: serviceUrl}) 30 32 const {data} = await agent.api.chat.bsky.convo.leaveConvo( 31 33 {convoId}, ··· 41 43 pageParams: Array<string | undefined> 42 44 pages: Array<ChatBskyConvoListConvos.OutputSchema> 43 45 }) => { 44 - console.log('old', old) 45 46 if (!old) return old 46 47 return { 47 48 ...old,
+50 -51
src/state/queries/messages/mute-conversation.ts
··· 1 1 import { 2 2 BskyAgent, 3 + ChatBskyConvoDefs, 4 + ChatBskyConvoListConvos, 3 5 ChatBskyConvoMuteConvo, 4 - ChatBskyConvoUnmuteConvo, 5 6 } from '@atproto-labs/api' 6 - import {useMutation, useQueryClient} from '@tanstack/react-query' 7 + import {InfiniteData, useMutation, useQueryClient} from '@tanstack/react-query' 7 8 8 - import {logger} from '#/logger' 9 9 import {useDmServiceUrlStorage} from '#/screens/Messages/Temp/useDmServiceUrlStorage' 10 10 import {RQKEY as CONVO_KEY} from './conversation' 11 11 import {RQKEY as CONVO_LIST_KEY} from './list-converations' 12 12 import {useHeaders} from './temp-headers' 13 13 14 14 export function useMuteConvo( 15 - convoId: string, 15 + convoId: string | undefined, 16 16 { 17 17 onSuccess, 18 18 onError, ··· 26 26 const {serviceUrl} = useDmServiceUrlStorage() 27 27 28 28 return useMutation({ 29 - mutationFn: async () => { 29 + mutationFn: async ({mute}: {mute: boolean}) => { 30 + if (!convoId) throw new Error('No convoId provided') 31 + 30 32 const agent = new BskyAgent({service: serviceUrl}) 31 - const {data} = await agent.api.chat.bsky.convo.muteConvo( 32 - {convoId}, 33 - {headers, encoding: 'application/json'}, 34 - ) 35 - 36 - return data 33 + if (mute) { 34 + const {data} = await agent.api.chat.bsky.convo.muteConvo( 35 + {convoId}, 36 + {headers, encoding: 'application/json'}, 37 + ) 38 + return data 39 + } else { 40 + const {data} = await agent.api.chat.bsky.convo.unmuteConvo( 41 + {convoId}, 42 + {headers, encoding: 'application/json'}, 43 + ) 44 + return data 45 + } 37 46 }, 38 - onSuccess: data => { 39 - queryClient.invalidateQueries({queryKey: CONVO_LIST_KEY}) 40 - queryClient.invalidateQueries({queryKey: CONVO_KEY(convoId)}) 41 - onSuccess?.(data) 42 - }, 43 - onError: error => { 44 - logger.error(error) 45 - onError?.(error) 46 - }, 47 - }) 48 - } 49 - 50 - export function useUnmuteConvo( 51 - convoId: string, 52 - { 53 - onSuccess, 54 - onError, 55 - }: { 56 - onSuccess?: (data: ChatBskyConvoUnmuteConvo.OutputSchema) => void 57 - onError?: (error: Error) => void 58 - }, 59 - ) { 60 - const queryClient = useQueryClient() 61 - const headers = useHeaders() 62 - const {serviceUrl} = useDmServiceUrlStorage() 63 - 64 - return useMutation({ 65 - mutationFn: async () => { 66 - const agent = new BskyAgent({service: serviceUrl}) 67 - const {data} = await agent.api.chat.bsky.convo.unmuteConvo( 68 - {convoId}, 69 - {headers, encoding: 'application/json'}, 47 + onSuccess: (data, params) => { 48 + queryClient.setQueryData<ChatBskyConvoDefs.ConvoView>( 49 + CONVO_KEY(data.convo.id), 50 + prev => { 51 + if (!prev) return 52 + return { 53 + ...prev, 54 + muted: params.mute, 55 + } 56 + }, 70 57 ) 58 + queryClient.setQueryData< 59 + InfiniteData<ChatBskyConvoListConvos.OutputSchema> 60 + >(CONVO_LIST_KEY, prev => { 61 + if (!prev?.pages) return 62 + return { 63 + ...prev, 64 + pages: prev.pages.map(page => ({ 65 + ...page, 66 + convos: page.convos.map(convo => { 67 + if (convo.id !== data.convo.id) return convo 68 + return { 69 + ...convo, 70 + muted: params.mute, 71 + } 72 + }), 73 + })), 74 + } 75 + }) 71 76 72 - return data 73 - }, 74 - onSuccess: data => { 75 - queryClient.invalidateQueries({queryKey: CONVO_LIST_KEY}) 76 - queryClient.invalidateQueries({queryKey: CONVO_KEY(convoId)}) 77 77 onSuccess?.(data) 78 78 }, 79 - onError: error => { 80 - logger.error(error) 81 - onError?.(error) 79 + onError: e => { 80 + onError?.(e) 82 81 }, 83 82 }) 84 83 }