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