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 {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 {PdsBadge} from '#/components/PdsBadge'
21import {ProfileBadges} from '#/components/ProfileBadges'
22import {Text} from '#/components/Typography'
23import {useActorStatus} from '#/features/liveNow'
24
25export function AccountList({
26 onSelectAccount,
27 onSelectOther,
28 otherLabel,
29 pendingDid,
30}: {
31 onSelectAccount: (account: SessionAccount) => void
32 onSelectOther: () => void
33 otherLabel?: string
34 pendingDid: string | null
35}) {
36 const {currentAccount, accounts} = useSession()
37 const t = useTheme()
38 const {_} = useLingui()
39 const enableSquareButtons = useEnableSquareButtons()
40 const {data: profiles} = useProfilesQuery({
41 handles: accounts.map(acc => acc.did),
42 })
43
44 const onPressAddAccount = useCallback(() => {
45 onSelectOther()
46 }, [onSelectOther])
47
48 return (
49 <View
50 pointerEvents={pendingDid ? 'none' : 'auto'}
51 style={[
52 a.rounded_lg,
53 a.overflow_hidden,
54 a.border,
55 t.atoms.border_contrast_low,
56 ]}>
57 {accounts.map(account => (
58 <Fragment key={account.did}>
59 <AccountItem
60 profile={profiles?.profiles.find(p => p.did === account.did)}
61 account={account}
62 onSelect={onSelectAccount}
63 isCurrentAccount={account.did === currentAccount?.did}
64 isPendingAccount={account.did === pendingDid}
65 />
66 <View style={[a.border_b, t.atoms.border_contrast_low]} />
67 </Fragment>
68 ))}
69 <Button
70 testID="chooseAddAccountBtn"
71 style={[a.flex_1]}
72 onPress={pendingDid ? undefined : onPressAddAccount}
73 label={_(msg`Sign in to account that is not listed`)}>
74 {({hovered, pressed}) => (
75 <View
76 style={[
77 a.flex_1,
78 a.flex_row,
79 a.align_center,
80 a.p_lg,
81 a.gap_sm,
82 (hovered || pressed) && t.atoms.bg_contrast_25,
83 ]}>
84 <View
85 style={[
86 t.atoms.bg_contrast_25,
87 enableSquareButtons ? a.rounded_sm : a.rounded_full,
88 {width: 48, height: 48},
89 a.justify_center,
90 a.align_center,
91 (hovered || pressed) && t.atoms.bg_contrast_50,
92 ]}>
93 <PlusIcon style={[t.atoms.text_contrast_low]} size="md" />
94 </View>
95 <Text style={[a.flex_1, a.leading_tight, a.text_md, a.font_medium]}>
96 {otherLabel ?? <Trans>Other account</Trans>}
97 </Text>
98 <ChevronIcon size="md" style={[t.atoms.text_contrast_low]} />
99 </View>
100 )}
101 </Button>
102 </View>
103 )
104}
105
106function AccountItem({
107 profile,
108 account,
109 onSelect,
110 isCurrentAccount,
111 isPendingAccount,
112}: {
113 profile?: AppBskyActorDefs.ProfileViewDetailed
114 account: SessionAccount
115 onSelect: (account: SessionAccount) => void
116 isCurrentAccount: boolean
117 isPendingAccount: boolean
118}) {
119 const t = useTheme()
120 const {_} = useLingui()
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 <PdsBadge did={account.did} size="sm" />
170 {profile && (
171 <ProfileBadges
172 profile={profile}
173 size="sm"
174 style={[{marginTop: -2}]}
175 />
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}