this repo has no description
1import {useCallback, useImperativeHandle, useRef, useState} from 'react'
2import {View} from 'react-native'
3import {msg} from '@lingui/core/macro'
4import {useLingui} from '@lingui/react'
5import {Trans} from '@lingui/react/macro'
6
7import {BSKY_SERVICE} from '#/lib/constants'
8import * as persisted from '#/state/persisted'
9import {useSession} from '#/state/session'
10import {atoms as a, platform, useBreakpoints, useTheme, web} from '#/alf'
11import {Admonition} from '#/components/Admonition'
12import {Button, ButtonText} from '#/components/Button'
13import * as Dialog from '#/components/Dialog'
14import * as SegmentedControl from '#/components/forms/SegmentedControl'
15import * as TextField from '#/components/forms/TextField'
16import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe'
17import {InlineLinkText} from '#/components/Link'
18import {Text} from '#/components/Typography'
19import {useAnalytics} from '#/analytics'
20
21type SegmentedControlOptions = typeof BSKY_SERVICE | 'custom'
22
23export function ServerInputDialog({
24 control,
25 onSelect,
26}: {
27 control: Dialog.DialogOuterProps['control']
28 onSelect: (url: string) => void
29}) {
30 const ax = useAnalytics()
31 const formRef = useRef<DialogInnerRef>(null)
32
33 // persist these options between dialog open/close
34 const [fixedOption, setFixedOption] =
35 useState<SegmentedControlOptions>(BSKY_SERVICE)
36 const [previousCustomAddress, setPreviousCustomAddress] = useState('')
37
38 const onClose = useCallback(() => {
39 const result = formRef.current?.getFormState()
40 if (result) {
41 onSelect(result)
42 if (result !== BSKY_SERVICE) {
43 setPreviousCustomAddress(result)
44 }
45 }
46 ax.metric('signin:hostingProviderPressed', {
47 hostingProviderDidChange: fixedOption !== BSKY_SERVICE,
48 })
49 }, [ax, onSelect, fixedOption])
50
51 return (
52 <Dialog.Outer
53 control={control}
54 onClose={onClose}
55 nativeOptions={{preventExpansion: true}}>
56 <Dialog.Handle />
57 <DialogInner
58 formRef={formRef}
59 fixedOption={fixedOption}
60 setFixedOption={setFixedOption}
61 initialCustomAddress={previousCustomAddress}
62 />
63 </Dialog.Outer>
64 )
65}
66
67type DialogInnerRef = {getFormState: () => string | null}
68
69function DialogInner({
70 formRef,
71 fixedOption,
72 setFixedOption,
73 initialCustomAddress,
74}: {
75 formRef: React.Ref<DialogInnerRef>
76 fixedOption: SegmentedControlOptions
77 setFixedOption: (opt: SegmentedControlOptions) => void
78 initialCustomAddress: string
79}) {
80 const control = Dialog.useDialogContext()
81 const {_} = useLingui()
82 const t = useTheme()
83 const {accounts} = useSession()
84 const {gtMobile} = useBreakpoints()
85 const [customAddress, setCustomAddress] = useState(initialCustomAddress)
86 const [pdsAddressHistory, setPdsAddressHistory] = useState<string[]>(
87 persisted.get('pdsAddressHistory') || [],
88 )
89
90 useImperativeHandle(
91 formRef,
92 () => ({
93 getFormState: () => {
94 let url
95 if (fixedOption === 'custom') {
96 url = customAddress.trim().toLowerCase()
97 if (!url) {
98 return null
99 }
100 } else {
101 url = fixedOption
102 }
103 if (!url.startsWith('http://') && !url.startsWith('https://')) {
104 if (url === 'localhost' || url.startsWith('localhost:')) {
105 url = `http://${url}`
106 } else {
107 url = `https://${url}`
108 }
109 }
110
111 if (fixedOption === 'custom') {
112 if (!pdsAddressHistory.includes(url)) {
113 const newHistory = [url, ...pdsAddressHistory.slice(0, 4)]
114 setPdsAddressHistory(newHistory)
115 persisted.write('pdsAddressHistory', newHistory)
116 }
117 }
118
119 return url
120 },
121 }),
122 [customAddress, fixedOption, pdsAddressHistory],
123 )
124
125 const isFirstTimeUser = accounts.length === 0
126
127 return (
128 <Dialog.ScrollableInner
129 accessibilityDescribedBy="dialog-description"
130 accessibilityLabelledBy="dialog-title"
131 style={web({maxWidth: 500})}>
132 <View style={[a.relative, a.gap_md, a.w_full]}>
133 <Text nativeID="dialog-title" style={[a.text_2xl, a.font_bold]}>
134 <Trans>Choose your account provider</Trans>
135 </Text>
136 <SegmentedControl.Root
137 type="tabs"
138 label={_(msg`Account provider`)}
139 value={fixedOption}
140 onChange={setFixedOption}>
141 <SegmentedControl.Item
142 testID="bskyServiceSelectBtn"
143 value={BSKY_SERVICE}
144 label={_(msg`Bluesky`)}>
145 <SegmentedControl.ItemText>
146 {_(msg`Bluesky`)}
147 </SegmentedControl.ItemText>
148 </SegmentedControl.Item>
149 <SegmentedControl.Item
150 testID="customSelectBtn"
151 value="custom"
152 label={_(msg`Custom`)}>
153 <SegmentedControl.ItemText>
154 {_(msg`Custom`)}
155 </SegmentedControl.ItemText>
156 </SegmentedControl.Item>
157 </SegmentedControl.Root>
158
159 {fixedOption === BSKY_SERVICE && isFirstTimeUser && (
160 <View role="tabpanel">
161 <Admonition type="tip">
162 <Trans>
163 Bluesky is an open network where you can choose your own
164 provider. If you're new here, we recommend sticking with the
165 default Bluesky Social option.
166 </Trans>
167 </Admonition>
168 </View>
169 )}
170
171 {fixedOption === 'custom' && (
172 <View role="tabpanel">
173 <TextField.LabelText nativeID="address-input-label">
174 <Trans>Server address</Trans>
175 </TextField.LabelText>
176 <TextField.Root>
177 <TextField.Icon icon={Globe} />
178 <Dialog.Input
179 testID="customServerTextInput"
180 value={customAddress}
181 onChangeText={setCustomAddress}
182 label="my-server.com"
183 accessibilityLabelledBy="address-input-label"
184 autoCapitalize="none"
185 keyboardType="url"
186 />
187 </TextField.Root>
188 {pdsAddressHistory.length > 0 && (
189 <View style={[a.flex_row, a.flex_wrap, a.mt_xs]}>
190 {pdsAddressHistory.map(uri => (
191 <Button
192 key={uri}
193 variant="ghost"
194 color="primary"
195 label={uri}
196 style={[a.px_sm, a.py_xs, a.rounded_sm, a.gap_sm]}
197 onPress={() => setCustomAddress(uri)}>
198 <ButtonText>{uri}</ButtonText>
199 </Button>
200 ))}
201 </View>
202 )}
203 </View>
204 )}
205
206 <View style={[a.py_xs]}>
207 <Text
208 style={[t.atoms.text_contrast_medium, a.text_sm, a.leading_snug]}>
209 {isFirstTimeUser ? (
210 <Trans>
211 If you're a developer, you can host your own server.
212 </Trans>
213 ) : (
214 <Trans>
215 Bluesky is an open network where you can choose your hosting
216 provider. If you're a developer, you can host your own server.
217 </Trans>
218 )}{' '}
219 <InlineLinkText
220 label={_(msg`Learn more about self hosting your PDS.`)}
221 to="https://atproto.com/guides/self-hosting">
222 <Trans>Learn more.</Trans>
223 </InlineLinkText>
224 </Text>
225 </View>
226
227 <View style={gtMobile && [a.flex_row, a.justify_end]}>
228 <Button
229 testID="doneBtn"
230 variant="solid"
231 color="primary"
232 size={platform({
233 native: 'large',
234 web: 'small',
235 })}
236 onPress={() => control.close()}
237 label={_(msg`Done`)}>
238 <ButtonText>
239 <Trans>Done</Trans>
240 </ButtonText>
241 </Button>
242 </View>
243 </View>
244 </Dialog.ScrollableInner>
245 )
246}