An ATproto social media client -- with an independent Appview.
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}