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} 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}