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