diff options
Diffstat (limited to 'src/components')
20 files changed, 1243 insertions, 70 deletions
diff --git a/src/components/AccountList.tsx b/src/components/AccountList.tsx index 52d149eb5..f2e781ccf 100644 --- a/src/components/AccountList.tsx +++ b/src/components/AccountList.tsx @@ -1,6 +1,6 @@ import React, {useCallback} from 'react' import {View} from 'react-native' -import {AppBskyActorDefs} from '@atproto/api' +import {type AppBskyActorDefs} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' @@ -12,6 +12,8 @@ import {UserAvatar} from '#/view/com/util/UserAvatar' import {atoms as a, useTheme} from '#/alf' import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check' import {ChevronRight_Stroke2_Corner0_Rounded as Chevron} from '#/components/icons/Chevron' +import {useSimpleVerificationState} from '#/components/verification' +import {VerificationCheck} from '#/components/verification/VerificationCheck' import {Button} from './Button' import {Text} from './Typography' @@ -74,11 +76,13 @@ export function AccountList({ ]}> <Text style={[ - a.align_baseline, + a.font_bold, a.flex_1, a.flex_row, a.py_sm, - {paddingLeft: 48}, + a.leading_tight, + t.atoms.text_contrast_medium, + {paddingLeft: 56}, ]}> {otherLabel ?? <Trans>Other account</Trans>} </Text> @@ -105,6 +109,7 @@ function AccountItem({ }) { const t = useTheme() const {_} = useLingui() + const verification = useSimpleVerificationState({profile}) const onPress = useCallback(() => { onSelect(account) @@ -114,7 +119,7 @@ function AccountItem({ <Button testID={`chooseAccountBtn-${account.handle}`} key={account.did} - style={[a.flex_1]} + style={[a.w_full]} onPress={onPress} label={ isCurrentAccount @@ -127,33 +132,45 @@ function AccountItem({ a.flex_1, a.flex_row, a.align_center, - {height: 48}, + a.px_md, + a.gap_sm, + {height: 56}, (hovered || pressed || isPendingAccount) && t.atoms.bg_contrast_25, ]}> - <View style={a.p_md}> - <UserAvatar - avatar={profile?.avatar} - size={24} - type={profile?.associated?.labeler ? 'labeler' : 'user'} - /> - </View> - <Text style={[a.align_baseline, a.flex_1, a.flex_row, a.py_sm]}> - <Text emoji style={[a.font_bold]}> - {sanitizeDisplayName( - profile?.displayName || profile?.handle || account.handle, + <UserAvatar + avatar={profile?.avatar} + size={36} + type={profile?.associated?.labeler ? 'labeler' : 'user'} + /> + + <View style={[a.flex_1, a.gap_2xs, a.pr_2xl]}> + <View style={[a.flex_row, a.align_center, a.gap_xs]}> + <Text + emoji + style={[a.font_bold, a.leading_tight]} + numberOfLines={1}> + {sanitizeDisplayName( + profile?.displayName || profile?.handle || account.handle, + )} + </Text> + {verification.showBadge && ( + <View> + <VerificationCheck + width={12} + verifier={verification.role === 'verifier'} + /> + </View> )} - </Text>{' '} - <Text emoji style={[t.atoms.text_contrast_medium]}> - {sanitizeHandle(account.handle)} + </View> + <Text style={[a.leading_tight, t.atoms.text_contrast_medium]}> + {sanitizeHandle(account.handle, '@')} </Text> - </Text> + </View> + {isCurrentAccount ? ( - <Check - size="sm" - style={[{color: t.palette.positive_600}, a.mr_md]} - /> + <Check size="sm" style={[{color: t.palette.positive_600}]} /> ) : ( - <Chevron size="sm" style={[t.atoms.text, a.mr_md]} /> + <Chevron size="sm" style={[t.atoms.text]} /> )} </View> )} diff --git a/src/components/Link.tsx b/src/components/Link.tsx index b5f0bc958..cca93c0c8 100644 --- a/src/components/Link.tsx +++ b/src/components/Link.tsx @@ -210,7 +210,9 @@ export function useLink({ } export type LinkProps = Omit<BaseLinkProps, 'disableMismatchWarning'> & - Omit<ButtonProps, 'onPress' | 'disabled'> + Omit<ButtonProps, 'onPress' | 'disabled'> & { + overridePresentation?: boolean + } /** * A interactive element that renders as a `<a>` tag on the web. On mobile it @@ -228,6 +230,7 @@ export function Link({ onLongPress: outerOnLongPress, download, shouldProxy, + overridePresentation, ...rest }: LinkProps) { const {href, isExternal, onPress, onLongPress} = useLink({ @@ -237,6 +240,7 @@ export function Link({ onPress: outerOnPress, onLongPress: outerOnLongPress, shouldProxy: shouldProxy, + overridePresentation, }) return ( diff --git a/src/components/ProfileCard.tsx b/src/components/ProfileCard.tsx index 1a64c51d5..c97911a3f 100644 --- a/src/components/ProfileCard.tsx +++ b/src/components/ProfileCard.tsx @@ -30,6 +30,8 @@ import {Link as InternalLink, type LinkProps} from '#/components/Link' import * as Pills from '#/components/Pills' import {RichText} from '#/components/RichText' import {Text} from '#/components/Typography' +import {useSimpleVerificationState} from '#/components/verification' +import {VerificationCheck} from '#/components/verification/VerificationCheck' import type * as bsky from '#/types/bsky' export function Default({ @@ -186,13 +188,24 @@ export function Name({ profile.displayName || sanitizeHandle(profile.handle), moderation.ui('displayName'), ) + const verification = useSimpleVerificationState({profile}) return ( - <Text - emoji - style={[a.text_md, a.font_bold, a.leading_snug, a.self_start]} - numberOfLines={1}> - {name} - </Text> + <View style={[a.flex_row, a.align_center]}> + <Text + emoji + style={[a.text_md, a.font_bold, a.leading_snug, a.self_start]} + numberOfLines={1}> + {name} + </Text> + {verification.showBadge && ( + <View style={[a.pl_xs]}> + <VerificationCheck + width={14} + verifier={verification.role === 'verifier'} + /> + </View> + )} + </View> ) } diff --git a/src/components/ProfileHoverCard/index.web.tsx b/src/components/ProfileHoverCard/index.web.tsx index 3e58ced90..09b587c5e 100644 --- a/src/components/ProfileHoverCard/index.web.tsx +++ b/src/components/ProfileHoverCard/index.web.tsx @@ -1,6 +1,10 @@ import React from 'react' import {View} from 'react-native' -import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api' +import { + type AppBskyActorDefs, + moderateProfile, + type ModerationOpts, +} from '@atproto/api' import {flip, offset, shift, size, useFloating} from '@floating-ui/react-dom' import {msg, plural} from '@lingui/macro' import {useLingui} from '@lingui/react' @@ -33,7 +37,9 @@ import * as Pills from '#/components/Pills' import {Portal} from '#/components/Portal' import {RichText} from '#/components/RichText' import {Text} from '#/components/Typography' -import {ProfileHoverCardProps} from './types' +import {useSimpleVerificationState} from '#/components/verification' +import {VerificationCheck} from '#/components/verification/VerificationCheck' +import {type ProfileHoverCardProps} from './types' const floatingMiddlewares = [ offset(4), @@ -412,6 +418,7 @@ function Inner({ [currentAccount, profile], ) const isLabeler = profile.associated?.labeler + const verification = useSimpleVerificationState({profile}) return ( <View> @@ -465,13 +472,30 @@ function Inner({ <Link to={profileURL} label={_(msg`View profile`)} onPress={hide}> <View style={[a.pb_sm, a.flex_1]}> - <Text - style={[a.pt_md, a.pb_xs, a.text_lg, a.font_bold, a.self_start]}> - {sanitizeDisplayName( - profile.displayName || sanitizeHandle(profile.handle), - moderation.ui('displayName'), + <View style={[a.flex_row, a.align_center, a.pt_md, a.pb_xs]}> + <Text + numberOfLines={1} + style={[a.text_lg, a.font_bold, a.self_start]}> + {sanitizeDisplayName( + profile.displayName || sanitizeHandle(profile.handle), + moderation.ui('displayName'), + )} + </Text> + {verification.showBadge && ( + <View + style={[ + a.pl_xs, + { + marginTop: -2, + }, + ]}> + <VerificationCheck + width={16} + verifier={verification.role === 'verifier'} + /> + </View> )} - </Text> + </View> <ProfileHeaderHandle profile={profileShadow} disableTaps /> </View> diff --git a/src/components/Prompt.tsx b/src/components/Prompt.tsx index 404790462..ed8c15f15 100644 --- a/src/components/Prompt.tsx +++ b/src/components/Prompt.tsx @@ -1,13 +1,13 @@ import React from 'react' -import {GestureResponderEvent, View} from 'react-native' +import {type GestureResponderEvent, View} from 'react-native' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {atoms as a, useBreakpoints, useTheme} from '#/alf' -import {Button, ButtonColor, ButtonText} from '#/components/Button' +import {atoms as a, useBreakpoints, useTheme, type ViewStyleProp} from '#/alf' +import {Button, type ButtonColor, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' import {Text} from '#/components/Typography' -import {BottomSheetViewProps} from '../../modules/bottom-sheet' +import {type BottomSheetViewProps} from '../../modules/bottom-sheet' export { type DialogControlProps as PromptControlProps, @@ -62,12 +62,22 @@ export function Outer({ ) } -export function TitleText({children}: React.PropsWithChildren<{}>) { +export function TitleText({ + children, + style, +}: React.PropsWithChildren<ViewStyleProp>) { const {titleId} = React.useContext(Context) return ( <Text nativeID={titleId} - style={[a.text_2xl, a.font_bold, a.pb_sm, a.leading_snug]}> + style={[ + a.flex_1, + a.text_2xl, + a.font_bold, + a.pb_sm, + a.leading_snug, + style, + ]}> {children} </Text> ) @@ -190,7 +200,7 @@ export function Basic({ }: React.PropsWithChildren<{ control: Dialog.DialogOuterProps['control'] title: string - description: string + description?: string cancelButtonCta?: string confirmButtonCta?: string /** @@ -207,7 +217,7 @@ export function Basic({ return ( <Outer control={control} testID="confirmModal"> <TitleText>{title}</TitleText> - <DescriptionText>{description}</DescriptionText> + {description && <DescriptionText>{description}</DescriptionText>} <Actions> <Action cta={confirmButtonCta} diff --git a/src/components/Typography.tsx b/src/components/Typography.tsx index 4ed7f8371..09d0eeaf0 100644 --- a/src/components/Typography.tsx +++ b/src/components/Typography.tsx @@ -6,9 +6,11 @@ import { childHasEmoji, normalizeTextStyles, renderChildrenWithEmoji, - TextProps, + type TextProps, } from '#/alf/typography' + export type {TextProps} +export {Text as Span} from 'react-native' /** * Our main text component. Use this most of the time. diff --git a/src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx b/src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx new file mode 100644 index 000000000..fb7550043 --- /dev/null +++ b/src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx @@ -0,0 +1,194 @@ +import {useCallback} from 'react' +import {View} from 'react-native' +import {Image} from 'expo-image' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {urls} from '#/lib/constants' +import {logger} from '#/logger' +import {isNative} from '#/platform/detection' +import {atoms as a, useBreakpoints, useTheme} from '#/alf' +import {Button, ButtonText} from '#/components/Button' +import * as Dialog from '#/components/Dialog' +import {useNuxDialogContext} from '#/components/dialogs/nuxs' +import {Sparkle_Stroke2_Corner0_Rounded as SparkleIcon} from '#/components/icons/Sparkle' +import {VerifierCheck} from '#/components/icons/VerifierCheck' +import {Link} from '#/components/Link' +import {Span, Text} from '#/components/Typography' + +export function InitialVerificationAnnouncement() { + const t = useTheme() + const {_} = useLingui() + const {gtMobile} = useBreakpoints() + const nuxDialogs = useNuxDialogContext() + const control = Dialog.useDialogControl() + + Dialog.useAutoOpen(control) + + const onClose = useCallback(() => { + nuxDialogs.dismissActiveNux() + }, [nuxDialogs]) + + return ( + <Dialog.Outer control={control} onClose={onClose}> + <Dialog.Handle /> + + <Dialog.ScrollableInner + label={_(msg`Announcing verification on Bluesky`)} + style={[ + gtMobile ? {width: 'auto', maxWidth: 400, minWidth: 200} : a.w_full, + ]}> + <View style={[a.align_start, a.gap_xl]}> + <View + style={[ + a.pl_sm, + a.pr_md, + a.py_sm, + a.rounded_full, + a.flex_row, + a.align_center, + a.gap_xs, + { + backgroundColor: t.palette.primary_25, + }, + ]}> + <SparkleIcon fill={t.palette.primary_700} size="sm" /> + <Text + style={[ + a.font_bold, + { + color: t.palette.primary_700, + }, + ]}> + <Trans>New Feature</Trans> + </Text> + </View> + + <View + style={[ + a.w_full, + a.rounded_md, + a.overflow_hidden, + t.atoms.bg_contrast_25, + {minHeight: 100}, + ]}> + <Image + accessibilityIgnoresInvertColors + source={require('../../../../assets/images/initial_verification_announcement_1.png')} + style={[ + { + aspectRatio: 353 / 160, + }, + ]} + alt={_( + msg`An illustration showing that Bluesky selects trusted verifiers, and trusted verifiers in turn verify individual user accounts.`, + )} + /> + </View> + + <View style={[a.gap_xs]}> + <Text style={[a.text_2xl, a.font_bold, a.leading_snug]}> + <Trans>A new form of verification</Trans> + </Text> + <Text style={[a.leading_snug, a.text_md]}> + <Trans> + We’re introducing a new layer of verification on Bluesky — an + easy-to-see checkmark. + </Trans> + </Text> + </View> + + <View + style={[ + a.w_full, + a.rounded_md, + a.overflow_hidden, + t.atoms.bg_contrast_25, + {minHeight: 100}, + ]}> + <Image + accessibilityIgnoresInvertColors + source={require('../../../../assets/images/initial_verification_announcement_2.png')} + style={[ + { + aspectRatio: 353 / 196, + }, + ]} + alt={_( + msg`An mockup of a iPhone showing the Bluesky app open to the profile of a verified user with a blue checkmark next to their display name.`, + )} + /> + </View> + + <View style={[a.gap_sm]}> + <View style={[a.flex_row, a.align_center, a.gap_xs]}> + <VerifierCheck width={14} /> + <Text style={[a.text_lg, a.font_bold, a.leading_snug]}> + <Trans>Who can verify?</Trans> + </Text> + </View> + <View style={[a.gap_sm]}> + <Text style={[a.leading_snug, a.text_md]}> + <Trans> + Bluesky will proactively verify notable and authentic + accounts. + </Trans> + </Text> + <Text style={[a.leading_snug, a.text_md]}> + <Trans> + Trust emerges from relationships, communities, and shared + context, so we’re also enabling{' '} + <Span style={[a.font_bold]}>trusted verifiers</Span>: + organizations that can directly issue verification. + </Trans> + </Text> + <Text style={[a.leading_snug, a.text_md]}> + <Trans> + When you tap on a check, you’ll see which organizations have + granted verification. + </Trans> + </Text> + </View> + </View> + + <View style={[a.w_full, a.gap_md]}> + <Link + overridePresentation + to={urls.website.blog.initialVerificationAnnouncement} + label={_(msg`Read blog post`)} + size="small" + variant="solid" + color="primary" + style={[a.justify_center, a.w_full]} + onPress={() => { + logger.metric('verification:learn-more', { + location: 'initialAnnouncementeNux', + }) + }}> + <ButtonText> + <Trans>Read blog post</Trans> + </ButtonText> + </Link> + {isNative && ( + <Button + label={_(msg`Close`)} + size="small" + variant="solid" + color="secondary" + style={[a.justify_center, a.w_full]} + onPress={() => { + control.close() + }}> + <ButtonText> + <Trans>Close</Trans> + </ButtonText> + </Button> + )} + </View> + </View> + + <Dialog.Close /> + </Dialog.ScrollableInner> + </Dialog.Outer> + ) +} diff --git a/src/components/dialogs/nuxs/index.tsx b/src/components/dialogs/nuxs/index.tsx index 10cae887b..c8c539b85 100644 --- a/src/components/dialogs/nuxs/index.tsx +++ b/src/components/dialogs/nuxs/index.tsx @@ -1,16 +1,17 @@ import React from 'react' -import {AppBskyActorDefs} from '@atproto/api' +import {type AppBskyActorDefs} from '@atproto/api' import {useGate} from '#/lib/statsig/statsig' import {logger} from '#/logger' import {Nux, useNuxs, useResetNuxs, useSaveNux} from '#/state/queries/nuxs' import { usePreferencesQuery, - UsePreferencesQueryResponse, + type UsePreferencesQueryResponse, } from '#/state/queries/preferences' import {useProfileQuery} from '#/state/queries/profile' -import {SessionAccount, useSession} from '#/state/session' +import {type SessionAccount, useSession} from '#/state/session' import {useOnboardingState} from '#/state/shell' +import {InitialVerificationAnnouncement} from '#/components/dialogs/nuxs/InitialVerificationAnnouncement' /* * NUXs */ @@ -29,7 +30,12 @@ const queuedNuxs: { currentProfile: AppBskyActorDefs.ProfileViewDetailed preferences: UsePreferencesQueryResponse }) => boolean -}[] = [] +}[] = [ + { + id: Nux.InitialVerificationAnnouncement, + enabled: () => true, + }, +] const Context = React.createContext<Context>({ activeNux: undefined, @@ -163,6 +169,9 @@ function Inner({ return ( <Context.Provider value={ctx}> {/*For example, activeNux === Nux.NeueTypography && <NeueTypography />*/} + {activeNux === Nux.InitialVerificationAnnouncement && ( + <InitialVerificationAnnouncement /> + )} </Context.Provider> ) } diff --git a/src/components/dms/MessagesListHeader.tsx b/src/components/dms/MessagesListHeader.tsx index 8da8c015f..c8ed98f88 100644 --- a/src/components/dms/MessagesListHeader.tsx +++ b/src/components/dms/MessagesListHeader.tsx @@ -1,9 +1,9 @@ import React, {useCallback} from 'react' import {TouchableOpacity, View} from 'react-native' import { - AppBskyActorDefs, - ModerationCause, - ModerationDecision, + type AppBskyActorDefs, + type ModerationCause, + type ModerationDecision, } from '@atproto/api' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {msg} from '@lingui/macro' @@ -12,12 +12,12 @@ import {useNavigation} from '@react-navigation/native' import {BACK_HITSLOP} from '#/lib/constants' import {makeProfileLink} from '#/lib/routes/links' -import {NavigationProp} from '#/lib/routes/types' +import {type NavigationProp} from '#/lib/routes/types' import {sanitizeDisplayName} from '#/lib/strings/display-names' import {isWeb} from '#/platform/detection' -import {Shadow} from '#/state/cache/profile-shadow' +import {type Shadow} from '#/state/cache/profile-shadow' import {isConvoActive, useConvo} from '#/state/messages/convo' -import {ConvoItem} from '#/state/messages/convo/types' +import {type ConvoItem} from '#/state/messages/convo/types' import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' import {atoms as a, useBreakpoints, useTheme, web} from '#/alf' import {ConvoMenu} from '#/components/dms/ConvoMenu' @@ -25,6 +25,8 @@ import {Bell2Off_Filled_Corner0_Rounded as BellStroke} from '#/components/icons/ import {Link} from '#/components/Link' import {PostAlerts} from '#/components/moderation/PostAlerts' import {Text} from '#/components/Typography' +import {useSimpleVerificationState} from '#/components/verification' +import {VerificationCheck} from '#/components/verification/VerificationCheck' const PFP_SIZE = isWeb ? 40 : 34 @@ -149,6 +151,9 @@ function HeaderReady({ const {_} = useLingui() const t = useTheme() const convoState = useConvo() + const verification = useSimpleVerificationState({ + profile, + }) const isDeletedAccount = profile?.handle === 'missing.invalid' const displayName = isDeletedAccount @@ -185,17 +190,27 @@ function HeaderReady({ /> </View> <View style={a.flex_1}> - <Text - emoji - style={[ - a.text_md, - a.font_bold, - a.self_start, - web(a.leading_normal), - ]} - numberOfLines={1}> - {displayName} - </Text> + <View style={[a.flex_row, a.align_center]}> + <Text + emoji + style={[ + a.text_md, + a.font_bold, + a.self_start, + web(a.leading_normal), + ]} + numberOfLines={1}> + {displayName} + </Text> + {verification.showBadge && ( + <View style={[a.pl_xs]}> + <VerificationCheck + width={14} + verifier={verification.role === 'verifier'} + /> + </View> + )} + </View> {!isDeletedAccount && ( <Text style={[ diff --git a/src/components/icons/CircleCheck.tsx b/src/components/icons/CircleCheck.tsx new file mode 100644 index 000000000..98abc9296 --- /dev/null +++ b/src/components/icons/CircleCheck.tsx @@ -0,0 +1,5 @@ +import {createSinglePathSVG} from './TEMPLATE' + +export const CircleCheck_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M12 4a8 8 0 1 0 0 16 8 8 0 0 0 0-16ZM2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Zm13.633-3.274a1 1 0 0 1 .141 1.407l-4.5 5.5a1 1 0 0 1-1.481.074l-2-2a1 1 0 1 1 1.414-1.414l1.219 1.219 3.8-4.645a1 1 0 0 1 1.407-.141Z', +}) diff --git a/src/components/icons/Sparkle.tsx b/src/components/icons/Sparkle.tsx new file mode 100644 index 000000000..73ce21d02 --- /dev/null +++ b/src/components/icons/Sparkle.tsx @@ -0,0 +1,5 @@ +import {createSinglePathSVG} from './TEMPLATE' + +export const Sparkle_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M12 2a1 1 0 0 1 1 1c0 3.188.669 5.256 1.882 6.536C16.084 10.805 18.01 11.5 21 11.5a1 1 0 1 1 0 2c-2.99 0-4.916.695-6.118 1.964C13.67 16.744 13 18.812 13 22a1 1 0 1 1-2 0c0-3.188-.669-5.256-1.882-6.536C7.916 14.195 5.99 13.5 3 13.5a1 1 0 1 1 0-2c2.99 0 4.916-.695 6.118-1.964C10.33 8.256 11 6.188 11 3a1 1 0 0 1 1-1Zm0 6.734a7.608 7.608 0 0 1-1.43 2.178A7.285 7.285 0 0 1 8.349 12.5c.846.397 1.589.921 2.22 1.588A7.607 7.607 0 0 1 12 16.267a7.607 7.607 0 0 1 1.43-2.179 7.284 7.284 0 0 1 2.221-1.588 7.284 7.284 0 0 1-2.22-1.588A7.608 7.608 0 0 1 12 8.734Z', +}) diff --git a/src/components/icons/VerifiedCheck.tsx b/src/components/icons/VerifiedCheck.tsx new file mode 100644 index 000000000..9299eb6e3 --- /dev/null +++ b/src/components/icons/VerifiedCheck.tsx @@ -0,0 +1,30 @@ +import React from 'react' +import Svg, {Circle, Path} from 'react-native-svg' + +import {type Props, useCommonSVGProps} from '#/components/icons/common' + +export const VerifiedCheck = React.forwardRef<Svg, Props>(function LogoImpl( + props, + ref, +) { + const {fill, size, style, ...rest} = useCommonSVGProps(props) + + return ( + <Svg + fill="none" + {...rest} + ref={ref} + viewBox="0 0 24 24" + width={size} + height={size} + style={[style]}> + <Circle cx="12" cy="12" r="12" fill={fill} /> + <Path + fill="#fff" + fillRule="evenodd" + clipRule="evenodd" + d="M18.311 7.421a1.437 1.437 0 0 1 0 2.033l-6.571 6.571a1.437 1.437 0 0 1-2.033 0L6.42 12.74a1.438 1.438 0 0 1 2.033-2.033l2.27 2.269 5.554-5.555a1.437 1.437 0 0 1 2.033 0Z" + /> + </Svg> + ) +}) diff --git a/src/components/icons/VerifierCheck.tsx b/src/components/icons/VerifierCheck.tsx new file mode 100644 index 000000000..7c3a0149d --- /dev/null +++ b/src/components/icons/VerifierCheck.tsx @@ -0,0 +1,35 @@ +import React from 'react' +import Svg, {Path} from 'react-native-svg' + +import {type Props, useCommonSVGProps} from '#/components/icons/common' + +export const VerifierCheck = React.forwardRef<Svg, Props>(function LogoImpl( + props, + ref, +) { + const {fill, size, style, ...rest} = useCommonSVGProps(props) + + return ( + <Svg + fill="none" + {...rest} + ref={ref} + viewBox="0 0 24 24" + width={size} + height={size} + style={[style]}> + <Path + fill={fill} + fillRule="evenodd" + clipRule="evenodd" + d="M8.792 1.54a4.11 4.11 0 0 1 6.416 0 4.128 4.128 0 0 0 3.146 1.54c2.616.04 4.544 2.5 4 5.1a4.277 4.277 0 0 0 .777 3.462c1.6 2.104.912 5.17-1.427 6.36a4.21 4.21 0 0 0-2.177 2.774c-.62 2.584-3.408 3.948-5.781 2.83a4.092 4.092 0 0 0-3.492 0c-2.373 1.118-5.16-.246-5.78-2.83a4.21 4.21 0 0 0-2.178-2.775c-2.34-1.19-3.028-4.256-1.427-6.36a4.277 4.277 0 0 0 .776-3.46c-.543-2.602 1.385-5.06 4.001-5.1a4.128 4.128 0 0 0 3.146-1.54Z" + /> + <Path + fill="#fff" + fillRule="evenodd" + clipRule="evenodd" + d="M17.659 8.399a1.361 1.361 0 0 1 0 1.925l-6.224 6.223a1.361 1.361 0 0 1-1.925 0L6.4 13.435a1.361 1.361 0 1 1 1.925-1.925l2.149 2.15 5.26-5.261a1.361 1.361 0 0 1 1.925 0Z" + /> + </Svg> + ) +}) diff --git a/src/components/verification/VerificationCheck.tsx b/src/components/verification/VerificationCheck.tsx new file mode 100644 index 000000000..4f41c6682 --- /dev/null +++ b/src/components/verification/VerificationCheck.tsx @@ -0,0 +1,12 @@ +import {type Props} from '#/components/icons/common' +import {VerifiedCheck} from '#/components/icons/VerifiedCheck' +import {VerifierCheck} from '#/components/icons/VerifierCheck' + +export function VerificationCheck({ + verifier, + ...rest +}: Props & { + verifier?: boolean +}) { + return verifier ? <VerifierCheck {...rest} /> : <VerifiedCheck {...rest} /> +} diff --git a/src/components/verification/VerificationCheckButton.tsx b/src/components/verification/VerificationCheckButton.tsx new file mode 100644 index 000000000..1b66cd90e --- /dev/null +++ b/src/components/verification/VerificationCheckButton.tsx @@ -0,0 +1,155 @@ +import {View} from 'react-native' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {logger} from '#/logger' +import {type Shadow} from '#/state/cache/types' +import {atoms as a, useBreakpoints, useTheme} from '#/alf' +import {Button} from '#/components/Button' +import {useDialogControl} from '#/components/Dialog' +import {useFullVerificationState} from '#/components/verification' +import {type FullVerificationState} from '#/components/verification' +import {VerificationCheck} from '#/components/verification/VerificationCheck' +import {VerificationsDialog} from '#/components/verification/VerificationsDialog' +import {VerifierDialog} from '#/components/verification/VerifierDialog' +import type * as bsky from '#/types/bsky' + +export function shouldShowVerificationCheckButton( + state: FullVerificationState, +) { + let ok = false + + if (state.profile.role === 'default') { + if (state.profile.isVerified) { + ok = true + } else if (state.profile.isViewer && state.profile.wasVerified) { + ok = true + } else if ( + state.viewer.role === 'verifier' && + state.viewer.hasIssuedVerification + ) { + ok = true + } + } else if (state.profile.role === 'verifier') { + if (state.profile.isViewer) { + ok = true + } else if (state.profile.isVerified) { + ok = true + } + } + + if ( + !state.profile.showBadge && + !state.profile.isViewer && + !(state.viewer.role === 'verifier' && state.viewer.hasIssuedVerification) + ) { + ok = false + } + + return ok +} + +export function VerificationCheckButton({ + profile, + size, +}: { + profile: Shadow<bsky.profile.AnyProfileView> + size: 'lg' | 'md' | 'sm' +}) { + const state = useFullVerificationState({ + profile, + }) + + if (shouldShowVerificationCheckButton(state)) { + return <Badge profile={profile} verificationState={state} size={size} /> + } + + return null +} + +export function Badge({ + profile, + verificationState: state, + size, +}: { + profile: Shadow<bsky.profile.AnyProfileView> + verificationState: FullVerificationState + size: 'lg' | 'md' | 'sm' +}) { + const t = useTheme() + const {_} = useLingui() + const verificationsDialogControl = useDialogControl() + const verifierDialogControl = useDialogControl() + const {gtPhone} = useBreakpoints() + let dimensions = 12 + if (size === 'lg') { + dimensions = gtPhone ? 20 : 18 + } else if (size === 'md') { + dimensions = 16 + } + + const verifiedByHidden = !state.profile.showBadge && state.profile.isViewer + + return ( + <> + <Button + label={ + state.profile.isViewer + ? _(msg`View your verifications`) + : _(msg`View this user's verifications`) + } + hitSlop={20} + onPress={() => { + logger.metric('verification:badge:click', {}) + if (state.profile.role === 'verifier') { + verifierDialogControl.open() + } else { + verificationsDialogControl.open() + } + }} + style={[]}> + {({hovered}) => ( + <View + style={[ + a.justify_end, + a.align_end, + a.transition_transform, + { + width: dimensions, + height: dimensions, + transform: [ + { + scale: hovered ? 1.1 : 1, + }, + ], + }, + ]}> + <VerificationCheck + width={dimensions} + fill={ + verifiedByHidden + ? t.atoms.bg_contrast_100.backgroundColor + : state.profile.isVerified + ? t.palette.primary_500 + : t.atoms.bg_contrast_100.backgroundColor + } + verifier={state.profile.role === 'verifier'} + /> + </View> + )} + </Button> + + <VerificationsDialog + control={verificationsDialogControl} + profile={profile} + verificationState={state} + /> + + <VerifierDialog + control={verifierDialogControl} + profile={profile} + verificationState={state} + /> + </> + ) +} diff --git a/src/components/verification/VerificationCreatePrompt.tsx b/src/components/verification/VerificationCreatePrompt.tsx new file mode 100644 index 000000000..39ac6dbf6 --- /dev/null +++ b/src/components/verification/VerificationCreatePrompt.tsx @@ -0,0 +1,70 @@ +import {useCallback} from 'react' +import {View} from 'react-native' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {logger} from '#/logger' +import {useModerationOpts} from '#/state/preferences/moderation-opts' +import {useVerificationCreateMutation} from '#/state/queries/verification/useVerificationCreateMutation' +import * as Toast from '#/view/com/util/Toast' +import {atoms as a} from '#/alf' +import {type DialogControlProps} from '#/components/Dialog' +import {VerifiedCheck} from '#/components/icons/VerifiedCheck' +import * as ProfileCard from '#/components/ProfileCard' +import * as Prompt from '#/components/Prompt' +import type * as bsky from '#/types/bsky' + +export function VerificationCreatePrompt({ + control, + profile, +}: { + control: DialogControlProps + profile: bsky.profile.AnyProfileView +}) { + const {_} = useLingui() + const moderationOpts = useModerationOpts() + const {mutateAsync: create} = useVerificationCreateMutation() + const onConfirm = useCallback(async () => { + try { + await create({profile}) + Toast.show(_(msg`Successfully verified`)) + } catch (e) { + Toast.show(_(msg`Failed to create a verification`), 'xmark') + logger.error('Failed to create a verification', { + safeMessage: e, + }) + } + }, [_, profile, create]) + + return ( + <Prompt.Outer control={control}> + <View style={[a.flex_row, a.align_center, a.gap_sm, a.pb_sm]}> + <VerifiedCheck width={18} /> + <Prompt.TitleText style={[a.pb_0]}> + {_(msg`Verify this account?`)} + </Prompt.TitleText> + </View> + <Prompt.DescriptionText> + {_(msg`This action can be undone at any time.`)} + </Prompt.DescriptionText> + <View style={[a.pb_xl]}> + {moderationOpts ? ( + <ProfileCard.Header> + <ProfileCard.Avatar + profile={profile} + moderationOpts={moderationOpts} + /> + <ProfileCard.NameAndHandle + profile={profile} + moderationOpts={moderationOpts} + /> + </ProfileCard.Header> + ) : null} + </View> + <Prompt.Actions> + <Prompt.Action cta={_(msg`Verify account`)} onPress={onConfirm} /> + <Prompt.Cancel /> + </Prompt.Actions> + </Prompt.Outer> + ) +} diff --git a/src/components/verification/VerificationRemovePrompt.tsx b/src/components/verification/VerificationRemovePrompt.tsx new file mode 100644 index 000000000..470b61c19 --- /dev/null +++ b/src/components/verification/VerificationRemovePrompt.tsx @@ -0,0 +1,50 @@ +import {useCallback} from 'react' +import {type AppBskyActorDefs} from '@atproto/api' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {logger} from '#/logger' +import {useVerificationsRemoveMutation} from '#/state/queries/verification/useVerificationsRemoveMutation' +import * as Toast from '#/view/com/util/Toast' +import {type DialogControlProps} from '#/components/Dialog' +import * as Prompt from '#/components/Prompt' +import type * as bsky from '#/types/bsky' + +export {useDialogControl as usePromptControl} from '#/components/Dialog' + +export function VerificationRemovePrompt({ + control, + profile, + verifications, + onConfirm: onConfirmInner, +}: { + control: DialogControlProps + profile: bsky.profile.AnyProfileView + verifications: AppBskyActorDefs.VerificationView[] + onConfirm?: () => void +}) { + const {_} = useLingui() + const {mutateAsync: remove} = useVerificationsRemoveMutation() + const onConfirm = useCallback(async () => { + onConfirmInner?.() + try { + await remove({profile, verifications}) + Toast.show(_(msg`Removed verification`)) + } catch (e) { + Toast.show(_(msg`Failed to remove verification`), 'xmark') + logger.error('Failed to remove verification', { + safeMessage: e, + }) + } + }, [_, profile, verifications, remove, onConfirmInner]) + + return ( + <Prompt.Basic + control={control} + title={_(msg`Remove your verification for this account?`)} + onConfirm={onConfirm} + confirmButtonCta={_(msg`Remove verification`)} + confirmButtonColor="negative" + /> + ) +} diff --git a/src/components/verification/VerificationsDialog.tsx b/src/components/verification/VerificationsDialog.tsx new file mode 100644 index 000000000..d61823968 --- /dev/null +++ b/src/components/verification/VerificationsDialog.tsx @@ -0,0 +1,257 @@ +import {View} from 'react-native' +import {type AppBskyActorDefs} from '@atproto/api' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {urls} from '#/lib/constants' +import {getUserDisplayName} from '#/lib/getUserDisplayName' +import {logger} from '#/logger' +import {useModerationOpts} from '#/state/preferences/moderation-opts' +import {useProfileQuery} from '#/state/queries/profile' +import {useSession} from '#/state/session' +import {atoms as a, useBreakpoints, useTheme} from '#/alf' +import {Admonition} from '#/components/Admonition' +import {Button, ButtonIcon, ButtonText} from '#/components/Button' +import * as Dialog from '#/components/Dialog' +import {useDialogControl} from '#/components/Dialog' +import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash' +import {Link} from '#/components/Link' +import * as ProfileCard from '#/components/ProfileCard' +import {Text} from '#/components/Typography' +import {type FullVerificationState} from '#/components/verification' +import {VerificationRemovePrompt} from '#/components/verification/VerificationRemovePrompt' +import type * as bsky from '#/types/bsky' + +export {useDialogControl} from '#/components/Dialog' + +export function VerificationsDialog({ + control, + profile, + verificationState, +}: { + control: Dialog.DialogControlProps + profile: bsky.profile.AnyProfileView + verificationState: FullVerificationState +}) { + return ( + <Dialog.Outer control={control}> + <Inner + control={control} + profile={profile} + verificationState={verificationState} + /> + <Dialog.Close /> + </Dialog.Outer> + ) +} + +function Inner({ + profile, + control, + verificationState: state, +}: { + control: Dialog.DialogControlProps + profile: bsky.profile.AnyProfileView + verificationState: FullVerificationState +}) { + const t = useTheme() + const {_} = useLingui() + const {gtMobile} = useBreakpoints() + + const userName = getUserDisplayName(profile) + const label = state.profile.isViewer + ? state.profile.isVerified + ? _(msg`You are verified`) + : _(msg`Your verifications`) + : state.profile.isVerified + ? _(msg`${userName} is verified`) + : _( + msg({ + message: `${userName}'s verifications`, + comment: `Possessive, meaning "the verifications of {userName}"`, + }), + ) + + return ( + <Dialog.ScrollableInner + label={label} + style={[ + gtMobile ? {width: 'auto', maxWidth: 400, minWidth: 200} : a.w_full, + ]}> + <Dialog.Handle /> + + <View style={[a.gap_sm, a.pb_lg]}> + <Text style={[a.text_2xl, a.font_bold, a.pr_4xl, a.leading_tight]}> + {label} + </Text> + <Text style={[a.text_md, a.leading_snug]}> + {state.profile.isVerified ? ( + <Trans> + This account has a checkmark because it's been verified by trusted + sources. + </Trans> + ) : ( + <Trans> + This account has one or more verifications, but it is not + currently verified. + </Trans> + )} + </Text> + </View> + + {profile.verification ? ( + <View style={[a.pb_xl, a.gap_md]}> + <Text style={[a.text_sm, t.atoms.text_contrast_medium]}> + <Trans>Verified by:</Trans> + </Text> + + <View style={[a.gap_lg]}> + {profile.verification.verifications.map(v => ( + <VerifierCard + key={v.uri} + verification={v} + subject={profile} + outerDialogControl={control} + /> + ))} + </View> + + {profile.verification.verifications.some(v => !v.isValid) && + state.profile.isViewer && ( + <Admonition type="warning" style={[a.mt_xs]}> + <Trans>Some of your verifications are invalid.</Trans> + </Admonition> + )} + </View> + ) : null} + + <View + style={[ + a.w_full, + a.gap_sm, + a.justify_end, + gtMobile + ? [a.flex_row, a.flex_row_reverse, a.justify_start] + : [a.flex_col], + ]}> + <Button + label={_(msg`Close dialog`)} + size="small" + variant="solid" + color="primary" + onPress={() => { + control.close() + }}> + <ButtonText> + <Trans>Close</Trans> + </ButtonText> + </Button> + <Link + overridePresentation + to={urls.website.blog.initialVerificationAnnouncement} + label={_(msg`Learn more about verification on Bluesky`)} + size="small" + variant="solid" + color="secondary" + style={[a.justify_center]} + onPress={() => { + logger.metric('verification:learn-more', { + location: 'verificationsDialog', + }) + }}> + <ButtonText> + <Trans>Learn more</Trans> + </ButtonText> + </Link> + </View> + + <Dialog.Close /> + </Dialog.ScrollableInner> + ) +} + +function VerifierCard({ + verification, + subject, + outerDialogControl, +}: { + verification: AppBskyActorDefs.VerificationView + subject: bsky.profile.AnyProfileView + outerDialogControl: Dialog.DialogControlProps +}) { + const t = useTheme() + const {_} = useLingui() + const {currentAccount} = useSession() + const moderationOpts = useModerationOpts() + const {data: profile, error} = useProfileQuery({did: verification.issuer}) + const verificationRemovePromptControl = useDialogControl() + const canAdminister = verification.issuer === currentAccount?.did + + return ( + <View + style={{ + opacity: verification.isValid ? 1 : 0.5, + }}> + <ProfileCard.Outer> + <ProfileCard.Header> + {error ? ( + <> + <ProfileCard.AvatarPlaceholder /> + <View style={[a.flex_1]}> + <Text + style={[a.text_md, a.font_bold, a.leading_snug]} + numberOfLines={1}> + <Trans>Unknown verifier</Trans> + </Text> + <Text + emoji + style={[a.leading_snug, t.atoms.text_contrast_medium]} + numberOfLines={1}> + {verification.issuer} + </Text> + </View> + </> + ) : profile && moderationOpts ? ( + <> + <ProfileCard.Avatar + profile={profile} + moderationOpts={moderationOpts} + /> + <ProfileCard.NameAndHandle + profile={profile} + moderationOpts={moderationOpts} + /> + {canAdminister && ( + <View> + <Button + label={_(msg`Remove verification`)} + size="small" + variant="outline" + color="negative" + shape="round" + onPress={() => { + verificationRemovePromptControl.open() + }}> + <ButtonIcon icon={TrashIcon} /> + </Button> + </View> + )} + </> + ) : ( + <> + <ProfileCard.AvatarPlaceholder /> + <ProfileCard.NameAndHandlePlaceholder /> + </> + )} + </ProfileCard.Header> + </ProfileCard.Outer> + + <VerificationRemovePrompt + control={verificationRemovePromptControl} + profile={subject} + verifications={[verification]} + onConfirm={() => outerDialogControl.close()} + /> + </View> + ) +} diff --git a/src/components/verification/VerifierDialog.tsx b/src/components/verification/VerifierDialog.tsx new file mode 100644 index 000000000..bfe49ec19 --- /dev/null +++ b/src/components/verification/VerifierDialog.tsx @@ -0,0 +1,153 @@ +import {Text as RNText, View} from 'react-native' +import {Image} from 'expo-image' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {urls} from '#/lib/constants' +import {getUserDisplayName} from '#/lib/getUserDisplayName' +import {NON_BREAKING_SPACE} from '#/lib/strings/constants' +import {logger} from '#/logger' +import {useSession} from '#/state/session' +import {atoms as a, useBreakpoints, useTheme} from '#/alf' +import {Button, ButtonText} from '#/components/Button' +import * as Dialog from '#/components/Dialog' +import {VerifierCheck} from '#/components/icons/VerifierCheck' +import {Link} from '#/components/Link' +import {Text} from '#/components/Typography' +import {type FullVerificationState} from '#/components/verification' +import type * as bsky from '#/types/bsky' + +export {useDialogControl} from '#/components/Dialog' + +export function VerifierDialog({ + control, + profile, + verificationState, +}: { + control: Dialog.DialogControlProps + profile: bsky.profile.AnyProfileView + verificationState: FullVerificationState +}) { + return ( + <Dialog.Outer control={control}> + <Inner + control={control} + profile={profile} + verificationState={verificationState} + /> + <Dialog.Close /> + </Dialog.Outer> + ) +} + +function Inner({ + profile, + control, +}: { + control: Dialog.DialogControlProps + profile: bsky.profile.AnyProfileView + verificationState: FullVerificationState +}) { + const t = useTheme() + const {_} = useLingui() + const {gtMobile} = useBreakpoints() + const {currentAccount} = useSession() + + const isSelf = profile.did === currentAccount?.did + const userName = getUserDisplayName(profile) + const label = isSelf + ? _(msg`You are a trusted verifier`) + : _(msg`${userName} is a trusted verifier`) + + return ( + <Dialog.ScrollableInner + label={label} + style={[ + gtMobile ? {width: 'auto', maxWidth: 400, minWidth: 200} : a.w_full, + ]}> + <Dialog.Handle /> + + <View style={[a.gap_lg]}> + <View + style={[ + a.w_full, + a.rounded_md, + a.overflow_hidden, + t.atoms.bg_contrast_25, + {minHeight: 100}, + ]}> + <Image + accessibilityIgnoresInvertColors + source={require('../../../assets/images/initial_verification_announcement_1.png')} + style={[ + { + aspectRatio: 353 / 160, + }, + ]} + alt={_( + msg`An illustration showing that Bluesky selects trusted verifiers, and trusted verifiers in turn verify individual user accounts.`, + )} + /> + </View> + + <View style={[a.gap_sm]}> + <Text style={[a.text_2xl, a.font_bold, a.pr_4xl, a.leading_tight]}> + {label} + </Text> + <Text style={[a.text_md, a.leading_snug]}> + <Trans> + Accounts with a scalloped blue check mark + <RNText> + {NON_BREAKING_SPACE} + <VerifierCheck width={14} /> + {NON_BREAKING_SPACE} + </RNText> + can verify others. These trusted verifiers are selected by + Bluesky. + </Trans> + </Text> + </View> + + <View + style={[ + a.w_full, + a.gap_sm, + a.justify_end, + gtMobile ? [a.flex_row, a.justify_end] : [a.flex_col], + ]}> + <Link + overridePresentation + to={urls.website.blog.initialVerificationAnnouncement} + label={_(msg`Learn more about verification on Bluesky`)} + size="small" + variant="solid" + color="primary" + style={[a.justify_center]} + onPress={() => { + logger.metric('verification:learn-more', { + location: 'verifierDialog', + }) + }}> + <ButtonText> + <Trans>Learn more</Trans> + </ButtonText> + </Link> + <Button + label={_(msg`Close dialog`)} + size="small" + variant="solid" + color="secondary" + onPress={() => { + control.close() + }}> + <ButtonText> + <Trans>Close</Trans> + </ButtonText> + </Button> + </View> + </View> + + <Dialog.Close /> + </Dialog.ScrollableInner> + ) +} diff --git a/src/components/verification/index.ts b/src/components/verification/index.ts new file mode 100644 index 000000000..7a83a160a --- /dev/null +++ b/src/components/verification/index.ts @@ -0,0 +1,113 @@ +import {useMemo} from 'react' + +import {usePreferencesQuery} from '#/state/queries/preferences' +import {useCurrentAccountProfile} from '#/state/queries/useCurrentAccountProfile' +import {useSession} from '#/state/session' +import type * as bsky from '#/types/bsky' + +export type FullVerificationState = { + profile: { + role: 'default' | 'verifier' + isVerified: boolean + wasVerified: boolean + isViewer: boolean + showBadge: boolean + } + viewer: + | { + role: 'default' + isVerified: boolean + } + | { + role: 'verifier' + isVerified: boolean + hasIssuedVerification: boolean + } +} + +export function useFullVerificationState({ + profile, +}: { + profile: bsky.profile.AnyProfileView +}): FullVerificationState { + const {currentAccount} = useSession() + const currentAccountProfile = useCurrentAccountProfile() + const profileState = useSimpleVerificationState({profile}) + const viewerState = useSimpleVerificationState({ + profile: currentAccountProfile, + }) + + return useMemo(() => { + const verifications = profile.verification?.verifications || [] + const wasVerified = + profileState.role === 'default' && + !profileState.isVerified && + verifications.length > 0 + const hasIssuedVerification = Boolean( + viewerState && + viewerState.role === 'verifier' && + profileState.role === 'default' && + verifications.find(v => v.issuer === currentAccount?.did), + ) + + return { + profile: { + ...profileState, + wasVerified, + isViewer: profile.did === currentAccount?.did, + showBadge: profileState.showBadge, + }, + viewer: + viewerState.role === 'verifier' + ? { + role: 'verifier', + isVerified: viewerState.isVerified, + hasIssuedVerification, + } + : { + role: 'default', + isVerified: viewerState.isVerified, + }, + } + }, [profile, currentAccount, profileState, viewerState]) +} + +export type SimpleVerificationState = { + role: 'default' | 'verifier' + isVerified: boolean + showBadge: boolean +} + +export function useSimpleVerificationState({ + profile, +}: { + profile?: bsky.profile.AnyProfileView +}): SimpleVerificationState { + const preferences = usePreferencesQuery() + const prefs = useMemo( + () => preferences.data?.verificationPrefs || {hideBadges: false}, + [preferences.data?.verificationPrefs], + ) + return useMemo(() => { + if (!profile || !profile.verification) { + return { + role: 'default', + isVerified: false, + showBadge: false, + } + } + + const {verifiedStatus, trustedVerifierStatus} = profile.verification + const isVerifiedUser = ['valid', 'invalid'].includes(verifiedStatus) + const isVerifierUser = ['valid', 'invalid'].includes(trustedVerifierStatus) + const isVerified = + (isVerifiedUser && verifiedStatus === 'valid') || + (isVerifierUser && trustedVerifierStatus === 'valid') + + return { + role: isVerifierUser ? 'verifier' : 'default', + isVerified, + showBadge: prefs.hideBadges ? false : isVerified, + } + }, [profile, prefs]) +} |