Bluesky app fork with some witchin' additions 💫 witchsky.app
bluesky fork client
117
fork

Configure Feed

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

feat: change plc directory

+ Image CDN host Reset button

+289 -75
+139 -8
src/screens/Settings/RunesSettings.tsx
··· 141 141 useSetPdsLabelHideBskyPds, 142 142 } from '#/state/preferences/pds-label' 143 143 import { 144 + usePlcDirectory, 145 + useSetPlcDirectory, 146 + } from '#/state/preferences/plc-directory' 147 + import { 144 148 usePostReplacement, 145 149 useSetPostReplacement, 146 150 } from '#/state/preferences/post-name-replacement' ··· 593 597 const setImageCdnHost = useSetImageCdnHost() 594 598 595 599 const submit = () => { 596 - try { 597 - setImageCdnHost(new URL(url).origin) 598 - } catch { 599 - setImageCdnHost(url) 600 + const trimmedUrl = url.trim() 601 + if (!trimmedUrl) { 602 + control.close(() => { 603 + setImageCdnHost(undefined) 604 + }) 605 + return 600 606 } 601 - control.close() 607 + 608 + control.close(() => { 609 + try { 610 + setImageCdnHost(new URL(trimmedUrl).origin) 611 + } catch { 612 + setImageCdnHost(trimmedUrl) 613 + } 614 + }) 602 615 } 616 + 617 + const isReset = url.trim().length === 0 603 618 604 619 const shouldDisable = () => { 620 + if (isReset) return false 621 + 605 622 try { 606 623 return !new URL(url).hostname.includes('.') 607 624 } catch (e) { ··· 639 656 640 657 <View style={IS_WEB && [a.flex_row, a.justify_end]}> 641 658 <Button 642 - label={_(msg`Save`)} 659 + label={isReset ? _(msg`Reset`) : _(msg`Save`)} 643 660 size="large" 644 661 onPress={() => void submit()} 645 662 variant="solid" 646 - color="primary" 663 + color={isReset ? 'secondary' : 'primary'} 647 664 disabled={shouldDisable()}> 648 665 <ButtonText> 649 - <Trans>Save</Trans> 666 + {isReset ? <Trans>Reset</Trans> : <Trans>Save</Trans>} 650 667 </ButtonText> 651 668 </Button> 652 669 </View> ··· 658 675 ) 659 676 } 660 677 678 + function PlcDirectoryDialog({control}: {control: Dialog.DialogControlProps}) { 679 + const pal = usePalette('default') 680 + const {_} = useLingui() 681 + 682 + const plcDirectory = usePlcDirectory() 683 + const [url, setUrl] = useState(plcDirectory ?? '') 684 + const setPlcDirectory = useSetPlcDirectory() 685 + 686 + const submit = () => { 687 + const trimmedUrl = url.trim() 688 + if (!trimmedUrl) { 689 + control.close(() => { 690 + setPlcDirectory(undefined) 691 + }) 692 + return 693 + } 694 + 695 + control.close(() => { 696 + try { 697 + setPlcDirectory(new URL(trimmedUrl).origin) 698 + } catch { 699 + setPlcDirectory(trimmedUrl) 700 + } 701 + }) 702 + } 703 + 704 + const isReset = url.trim().length === 0 705 + 706 + const shouldDisable = () => { 707 + if (isReset) return false 708 + 709 + try { 710 + const nextUrl = new URL(url) 711 + return nextUrl.protocol !== 'https:' && nextUrl.protocol !== 'http:' 712 + } catch { 713 + return true 714 + } 715 + } 716 + 717 + return ( 718 + <Dialog.Outer 719 + control={control} 720 + nativeOptions={{preventExpansion: true}} 721 + onClose={() => setUrl(plcDirectory ?? '')}> 722 + <Dialog.Handle /> 723 + <Dialog.ScrollableInner label={_(msg`PLC Directory URL`)}> 724 + <View style={[a.gap_sm, a.pb_lg]}> 725 + <Text style={[a.text_2xl, a.font_bold]}> 726 + <Trans>PLC Directory URL</Trans> 727 + </Text> 728 + </View> 729 + 730 + <View style={a.gap_lg}> 731 + <Dialog.Input 732 + label="Text input field" 733 + autoFocus 734 + style={[styles.textInput, pal.border, pal.text]} 735 + onChangeText={value => { 736 + setUrl(value) 737 + }} 738 + placeholder={persisted.defaults.plcDirectory} 739 + placeholderTextColor={pal.colors.textLight} 740 + onSubmitEditing={submit} 741 + accessibilityHint={_( 742 + msg`Input the URL of the PLC directory to use`, 743 + )} 744 + defaultValue={plcDirectory} 745 + /> 746 + 747 + <View style={IS_WEB && [a.flex_row, a.justify_end]}> 748 + <Button 749 + label={isReset ? _(msg`Reset`) : _(msg`Save`)} 750 + size="large" 751 + onPress={() => void submit()} 752 + variant="solid" 753 + color={isReset ? 'secondary' : 'primary'} 754 + disabled={shouldDisable()}> 755 + <ButtonText> 756 + {isReset ? <Trans>Reset</Trans> : <Trans>Save</Trans>} 757 + </ButtonText> 758 + </Button> 759 + </View> 760 + </View> 761 + 762 + <Dialog.Close /> 763 + </Dialog.ScrollableInner> 764 + </Dialog.Outer> 765 + ) 766 + } 661 767 function PostReplacementDialog({ 662 768 control, 663 769 }: { ··· 1067 1173 const highQualityImages = useHighQualityImages() 1068 1174 const setHighQualityImages = useSetHighQualityImages() 1069 1175 const imageCdnHost = useImageCdnHost() 1176 + const plcDirectory = usePlcDirectory() 1070 1177 1071 1178 const hideFeedsPromoTab = useHideFeedsPromoTab() 1072 1179 const setHideFeedsPromoTab = useSetHideFeedsPromoTab() ··· 1151 1258 const setLibreTranslateInstanceControl = Dialog.useDialogControl() 1152 1259 1153 1260 const setImageCdnHostControl = Dialog.useDialogControl() 1261 + 1262 + const setPlcDirectoryControl = Dialog.useDialogControl() 1154 1263 1155 1264 const setPostReplacementDialogControl = Dialog.useDialogControl() 1156 1265 ··· 1877 1986 </Admonition> 1878 1987 </SettingsList.Item> 1879 1988 1989 + <SettingsList.Item> 1990 + <SettingsList.ItemIcon icon={EarthIcon} /> 1991 + <SettingsList.ItemText> 1992 + <Trans>{`PLC Directory`}</Trans> 1993 + </SettingsList.ItemText> 1994 + <SettingsList.BadgeButton 1995 + label={_(msg`Change`)} 1996 + onPress={() => setPlcDirectoryControl.open()} 1997 + /> 1998 + </SettingsList.Item> 1999 + <SettingsList.Item> 2000 + <Admonition type="info" style={[a.flex_1]}> 2001 + <Trans> 2002 + Override the PLC directory used to resolve DIDs. Current:  2003 + <InlineLinkText to={plcDirectory} label={plcDirectory}> 2004 + {plcDirectory} 2005 + </InlineLinkText> 2006 + </Trans> 2007 + </Admonition> 2008 + </SettingsList.Item> 2009 + 1880 2010 <SettingsList.Divider /> 1881 2011 1882 2012 <SettingsList.Item> ··· 1943 2073 control={setLibreTranslateInstanceControl} 1944 2074 /> 1945 2075 <ImageCdnHostDialog control={setImageCdnHostControl} /> 2076 + <PlcDirectoryDialog control={setPlcDirectoryControl} /> 1946 2077 <PostReplacementDialog control={setPostReplacementDialogControl} /> 1947 2078 <OpenRouterApiKeyDialog control={setOpenRouterApiKeyControl} /> 1948 2079 <OpenRouterModelDialog control={setOpenRouterModelControl} />
+2
src/state/persisted/schema.ts
··· 177 177 .optional(), 178 178 highQualityImages: z.boolean().optional(), 179 179 imageCdnHost: z.string().optional(), 180 + plcDirectory: z.string().optional(), 180 181 hideUnreplyablePosts: z.boolean().optional(), 181 182 pdsLabel: z 182 183 .object({ ··· 321 322 }, 322 323 highQualityImages: false, 323 324 imageCdnHost: 'https://cdn.bsky.app', 325 + plcDirectory: 'https://plc.directory', 324 326 hideUnreplyablePosts: false, 325 327 pdsLabel: { 326 328 enabled: true,
+71 -63
src/state/preferences/index.tsx
··· 39 39 import {Provider as NoDiscoverProvider} from './no-discover-fallback' 40 40 import {Provider as OpenRouterProvider} from './openrouter' 41 41 import {Provider as PdsLabelProvider} from './pds-label' 42 + import {Provider as PlcDirectoryProvider} from './plc-directory' 42 43 import {Provider as PostNameReplacementProvider} from './post-name-replacement.tsx' 43 44 import {Provider as RepostCarouselProvider} from './repost-carousel-enabled' 44 45 import {Provider as ShowFollowsYouBadgeProvider} from './show-follows-you-badge' ··· 85 86 useSetOpenRouterApiKey, 86 87 useSetOpenRouterModel, 87 88 } from './openrouter' 89 + export { 90 + readPlcDirectory, 91 + usePlcDirectory, 92 + useSetPlcDirectory, 93 + } from './plc-directory' 88 94 export {useSetSubtitlesEnabled, useSubtitlesEnabled} from './subtitles' 89 95 export { 90 96 useSetTranslationServicePreference, ··· 114 120 <HiddenPostsProvider> 115 121 <HighQualityImagesProvider> 116 122 <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> 123 + <PlcDirectoryProvider> 124 + <InAppBrowserProvider> 125 + <DisableHapticsProvider> 126 + <AutoplayProvider> 127 + <UsedStarterPacksProvider> 128 + <SubtitlesProvider> 129 + <TrendingSettingsProvider> 130 + <RepostCarouselProvider> 131 + <KawaiiProvider> 132 + <HideFeedsPromoTabProvider> 133 + <DisableViaRepostNotificationProvider> 134 + <DisableLikesMetricsProvider> 135 + <DisableRepostsMetricsProvider> 136 + <DisableQuotesMetricsProvider> 137 + <DisableSavesMetricsProvider> 138 + <DisableReplyMetricsProvider> 139 + <DisableFollowersMetricsProvider> 140 + <DisableFollowingMetricsProvider> 141 + <DisableFollowedByMetricsProvider> 142 + <DisablePostsMetricsProvider> 143 + <ShowFollowsYouBadgeProvider> 144 + <HideSimilarAccountsRecommProvider> 145 + <HideUnreplyablePostsProvider> 146 + <EnableSquareAvatarsProvider> 147 + <EnableSquareButtonsProvider> 148 + <PostNameReplacementProvider> 149 + <DisableVerifyEmailReminderProvider> 150 + <TranslationServicePreferenceProvider> 151 + <OpenRouterProvider> 152 + <DisableComposerPromptProvider> 153 + <DiscoverContextEnabledProvider> 154 + { 155 + children 156 + } 157 + </DiscoverContextEnabledProvider> 158 + </DisableComposerPromptProvider> 159 + </OpenRouterProvider> 160 + </TranslationServicePreferenceProvider> 161 + </DisableVerifyEmailReminderProvider> 162 + </PostNameReplacementProvider> 163 + </EnableSquareButtonsProvider> 164 + </EnableSquareAvatarsProvider> 165 + </HideUnreplyablePostsProvider> 166 + </HideSimilarAccountsRecommProvider> 167 + </ShowFollowsYouBadgeProvider> 168 + </DisablePostsMetricsProvider> 169 + </DisableFollowedByMetricsProvider> 170 + </DisableFollowingMetricsProvider> 171 + </DisableFollowersMetricsProvider> 172 + </DisableReplyMetricsProvider> 173 + </DisableSavesMetricsProvider> 174 + </DisableQuotesMetricsProvider> 175 + </DisableRepostsMetricsProvider> 176 + </DisableLikesMetricsProvider> 177 + </DisableViaRepostNotificationProvider> 178 + </HideFeedsPromoTabProvider> 179 + </KawaiiProvider> 180 + </RepostCarouselProvider> 181 + </TrendingSettingsProvider> 182 + </SubtitlesProvider> 183 + </UsedStarterPacksProvider> 184 + </AutoplayProvider> 185 + </DisableHapticsProvider> 186 + </InAppBrowserProvider> 187 + </PlcDirectoryProvider> 180 188 </ImageCdnHostProvider> 181 189 </HighQualityImagesProvider> 182 190 </HiddenPostsProvider>
+67
src/state/preferences/plc-directory.tsx
··· 1 + import React from 'react' 2 + 3 + import * as persisted from '#/state/persisted' 4 + 5 + type StateContext = persisted.Schema['plcDirectory'] 6 + type SetContext = (v: persisted.Schema['plcDirectory']) => void 7 + 8 + const stateContext = React.createContext<StateContext>( 9 + persisted.defaults.plcDirectory, 10 + ) 11 + const setContext = React.createContext<SetContext>( 12 + (_: persisted.Schema['plcDirectory']) => {}, 13 + ) 14 + 15 + function normalizeOrigin(input: string) { 16 + try { 17 + return new URL(input).origin 18 + } catch { 19 + return null 20 + } 21 + } 22 + 23 + export function Provider({children}: React.PropsWithChildren<{}>) { 24 + const [state, setState] = React.useState(persisted.get('plcDirectory')) 25 + 26 + const setStateWrapped = React.useCallback( 27 + (plcDirectory: persisted.Schema['plcDirectory']) => { 28 + setState(plcDirectory) 29 + persisted.write('plcDirectory', plcDirectory) 30 + }, 31 + [setState], 32 + ) 33 + 34 + React.useEffect(() => { 35 + return persisted.onUpdate('plcDirectory', nextPlcDirectory => { 36 + setState(nextPlcDirectory) 37 + }) 38 + }, [setStateWrapped]) 39 + 40 + return ( 41 + <stateContext.Provider value={state}> 42 + <setContext.Provider value={setStateWrapped}> 43 + {children} 44 + </setContext.Provider> 45 + </stateContext.Provider> 46 + ) 47 + } 48 + 49 + export function usePlcDirectory() { 50 + return ( 51 + normalizeOrigin( 52 + React.useContext(stateContext) ?? persisted.defaults.plcDirectory!, 53 + ) ?? persisted.defaults.plcDirectory! 54 + ) 55 + } 56 + 57 + export function useSetPlcDirectory() { 58 + return React.useContext(setContext) 59 + } 60 + 61 + export function readPlcDirectory() { 62 + return ( 63 + normalizeOrigin( 64 + persisted.get('plcDirectory') ?? persisted.defaults.plcDirectory!, 65 + ) ?? persisted.defaults.plcDirectory! 66 + ) 67 + }
+8 -3
src/state/queries/resolve-identity.ts
··· 1 1 import {type Did, isDid} from '@atproto/api' 2 2 import {useQuery} from '@tanstack/react-query' 3 3 4 + import {readPlcDirectory} from '#/state/preferences/plc-directory' 4 5 import {STALE} from '.' 5 6 import {LRU} from './direct-fetch-record' 6 7 const RQKEY_ROOT = 'resolve-identity' ··· 28 29 serviceEndpoint?: string 29 30 } 30 31 31 - const serviceCache = new LRU<Did, DidDocument>() 32 + const serviceCache = new LRU<string, DidDocument>() 32 33 33 34 export async function resolveDidDocument(did: Did) { 34 - return await serviceCache.getOrTryInsertWith(did, async () => { 35 + const cacheKey = did.startsWith('did:plc:') 36 + ? `${readPlcDirectory()}|${did}` 37 + : did 38 + 39 + return await serviceCache.getOrTryInsertWith(cacheKey, async () => { 35 40 const docUrl = did.startsWith('did:plc:') 36 - ? `https://plc.directory/${did}` 41 + ? `${readPlcDirectory()}/${did}` 37 42 : `https://${did.substring(8)}/.well-known/did.json` 38 43 39 44 // TODO: we should probably validate this...
+2 -1
src/state/session/identity-resolver.ts
··· 5 5 } from '@atproto-labs/identity-resolver' 6 6 7 7 import {DOH_ENDPOINT} from '#/lib/constants' 8 + import {readPlcDirectory} from '#/state/preferences/plc-directory' 8 9 import {createPublicAgent} from './agent' 9 10 10 11 type AtprotoDid = `did:plc:${string}` | `did:web:${string}` ··· 166 167 signal?: AbortSignal, 167 168 ): Promise<DidDocument> { 168 169 const docUrl = did.startsWith('did:plc:') 169 - ? `https://plc.directory/${did}` 170 + ? `${readPlcDirectory()}/${did}` 170 171 : `https://${did.substring(8)}/.well-known/did.json` 171 172 172 173 const res = await fetch(docUrl, {