Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

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

feat: Only show URL on handles with working links toggle

+ make handle links clickable in profile hover cards
+ put DOH_ENDPOINT in constants
+ void dialog submissions in RuneSettings
+ fix type errors (+ ignore config)

xan.lol 41efeedf 6c026456

+378 -143
+1
eslint.config.mjs
··· 37 37 '*.e2e.ts', 38 38 '*.e2e.tsx', 39 39 'eslint.config.mjs', 40 + 'svgo.config.mjs', 40 41 '.jscodeshift/**', 41 42 'rspack.config.ts', 42 43 'scripts/post-web-build.js',
+9 -5
src/components/ProfileHoverCard/index.web.tsx
··· 528 528 ))} 529 529 </View> 530 530 531 - <Link to={profileURL} label={_(msg`View profile`)} onPress={hide}> 532 - <View style={[a.pb_sm, a.flex_1]}> 531 + <View style={[a.pb_sm, a.flex_1]}> 532 + <Link to={profileURL} label={_(msg`View profile`)} onPress={hide}> 533 533 <View style={[a.flex_row, a.align_center, a.pt_md, a.pb_xs]}> 534 534 <Text 535 535 numberOfLines={1} ··· 556 556 ]} 557 557 /> 558 558 </View> 559 + </Link> 559 560 560 - <ProfileHeaderHandle profile={profileShadow} disableTaps /> 561 - </View> 562 - </Link> 561 + <ProfileHeaderHandle 562 + profile={profileShadow} 563 + disableAuxiliaryTaps 564 + onLinkPress={hide} 565 + /> 566 + </View> 563 567 564 568 {isBlockedUser && ( 565 569 <View style={[a.flex_row, a.flex_wrap, a.gap_xs]}>
+2
src/lib/constants.ts
··· 251 251 community: `https://bsky.social/about/support/community-guidelines`, 252 252 communityDeprecated: `https://bsky.social/about/support/community-guidelines-deprecated`, 253 253 } 254 + 255 + export const DOH_ENDPOINT = 'https://cloudflare-dns.com/dns-query'
+59 -36
src/screens/Profile/Header/Handle.tsx
··· 1 - import {View} from 'react-native' 1 + import {type GestureResponderEvent, View} from 'react-native' 2 2 import {type AppBskyActorDefs} from '@atproto/api' 3 3 import {msg} from '@lingui/core/macro' 4 4 import {useLingui} from '@lingui/react' ··· 9 9 import {type Shadow} from '#/state/cache/types' 10 10 import {useShowFollowsYouBadge} from '#/state/preferences/show-follows-you-badge' 11 11 import {useShowLinkInHandle} from '#/state/preferences/show-link-in-handle.tsx' 12 + import {useShowLinkInHandleOnlyOnWorkingLinks} from '#/state/preferences/show-link-in-handle-only-on-working-links' 13 + import {useHandleLinkQuery} from '#/state/queries/handle-link' 12 14 import {atoms as a, useTheme, web} from '#/alf' 13 15 import {InlineLinkText} from '#/components/Link.tsx' 14 16 import {NewskieDialog} from '#/components/NewskieDialog' ··· 18 20 export function ProfileHeaderHandle({ 19 21 profile, 20 22 disableTaps, 23 + disableAuxiliaryTaps, 24 + onLinkPress, 21 25 }: { 22 26 profile: Shadow<AppBskyActorDefs.ProfileViewDetailed> 23 27 disableTaps?: boolean 28 + disableAuxiliaryTaps?: boolean 29 + onLinkPress?: (e: GestureResponderEvent) => void | false 24 30 }) { 25 31 const t = useTheme() 26 32 const {_} = useLingui() ··· 30 36 const isBskySocialHandle = profile.handle.endsWith('.bsky.social') 31 37 const showFollowsYouBadge = useShowFollowsYouBadge() 32 38 const showProfileInHandle = useShowLinkInHandle() 39 + const showLinkInHandleOnlyOnWorkingLinks = 40 + useShowLinkInHandleOnlyOnWorkingLinks() 41 + const shouldCheckHandleLink = 42 + showProfileInHandle && 43 + showLinkInHandleOnlyOnWorkingLinks && 44 + !invalidHandle && 45 + !isBskySocialHandle 46 + const {data: hasWorkingHandleLink = false} = useHandleLinkQuery( 47 + profile.handle, 48 + shouldCheckHandleLink, 49 + ) 50 + const shouldShowProfileLink = 51 + showProfileInHandle && 52 + !isBskySocialHandle && 53 + (!showLinkInHandleOnlyOnWorkingLinks || hasWorkingHandleLink) 54 + const disableNewskieDialog = disableTaps || disableAuxiliaryTaps 33 55 const sanitized = sanitizeHandle( 34 56 profile.handle, 35 57 '@', 36 58 // forceLTR handled by CSS above on web 37 59 IS_NATIVE, 38 60 ) 61 + const handleTextStyle = [ 62 + invalidHandle 63 + ? [ 64 + a.border, 65 + a.text_xs, 66 + a.px_sm, 67 + a.py_xs, 68 + a.rounded_xs, 69 + {borderColor: t.palette.contrast_200}, 70 + ] 71 + : [a.text_md, a.leading_tight, t.atoms.text_contrast_medium], 72 + web({ 73 + wordBreak: 'break-all', 74 + direction: 'ltr', 75 + unicodeBidi: 'isolate', 76 + }), 77 + ] 39 78 return ( 40 79 <View 41 80 style={[a.flex_row, a.gap_sm, a.align_center, {maxWidth: '100%'}]} 42 81 pointerEvents={disableTaps ? 'none' : IS_IOS ? 'auto' : 'box-none'}> 43 - <NewskieDialog profile={profile} disabled={disableTaps} /> 82 + <NewskieDialog profile={profile} disabled={disableNewskieDialog} /> 44 83 {showFollowsYouBadge && profile.viewer?.followedBy && !blockHide ? ( 45 84 <View style={[t.atoms.bg_contrast_50, a.rounded_xs, a.px_sm, a.py_xs]}> 46 85 <Text style={[t.atoms.text, a.text_sm]}> ··· 49 88 </View> 50 89 ) : undefined} 51 90 <View style={[a.flex_row, a.flex_wrap, {gap: 6}]}> 52 - <Text 53 - emoji 54 - numberOfLines={1} 55 - style={[ 56 - invalidHandle 57 - ? [ 58 - a.border, 59 - a.text_xs, 60 - a.px_sm, 61 - a.py_xs, 62 - a.rounded_xs, 63 - {borderColor: t.palette.contrast_200}, 64 - ] 65 - : [a.text_md, a.leading_tight, t.atoms.text_contrast_medium], 66 - web({ 67 - wordBreak: 'break-all', 68 - direction: 'ltr', 69 - unicodeBidi: 'isolate', 70 - }), 71 - ]}> 72 - {invalidHandle ? ( 73 - _(msg`⚠Invalid Handle`) 74 - ) : showProfileInHandle && !isBskySocialHandle ? ( 75 - <InlineLinkText 76 - to={`https://${profile.handle}`} 77 - label={profile.handle}> 78 - <Text style={[a.text_md, {color: t.palette.primary_500}]}> 79 - {sanitized} 80 - </Text> 81 - </InlineLinkText> 82 - ) : ( 83 - sanitized 84 - )} 85 - </Text> 91 + {invalidHandle ? ( 92 + <Text emoji numberOfLines={1} style={handleTextStyle}> 93 + {_(msg`⚠Invalid Handle`)} 94 + </Text> 95 + ) : shouldShowProfileLink ? ( 96 + <InlineLinkText 97 + to={`https://${profile.handle}`} 98 + label={profile.handle} 99 + numberOfLines={1} 100 + style={[a.text_md, a.leading_tight, web({direction: 'ltr'})]} 101 + onPress={onLinkPress}> 102 + {sanitized} 103 + </InlineLinkText> 104 + ) : ( 105 + <Text emoji numberOfLines={1} style={handleTextStyle}> 106 + {sanitized} 107 + </Text> 108 + )} 86 109 {pronouns && ( 87 110 <Text style={[t.atoms.text_contrast_low, a.text_md, a.leading_tight]}> 88 111 {sanitizePronouns(pronouns, IS_NATIVE)}
+30 -9
src/screens/Settings/RunesSettings.tsx
··· 157 157 useShowLinkInHandle, 158 158 } from '#/state/preferences/show-link-in-handle.tsx' 159 159 import { 160 + useSetShowLinkInHandleOnlyOnWorkingLinks, 161 + useShowLinkInHandleOnlyOnWorkingLinks, 162 + } from '#/state/preferences/show-link-in-handle-only-on-working-links' 163 + import { 160 164 useLibreTranslateInstance, 161 165 useSetLibreTranslateInstance, 162 166 useSetTranslationServicePreference, ··· 256 260 <Button 257 261 label={_(msg`Save`)} 258 262 size="large" 259 - onPress={submit} 263 + onPress={() => void submit()} 260 264 variant="solid" 261 265 color="primary" 262 266 disabled={shouldDisable()}> ··· 388 392 <Button 389 393 label={_(msg`Save`)} 390 394 size="large" 391 - onPress={submit} 395 + onPress={() => void submit()} 392 396 variant="solid" 393 397 color={did.length > 0 ? 'primary' : 'secondary'} 394 398 disabled={ ··· 487 491 <Button 488 492 label={_(msg`Save`)} 489 493 size="large" 490 - onPress={submit} 494 + onPress={() => void submit()} 491 495 variant="solid" 492 496 color="primary" 493 497 disabled={shouldDisable()}> ··· 563 567 <Button 564 568 label={_(msg`Save`)} 565 569 size="large" 566 - onPress={submit} 570 + onPress={() => void submit()} 567 571 variant="solid" 568 572 color="primary" 569 573 disabled={shouldDisable()}> ··· 637 641 <Button 638 642 label={_(msg`Save`)} 639 643 size="large" 640 - onPress={submit} 644 + onPress={() => void submit()} 641 645 variant="solid" 642 646 color="primary" 643 647 disabled={shouldDisable()}> ··· 773 777 <Button 774 778 label={_(msg`Save`)} 775 779 size="large" 776 - onPress={submit} 780 + onPress={() => void submit()} 777 781 variant="solid" 778 782 color="primary" 779 783 disabled={shouldDisable()}> ··· 891 895 <Button 892 896 label={_(msg`Save`)} 893 897 size="large" 894 - onPress={submit} 898 + onPress={() => void submit()} 895 899 variant="solid" 896 900 color="primary"> 897 901 <ButtonText> ··· 956 960 <Button 957 961 label={_(msg`Save`)} 958 962 size="large" 959 - onPress={submit} 963 + onPress={() => void submit()} 960 964 variant="solid" 961 965 color="primary"> 962 966 <ButtonText> ··· 1026 1030 <Button 1027 1031 label={_(msg`Save`)} 1028 1032 size="large" 1029 - onPress={submit} 1033 + onPress={() => void submit()} 1030 1034 variant="solid" 1031 1035 color="primary"> 1032 1036 <ButtonText> ··· 1133 1137 1134 1138 const showLinkInHandle = useShowLinkInHandle() 1135 1139 const setShowLinkInHandle = useSetShowLinkInHandle() 1140 + const showLinkInHandleOnlyOnWorkingLinks = 1141 + useShowLinkInHandleOnlyOnWorkingLinks() 1142 + const setShowLinkInHandleOnlyOnWorkingLinks = 1143 + useSetShowLinkInHandleOnlyOnWorkingLinks() 1136 1144 1137 1145 const handleInLinks = useHandleInLinks() 1138 1146 const setHandleInLinks = useSetHandleInLinks() ··· 1395 1403 </Toggle.LabelText> 1396 1404 <Toggle.Platform /> 1397 1405 </Toggle.Item> 1406 + {showLinkInHandle && ( 1407 + <Toggle.Item 1408 + name="show_link_in_handle_only_on_working_links" 1409 + label={_(msg`Only show URL on handles with working links`)} 1410 + value={showLinkInHandleOnlyOnWorkingLinks} 1411 + onChange={value => setShowLinkInHandleOnlyOnWorkingLinks(value)} 1412 + style={[a.w_full]}> 1413 + <Toggle.LabelText style={[a.flex_1]}> 1414 + <Trans>Only show URL on handles with working links</Trans> 1415 + </Toggle.LabelText> 1416 + <Toggle.Platform /> 1417 + </Toggle.Item> 1418 + )} 1398 1419 1399 1420 <Toggle.Item 1400 1421 name="no_discover_fallback"
+2
src/state/persisted/schema.ts
··· 150 150 repostCarouselEnabled: z.boolean().optional(), 151 151 constellationInstance: z.string().optional(), 152 152 showLinkInHandle: z.boolean().optional(), 153 + showLinkInHandleOnlyOnWorkingLinks: z.boolean().optional(), 153 154 hideFeedsPromoTab: z.boolean().optional(), 154 155 disableViaRepostNotification: z.boolean().optional(), 155 156 disableComposerPrompt: z.boolean().optional(), ··· 272 273 repostCarouselEnabled: false, 273 274 constellationInstance: 'https://constellation.microcosm.blue/', 274 275 showLinkInHandle: true, 276 + showLinkInHandleOnlyOnWorkingLinks: true, 275 277 hideFeedsPromoTab: false, 276 278 disableViaRepostNotification: false, 277 279 disableComposerPrompt: true,
+78 -75
src/state/preferences/index.tsx
··· 43 43 import {Provider as RepostCarouselProvider} from './repost-carousel-enabled' 44 44 import {Provider as ShowFollowsYouBadgeProvider} from './show-follows-you-badge' 45 45 import {Provider as ShowLinkInHandleProvider} from './show-link-in-handle' 46 + import {Provider as ShowLinkInHandleOnlyOnWorkingLinksProvider} from './show-link-in-handle-only-on-working-links' 46 47 import {Provider as SubtitlesProvider} from './subtitles' 47 48 import {Provider as TranslationServicePreferenceProvider} from './translation-service-preference' 48 49 import {Provider as TrendingSettingsProvider} from './trending' ··· 106 107 <PdsLabelProvider> 107 108 <NoDiscoverProvider> 108 109 <ShowLinkInHandleProvider> 109 - <UseHandleInLinksProvider> 110 - <LargeAltBadgeProvider> 111 - <ExternalEmbedsProvider> 112 - <HiddenPostsProvider> 113 - <HighQualityImagesProvider> 114 - <ImageCdnHostProvider> 115 - <InAppBrowserProvider> 116 - <DisableHapticsProvider> 117 - <AutoplayProvider> 118 - <UsedStarterPacksProvider> 119 - <SubtitlesProvider> 120 - <TrendingSettingsProvider> 121 - <RepostCarouselProvider> 122 - <KawaiiProvider> 123 - <HideFeedsPromoTabProvider> 124 - <DisableViaRepostNotificationProvider> 125 - <DisableLikesMetricsProvider> 126 - <DisableRepostsMetricsProvider> 127 - <DisableQuotesMetricsProvider> 128 - <DisableSavesMetricsProvider> 129 - <DisableReplyMetricsProvider> 130 - <DisableFollowersMetricsProvider> 131 - <DisableFollowingMetricsProvider> 132 - <DisableFollowedByMetricsProvider> 133 - <DisablePostsMetricsProvider> 134 - <ShowFollowsYouBadgeProvider> 135 - <HideSimilarAccountsRecommProvider> 136 - <HideUnreplyablePostsProvider> 137 - <EnableSquareAvatarsProvider> 138 - <EnableSquareButtonsProvider> 139 - <PostNameReplacementProvider> 140 - <DisableVerifyEmailReminderProvider> 141 - <TranslationServicePreferenceProvider> 142 - <OpenRouterProvider> 143 - <DisableComposerPromptProvider> 144 - <DiscoverContextEnabledProvider> 145 - { 146 - children 147 - } 148 - </DiscoverContextEnabledProvider> 149 - </DisableComposerPromptProvider> 150 - </OpenRouterProvider> 151 - </TranslationServicePreferenceProvider> 152 - </DisableVerifyEmailReminderProvider> 153 - </PostNameReplacementProvider> 154 - </EnableSquareButtonsProvider> 155 - </EnableSquareAvatarsProvider> 156 - </HideUnreplyablePostsProvider> 157 - </HideSimilarAccountsRecommProvider> 158 - </ShowFollowsYouBadgeProvider> 159 - </DisablePostsMetricsProvider> 160 - </DisableFollowedByMetricsProvider> 161 - </DisableFollowingMetricsProvider> 162 - </DisableFollowersMetricsProvider> 163 - </DisableReplyMetricsProvider> 164 - </DisableSavesMetricsProvider> 165 - </DisableQuotesMetricsProvider> 166 - </DisableRepostsMetricsProvider> 167 - </DisableLikesMetricsProvider> 168 - </DisableViaRepostNotificationProvider> 169 - </HideFeedsPromoTabProvider> 170 - </KawaiiProvider> 171 - </RepostCarouselProvider> 172 - </TrendingSettingsProvider> 173 - </SubtitlesProvider> 174 - </UsedStarterPacksProvider> 175 - </AutoplayProvider> 176 - </DisableHapticsProvider> 177 - </InAppBrowserProvider> 178 - </ImageCdnHostProvider> 179 - </HighQualityImagesProvider> 180 - </HiddenPostsProvider> 181 - </ExternalEmbedsProvider> 182 - </LargeAltBadgeProvider> 183 - </UseHandleInLinksProvider> 110 + <ShowLinkInHandleOnlyOnWorkingLinksProvider> 111 + <UseHandleInLinksProvider> 112 + <LargeAltBadgeProvider> 113 + <ExternalEmbedsProvider> 114 + <HiddenPostsProvider> 115 + <HighQualityImagesProvider> 116 + <ImageCdnHostProvider> 117 + <InAppBrowserProvider> 118 + <DisableHapticsProvider> 119 + <AutoplayProvider> 120 + <UsedStarterPacksProvider> 121 + <SubtitlesProvider> 122 + <TrendingSettingsProvider> 123 + <RepostCarouselProvider> 124 + <KawaiiProvider> 125 + <HideFeedsPromoTabProvider> 126 + <DisableViaRepostNotificationProvider> 127 + <DisableLikesMetricsProvider> 128 + <DisableRepostsMetricsProvider> 129 + <DisableQuotesMetricsProvider> 130 + <DisableSavesMetricsProvider> 131 + <DisableReplyMetricsProvider> 132 + <DisableFollowersMetricsProvider> 133 + <DisableFollowingMetricsProvider> 134 + <DisableFollowedByMetricsProvider> 135 + <DisablePostsMetricsProvider> 136 + <ShowFollowsYouBadgeProvider> 137 + <HideSimilarAccountsRecommProvider> 138 + <HideUnreplyablePostsProvider> 139 + <EnableSquareAvatarsProvider> 140 + <EnableSquareButtonsProvider> 141 + <PostNameReplacementProvider> 142 + <DisableVerifyEmailReminderProvider> 143 + <TranslationServicePreferenceProvider> 144 + <OpenRouterProvider> 145 + <DisableComposerPromptProvider> 146 + <DiscoverContextEnabledProvider> 147 + { 148 + children 149 + } 150 + </DiscoverContextEnabledProvider> 151 + </DisableComposerPromptProvider> 152 + </OpenRouterProvider> 153 + </TranslationServicePreferenceProvider> 154 + </DisableVerifyEmailReminderProvider> 155 + </PostNameReplacementProvider> 156 + </EnableSquareButtonsProvider> 157 + </EnableSquareAvatarsProvider> 158 + </HideUnreplyablePostsProvider> 159 + </HideSimilarAccountsRecommProvider> 160 + </ShowFollowsYouBadgeProvider> 161 + </DisablePostsMetricsProvider> 162 + </DisableFollowedByMetricsProvider> 163 + </DisableFollowingMetricsProvider> 164 + </DisableFollowersMetricsProvider> 165 + </DisableReplyMetricsProvider> 166 + </DisableSavesMetricsProvider> 167 + </DisableQuotesMetricsProvider> 168 + </DisableRepostsMetricsProvider> 169 + </DisableLikesMetricsProvider> 170 + </DisableViaRepostNotificationProvider> 171 + </HideFeedsPromoTabProvider> 172 + </KawaiiProvider> 173 + </RepostCarouselProvider> 174 + </TrendingSettingsProvider> 175 + </SubtitlesProvider> 176 + </UsedStarterPacksProvider> 177 + </AutoplayProvider> 178 + </DisableHapticsProvider> 179 + </InAppBrowserProvider> 180 + </ImageCdnHostProvider> 181 + </HighQualityImagesProvider> 182 + </HiddenPostsProvider> 183 + </ExternalEmbedsProvider> 184 + </LargeAltBadgeProvider> 185 + </UseHandleInLinksProvider> 186 + </ShowLinkInHandleOnlyOnWorkingLinksProvider> 184 187 </ShowLinkInHandleProvider> 185 188 </NoDiscoverProvider> 186 189 </PdsLabelProvider>
+64
src/state/queries/handle-link.ts
··· 1 + import {useQuery} from '@tanstack/react-query' 2 + 3 + import {DOH_ENDPOINT} from '#/lib/constants' 4 + import {STALE} from '#/state/queries' 5 + 6 + const DNS_RECORD_TYPES = ['A', 'AAAA', 'CNAME'] as const 7 + 8 + type DnsJsonResponse = { 9 + Answer?: Array<{data?: string}> 10 + } 11 + 12 + export const RQKEY_ROOT = 'handle-link' 13 + export const RQKEY = (handle: string) => [RQKEY_ROOT, handle] 14 + 15 + async function hasDnsAnswers( 16 + handle: string, 17 + type: (typeof DNS_RECORD_TYPES)[number], 18 + signal?: AbortSignal, 19 + ) { 20 + const url = new URL(DOH_ENDPOINT) 21 + url.searchParams.set('name', handle) 22 + url.searchParams.set('type', type) 23 + 24 + const response = await fetch(url, { 25 + headers: { 26 + accept: 'application/dns-json', 27 + }, 28 + redirect: 'follow', 29 + signal, 30 + }) 31 + 32 + if (!response.ok) { 33 + return false 34 + } 35 + 36 + const result = (await response.json()) as DnsJsonResponse 37 + return Array.isArray(result.Answer) && result.Answer.length > 0 38 + } 39 + 40 + export async function hasWorkingHandleLink( 41 + handle: string, 42 + signal?: AbortSignal, 43 + ) { 44 + const results = await Promise.allSettled( 45 + DNS_RECORD_TYPES.map(type => hasDnsAnswers(handle, type, signal)), 46 + ) 47 + 48 + signal?.throwIfAborted() 49 + 50 + return results.some( 51 + result => result.status === 'fulfilled' && Boolean(result.value), 52 + ) 53 + } 54 + 55 + export function useHandleLinkQuery(handle: string, enabled = true) { 56 + return useQuery({ 57 + queryKey: RQKEY(handle), 58 + queryFn: async ({signal}) => { 59 + return hasWorkingHandleLink(handle, signal) 60 + }, 61 + enabled, 62 + staleTime: STALE.HOURS.ONE, 63 + }) 64 + }
+7 -18
src/state/session/identity-resolver.ts
··· 1 1 import {type ComAtprotoIdentityDefs, isDid} from '@atproto/api' 2 + import { 3 + type IdentityInfo, 4 + type IdentityResolver, 5 + } from '@atproto-labs/identity-resolver' 2 6 7 + import {DOH_ENDPOINT} from '#/lib/constants' 3 8 import {createPublicAgent} from './agent' 4 9 5 10 type AtprotoDid = `did:plc:${string}` | `did:web:${string}` ··· 15 20 serviceEndpoint?: string 16 21 } 17 22 18 - type IdentityResolver = { 19 - resolve( 20 - input: string, 21 - options?: {signal?: AbortSignal}, 22 - ): Promise<{ 23 - did: AtprotoDid 24 - didDoc: DidDocument 25 - handle: string 26 - }> 27 - } 28 - 29 23 const HANDLE_INVALID = 'handle.invalid' 30 - const DOH_ENDPOINT = 'https://cloudflare-dns.com/dns-query' 31 24 32 25 function asNormalizedHandle(input: string) { 33 26 const handle = input.toLowerCase() ··· 242 235 async resolve( 243 236 input: string, 244 237 options?: {signal?: AbortSignal}, 245 - ): Promise<{ 246 - did: AtprotoDid 247 - didDoc: DidDocument 248 - handle: string 249 - }> { 238 + ): Promise<IdentityInfo> { 250 239 const identity = await resolveIdentityUsingAppView(input, options?.signal) 251 240 252 241 return { 253 242 did: identity.did as AtprotoDid, 254 - didDoc: identity.didDoc as DidDocument, 243 + didDoc: identity.didDoc as IdentityInfo['didDoc'], 255 244 handle: identity.handle, 256 245 } 257 246 },