Bluesky app fork with some witchin' additions 馃挮
at 5ee667f307bc459ba53cdaabdad00a0ea1ee6846 218 lines 6.8 kB view raw
1import React, {useCallback} from 'react' 2import {View} from 'react-native' 3import {type AppBskyActorDefs} from '@atproto/api' 4import {msg} from '@lingui/core/macro' 5import {useLingui} from '@lingui/react' 6import {Trans} from '@lingui/react/macro' 7 8import {isJwtExpired} from '#/lib/jwt' 9import {sanitizeDisplayName} from '#/lib/strings/display-names' 10import {sanitizeHandle} from '#/lib/strings/handles' 11import {useProfilesQuery} from '#/state/queries/profile' 12import {type SessionAccount, useSession} from '#/state/session' 13import {UserAvatar} from '#/view/com/util/UserAvatar' 14import {atoms as a, useTheme} from '#/alf' 15import {Button} from '#/components/Button' 16import {CheckThick_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check' 17import {ChevronRight_Stroke2_Corner0_Rounded as ChevronIcon} from '#/components/icons/Chevron' 18import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus' 19import {Text} from '#/components/Typography' 20import {useSimpleVerificationState} from '#/components/verification' 21import {VerificationCheck} from '#/components/verification/VerificationCheck' 22import {useActorStatus} from '#/features/liveNow' 23 24export function AccountList({ 25 onSelectAccount, 26 onSelectOther, 27 otherLabel, 28 pendingDid, 29}: { 30 onSelectAccount: (account: SessionAccount) => void 31 onSelectOther: () => void 32 otherLabel?: string 33 pendingDid: string | null 34}) { 35 const {currentAccount, accounts} = useSession() 36 const t = useTheme() 37 const {_} = useLingui() 38 const {data: profiles} = useProfilesQuery({ 39 handles: accounts.map(acc => acc.did), 40 }) 41 42 const onPressAddAccount = useCallback(() => { 43 onSelectOther() 44 }, [onSelectOther]) 45 46 return ( 47 <View 48 pointerEvents={pendingDid ? 'none' : 'auto'} 49 style={[ 50 a.rounded_lg, 51 a.overflow_hidden, 52 a.border, 53 t.atoms.border_contrast_low, 54 ]}> 55 {accounts.map(account => ( 56 <React.Fragment key={account.did}> 57 <AccountItem 58 profile={profiles?.profiles.find(p => p.did === account.did)} 59 account={account} 60 onSelect={onSelectAccount} 61 isCurrentAccount={account.did === currentAccount?.did} 62 isPendingAccount={account.did === pendingDid} 63 /> 64 <View style={[a.border_b, t.atoms.border_contrast_low]} /> 65 </React.Fragment> 66 ))} 67 <Button 68 testID="chooseAddAccountBtn" 69 style={[a.flex_1]} 70 onPress={pendingDid ? undefined : onPressAddAccount} 71 label={_(msg`Sign in to account that is not listed`)}> 72 {({hovered, pressed}) => ( 73 <View 74 style={[ 75 a.flex_1, 76 a.flex_row, 77 a.align_center, 78 a.p_lg, 79 a.gap_sm, 80 (hovered || pressed) && t.atoms.bg_contrast_25, 81 ]}> 82 <View 83 style={[ 84 t.atoms.bg_contrast_25, 85 a.rounded_full, 86 {width: 48, height: 48}, 87 a.justify_center, 88 a.align_center, 89 (hovered || pressed) && t.atoms.bg_contrast_50, 90 ]}> 91 <PlusIcon style={[t.atoms.text_contrast_low]} size="md" /> 92 </View> 93 <Text style={[a.flex_1, a.leading_tight, a.text_md, a.font_medium]}> 94 {otherLabel ?? <Trans>Other account</Trans>} 95 </Text> 96 <ChevronIcon size="md" style={[t.atoms.text_contrast_low]} /> 97 </View> 98 )} 99 </Button> 100 </View> 101 ) 102} 103 104function AccountItem({ 105 profile, 106 account, 107 onSelect, 108 isCurrentAccount, 109 isPendingAccount, 110}: { 111 profile?: AppBskyActorDefs.ProfileViewDetailed 112 account: SessionAccount 113 onSelect: (account: SessionAccount) => void 114 isCurrentAccount: boolean 115 isPendingAccount: boolean 116}) { 117 const t = useTheme() 118 const {_} = useLingui() 119 const verification = useSimpleVerificationState({profile}) 120 const {isActive: live} = useActorStatus(profile) 121 122 const onPress = useCallback(() => { 123 onSelect(account) 124 }, [account, onSelect]) 125 126 const isLoggedOut = !account.refreshJwt || isJwtExpired(account.refreshJwt) 127 128 return ( 129 <Button 130 testID={`chooseAccountBtn-${account.handle}`} 131 key={account.did} 132 style={[a.w_full]} 133 onPress={onPress} 134 label={ 135 isCurrentAccount 136 ? _(msg`Continue as ${account.handle} (currently signed in)`) 137 : _(msg`Sign in as ${account.handle}`) 138 }> 139 {({hovered, pressed}) => ( 140 <View 141 style={[ 142 a.flex_1, 143 a.flex_row, 144 a.align_center, 145 a.p_lg, 146 a.gap_sm, 147 (hovered || pressed || isPendingAccount) && t.atoms.bg_contrast_25, 148 ]}> 149 <UserAvatar 150 avatar={profile?.avatar} 151 size={48} 152 type={profile?.associated?.labeler ? 'labeler' : 'user'} 153 live={live} 154 hideLiveBadge 155 /> 156 157 <View style={[a.flex_1, a.gap_2xs, a.pr_2xl]}> 158 <View style={[a.flex_row, a.align_center, a.gap_xs]}> 159 <Text 160 emoji 161 style={[a.font_medium, a.leading_tight, a.text_md]} 162 numberOfLines={1}> 163 {sanitizeDisplayName( 164 profile?.displayName || profile?.handle || account.handle, 165 )} 166 </Text> 167 {verification.showBadge && ( 168 <View> 169 <VerificationCheck 170 width={12} 171 verifier={verification.role === 'verifier'} 172 /> 173 </View> 174 )} 175 </View> 176 <Text 177 style={[ 178 a.leading_tight, 179 t.atoms.text_contrast_medium, 180 a.text_sm, 181 ]}> 182 {sanitizeHandle(account.handle, '@')} 183 </Text> 184 {isLoggedOut && ( 185 <Text 186 style={[ 187 a.leading_tight, 188 a.text_xs, 189 a.italic, 190 t.atoms.text_contrast_medium, 191 ]}> 192 <Trans>Logged out</Trans> 193 </Text> 194 )} 195 </View> 196 197 {isCurrentAccount ? ( 198 <View 199 style={[ 200 { 201 width: 20, 202 height: 20, 203 backgroundColor: t.palette.positive_500, 204 }, 205 a.rounded_full, 206 a.justify_center, 207 a.align_center, 208 ]}> 209 <CheckIcon size="xs" style={[{color: t.palette.white}]} /> 210 </View> 211 ) : ( 212 <ChevronIcon size="md" style={[t.atoms.text_contrast_low]} /> 213 )} 214 </View> 215 )} 216 </Button> 217 ) 218}