An ATproto social media client -- with an independent Appview.
at main 5.6 kB view raw
1import React, {useCallback} from 'react' 2import {View} from 'react-native' 3import {type AppBskyActorDefs} from '@atproto/api' 4import {msg, Trans} from '@lingui/macro' 5import {useLingui} from '@lingui/react' 6 7import {useActorStatus} from '#/lib/actor-status' 8import {sanitizeDisplayName} from '#/lib/strings/display-names' 9import {sanitizeHandle} from '#/lib/strings/handles' 10import {useProfilesQuery} from '#/state/queries/profile' 11import {type SessionAccount, useSession} from '#/state/session' 12import {UserAvatar} from '#/view/com/util/UserAvatar' 13import {atoms as a, useTheme} from '#/alf' 14import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check' 15import {ChevronRight_Stroke2_Corner0_Rounded as Chevron} from '#/components/icons/Chevron' 16import {useSimpleVerificationState} from '#/components/verification' 17import {VerificationCheck} from '#/components/verification/VerificationCheck' 18import {Button} from './Button' 19import {Text} from './Typography' 20 21export function AccountList({ 22 onSelectAccount, 23 onSelectOther, 24 otherLabel, 25 pendingDid, 26}: { 27 onSelectAccount: (account: SessionAccount) => void 28 onSelectOther: () => void 29 otherLabel?: string 30 pendingDid: string | null 31}) { 32 const {currentAccount, accounts} = useSession() 33 const t = useTheme() 34 const {_} = useLingui() 35 const {data: profiles} = useProfilesQuery({ 36 handles: accounts.map(acc => acc.did), 37 }) 38 39 const onPressAddAccount = useCallback(() => { 40 onSelectOther() 41 }, [onSelectOther]) 42 43 return ( 44 <View 45 pointerEvents={pendingDid ? 'none' : 'auto'} 46 style={[ 47 a.rounded_md, 48 a.overflow_hidden, 49 {borderWidth: 1}, 50 t.atoms.border_contrast_low, 51 ]}> 52 {accounts.map(account => ( 53 <React.Fragment key={account.did}> 54 <AccountItem 55 profile={profiles?.profiles.find(p => p.did === account.did)} 56 account={account} 57 onSelect={onSelectAccount} 58 isCurrentAccount={account.did === currentAccount?.did} 59 isPendingAccount={account.did === pendingDid} 60 /> 61 <View style={[{borderBottomWidth: 1}, t.atoms.border_contrast_low]} /> 62 </React.Fragment> 63 ))} 64 <Button 65 testID="chooseAddAccountBtn" 66 style={[a.flex_1]} 67 onPress={pendingDid ? undefined : onPressAddAccount} 68 label={_(msg`Sign in to account that is not listed`)}> 69 {({hovered, pressed}) => ( 70 <View 71 style={[ 72 a.flex_1, 73 a.flex_row, 74 a.align_center, 75 {height: 48}, 76 (hovered || pressed) && t.atoms.bg_contrast_25, 77 ]}> 78 <Text 79 style={[ 80 a.font_bold, 81 a.flex_1, 82 a.flex_row, 83 a.py_sm, 84 a.leading_tight, 85 t.atoms.text_contrast_medium, 86 {paddingLeft: 56}, 87 ]}> 88 {otherLabel ?? <Trans>Other account</Trans>} 89 </Text> 90 <Chevron size="sm" style={[t.atoms.text, a.mr_md]} /> 91 </View> 92 )} 93 </Button> 94 </View> 95 ) 96} 97 98function AccountItem({ 99 profile, 100 account, 101 onSelect, 102 isCurrentAccount, 103 isPendingAccount, 104}: { 105 profile?: AppBskyActorDefs.ProfileViewDetailed 106 account: SessionAccount 107 onSelect: (account: SessionAccount) => void 108 isCurrentAccount: boolean 109 isPendingAccount: boolean 110}) { 111 const t = useTheme() 112 const {_} = useLingui() 113 const verification = useSimpleVerificationState({profile}) 114 const {isActive: live} = useActorStatus(profile) 115 116 const onPress = useCallback(() => { 117 onSelect(account) 118 }, [account, onSelect]) 119 120 return ( 121 <Button 122 testID={`chooseAccountBtn-${account.handle}`} 123 key={account.did} 124 style={[a.w_full]} 125 onPress={onPress} 126 label={ 127 isCurrentAccount 128 ? _(msg`Continue as ${account.handle} (currently signed in)`) 129 : _(msg`Sign in as ${account.handle}`) 130 }> 131 {({hovered, pressed}) => ( 132 <View 133 style={[ 134 a.flex_1, 135 a.flex_row, 136 a.align_center, 137 a.px_md, 138 a.gap_sm, 139 {height: 56}, 140 (hovered || pressed || isPendingAccount) && t.atoms.bg_contrast_25, 141 ]}> 142 <UserAvatar 143 avatar={profile?.avatar} 144 size={36} 145 type={profile?.associated?.labeler ? 'labeler' : 'user'} 146 live={live} 147 hideLiveBadge 148 /> 149 150 <View style={[a.flex_1, a.gap_2xs, a.pr_2xl]}> 151 <View style={[a.flex_row, a.align_center, a.gap_xs]}> 152 <Text 153 emoji 154 style={[a.font_bold, a.leading_tight]} 155 numberOfLines={1}> 156 {sanitizeDisplayName( 157 profile?.displayName || profile?.handle || account.handle, 158 )} 159 </Text> 160 {verification.showBadge && ( 161 <View> 162 <VerificationCheck 163 width={12} 164 verifier={verification.role === 'verifier'} 165 /> 166 </View> 167 )} 168 </View> 169 <Text style={[a.leading_tight, t.atoms.text_contrast_medium]}> 170 {sanitizeHandle(account.handle, '@')} 171 </Text> 172 </View> 173 174 {isCurrentAccount ? ( 175 <Check size="sm" style={[{color: t.palette.positive_600}]} /> 176 ) : ( 177 <Chevron size="sm" style={[t.atoms.text]} /> 178 )} 179 </View> 180 )} 181 </Button> 182 ) 183}