forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import {useState} from 'react'
2import {Image, View} from 'react-native'
3import {
4 FontAwesomeIcon,
5 type FontAwesomeIconStyle,
6} from '@fortawesome/react-native-fontawesome'
7import {msg, Trans} from '@lingui/macro'
8import {useLingui} from '@lingui/react'
9
10import {isBridgedPdsUrl, isBskyPdsUrl} from '#/state/queries/pds-label'
11import {atoms as a, useBreakpoints, useTheme} from '#/alf'
12import {Button, ButtonText} from '#/components/Button'
13import * as Dialog from '#/components/Dialog'
14import {Fediverse as FediverseIcon} from '#/components/icons/Fediverse'
15import {Mark as BskyMark} from '#/components/icons/Logo'
16import {InlineLinkText} from '#/components/Link'
17import {Text} from '#/components/Typography'
18
19function formatBskyPdsDisplayName(hostname: string): string {
20 const match = hostname.match(/^([^.]+)\.([^.]+)\.host\.bsky\.network$/)
21 if (match) {
22 const name = match[1].charAt(0).toUpperCase() + match[1].slice(1)
23 const rawRegion = match[2]
24 const region = rawRegion
25 .replace(/^us-east$/, 'US East')
26 .replace(/^us-west$/, 'US West')
27 .replace(/^eu-west$/, 'EU West')
28 .replace(
29 /^ap-(.+)$/,
30 (_match: string, r: string) =>
31 `AP ${r.charAt(0).toUpperCase()}${r.slice(1)}`,
32 )
33 return `${name} (${region})`
34 }
35 if (hostname === 'bsky.social') return 'Bluesky Social'
36 return hostname
37}
38
39export function PdsDialog({
40 control,
41 pdsUrl,
42 faviconUrl,
43}: {
44 control: Dialog.DialogControlProps
45 pdsUrl: string
46 faviconUrl: string
47}) {
48 const {_} = useLingui()
49 const {gtMobile} = useBreakpoints()
50
51 let hostname = pdsUrl
52 try {
53 hostname = new URL(pdsUrl).hostname
54 } catch {}
55
56 const isBsky = isBskyPdsUrl(pdsUrl)
57 const isBridged = isBridgedPdsUrl(pdsUrl)
58 const displayName = isBsky ? formatBskyPdsDisplayName(hostname) : hostname
59
60 return (
61 <Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}>
62 <Dialog.Handle />
63 <Dialog.ScrollableInner
64 label={_(msg`PDS Information`)}
65 style={[
66 gtMobile ? {width: 'auto', maxWidth: 400, minWidth: 200} : a.w_full,
67 ]}>
68 <View style={[a.gap_md, a.pb_lg]}>
69 <View style={[a.flex_row, a.align_center, a.gap_md]}>
70 <FaviconOrGlobe
71 faviconUrl={faviconUrl}
72 isBsky={isBsky}
73 isBridged={isBridged}
74 size={36}
75 />
76 <View style={[a.flex_1]}>
77 <Text
78 style={[a.text_2xl, a.font_semi_bold, a.leading_tight]}
79 numberOfLines={1}>
80 {displayName}
81 </Text>
82 {isBsky && (
83 <Text style={[a.text_sm]}>
84 <Trans>Bluesky-hosted PDS</Trans>
85 </Text>
86 )}
87 {isBridged && (
88 <Text style={[a.text_sm]}>
89 <Trans>Fediverse bridge</Trans>
90 </Text>
91 )}
92 </View>
93 </View>
94
95 <Text style={[a.text_md, a.leading_snug]}>
96 <Trans>
97 This account's data is stored on a Personal Data Server (PDS):{' '}
98 <InlineLinkText
99 to={pdsUrl}
100 label={displayName}
101 style={[a.text_md, a.font_semi_bold]}>
102 {displayName}
103 </InlineLinkText>
104 {'. '}A PDS is where your posts, follows, and other data live on
105 the AT Protocol network.
106 </Trans>
107 </Text>
108
109 {isBridged && (
110 <Text style={[a.text_md, a.leading_snug]}>
111 <Trans>
112 This account is bridged from the Fediverse via{' '}
113 <InlineLinkText
114 to="https://fed.brid.gy"
115 label="Bridgy Fed"
116 style={[a.text_md, a.font_semi_bold]}>
117 Bridgy Fed
118 </InlineLinkText>
119 . Their original account lives on a Fediverse platform such as
120 Mastodon.
121 </Trans>
122 </Text>
123 )}
124
125 {!isBsky && !isBridged && (
126 <Text style={[a.text_md, a.leading_snug]}>
127 <Trans>
128 This account is self-hosted or uses a third-party PDS provider.
129 </Trans>
130 </Text>
131 )}
132 </View>
133
134 <View
135 style={[
136 a.w_full,
137 a.gap_sm,
138 gtMobile
139 ? [a.flex_row, a.flex_row_reverse, a.justify_start]
140 : [a.flex_col],
141 ]}>
142 <Button
143 label={_(msg`Close dialog`)}
144 size="small"
145 variant="solid"
146 color="primary"
147 onPress={() => control.close()}>
148 <ButtonText>
149 <Trans>Close</Trans>
150 </ButtonText>
151 </Button>
152 </View>
153
154 <Dialog.Close />
155 </Dialog.ScrollableInner>
156 </Dialog.Outer>
157 )
158}
159
160export function FaviconOrGlobe({
161 faviconUrl,
162 isBsky,
163 isBridged,
164 size,
165 borderRadius,
166}: {
167 faviconUrl: string
168 isBsky: boolean
169 isBridged: boolean
170 size: number
171 borderRadius?: number
172}) {
173 const t = useTheme()
174 const [imgError, setImgError] = useState(false)
175 const resolvedBorderRadius = borderRadius ?? size / 5
176
177 if (isBsky) {
178 return (
179 <View
180 style={[
181 a.align_center,
182 a.justify_center,
183 a.overflow_hidden,
184 {
185 width: size,
186 height: size,
187 borderRadius: resolvedBorderRadius,
188 backgroundColor: '#0085ff',
189 },
190 ]}>
191 <BskyMark width={Math.round(size * 0.8)} style={{color: '#fff'}} />
192 </View>
193 )
194 }
195
196 if (isBridged) {
197 return (
198 <View
199 style={[
200 a.align_center,
201 a.justify_center,
202 a.overflow_hidden,
203 {
204 width: size,
205 height: size,
206 borderRadius: resolvedBorderRadius,
207 backgroundColor: '#6364FF',
208 },
209 ]}>
210 <FediverseIcon width={Math.round(size * 0.8)} style={{color: '#fff'}} />
211 </View>
212 )
213 }
214
215 if (!imgError && faviconUrl) {
216 return (
217 <View
218 style={[
219 a.overflow_hidden,
220 a.align_center,
221 a.justify_center,
222 {
223 width: size,
224 height: size,
225 borderRadius: resolvedBorderRadius,
226 backgroundColor: t.atoms.bg_contrast_100.backgroundColor,
227 },
228 ]}>
229 <Image
230 source={{uri: faviconUrl}}
231 style={{width: size, height: size}}
232 onError={() => setImgError(true)}
233 accessibilityIgnoresInvertColors
234 />
235 </View>
236 )
237 }
238
239 return (
240 <View
241 style={[
242 a.align_center,
243 a.justify_center,
244 {
245 width: size,
246 height: size,
247 borderRadius: resolvedBorderRadius,
248 backgroundColor: t.atoms.bg_contrast_100.backgroundColor,
249 },
250 ]}>
251 <FontAwesomeIcon
252 icon="database"
253 size={Math.round(size * 0.7)}
254 style={
255 {color: t.atoms.text_contrast_medium.color} as FontAwesomeIconStyle
256 }
257 />
258 </View>
259 )
260}