Bluesky app fork with some witchin' additions 馃挮
1import {useState} from 'react'
2import {Image, View} from 'react-native'
3import Svg, {G, Path, Rect} from 'react-native-svg'
4import {
5 FontAwesomeIcon,
6 type FontAwesomeIconStyle,
7} from '@fortawesome/react-native-fontawesome'
8import {msg} from '@lingui/core/macro'
9import {useLingui} from '@lingui/react'
10import {Trans} from '@lingui/react/macro'
11
12import {
13 getPdsFallbackFaviconUrls,
14 isBridgedPdsUrl,
15 isBskyPdsUrl,
16} from '#/state/queries/pds-label.util'
17import {atoms as a, useBreakpoints, useTheme} from '#/alf'
18import {Button, ButtonText} from '#/components/Button'
19import * as Dialog from '#/components/Dialog'
20import {InlineLinkText} from '#/components/Link'
21import {Text} from '#/components/Typography'
22
23const failedFaviconUrls = new Set<string>()
24
25function formatBskyPdsDisplayName(hostname: string): string {
26 const match = hostname.match(/^([^.]+)\.([^.]+)\.host\.bsky\.network$/)
27 if (match) {
28 const name = match[1].charAt(0).toUpperCase() + match[1].slice(1)
29 const rawRegion = match[2]
30 const region = rawRegion
31 .replace(/^us-east$/, 'US East')
32 .replace(/^us-west$/, 'US West')
33 .replace(/^eu-west$/, 'EU West')
34 .replace(
35 /^ap-(.+)$/,
36 (_match: string, r: string) =>
37 `AP ${r.charAt(0).toUpperCase()}${r.slice(1)}`,
38 )
39 return `${name} (${region})`
40 }
41 if (hostname === 'bsky.social') return 'Bluesky Social'
42 return hostname
43}
44
45export function PdsDialog({
46 control,
47 pdsUrl,
48 faviconUrl,
49}: {
50 control: Dialog.DialogControlProps
51 pdsUrl: string
52 faviconUrl: string | undefined
53}) {
54 const {_} = useLingui()
55 const {gtMobile} = useBreakpoints()
56
57 let hostname = pdsUrl
58 try {
59 hostname = new URL(pdsUrl).hostname
60 } catch {}
61
62 const isBsky = isBskyPdsUrl(pdsUrl)
63 const isBridged = isBridgedPdsUrl(pdsUrl)
64 const displayName = isBsky ? formatBskyPdsDisplayName(hostname) : hostname
65
66 return (
67 <Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}>
68 <Dialog.Handle />
69 <Dialog.ScrollableInner
70 label={_(msg`PDS Information`)}
71 style={[
72 gtMobile ? {width: 'auto', maxWidth: 400, minWidth: 200} : a.w_full,
73 ]}>
74 <View style={[a.gap_md, a.pb_lg]}>
75 <View style={[a.flex_row, a.align_center, a.gap_md]}>
76 <PdsBadgeIcon
77 faviconUrl={faviconUrl}
78 pdsUrl={pdsUrl}
79 isBsky={isBsky}
80 isBridged={isBridged}
81 size={36}
82 />
83 <View style={[a.flex_1]}>
84 {
85 <Text
86 style={[a.text_2xl, a.font_semi_bold, a.leading_tight]}
87 numberOfLines={1}>
88 {isBridged ? <Trans>Fediverse</Trans> : displayName}
89 </Text>
90 }
91 {isBsky && (
92 <Text style={[a.text_sm, a.leading_tight]}>
93 <Trans>Bluesky Social</Trans>
94 </Text>
95 )}
96 </View>
97 </View>
98
99 <Text style={[a.text_md, a.leading_snug]}>
100 <Trans>
101 This badge represents the Personal Data Server this account is
102 stored on:{' '}
103 <InlineLinkText
104 to={pdsUrl}
105 label={displayName}
106 style={[a.text_md, a.font_semi_bold]}>
107 {displayName}
108 </InlineLinkText>
109 . A PDS is where posts, follows, and other data live on the AT
110 Protocol network.
111 </Trans>
112 </Text>
113
114 {isBridged && (
115 <Text style={[a.text_md, a.leading_snug]}>
116 <Trans>
117 This account is bridged from the Fediverse via{' '}
118 <InlineLinkText
119 to="https://witchsky.app/profile/ap.brid.gy"
120 label="Bridgy Fed"
121 style={[a.text_md, a.font_semi_bold]}>
122 Bridgy Fed
123 </InlineLinkText>
124 .{' '}
125 {/* Their original account is avaiable at:{' '}
126 <InlineLinkText
127 to={BridgedUrl}
128 label="Federated account address"
129 style={[a.text_md, a.font_semi_bold]}>
130 {BridgedUrl}
131 </InlineLinkText> */}
132 </Trans>
133 </Text>
134 )}
135
136 {!isBridged && (
137 <Text style={[a.text_md, a.leading_snug]}>
138 <Trans>
139 <InlineLinkText
140 to="https://atproto.com/guides/glossary#pds-personal-data-server"
141 label="PDS Glossary definition"
142 style={[a.text_md, a.font_semi_bold]}>
143 Learn more
144 </InlineLinkText>{' '}
145 about what a PDS is and how to{' '}
146 <InlineLinkText
147 to="https://atproto.com/guides/self-hosting#pds"
148 label="Self-hosting PDS documentation"
149 style={[a.text_md, a.font_semi_bold]}>
150 self-host
151 </InlineLinkText>{' '}
152 your own.
153 </Trans>
154 </Text>
155 )}
156 </View>
157
158 <View
159 style={[
160 a.w_full,
161 a.gap_sm,
162 gtMobile
163 ? [a.flex_row, a.flex_row_reverse, a.justify_start]
164 : [a.flex_col],
165 ]}>
166 <Button
167 label={_(msg`Close dialog`)}
168 size="small"
169 variant="solid"
170 color="primary"
171 onPress={() => control.close()}>
172 <ButtonText>
173 <Trans>Close</Trans>
174 </ButtonText>
175 </Button>
176 </View>
177
178 <Dialog.Close />
179 </Dialog.ScrollableInner>
180 </Dialog.Outer>
181 )
182}
183
184function BskyBadgeSVG({size}: {size: number}) {
185 return (
186 <Svg width={size} height={size} viewBox="0 0 24 24">
187 <Rect width={24} height={24} rx={6} fill="#0085ff" />
188 <G transform="translate(2.4 2.4) scale(0.8)">
189 <Path
190 fill="#fff"
191 fillRule="evenodd"
192 clipRule="evenodd"
193 d="M6.335 4.212c2.293 1.76 4.76 5.327 5.665 7.241.906-1.914 3.372-5.482 5.665-7.241C19.319 2.942 22 1.96 22 5.086c0 .624-.35 5.244-.556 5.994-.713 2.608-3.315 3.273-5.629 2.87 4.045.704 5.074 3.035 2.852 5.366-4.22 4.426-6.066-1.111-6.54-2.53-.086-.26-.126-.382-.127-.278 0-.104-.041.018-.128.278-.473 1.419-2.318 6.956-6.539 2.53-2.222-2.331-1.193-4.662 2.852-5.366-2.314.403-4.916-.262-5.63-2.87C2.35 10.33 2 5.71 2 5.086c0-3.126 2.68-2.144 4.335-.874Z"
194 />
195 </G>
196 </Svg>
197 )
198}
199
200function FediverseBadgeSVG({size}: {size: number}) {
201 return (
202 <Svg width={size} height={size} viewBox="0 0 24 24">
203 <Rect width={24} height={24} rx={6} fill="#6364FF" />
204 <G transform="translate(2.4 2.4) scale(0.03)">
205 <Path
206 fill="#fff"
207 fillRule="evenodd"
208 clipRule="evenodd"
209 d="M426.8 590.9C407.1 590.4 389.3 579.3 380.2 561.8C371.2 544.4 372.3 523.4 383.2 507C394.1 490.6 413 481.5 432.6 483.1C452.3 483.6 470.1 494.7 479.2 512.2C488.2 529.6 487.1 550.6 476.2 567C465.3 583.4 446.4 592.5 426.8 590.9zM376.7 510.3C371.2 521.2 369.3 533.6 371.1 545.7L200.7 518.4C206.2 507.5 208.2 495.1 206.4 483L376.7 510.3zM144.7 545.6C125.1 545.1 107.3 533.9 98.3 516.5C89.2 499 90.4 478.1 101.3 461.7C112.1 445.4 131 436.2 150.6 437.8C170.2 438.3 188 449.5 197 466.9C206.1 484.4 204.9 505.3 194 521.7C183.2 538 164.3 547.2 144.7 545.6zM402.4 484.2C391.5 489.8 382.7 498.6 377 509.5L306.4 438.6L340 421.6L402.4 484.3zM518.1 325C526.8 333.6 537.9 339.3 550 341.4L471.4 494.8C462.7 486.2 451.6 480.5 439.5 478.4L518.1 325zM408.7 283.3L439.2 478.4C427.1 476.5 414.7 478.3 403.8 483.7L371.6 277.4L408.8 283.4zM382.4 392.9L206.2 482.2C204.2 470.1 198.6 459 190 450.2L376.6 355.6L382.4 392.8zM229.7 370.9L189.4 449.6C180.7 441 169.6 435.3 157.5 433.3L203.1 344.3L229.7 371zM156.7 433C144.6 431.2 132.3 433.2 121.3 438.6L94.7 268.3C106.8 270.1 119.2 268.2 130.1 262.7L156.7 433zM303.8 385.2L270.2 402.2L130.8 262.3C141.7 256.7 150.5 247.9 156.2 237L303.8 385.2zM501.3 292.4C503.3 304.5 508.9 315.6 517.5 324.3L428.2 369.5L422.4 332.3L501.3 292.3zM556.9 336.7C537.3 336.2 519.5 325 510.5 307.6C501.4 290.1 502.6 269.2 513.5 252.8C524.3 236.5 543.2 227.3 562.8 228.9C582.4 229.4 600.2 240.6 609.2 258C618.3 275.5 617.1 296.4 606.2 312.8C595.4 329.1 576.5 338.3 556.9 336.7zM316.6 122.7C325.3 131.3 336.4 137 348.4 139L253.1 325.1L226.5 298.4L316.5 122.6zM506.9 256.1C501.4 267 499.4 279.4 501.2 291.4L294.8 258.3L312 224.8L507 256.1zM100.7 263.6C81.1 263.1 63.3 251.9 54.3 234.5C45.2 217 46.4 196.1 57.3 179.7C68.1 163.4 87 154.2 106.6 155.8C126.2 156.3 144 167.5 153 184.9C162.1 202.4 160.9 223.3 150 239.7C139.2 256 120.3 265.2 100.7 263.6zM532.7 230.2C521.8 235.8 513 244.6 507.3 255.5L385.5 133.3C396.4 127.7 405.2 118.9 410.9 108L532.6 230.2zM261.3 216.6L244.1 250.1L156.7 236.1C162.1 225.2 164.1 212.8 162.2 200.7L261.2 216.6zM400.8 232.5L363.6 226.5L350 139.3C362.1 141 374.5 139 385.3 133.4L400.8 232.5zM299.8 90.2C301.8 102.3 307.4 113.4 316 122.1L162.1 200.1C160.1 188 154.5 176.9 145.9 168.2L299.8 90.2zM355.4 134.5C335.7 134 317.9 122.9 308.8 105.4C299.8 88 300.9 67 311.8 50.6C322.7 34.2 341.6 25.1 361.2 26.7C380.9 27.2 398.7 38.3 407.8 55.8C416.8 73.2 415.7 94.2 404.8 110.6C393.9 127 375 136.1 355.4 134.5z"
210 />
211 </G>
212 </Svg>
213 )
214}
215
216function DbBadgeIcon({
217 size,
218 borderRadius,
219}: {
220 size: number
221 borderRadius: number
222}) {
223 const t = useTheme()
224 return (
225 <View
226 style={[
227 a.align_center,
228 a.justify_center,
229 {
230 width: size,
231 height: size,
232 borderRadius,
233 backgroundColor: t.atoms.bg_contrast_100.backgroundColor,
234 },
235 ]}>
236 <FontAwesomeIcon
237 icon="database"
238 size={Math.round(size * 0.7)}
239 style={
240 {color: t.atoms.text_contrast_medium.color} as FontAwesomeIconStyle
241 }
242 />
243 </View>
244 )
245}
246
247function FaviconBadgeIcon({
248 size,
249 borderRadius,
250 faviconUrls,
251}: {
252 size: number
253 borderRadius: number
254 faviconUrls: string[]
255}) {
256 const t = useTheme()
257 const getNextUrl = (currentUrl?: string) =>
258 faviconUrls.find(
259 url => url !== currentUrl && url && !failedFaviconUrls.has(url),
260 )
261 const [currentUrl, setCurrentUrl] = useState<string | undefined>(getNextUrl)
262 const [imageLoaded, setImageLoaded] = useState(false)
263
264 if (!currentUrl) {
265 return <DbBadgeIcon size={size} borderRadius={borderRadius} />
266 }
267
268 return (
269 <View
270 style={[
271 a.overflow_hidden,
272 {
273 width: size,
274 height: size,
275 borderRadius,
276 backgroundColor: t.atoms.bg_contrast_100.backgroundColor,
277 },
278 ]}>
279 {!imageLoaded ? (
280 <DbBadgeIcon size={size} borderRadius={borderRadius} />
281 ) : null}
282 <Image
283 key={currentUrl}
284 source={{uri: currentUrl}}
285 style={{
286 width: size,
287 height: size,
288 position: 'absolute',
289 top: 0,
290 left: 0,
291 opacity: imageLoaded ? 1 : 0,
292 }}
293 accessibilityIgnoresInvertColors
294 onLoad={() => {
295 setImageLoaded(true)
296 }}
297 onError={() => {
298 failedFaviconUrls.add(currentUrl)
299 setImageLoaded(false)
300
301 setCurrentUrl(getNextUrl(currentUrl))
302 }}
303 />
304 </View>
305 )
306}
307
308export function PdsBadgeIcon({
309 faviconUrl,
310 pdsUrl,
311 isBsky,
312 isBridged,
313 size,
314 borderRadius,
315}: {
316 faviconUrl?: string
317 pdsUrl?: string
318 isBsky: boolean
319 isBridged: boolean
320 size: number
321 borderRadius?: number
322}) {
323 const r = borderRadius ?? size / 5
324 if (isBsky) return <BskyBadgeSVG size={size} />
325 if (isBridged) return <FediverseBadgeSVG size={size} />
326 const faviconCandidates = Array.from(
327 new Set(
328 [faviconUrl, ...(pdsUrl ? getPdsFallbackFaviconUrls(pdsUrl) : [])].filter(
329 Boolean,
330 ) as string[],
331 ),
332 )
333 if (faviconCandidates.length > 0)
334 return (
335 <FaviconBadgeIcon
336 key={faviconCandidates.join('|')}
337 size={size}
338 borderRadius={r}
339 faviconUrls={faviconCandidates}
340 />
341 )
342 return <DbBadgeIcon size={size} borderRadius={r} />
343}