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