Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

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

fix: pds badge, relike on autolike on repost, misc errors

slightly improved favicon fetching (actually goes for favicon.ico files again after the set favicon fetcher fails. also shows the filler db icon until something else becomes available)

+183 -62
+26
__tests__/lib/pds-label.test.ts
··· 1 + import { 2 + getPdsFallbackFaviconUrl, 3 + isBridgedPdsUrl, 4 + isBskyPdsUrl, 5 + } from '#/state/queries/pds-label.util' 6 + 7 + describe('pds-label helpers', () => { 8 + it('builds a favicon.ico fallback URL from the PDS origin', () => { 9 + expect(getPdsFallbackFaviconUrl('https://pds.example')).toBe( 10 + 'https://pds.example/favicon.ico', 11 + ) 12 + expect(getPdsFallbackFaviconUrl('https://pds.example/xrpc')).toBe( 13 + 'https://pds.example/favicon.ico', 14 + ) 15 + }) 16 + 17 + it('returns undefined for an invalid PDS URL', () => { 18 + expect(getPdsFallbackFaviconUrl('not a url')).toBeUndefined() 19 + }) 20 + 21 + it('detects special-case PDS hosts', () => { 22 + expect(isBskyPdsUrl('https://bsky.social')).toBe(true) 23 + expect(isBskyPdsUrl('https://foo.host.bsky.network')).toBe(true) 24 + expect(isBridgedPdsUrl('https://atproto.brid.gy')).toBe(true) 25 + }) 26 + })
+7
metro.config.js
··· 35 35 if (moduleName === '@ipld/dag-cbor') { 36 36 return context.resolveRequest(context, '@ipld/dag-cbor/src', platform) 37 37 } 38 + if (moduleName === '@easrng/tr58') { 39 + return context.resolveRequest( 40 + context, 41 + '@easrng/tr58/dist/index.js', 42 + platform, 43 + ) 44 + } 38 45 if (process.env.BSKY_PROFILE) { 39 46 if (moduleName.endsWith('ReactNativeRenderer-prod')) { 40 47 return context.resolveRequest(
+72 -19
src/components/PdsDialog.tsx
··· 1 1 import {useState} from 'react' 2 2 import {Image, View} from 'react-native' 3 + import Svg, {G, Path, Rect} from 'react-native-svg' 3 4 import { 4 5 FontAwesomeIcon, 5 6 type FontAwesomeIconStyle, ··· 8 9 import {useLingui} from '@lingui/react' 9 10 import {Trans} from '@lingui/react/macro' 10 11 11 - import {isBridgedPdsUrl, isBskyPdsUrl} from '#/state/queries/pds-label' 12 - 13 - const failedFaviconUrls = new Set<string>() 14 - import Svg, {G, Path, Rect} from 'react-native-svg' 15 - 12 + import { 13 + getPdsFallbackFaviconUrl, 14 + isBridgedPdsUrl, 15 + isBskyPdsUrl, 16 + } from '#/state/queries/pds-label.util' 16 17 import {atoms as a, useBreakpoints, useTheme} from '#/alf' 17 18 import {Button, ButtonText} from '#/components/Button' 18 19 import * as Dialog from '#/components/Dialog' 19 20 import {InlineLinkText} from '#/components/Link' 20 21 import {Text} from '#/components/Typography' 22 + 23 + const failedFaviconUrls = new Set<string>() 21 24 22 25 function formatBskyPdsDisplayName(hostname: string): string { 23 26 const match = hostname.match(/^([^.]+)\.([^.]+)\.host\.bsky\.network$/) ··· 72 75 <View style={[a.flex_row, a.align_center, a.gap_md]}> 73 76 <PdsBadgeIcon 74 77 faviconUrl={faviconUrl} 78 + pdsUrl={pdsUrl} 75 79 isBsky={isBsky} 76 80 isBridged={isBridged} 77 81 size={36} ··· 84 88 {isBridged ? <Trans>Fediverse</Trans> : displayName} 85 89 </Text> 86 90 } 87 - {isBsky && 91 + {isBsky && ( 88 92 <Text style={[a.text_sm, a.leading_tight]}> 89 93 <Trans>Bluesky Social</Trans> 90 94 </Text> 91 - } 95 + )} 92 96 </View> 93 97 </View> 94 98 ··· 244 248 size, 245 249 borderRadius, 246 250 faviconUrl, 251 + fallbackFaviconUrl, 247 252 }: { 248 253 size: number 249 254 borderRadius: number 250 255 faviconUrl: string 256 + fallbackFaviconUrl?: string 251 257 }) { 252 - const t = useTheme() 253 - const [imgError, setImgError] = useState(() => 254 - failedFaviconUrls.has(faviconUrl), 258 + const getInitialUrl = () => { 259 + if (!failedFaviconUrls.has(faviconUrl)) return faviconUrl 260 + if (fallbackFaviconUrl && !failedFaviconUrls.has(fallbackFaviconUrl)) { 261 + return fallbackFaviconUrl 262 + } 263 + return undefined 264 + } 265 + const [currentUrl, setCurrentUrl] = useState<string | undefined>( 266 + getInitialUrl, 255 267 ) 268 + const [imageLoaded, setImageLoaded] = useState(false) 256 269 257 - if (imgError) { 270 + if (!currentUrl) { 258 271 return <DbBadgeIcon size={size} borderRadius={borderRadius} /> 259 272 } 260 273 ··· 262 275 <View 263 276 style={[ 264 277 a.overflow_hidden, 265 - a.align_center, 266 - a.justify_center, 267 278 { 268 279 width: size, 269 280 height: size, 270 281 borderRadius, 271 - backgroundColor: t.atoms.bg_contrast_100.backgroundColor, 272 282 }, 273 283 ]}> 284 + <DbBadgeIcon size={size} borderRadius={borderRadius} /> 274 285 <Image 275 - source={{uri: faviconUrl}} 276 - style={{width: size, height: size}} 286 + key={currentUrl} 287 + source={{uri: currentUrl}} 288 + style={{ 289 + width: size, 290 + height: size, 291 + position: 'absolute', 292 + top: 0, 293 + left: 0, 294 + opacity: imageLoaded ? 1 : 0, 295 + }} 277 296 accessibilityIgnoresInvertColors 297 + onLoad={() => { 298 + setImageLoaded(true) 299 + }} 278 300 onError={() => { 279 - failedFaviconUrls.add(faviconUrl) 280 - setImgError(true) 301 + failedFaviconUrls.add(currentUrl) 302 + setImageLoaded(false) 303 + 304 + if ( 305 + fallbackFaviconUrl && 306 + currentUrl !== fallbackFaviconUrl && 307 + !failedFaviconUrls.has(fallbackFaviconUrl) 308 + ) { 309 + setCurrentUrl(fallbackFaviconUrl) 310 + return 311 + } 312 + 313 + setCurrentUrl(undefined) 281 314 }} 282 315 /> 283 316 </View> ··· 286 319 287 320 export function PdsBadgeIcon({ 288 321 faviconUrl, 322 + pdsUrl, 289 323 isBsky, 290 324 isBridged, 291 325 size, 292 326 borderRadius, 293 327 }: { 294 328 faviconUrl?: string 329 + pdsUrl?: string 295 330 isBsky: boolean 296 331 isBridged: boolean 297 332 size: number ··· 300 335 const r = borderRadius ?? size / 5 301 336 if (isBsky) return <BskyBadgeSVG size={size} /> 302 337 if (isBridged) return <FediverseBadgeSVG size={size} /> 338 + const fallbackFaviconUrl = pdsUrl 339 + ? getPdsFallbackFaviconUrl(pdsUrl) 340 + : undefined 303 341 if (faviconUrl) 304 342 return ( 305 - <FaviconBadgeIcon size={size} borderRadius={r} faviconUrl={faviconUrl} /> 343 + <FaviconBadgeIcon 344 + key={`${faviconUrl}|${fallbackFaviconUrl ?? ''}`} 345 + size={size} 346 + borderRadius={r} 347 + faviconUrl={faviconUrl} 348 + fallbackFaviconUrl={fallbackFaviconUrl} 349 + /> 350 + ) 351 + if (fallbackFaviconUrl) 352 + return ( 353 + <FaviconBadgeIcon 354 + key={fallbackFaviconUrl} 355 + size={size} 356 + borderRadius={r} 357 + faviconUrl={fallbackFaviconUrl} 358 + /> 306 359 ) 307 360 return <DbBadgeIcon size={size} borderRadius={r} /> 308 361 }
+3 -1
src/components/PostControls/ShareMenu/ShareMenuItems.tsx
··· 98 98 } else { 99 99 await ExpoClipboard.setStringAsync(url) 100 100 } 101 - Toast.show(_(msg`Copied to clipboard`), 'clipboard-check') 101 + Toast.show(_(msg`Copied to clipboard`), { 102 + type: 'success', 103 + }) 102 104 onShareProp() 103 105 } 104 106
+19 -1
src/components/PostControls/index.tsx
··· 20 20 import {useDisableReplyMetrics} from '#/state/preferences/disable-reply-metrics' 21 21 import {useDisableRepostsMetrics} from '#/state/preferences/disable-reposts-metrics' 22 22 import { 23 + useGetPost, 23 24 usePostLikeMutationQueue, 24 25 usePostRepostMutationQueue, 25 26 } from '#/state/queries/post' ··· 28 29 ProgressGuideAction, 29 30 useProgressGuideControls, 30 31 } from '#/state/shell/progress-guide' 32 + import * as userActionHistory from '#/state/userActionHistory' 31 33 import {atoms as a, useBreakpoints, useTheme} from '#/alf' 32 34 import {Reply as Bubble} from '#/components/icons/Reply' 33 35 import {useFormatPostStatCount} from '#/components/PostControls/util' ··· 83 85 const {t: l} = useLingui() 84 86 const {openComposer} = useOpenComposer() 85 87 const {feedDescriptor} = useFeedFeedbackContext() 88 + const getPost = useGetPost() 86 89 const [queueLike, queueUnlike] = usePostLikeMutationQueue( 87 90 post, 88 91 viaRepost, ··· 118 121 119 122 const autoLikeOnRepost = useAutoLikeOnRepost() 120 123 124 + const shouldAutoLikeOnRepost = async () => { 125 + if (post.viewer?.like) return false 126 + 127 + if (userActionHistory.getActionHistory().likes.includes(post.uri)) { 128 + return false 129 + } 130 + 131 + try { 132 + const latestPost = await getPost({uri: post.uri}) 133 + return !latestPost.viewer?.like 134 + } catch { 135 + return false 136 + } 137 + } 138 + 121 139 const onPressToggleLike = async () => { 122 140 if (isBlocked) { 123 141 Toast.show(l`Cannot interact with a blocked user`, { ··· 167 185 }) 168 186 await queueRepost() 169 187 setHasLikeIconBeenToggled(true) 170 - if (!post.viewer?.like && autoLikeOnRepost) { 188 + if (autoLikeOnRepost && (await shouldAutoLikeOnRepost())) { 171 189 sendInteraction({ 172 190 item: post.uri, 173 191 event: 'app.bsky.feed.defs#interactionLike',
+1
src/components/ProfileBadges.tsx
··· 173 173 const icon = ( 174 174 <PdsBadgeIcon 175 175 faviconUrl={faviconUrl} 176 + pdsUrl={pdsUrl} 176 177 isBsky={isBsky} 177 178 isBridged={isBridged} 178 179 size={dimensions}
+1 -1
src/lib/api/index.ts
··· 420 420 captions: captions.length === 0 ? undefined : captions, 421 421 aspectRatio, 422 422 presentation: 423 - videoDraft.video.mimeType === 'image/gif' ? 'gif' : 'default', 423 + videoDraft.video?.mimeType === 'image/gif' ? 'gif' : 'default', 424 424 } 425 425 } 426 426 if (embedDraft.media?.type === 'gif') {
+8 -37
src/state/queries/pds-label.ts
··· 1 1 import {useQuery} from '@tanstack/react-query' 2 2 3 3 import {useFaviconService} from '#/state/preferences/favicon-service' 4 + import { 5 + getFaviconServiceUrl, 6 + getPdsFallbackFaviconUrl, 7 + isBridgedPdsUrl, 8 + isBskyPdsUrl, 9 + } from '#/state/queries/pds-label.util' 4 10 import {resolvePdsServiceUrl} from '#/state/queries/resolve-identity' 5 11 6 - const BSKY_PDS_HOSTNAMES = ['bsky.social', 'staging.bsky.dev'] 7 - const BSKY_PDS_SUFFIX = '.bsky.network' 8 - const BRIDGY_FED_HOSTNAME = 'atproto.brid.gy' 9 - 10 - export function isBskyPdsUrl(url: string): boolean { 11 - try { 12 - const hostname = new URL(url).hostname 13 - return ( 14 - BSKY_PDS_HOSTNAMES.includes(hostname) || 15 - hostname.endsWith(BSKY_PDS_SUFFIX) 16 - ) 17 - } catch { 18 - return false 19 - } 20 - } 21 - 22 - export function isBridgedPdsUrl(url: string): boolean { 23 - try { 24 - return new URL(url).hostname === BRIDGY_FED_HOSTNAME 25 - } catch { 26 - return false 27 - } 28 - } 29 - 30 - function getFaviconUrl( 31 - pdsUrl: string, 32 - faviconService: string, 33 - ): string | undefined { 34 - try { 35 - const hostname = new URL(pdsUrl).hostname 36 - // Replace the (pds) placeholder with the actual PDS domain 37 - return faviconService.replace('(pds)', hostname) 38 - } catch { 39 - return undefined 40 - } 41 - } 12 + export {getPdsFallbackFaviconUrl, isBridgedPdsUrl, isBskyPdsUrl} 42 13 43 14 export const RQKEY_ROOT = 'pds-label' 44 15 export const RQKEY = (did: string) => [RQKEY_ROOT, did] ··· 78 49 return useQuery({ 79 50 queryKey, 80 51 queryFn: () => 81 - isEnabled ? getFaviconUrl(pdsUrl!, faviconService!) : undefined, 52 + isEnabled ? getFaviconServiceUrl(pdsUrl!, faviconService!) : undefined, 82 53 enabled: isEnabled, 83 54 staleTime: 1000 * 60 * 60, // 1 hour 84 55 })
+43
src/state/queries/pds-label.util.ts
··· 1 + const BSKY_PDS_HOSTNAMES = ['bsky.social', 'staging.bsky.dev'] 2 + const BSKY_PDS_SUFFIX = '.bsky.network' 3 + const BRIDGY_FED_HOSTNAME = 'atproto.brid.gy' 4 + 5 + export function isBskyPdsUrl(url: string): boolean { 6 + try { 7 + const hostname = new URL(url).hostname 8 + return ( 9 + BSKY_PDS_HOSTNAMES.includes(hostname) || 10 + hostname.endsWith(BSKY_PDS_SUFFIX) 11 + ) 12 + } catch { 13 + return false 14 + } 15 + } 16 + 17 + export function isBridgedPdsUrl(url: string): boolean { 18 + try { 19 + return new URL(url).hostname === BRIDGY_FED_HOSTNAME 20 + } catch { 21 + return false 22 + } 23 + } 24 + 25 + export function getFaviconServiceUrl( 26 + pdsUrl: string, 27 + faviconService: string, 28 + ): string | undefined { 29 + try { 30 + const hostname = new URL(pdsUrl).hostname 31 + return faviconService.replace('(pds)', hostname) 32 + } catch { 33 + return undefined 34 + } 35 + } 36 + 37 + export function getPdsFallbackFaviconUrl(pdsUrl: string): string | undefined { 38 + try { 39 + return new URL('/favicon.ico', pdsUrl).toString() 40 + } catch { 41 + return undefined 42 + } 43 + }
+2 -2
src/state/session/index.tsx
··· 8 8 useState, 9 9 useSyncExternalStore, 10 10 } from 'react' 11 - import {type AtpAgent, type AtpSessionEvent} from '@atproto/api' 11 + import {type Agent, type AtpAgent, type AtpSessionEvent} from '@atproto/api' 12 12 13 13 import * as persisted from '#/state/persisted' 14 14 import {useCloseAllActiveElements} from '#/state/util' ··· 482 482 return agent 483 483 } 484 484 485 - export function useBlankPrefAuthedAgent(): BskyAgent { 485 + export function useBlankPrefAuthedAgent(): Agent { 486 486 const agent = useContext(AgentContext) 487 487 if (!agent) { 488 488 throw Error('useAgent() must be below <SessionProvider>.')
+1 -1
src/view/com/notifications/NotificationFeedItem.tsx
··· 504 504 ) : ( 505 505 <Trans>{firstAuthorLink} liked your repost</Trans> 506 506 ) 507 - icon = <RepostHeartIcon size="xl" style={[s.likeColor]} /> 507 + icon = <RepostHeartIcon size="xl" style={{color: t.palette.like}} /> 508 508 } else if (item.type === 'repost-via-repost') { 509 509 a11yLabel = hasMultipleAuthors 510 510 ? _(