Bluesky app fork with some witchin' additions 馃挮
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at cope-settings-sync 343 lines 12 kB view raw
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}