mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
0
fork

Configure Feed

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

Clean up issued and create screens/flows

+594 -121
+25
src/lib/async/poll.ts
··· 1 + import {timeout} from './timeout' 2 + 3 + export async function poll<T>( 4 + retries: number, 5 + delay: number, 6 + shouldExit: ( 7 + props: {response: T; error: undefined} | {response: undefined; error: any}, 8 + ) => boolean, 9 + request: () => Promise<T>, 10 + ): Promise<T | undefined> { 11 + while (retries > 0) { 12 + try { 13 + const v = await request() 14 + if (shouldExit({response: v, error: undefined})) { 15 + return v 16 + } 17 + } catch (e: any) { 18 + if (shouldExit({response: undefined, error: e})) { 19 + return undefined 20 + } 21 + } 22 + await timeout(delay) 23 + retries-- 24 + } 25 + }
+15 -6
src/screens/Profile/Vouches/Create/index.tsx
··· 1 1 import React from 'react' 2 2 import {View} from 'react-native' 3 - import {Trans, msg} from '@lingui/macro' 3 + import {msg,Trans} from '@lingui/macro' 4 4 import {useLingui} from '@lingui/react' 5 5 import {ZodError} from 'zod' 6 6 7 - import * as Layout from '#/components/Layout' 7 + import {useNavigationDeduped} from '#/lib/hooks/useNavigationDeduped' 8 + import {useCreateVouch} from '#/state/queries/vouches/useCreateVouch' 9 + import {useSession} from '#/state/session' 10 + import * as Toast from '#/view/com/util/Toast' 8 11 import {atoms as a, useGutters} from '#/alf' 9 - import {Button, ButtonText, ButtonIcon} from '#/components/Button' 12 + import {Admonition} from '#/components/Admonition' 13 + import {Button, ButtonIcon,ButtonText} from '#/components/Button' 10 14 import * as TextField from '#/components/forms/TextField' 11 - import {useCreateVouch} from '#/state/queries/vouches/useCreateVouch' 15 + import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' 16 + import * as Layout from '#/components/Layout' 12 17 import {Loader} from '#/components/Loader' 13 - import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' 14 - import {Admonition} from '#/components/Admonition' 15 18 16 19 export function Screen() { 17 20 const baseGutters = useGutters(['base']) ··· 39 42 40 43 function Form() { 41 44 const {_} = useLingui() 45 + const navigation = useNavigationDeduped() 46 + const {currentAccount} = useSession() 42 47 const [subject, setSubject] = React.useState('') 43 48 const [relationship, setRelationship] = React.useState('') 44 49 const [errors, setErrors] = React.useState<string[]>([]) ··· 49 54 setErrors([]) 50 55 try { 51 56 await createVouch({subject, relationship}) 57 + navigation.navigate('ProfileVouches', { 58 + name: currentAccount!.handle, 59 + }) 60 + Toast.show(_(`Vouch created`), 'check') 52 61 } catch (e: any) { 53 62 if (e instanceof ZodError) { 54 63 setErrors(e.errors.map(err => err.message))
+149 -38
src/screens/Profile/Vouches/Issued/index.tsx
··· 1 1 import React from 'react' 2 - import {View} from 'react-native' 3 - import {Trans, msg} from '@lingui/macro' 2 + import {ListRenderItemInfo,View} from 'react-native' 3 + import {AppBskyActorDefs,AppBskyGraphDefs} from '@atproto/api' 4 + import {msg,Trans} from '@lingui/macro' 4 5 import {useLingui} from '@lingui/react' 5 - import {ZodError} from 'zod' 6 6 7 - import * as Layout from '#/components/Layout' 8 - import {atoms as a, useGutters} from '#/alf' 9 - import {Button, ButtonText, ButtonIcon} from '#/components/Button' 10 - import * as TextField from '#/components/forms/TextField' 11 - import {useCreateVouch} from '#/state/queries/vouches/useCreateVouch' 12 - import {Loader} from '#/components/Loader' 13 - import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' 7 + import {useVouchesIssued} from '#/state/queries/vouches/useVouchesIssued' 8 + import {List} from '#/view/com/util/List' 9 + import {VouchList} from '#/screens/Profile/Vouches/components/Vouch' 10 + import {atoms as a, useGutters,useTheme} from '#/alf' 14 11 import {Admonition} from '#/components/Admonition' 15 - import {useVouchesIssued} from '#/state/queries/vouches/useVouchesIssued' 12 + import * as Layout from '#/components/Layout' 13 + import {InlineLinkText} from '#/components/Link' 16 14 import {Text} from '#/components/Typography' 17 15 18 16 export function Screen() { 19 - const baseGutters = useGutters(['base']) 20 17 return ( 21 18 <Layout.Screen> 22 19 <Layout.Header.Outer> ··· 29 26 <Layout.Header.Slot /> 30 27 </Layout.Header.Outer> 31 28 32 - <Layout.Content> 33 - <View style={[baseGutters]}> 34 - <Inner /> 35 - </View> 36 - </Layout.Content> 29 + <Inner /> 37 30 </Layout.Screen> 38 31 ) 39 32 } 33 + 34 + type ListItem = 35 + | { 36 + key: string 37 + type: 'error' 38 + } 39 + | { 40 + key: string 41 + type: 'placeholder' 42 + } 43 + | { 44 + key: string 45 + type: 'empty' 46 + } 47 + | { 48 + key: string 49 + type: 'vouch' 50 + vouch: AppBskyGraphDefs.VouchView 51 + subject: AppBskyActorDefs.ProfileViewBasic 52 + } 40 53 41 54 export function Inner() { 42 - const {data: vouches, isLoading, error} = useVouchesIssued() 55 + const [isPTR, setIsPTR] = React.useState(false) 56 + const { 57 + data, 58 + isFetching, 59 + error, 60 + refetch, 61 + fetchNextPage, 62 + isFetchingNextPage, 63 + hasNextPage, 64 + } = useVouchesIssued() 65 + 66 + const onEndReached = React.useCallback(() => { 67 + if (!hasNextPage || isFetchingNextPage) return 68 + fetchNextPage() 69 + }, [fetchNextPage, isFetchingNextPage, hasNextPage]) 70 + 71 + const onPullToRefresh = React.useCallback(async () => { 72 + setIsPTR(true) 73 + await refetch() 74 + setIsPTR(false) 75 + }, [setIsPTR, refetch]) 76 + 77 + const items = React.useMemo<ListItem[]>(() => { 78 + const _items: ListItem[] = [] 79 + 80 + const vouches = data?.pages.flatMap(page => page.vouches) || [] 81 + 82 + if (vouches.length) { 83 + for (const vouch of vouches) { 84 + _items.push({ 85 + key: vouch.cid, 86 + type: 'vouch', 87 + vouch, 88 + subject: vouch.subject!, 89 + }) 90 + } 91 + } else { 92 + _items.push({key: 'empty', type: 'empty'}) 93 + } 94 + 95 + if (isFetching) { 96 + _items.push({key: 'loading', type: 'placeholder'}) 97 + } else if (error) { 98 + _items.push({key: 'error', type: 'error'}) 99 + } 100 + 101 + return _items 102 + }, [data, error, isFetching]) 103 + 104 + const renderItem = React.useCallback( 105 + ({item, index}: ListRenderItemInfo<ListItem>) => { 106 + switch (item.type) { 107 + case 'vouch': { 108 + return ( 109 + <VouchList 110 + vouch={item.vouch} 111 + subject={item.subject} 112 + first={index === 0} 113 + /> 114 + ) 115 + } 116 + case 'empty': { 117 + return <Empty /> 118 + } 119 + case 'placeholder': { 120 + // TODO 121 + return <View style={[a.gap_md]} /> 122 + } 123 + case 'error': { 124 + // TODO 125 + return ( 126 + <Admonition type="error"> 127 + <Trans>Error</Trans> 128 + </Admonition> 129 + ) 130 + } 131 + } 132 + }, 133 + [], 134 + ) 135 + 136 + return ( 137 + <List 138 + data={items} 139 + keyExtractor={item => item.key} 140 + renderItem={renderItem} 141 + refreshing={isPTR} 142 + onRefresh={onPullToRefresh} 143 + initialNumToRender={10} 144 + onEndReached={onEndReached} 145 + desktopFixedHeight 146 + keyboardShouldPersistTaps="handled" 147 + keyboardDismissMode="on-drag" 148 + /> 149 + ) 150 + } 151 + 152 + export function Empty() { 153 + const t = useTheme() 154 + const {_} = useLingui() 155 + const gutters = useGutters(['base', 'wide']) 43 156 44 157 return ( 45 - <View style={[]}> 46 - <View style={[a.gap_sm]}> 47 - {isLoading ? ( 48 - <Loader /> 49 - ) : error || !vouches ? ( 50 - <></> 51 - ) : vouches.length ? ( 52 - <> 53 - {vouches.map(v => ( 54 - <View> 55 - <Text>{v.record.subject}</Text> 56 - </View> 57 - ))} 58 - </> 59 - ) : ( 60 - <> 61 - <Text>No vouches</Text> 62 - </> 63 - )} 64 - </View> 158 + <View style={[gutters]}> 159 + <Text 160 + style={[ 161 + a.text_md, 162 + a.leading_snug, 163 + a.text_center, 164 + t.atoms.text_contrast_medium, 165 + ]}> 166 + <Trans> 167 + You haven't vouched for anyone.{' '} 168 + <InlineLinkText 169 + label={_(msg`Create a vouch`)} 170 + to={{screen: 'ProfileVouchesCreate'}} 171 + style={[a.text_md, a.leading_snug]}> 172 + <Trans>Create one here.</Trans> 173 + </InlineLinkText> 174 + </Trans> 175 + </Text> 65 176 </View> 66 177 ) 67 178 }
+177
src/screens/Profile/Vouches/components/Vouch.tsx
··· 1 + import React from 'react' 2 + import {View} from 'react-native' 3 + import { 4 + AppBskyActorDefs, 5 + AppBskyGraphDefs, 6 + AppBskyGraphVouch, 7 + } from '@atproto/api' 8 + import {msg,Trans} from '@lingui/macro' 9 + import {useLingui} from '@lingui/react' 10 + 11 + import {useGetTimeAgo} from '#/lib/hooks/useTimeAgo' 12 + import {useRevokeVouch} from '#/state/queries/vouches/useRevokeVouch' 13 + import * as Toast from '#/view/com/util/Toast' 14 + import {UserAvatar} from '#/view/com/util/UserAvatar' 15 + import {atoms as a, useGutters,useTheme} from '#/alf' 16 + import {Button, ButtonIcon,ButtonText} from '#/components/Button' 17 + import {Divider} from '#/components/Divider' 18 + import {TimesLarge_Stroke2_Corner0_Rounded as Times} from '#/components/icons/Times' 19 + import {Loader} from '#/components/Loader' 20 + import {Text} from '#/components/Typography' 21 + 22 + export function Vouch({ 23 + vouch, 24 + subject, 25 + }: { 26 + vouch: AppBskyGraphDefs.VouchView 27 + subject: AppBskyActorDefs.ProfileViewBasic 28 + }) { 29 + const t = useTheme() 30 + const record = vouch.record 31 + const ago = useGetTimeAgo() 32 + 33 + if (!AppBskyGraphVouch.isRecord(record)) return null 34 + 35 + return ( 36 + <View 37 + style={[ 38 + a.p_sm, 39 + a.rounded_md, 40 + a.flex_1, 41 + a.gap_sm, 42 + t.atoms.bg_contrast_25, 43 + { 44 + width: 240, 45 + }, 46 + ]}> 47 + <View style={[a.flex_row, a.align_start, a.gap_sm]}> 48 + <UserAvatar size={32} avatar={subject.avatar} /> 49 + <View style={[a.gap_2xs]}> 50 + <Text style={[a.text_md, a.font_bold, a.leading_tight]}> 51 + @{subject.handle} 52 + </Text> 53 + <Text style={[a.leading_tight, t.atoms.text_contrast_medium]}> 54 + {record.relationship} 55 + </Text> 56 + </View> 57 + </View> 58 + <Divider /> 59 + <View style={[a.flex_row, a.align_start, a.justify_between, a.gap_xl]}> 60 + <Text style={[a.text_xs, a.font_bold, a.leading_tight]}> 61 + {vouch.accept ? 'Accepted' : 'Pending'} 62 + </Text> 63 + <Text style={[a.text_xs, a.leading_tight]}> 64 + <Trans>{ago(record.createdAt, new Date())} ago</Trans> 65 + </Text> 66 + </View> 67 + </View> 68 + ) 69 + } 70 + 71 + export function VouchList({ 72 + vouch, 73 + subject, 74 + first, 75 + }: { 76 + vouch: AppBskyGraphDefs.VouchView 77 + subject: AppBskyActorDefs.ProfileViewBasic 78 + first?: boolean 79 + }) { 80 + const t = useTheme() 81 + const {_} = useLingui() 82 + const record = vouch.record as AppBskyGraphVouch.Record 83 + const ago = useGetTimeAgo() 84 + const gutters = useGutters(['compact', 'base']) 85 + const relationship = useRelationshipLabel(record.relationship) 86 + const {mutateAsync, isPending} = useRevokeVouch() 87 + 88 + const revoke = React.useCallback(async () => { 89 + try { 90 + await mutateAsync({vouch}) 91 + Toast.show(_(msg`Vouch revoked`), 'check') 92 + } catch (e: any) { 93 + Toast.show(_(msg`Failed to revoke vouch`), 'xmark') 94 + } 95 + }, [_, vouch, mutateAsync]) 96 + 97 + return ( 98 + <View 99 + style={[ 100 + gutters, 101 + a.w_full, 102 + !first && a.border_t, 103 + t.atoms.border_contrast_low, 104 + ]}> 105 + <View style={[a.flex_row, a.align_center, a.justify_between, a.gap_sm]}> 106 + <UserAvatar size={40} avatar={subject.avatar} /> 107 + 108 + <View style={[a.flex_1, a.gap_xs]}> 109 + <Text style={[a.text_md, a.font_bold, a.leading_tight]}> 110 + @{subject.handle} 111 + </Text> 112 + <View style={[a.flex_row, a.align_center, a.gap_sm]}> 113 + <View 114 + style={[ 115 + a.rounded_xs, 116 + t.atoms.bg_contrast_25, 117 + { 118 + paddingVertical: 2, 119 + paddingHorizontal: 6, 120 + }, 121 + ]}> 122 + <Text style={[a.leading_tight, t.atoms.text_contrast_medium]}> 123 + {relationship} 124 + </Text> 125 + </View> 126 + <View 127 + style={[ 128 + a.rounded_xs, 129 + t.atoms.bg_contrast_25, 130 + { 131 + paddingVertical: 2, 132 + paddingHorizontal: 6, 133 + }, 134 + ]}> 135 + <Text style={[a.leading_tight, t.atoms.text_contrast_medium]}> 136 + {vouch.accept ? _(msg`Accepted`) : _(msg`Pending`)} 137 + </Text> 138 + </View> 139 + <Text style={[a.text_sm, a.leading_tight]}> 140 + <Trans>Issued: {ago(record.createdAt, new Date())} ago</Trans> 141 + </Text> 142 + </View> 143 + </View> 144 + 145 + <Button 146 + disabled={isPending} 147 + label={_(msg`Revoke vouch from ${subject.handle}`)} 148 + size="small" 149 + variant="solid" 150 + color="secondary" 151 + onPress={revoke}> 152 + <ButtonText> 153 + <Trans>Revoke</Trans> 154 + </ButtonText> 155 + <ButtonIcon icon={isPending ? Loader : Times} position="right" /> 156 + </Button> 157 + </View> 158 + </View> 159 + ) 160 + } 161 + 162 + function useRelationshipLabel( 163 + relationship: AppBskyGraphVouch.Record['relationship'], 164 + ) { 165 + const {_} = useLingui() 166 + 167 + return React.useMemo(() => { 168 + switch (relationship) { 169 + case 'verifiedBy': 170 + return _(msg`Bopped`) 171 + case 'employeeOf': 172 + return _(msg`Beeped`) 173 + default: 174 + return _(msg`Unknown`) 175 + } 176 + }, [_, relationship]) 177 + }
+60 -67
src/screens/Profile/Vouches/index.tsx
··· 1 - import {View, ScrollView} from 'react-native' 2 - import {Trans, msg} from '@lingui/macro' 1 + import {ScrollView,View} from 'react-native' 2 + import {msg,Trans} from '@lingui/macro' 3 3 import {useLingui} from '@lingui/react' 4 4 5 - import {Loader} from '#/components/Loader' 6 - import * as Layout from '#/components/Layout' 7 - import {Text} from '#/components/Typography' 8 - import {atoms as a, useTheme, useGutters} from '#/alf' 9 - import {Link} from '#/components/Link' 10 - import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron' 11 - import {useSession} from '#/state/session' 12 5 import {useVouchesIssued} from '#/state/queries/vouches/useVouchesIssued' 13 - import {Button, ButtonText, ButtonIcon} from '#/components/Button' 14 - import {UserAvatar} from '#/view/com/util/UserAvatar' 6 + import {useSession} from '#/state/session' 7 + import {Vouch} from '#/screens/Profile/Vouches/components/Vouch' 8 + import {atoms as a, useGutters,useTheme} from '#/alf' 9 + import {ButtonIcon,ButtonText} from '#/components/Button' 15 10 import {Divider} from '#/components/Divider' 11 + import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron' 12 + import * as Layout from '#/components/Layout' 13 + import {Link} from '#/components/Link' 14 + import {Loader} from '#/components/Loader' 15 + import {Text} from '#/components/Typography' 16 16 17 17 export function Screen() { 18 18 const t = useTheme() ··· 79 79 80 80 return ( 81 81 <View style={[]}> 82 - <View style={[a.flex_row, a.align_center, a.justify_between, baseGutters]}> 83 - <Text style={[a.text_md, a.font_bold, t.atoms.text_contrast_medium]}> 84 - <Trans>Vouches Issued</Trans> 85 - </Text> 82 + <View style={[baseGutters, a.pb_md]}> 83 + <View style={[a.flex_row, a.align_center, a.justify_between, a.pb_xs]}> 84 + <Text style={[a.text_lg, a.font_heavy, t.atoms.text_contrast_medium]}> 85 + <Trans>Vouches Issued</Trans> 86 + </Text> 86 87 87 - <Link 88 - label={_(msg`View All`)} 89 - to={{ 90 - screen: 'ProfileVouchesIssued', 91 - params: {name: currentAccount!.handle}, 92 - }} 93 - size="small" 94 - variant="ghost" 95 - color="secondary" 96 - shape="round" 97 - style={[a.flex_row, a.align_center, a.justify_center]}> 98 - <ButtonIcon icon={ChevronRight} /> 99 - </Link> 88 + <Link 89 + label={_(msg`View All`)} 90 + to={{ 91 + screen: 'ProfileVouchesIssued', 92 + params: {name: currentAccount!.handle}, 93 + }} 94 + size="small" 95 + variant="ghost" 96 + color="secondary" 97 + style={[a.flex_row, a.align_center, a.justify_center]}> 98 + <ButtonText> 99 + <Trans>See all</Trans> 100 + </ButtonText> 101 + <ButtonIcon icon={ChevronRight} position="right" /> 102 + </Link> 103 + </View> 104 + <Divider /> 100 105 </View> 101 106 102 - <View style={[a.gap_sm]}> 103 - {isLoading ? ( 107 + {isLoading ? ( 108 + <View style={[baseGutters, a.py_lg]}> 104 109 <Loader /> 105 - ) : error || !vouches ? ( 106 - <> 107 - {error ? ( 108 - <Text>{error.toString()}</Text> 109 - ) : ( 110 - <Text> 111 - <Trans>Somthing went wrong</Trans> 112 - </Text> 113 - )} 114 - </> 115 - ) : vouches.length ? ( 116 - <ScrollView horizontal style={[baseGutters]}> 117 - {vouches.map(v => ( 118 - <View style={[a.p_sm, a.rounded_md, a.flex_1, a.gap_sm, t.atoms.bg_contrast_25, { 119 - maxWidth: 300, 120 - }]}> 121 - <View style={[a.flex_row, a.align_start, a.gap_sm]}> 122 - <UserAvatar size={32} avatar={v.subject?.avatar} /> 123 - <View style={[a.gap_2xs]}> 124 - <Text style={[a.text_md, a.font_bold, a.leading_tight]}>@{v.subject?.handle}</Text> 125 - <Text style={[a.leading_tight, t.atoms.text_contrast_medium]}>{v.record.relationship}</Text> 126 - </View> 127 - </View> 128 - <Divider /> 129 - <View style={[a.flex_row, a.align_start, a.gap_xl]}> 130 - <Text style={[a.font_bold, a.leading_tight]}>{v.accept ? 'Accepted' : 'Pending'}</Text> 131 - <Text style={[a.leading_tight]}>{v.record.createdAt}</Text> 132 - </View> 133 - </View> 134 - ))} 135 - </ScrollView> 136 - ) : ( 137 - <> 138 - <Text>No vouches</Text> 139 - </> 140 - )} 141 - </View> 110 + </View> 111 + ) : error || !vouches ? ( 112 + <View style={[baseGutters, a.py_lg]}> 113 + {error ? ( 114 + <Text>{error.toString()}</Text> 115 + ) : ( 116 + <Text> 117 + <Trans>Somthing went wrong</Trans> 118 + </Text> 119 + )} 120 + </View> 121 + ) : vouches.pages.at(0)?.vouches?.length ? ( 122 + <ScrollView 123 + horizontal 124 + style={[baseGutters]} 125 + contentContainerStyle={[a.gap_md]}> 126 + {vouches.pages[0].vouches.map(v => ( 127 + <Vouch key={v.cid} vouch={v} subject={v.subject!} /> 128 + ))} 129 + </ScrollView> 130 + ) : ( 131 + <View style={[baseGutters, a.py_lg]}> 132 + <Text>No vouches</Text> 133 + </View> 134 + )} 142 135 </View> 143 136 ) 144 137 }
+50 -4
src/state/queries/vouches/useCreateVouch.ts
··· 1 1 import {AppBskyGraphVouch} from '@atproto/api' 2 - import {useMutation} from '@tanstack/react-query' 2 + import {useMutation, useQueryClient} from '@tanstack/react-query' 3 3 4 - import {useSession, useAgent} from '#/state/session' 4 + import {poll} from '#/lib/async/poll' 5 + import { 6 + useUpdateVouchesIssuedQueryCache, 7 + vouchesIssuedQueryKey, 8 + } from '#/state/queries/vouches/useVouchesIssued' 5 9 import {useVouchRecordSchema} from '#/state/queries/vouches/util' 10 + import {useAgent,useSession} from '#/state/session' 6 11 7 12 export type CreateVouchProps = { 8 13 subject: string ··· 10 15 } 11 16 12 17 export function useCreateVouch() { 18 + const queryClient = useQueryClient() 13 19 const {currentAccount} = useSession() 14 20 const agent = useAgent() 15 21 const vouchRecordSchema = useVouchRecordSchema() 22 + const updateCache = useUpdateVouchesIssuedQueryCache() 16 23 17 24 return useMutation({ 18 25 mutationFn: async (props: CreateVouchProps) => { ··· 23 30 } 24 31 vouchRecordSchema.parse(record) 25 32 return agent.app.bsky.graph.vouch.create( 26 - { repo: currentAccount!.did }, 33 + {repo: currentAccount!.did}, 27 34 record, 28 35 ) 29 - } 36 + }, 37 + async onSuccess({uri}) { 38 + const vouch = await poll( 39 + 5, 40 + 1e3, 41 + ({response}) => { 42 + if (!response) return false 43 + if (response.uri === uri) return true 44 + return false 45 + }, 46 + async () => { 47 + const {data} = await agent.app.bsky.graph.getVouchesGiven({ 48 + actor: currentAccount!.did, 49 + includeUnaccepted: true, 50 + limit: 1, 51 + }) 52 + return data.vouches.at(0) 53 + }, 54 + ) 55 + 56 + if (vouch) { 57 + updateCache(data => { 58 + if (!data) { 59 + // no cache, fetch fresh 60 + queryClient.invalidateQueries({queryKey: vouchesIssuedQueryKey}) 61 + return 62 + } 63 + 64 + return { 65 + ...data, 66 + pages: data.pages.map((page, i) => { 67 + return { 68 + ...page, 69 + vouches: i === 0 ? [vouch, ...page.vouches] : page.vouches, 70 + } 71 + }), 72 + } 73 + }) 74 + } 75 + }, 30 76 }) 31 77 }
+39
src/state/queries/vouches/useRevokeVouch.ts
··· 1 + import {AppBskyGraphDefs, AtUri} from '@atproto/api' 2 + import {useMutation} from '@tanstack/react-query' 3 + 4 + import {useUpdateVouchesIssuedQueryCache} from '#/state/queries/vouches/useVouchesIssued' 5 + import {useAgent,useSession} from '#/state/session' 6 + 7 + export type RevokeVouchProps = { 8 + vouch: AppBskyGraphDefs.VouchView 9 + } 10 + 11 + export function useRevokeVouch() { 12 + const {currentAccount} = useSession() 13 + const agent = useAgent() 14 + const updateCache = useUpdateVouchesIssuedQueryCache() 15 + 16 + return useMutation({ 17 + mutationFn: async (props: RevokeVouchProps) => { 18 + const {rkey} = new AtUri(props.vouch.uri) 19 + return agent.app.bsky.graph.vouch.delete({ 20 + repo: currentAccount!.did, 21 + rkey, 22 + }) 23 + }, 24 + onSuccess(_, {vouch}) { 25 + updateCache(data => { 26 + if (!data) return data 27 + return { 28 + ...data, 29 + pages: data.pages.map(page => { 30 + return { 31 + ...page, 32 + vouches: page.vouches.filter(v => v.uri !== vouch.uri), 33 + } 34 + }), 35 + } 36 + }) 37 + }, 38 + }) 39 + }
+20
src/state/queries/vouches/useVouchesAccepted.ts
··· 1 + import {useQuery} from '@tanstack/react-query' 2 + 3 + import {useAgent,useSession} from '#/state/session' 4 + 5 + export const vouchesAcceptedQueryKey = ['vouches-accepted'] 6 + 7 + export function useVouchesAccepted() { 8 + const {currentAccount} = useSession() 9 + const agent = useAgent() 10 + 11 + return useQuery({ 12 + queryKey: vouchesAcceptedQueryKey, 13 + queryFn: async () => { 14 + const {data} = await agent.app.bsky.graph.getVouchesReceived({ 15 + actor: currentAccount!.did, 16 + }) 17 + return data.vouches 18 + }, 19 + }) 20 + }
+42 -6
src/state/queries/vouches/useVouchesIssued.ts
··· 1 - import {useQuery} from '@tanstack/react-query' 1 + import React from 'react' 2 + import {AppBskyGraphGetVouchesGiven} from '@atproto/api' 3 + import { 4 + InfiniteData, 5 + QueryKey, 6 + useInfiniteQuery, 7 + useQueryClient, 8 + } from '@tanstack/react-query' 2 9 3 - import {useSession, useAgent} from '#/state/session' 10 + import {useAgent,useSession} from '#/state/session' 4 11 5 12 export const vouchesIssuedQueryKey = ['vouches-issued'] 6 13 ··· 8 15 const {currentAccount} = useSession() 9 16 const agent = useAgent() 10 17 11 - return useQuery({ 18 + return useInfiniteQuery< 19 + AppBskyGraphGetVouchesGiven.OutputSchema, 20 + Error, 21 + InfiniteData<AppBskyGraphGetVouchesGiven.OutputSchema>, 22 + QueryKey, 23 + string | undefined 24 + >({ 12 25 queryKey: vouchesIssuedQueryKey, 13 - queryFn: async () => { 26 + initialPageParam: undefined, 27 + getNextPageParam: lastPage => lastPage.cursor, 28 + queryFn: async ({pageParam: cursor}) => { 14 29 const {data} = await agent.app.bsky.graph.getVouchesGiven({ 15 30 actor: currentAccount!.did, 16 31 includeUnaccepted: true, 32 + cursor, 17 33 }) 18 - return data.vouches 19 - } 34 + return data 35 + }, 20 36 }) 21 37 } 38 + 39 + export function useUpdateVouchesIssuedQueryCache() { 40 + const q = useQueryClient() 41 + return React.useCallback( 42 + ( 43 + callback: ( 44 + data: 45 + | InfiniteData<AppBskyGraphGetVouchesGiven.OutputSchema> 46 + | undefined, 47 + ) => InfiniteData<AppBskyGraphGetVouchesGiven.OutputSchema> | undefined, 48 + ) => { 49 + const data = q.getQueryData< 50 + InfiniteData<AppBskyGraphGetVouchesGiven.OutputSchema> 51 + >(vouchesIssuedQueryKey) 52 + const updated = callback(data) 53 + q.setQueryData(vouchesIssuedQueryKey, updated) 54 + }, 55 + [q], 56 + ) 57 + }
+17
src/state/queries/vouches/useVouchesReceived.ts
··· 1 + import {useQuery} from '@tanstack/react-query' 2 + 3 + import {useAgent} from '#/state/session' 4 + 5 + export const vouchesReceivedQueryKey = ['vouches-received'] 6 + 7 + export function useVouchesReceived() { 8 + const agent = useAgent() 9 + 10 + return useQuery({ 11 + queryKey: vouchesReceivedQueryKey, 12 + queryFn: async () => { 13 + const {data} = await agent.app.bsky.graph.getVouchesOffered() 14 + return data.vouches 15 + }, 16 + }) 17 + }