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