diff --git a/src/screens/Profile/Vouches/Create/index.tsx b/src/screens/Profile/Vouches/Create/index.tsx index a7da764a32..1f193f074f 100644 --- a/src/screens/Profile/Vouches/Create/index.tsx +++ b/src/screens/Profile/Vouches/Create/index.tsx @@ -1,9 +1,21 @@ -import {Trans} from '@lingui/macro' +import React from 'react' +import {View} from 'react-native' +import {Trans, msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {ZodError} from 'zod' import * as Layout from '#/components/Layout' -import {Text} from '#/components/Typography' +import {atoms as a, useGutters} from '#/alf' +import {Button, ButtonText, ButtonIcon} from '#/components/Button' +import * as TextField from '#/components/forms/TextField' +import {useCreateVouch} from '#/state/queries/vouches/useCreateVouch' +import {Loader} from '#/components/Loader' +import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' +import {Admonition} from '#/components/Admonition' export function Screen() { + const baseGutters = useGutters(['base']) + return ( @@ -17,8 +29,77 @@ export function Screen() { - Create + +
+ ) } + +function Form() { + const {_} = useLingui() + const [subject, setSubject] = React.useState('') + const [relationship, setRelationship] = React.useState('') + const [errors, setErrors] = React.useState([]) + + const {mutateAsync: createVouch, isPending} = useCreateVouch() + + const onSubmit = async () => { + setErrors([]) + try { + await createVouch({subject, relationship}) + } catch (e: any) { + if (e instanceof ZodError) { + setErrors(e.errors.map(err => err.message)) + } else { + setErrors([e.message]) + } + } + } + + return ( + + + + User DID + + + + + + User DID + + + + + + + + + {!!errors.length && ( + + {errors.map((error, i) => ( + + {' '} + {error}{' '} + + ))} + + )} + + ) +} diff --git a/src/screens/Profile/Vouches/Issued/index.tsx b/src/screens/Profile/Vouches/Issued/index.tsx index c45d94abdd..5f30b14298 100644 --- a/src/screens/Profile/Vouches/Issued/index.tsx +++ b/src/screens/Profile/Vouches/Issued/index.tsx @@ -1,9 +1,22 @@ -import {Trans} from '@lingui/macro' +import React from 'react' +import {View} from 'react-native' +import {Trans, msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {ZodError} from 'zod' import * as Layout from '#/components/Layout' +import {atoms as a, useGutters} from '#/alf' +import {Button, ButtonText, ButtonIcon} from '#/components/Button' +import * as TextField from '#/components/forms/TextField' +import {useCreateVouch} from '#/state/queries/vouches/useCreateVouch' +import {Loader} from '#/components/Loader' +import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' +import {Admonition} from '#/components/Admonition' +import {useVouchesIssued} from '#/state/queries/vouches/useVouchesIssued' import {Text} from '#/components/Typography' export function Screen() { + const baseGutters = useGutters(['base']) return ( @@ -17,8 +30,38 @@ export function Screen() { - Create + + + ) } + +export function Inner() { + const {data: vouches, isLoading, error} = useVouchesIssued() + + return ( + + + {isLoading ? ( + + ) : error || !vouches ? ( + <> + ) : vouches.length ? ( + <> + {vouches.map(v => ( + + {v.record.subject} + + ))} + + ) : ( + <> + No vouches + + )} + + + ) +} diff --git a/src/screens/Profile/Vouches/index.tsx b/src/screens/Profile/Vouches/index.tsx index f52f0f65fc..7323b61b3d 100644 --- a/src/screens/Profile/Vouches/index.tsx +++ b/src/screens/Profile/Vouches/index.tsx @@ -1,9 +1,26 @@ -import {Trans} from '@lingui/macro' +import {View, ScrollView} from 'react-native' +import {Trans, msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {Loader} from '#/components/Loader' import * as Layout from '#/components/Layout' import {Text} from '#/components/Typography' +import {atoms as a, useTheme, useGutters} from '#/alf' +import {Link} from '#/components/Link' +import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron' +import {useSession} from '#/state/session' +import {useVouchesIssued} from '#/state/queries/vouches/useVouchesIssued' +import {Button, ButtonText, ButtonIcon} from '#/components/Button' +import {UserAvatar} from '#/view/com/util/UserAvatar' +import {Divider} from '#/components/Divider' export function Screen() { + const t = useTheme() + const {_} = useLingui() + const {currentAccount} = useSession() + const baseGutters = useGutters(['base']) + const compactGutters = useGutters(['compact']) + return ( @@ -17,8 +34,111 @@ export function Screen() { - Vouches + + + + {({hovered}) => ( + + + Create vouch + + + + + )} + + + + + ) } + +export function VouchesIssued() { + const t = useTheme() + const {_} = useLingui() + const {currentAccount} = useSession() + const {data: vouches, isLoading, error} = useVouchesIssued() + const baseGutters = useGutters([0, 'base']) + + return ( + + + + Vouches Issued + + + + + + + + + {isLoading ? ( + + ) : error || !vouches ? ( + <> + {error ? ( + {error.toString()} + ) : ( + + Somthing went wrong + + )} + + ) : vouches.length ? ( + + {vouches.map(v => ( + + + + + @{v.subject?.handle} + {v.record.relationship} + + + + + {v.accept ? 'Accepted' : 'Pending'} + {v.record.createdAt} + + + ))} + + ) : ( + <> + No vouches + + )} + + + ) +} diff --git a/src/state/queries/vouches/useCreateVouch.ts b/src/state/queries/vouches/useCreateVouch.ts new file mode 100644 index 0000000000..0c871cf5b0 --- /dev/null +++ b/src/state/queries/vouches/useCreateVouch.ts @@ -0,0 +1,31 @@ +import {AppBskyGraphVouch} from '@atproto/api' +import {useMutation} from '@tanstack/react-query' + +import {useSession, useAgent} from '#/state/session' +import {useVouchRecordSchema} from '#/state/queries/vouches/util' + +export type CreateVouchProps = { + subject: string + relationship: AppBskyGraphVouch.Record['relationship'] +} + +export function useCreateVouch() { + const {currentAccount} = useSession() + const agent = useAgent() + const vouchRecordSchema = useVouchRecordSchema() + + return useMutation({ + mutationFn: async (props: CreateVouchProps) => { + const record: AppBskyGraphVouch.Record = { + subject: props.subject, + relationship: props.relationship, + createdAt: new Date().toISOString(), + } + vouchRecordSchema.parse(record) + return agent.app.bsky.graph.vouch.create( + { repo: currentAccount!.did }, + record, + ) + } + }) +} diff --git a/src/state/queries/vouches/useVouchesIssued.ts b/src/state/queries/vouches/useVouchesIssued.ts new file mode 100644 index 0000000000..331a17b4ab --- /dev/null +++ b/src/state/queries/vouches/useVouchesIssued.ts @@ -0,0 +1,21 @@ +import {useQuery} from '@tanstack/react-query' + +import {useSession, useAgent} from '#/state/session' + +export const vouchesIssuedQueryKey = ['vouches-issued'] + +export function useVouchesIssued() { + const {currentAccount} = useSession() + const agent = useAgent() + + return useQuery({ + queryKey: vouchesIssuedQueryKey, + queryFn: async () => { + const {data} = await agent.app.bsky.graph.getVouchesGiven({ + actor: currentAccount!.did, + includeUnaccepted: true, + }) + return data.vouches + } + }) +} diff --git a/src/state/queries/vouches/util.ts b/src/state/queries/vouches/util.ts new file mode 100644 index 0000000000..9394858d3c --- /dev/null +++ b/src/state/queries/vouches/util.ts @@ -0,0 +1,16 @@ +import {useMemo} from 'react' +import zod from 'zod' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +export function useVouchRecordSchema() { + const {_} = useLingui() + + return useMemo(() => { + return zod.object({ + subject: zod.string().startsWith('did:', { message: _(msg`Must be a valid DID`) }), + relationship: zod.enum(['verifiedBy', 'employeeOf']), + createdAt: zod.string().datetime(), + }) + }, [_]) +}