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