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

Configure Feed

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

feat: let you edit (pds) favicon fetching service

how are all the options bad ts pmo
if someone has a better default PLEASE share

xan.lol 748d0b2f 4d768cf8

+282 -204
+121 -4
src/screens/Settings/RunesSettings.tsx
··· 93 93 useShowExternalShareButtons, 94 94 } from '#/state/preferences/external-share-buttons' 95 95 import { 96 + useFaviconService, 97 + useSetFaviconService, 98 + } from '#/state/preferences/favicon-service' 99 + import { 96 100 useHideFeedsPromoTab, 97 101 useSetHideFeedsPromoTab, 98 102 } from '#/state/preferences/hide-feeds-promo-tab' ··· 175 179 import {ChainLink_Stroke2_Corner0_Rounded as ChainLinkIcon} from '#/components/icons/ChainLink' 176 180 import {Eye_Stroke2_Corner0_Rounded as VisibilityIcon} from '#/components/icons/Eye' 177 181 import {Earth_Stroke2_Corner2_Rounded as EarthIcon} from '#/components/icons/Globe' 178 - import {Lab_Stroke2_Corner0_Rounded as _BeakerIcon} from '#/components/icons/Lab' 182 + import {Lab_Stroke2_Corner0_Rounded as BeakerIcon} from '#/components/icons/Lab' 179 183 import {PaintRoller_Stroke2_Corner2_Rounded as PaintRollerIcon} from '#/components/icons/PaintRoller' 180 184 import {Pencil_Stroke2_Corner0_Rounded as PencilIcon} from '#/components/icons/Pencil' 181 185 import {RaisingHand4Finger_Stroke2_Corner0_Rounded as RaisingHandIcon} from '#/components/icons/RaisingHand' ··· 403 407 ) 404 408 } 405 409 410 + function FaviconServiceDialog({control}: {control: Dialog.DialogControlProps}) { 411 + const pal = usePalette('default') 412 + const {_} = useLingui() 413 + 414 + const faviconService = useFaviconService() 415 + const [url, setUrl] = useState(faviconService ?? '') 416 + const [inputVersion, setInputVersion] = useState(0) 417 + const setFaviconService = useSetFaviconService() 418 + 419 + const updateInputValue = (nextUrl: string) => { 420 + setUrl(nextUrl) 421 + setInputVersion(v => v + 1) 422 + } 423 + 424 + const submit = () => { 425 + setFaviconService(url.trim()) 426 + control.close() 427 + } 428 + 429 + const shouldDisable = () => { 430 + return url.length > 0 && !url.includes('(pds)') 431 + } 432 + 433 + const presets = [ 434 + 'https://twenty-icons.com/(pds)', 435 + 'https://favicon.im/(pds)?larger=true&throw-error-on-404=true', 436 + ] 437 + 438 + return ( 439 + <Dialog.Outer 440 + control={control} 441 + nativeOptions={{preventExpansion: true}} 442 + onClose={() => updateInputValue(faviconService ?? '')}> 443 + <Dialog.Handle /> 444 + <Dialog.ScrollableInner label={_(msg`Favicon Service URL`)}> 445 + <View style={[a.gap_sm, a.pb_lg]}> 446 + <Text style={[a.text_2xl, a.font_bold]}> 447 + <Trans>Favicon Service URL</Trans> 448 + </Text> 449 + <Text style={[a.text_sm, {color: pal.colors.textLight}]}> 450 + <Trans> 451 + (pds) is replaced with the domain of an account's host. 452 + </Trans> 453 + </Text> 454 + </View> 455 + 456 + <View style={a.gap_lg}> 457 + <Dialog.Input 458 + key={`favicon-service-input-${inputVersion}`} 459 + label="Text input field" 460 + autoFocus 461 + style={[styles.textInput, pal.border, pal.text]} 462 + onChangeText={value => setUrl(value)} 463 + placeholder={persisted.defaults.faviconService} 464 + placeholderTextColor={pal.colors.textLight} 465 + onSubmitEditing={submit} 466 + accessibilityHint={_( 467 + msg`Enter the favicon service URL with (pds) as placeholder`, 468 + )} 469 + defaultValue={url} 470 + /> 471 + 472 + <View style={[a.flex_row, a.flex_wrap, a.mb_xs]}> 473 + {presets.map(preset => ( 474 + <Button 475 + key={preset} 476 + variant="ghost" 477 + color="primary" 478 + label={preset} 479 + style={[a.px_sm, a.py_xs, a.rounded_sm, a.gap_sm]} 480 + onPress={() => updateInputValue(preset)}> 481 + <ButtonText>{preset}</ButtonText> 482 + </Button> 483 + ))} 484 + </View> 485 + 486 + <View style={IS_WEB && [a.flex_row, a.justify_end]}> 487 + <Button 488 + label={_(msg`Save`)} 489 + size="large" 490 + onPress={submit} 491 + variant="solid" 492 + color="primary" 493 + disabled={shouldDisable()}> 494 + <ButtonText> 495 + <Trans>Save</Trans> 496 + </ButtonText> 497 + </Button> 498 + </View> 499 + </View> 500 + 501 + <Dialog.Close /> 502 + </Dialog.ScrollableInner> 503 + </Dialog.Outer> 504 + ) 505 + } 506 + 406 507 function LibreTranslateInstanceDialog({ 407 508 control, 408 509 }: { ··· 1057 1158 const [customAppViewDid] = useCustomAppViewDid() 1058 1159 const setCustomAppViewDidControl = Dialog.useDialogControl() 1059 1160 1161 + const setFaviconServiceControl = Dialog.useDialogControl() 1162 + 1060 1163 return ( 1061 1164 <Layout.Screen> 1062 1165 <Layout.Header.Outer> ··· 1439 1542 </Toggle.Item> 1440 1543 </SettingsList.Group> 1441 1544 1545 + {pdsLabelEnabled && ( 1546 + <SettingsList.Item> 1547 + <SettingsList.ItemIcon icon={StarIcon} /> 1548 + <SettingsList.ItemText> 1549 + <Trans>Favicon service</Trans> 1550 + </SettingsList.ItemText> 1551 + <SettingsList.BadgeButton 1552 + label={_(msg`Change`)} 1553 + onPress={() => setFaviconServiceControl.open()} 1554 + /> 1555 + </SettingsList.Item> 1556 + )} 1557 + 1442 1558 <SettingsList.Divider /> 1443 1559 1444 1560 <SettingsList.Group contentContainerStyle={[a.gap_sm]}> ··· 1512 1628 <SettingsList.Divider /> 1513 1629 1514 1630 <SettingsList.Item> 1515 - <SettingsList.ItemIcon icon={_BeakerIcon} /> 1631 + <SettingsList.ItemIcon icon={BeakerIcon} /> 1516 1632 <SettingsList.ItemText> 1517 1633 <Trans>OpenRouter API Key</Trans> 1518 1634 </SettingsList.ItemText> ··· 1538 1654 1539 1655 {openRouterConfigured && ( 1540 1656 <SettingsList.Item> 1541 - <SettingsList.ItemIcon icon={_BeakerIcon} /> 1657 + <SettingsList.ItemIcon icon={BeakerIcon} /> 1542 1658 <SettingsList.ItemText> 1543 1659 <Trans>{`OpenRouter Model`}</Trans> 1544 1660 </SettingsList.ItemText> ··· 1566 1682 1567 1683 {openRouterConfigured && ( 1568 1684 <SettingsList.Item> 1569 - <SettingsList.ItemIcon icon={_BeakerIcon} /> 1685 + <SettingsList.ItemIcon icon={BeakerIcon} /> 1570 1686 <SettingsList.ItemText> 1571 1687 <Trans>Alt Text Prompt</Trans> 1572 1688 </SettingsList.ItemText> ··· 1800 1916 </Layout.Content> 1801 1917 <ConstellationInstanceDialog control={setConstellationInstanceControl} /> 1802 1918 <CustomAppViewDidDialog control={setCustomAppViewDidControl} /> 1919 + <FaviconServiceDialog control={setFaviconServiceControl} /> 1803 1920 <TrustedVerifiersDialog control={setTrustedVerifiersDialogControl} /> 1804 1921 <LibreTranslateInstanceDialog 1805 1922 control={setLibreTranslateInstanceControl}
+2
src/state/persisted/schema.ts
··· 183 183 hideBskyPds: z.boolean(), 184 184 }) 185 185 .optional(), 186 + faviconService: z.string().optional(), 186 187 187 188 postReplacement: z.object({ 188 189 enabled: z.boolean().optional(), ··· 323 324 enabled: true, 324 325 hideBskyPds: true, 325 326 }, 327 + faviconService: 'https://twenty-icons.com/(pds)', 326 328 showExternalShareButtons: false, 327 329 translationServicePreference: 'google', 328 330 libreTranslateInstance: 'https://libretranslate.com/',
+48
src/state/preferences/favicon-service.tsx
··· 1 + import React from 'react' 2 + 3 + import * as persisted from '#/state/persisted' 4 + 5 + type StateContext = persisted.Schema['faviconService'] 6 + type SetContext = (v: persisted.Schema['faviconService']) => void 7 + 8 + const stateContext = React.createContext<StateContext>( 9 + persisted.defaults.faviconService, 10 + ) 11 + const setContext = React.createContext<SetContext>( 12 + (_: persisted.Schema['faviconService']) => {}, 13 + ) 14 + 15 + export function Provider({children}: React.PropsWithChildren<{}>) { 16 + const [state, setState] = React.useState(persisted.get('faviconService')) 17 + 18 + const setStateWrapped = React.useCallback( 19 + (faviconService: persisted.Schema['faviconService']) => { 20 + setState(faviconService) 21 + persisted.write('faviconService', faviconService) 22 + }, 23 + [setState], 24 + ) 25 + 26 + React.useEffect(() => { 27 + return persisted.onUpdate('faviconService', next => { 28 + setState(next) 29 + }) 30 + }, [setStateWrapped]) 31 + 32 + return ( 33 + <stateContext.Provider value={state}> 34 + <setContext.Provider value={setStateWrapped}> 35 + {children} 36 + </setContext.Provider> 37 + </stateContext.Provider> 38 + ) 39 + } 40 + 41 + export function useFaviconService() { 42 + return React.useContext(stateContext) ?? persisted.defaults.faviconService 43 + } 44 + 45 + export function useSetFaviconService() { 46 + const setFaviconService = React.useContext(setContext) 47 + return setFaviconService 48 + }
+85 -81
src/state/preferences/index.tsx
··· 23 23 import {Provider as EnableSquareButtonsProvider} from './enable-square-buttons' 24 24 import {Provider as ExternalEmbedsProvider} from './external-embeds-prefs' 25 25 import {Provider as ExternalShareButtonsProvider} from './external-share-buttons' 26 + import {Provider as FaviconServiceProvider} from './favicon-service' 26 27 import {Provider as GoLinksProvider} from './go-links-enabled' 27 28 import {Provider as HiddenPostsProvider} from './hidden-posts' 28 29 import {Provider as HideFeedsPromoTabProvider} from './hide-feeds-promo-tab' ··· 66 67 useExternalEmbedsPrefs, 67 68 useSetExternalEmbedPref, 68 69 } from './external-embeds-prefs' 70 + export {useFaviconService, useSetFaviconService} from './favicon-service' 69 71 export {useGoLinksEnabled, useSetGoLinksEnabled} from './go-links-enabled' 70 72 export {useHiddenPosts, useHiddenPostsApi} from './hidden-posts' 71 73 export { ··· 100 102 <ConstellationProvider> 101 103 <ConstellationInstanceProvider> 102 104 <DeerVerificationProvider> 103 - <PdsLabelProvider> 104 - <NoDiscoverProvider> 105 - <ShowLinkInHandleProvider> 106 - <UseHandleInLinksProvider> 107 - <LargeAltBadgeProvider> 108 - <ExternalEmbedsProvider> 109 - <HiddenPostsProvider> 110 - <HighQualityImagesProvider> 111 - <ImageCdnHostProvider> 112 - <InAppBrowserProvider> 113 - <DisableHapticsProvider> 114 - <AutoplayProvider> 115 - <UsedStarterPacksProvider> 116 - <SubtitlesProvider> 117 - <TrendingSettingsProvider> 118 - <RepostCarouselProvider> 119 - <KawaiiProvider> 120 - <HideFeedsPromoTabProvider> 121 - <DisableViaRepostNotificationProvider> 122 - <DisableLikesMetricsProvider> 123 - <DisableRepostsMetricsProvider> 124 - <DisableQuotesMetricsProvider> 125 - <DisableSavesMetricsProvider> 126 - <DisableReplyMetricsProvider> 127 - <DisableFollowersMetricsProvider> 128 - <DisableFollowingMetricsProvider> 129 - <DisableFollowedByMetricsProvider> 130 - <DisablePostsMetricsProvider> 131 - <ShowFollowsYouBadgeProvider> 132 - <HideSimilarAccountsRecommProvider> 133 - <HideUnreplyablePostsProvider> 134 - <EnableSquareAvatarsProvider> 135 - <EnableSquareButtonsProvider> 136 - <PostNameReplacementProvider> 137 - <DisableVerifyEmailReminderProvider> 138 - <TranslationServicePreferenceProvider> 139 - <OpenRouterProvider> 140 - <DisableComposerPromptProvider> 141 - <DiscoverContextEnabledProvider> 142 - { 143 - children 144 - } 145 - </DiscoverContextEnabledProvider> 146 - </DisableComposerPromptProvider> 147 - </OpenRouterProvider> 148 - </TranslationServicePreferenceProvider> 149 - </DisableVerifyEmailReminderProvider> 150 - </PostNameReplacementProvider> 151 - </EnableSquareButtonsProvider> 152 - </EnableSquareAvatarsProvider> 153 - </HideUnreplyablePostsProvider> 154 - </HideSimilarAccountsRecommProvider> 155 - </ShowFollowsYouBadgeProvider> 156 - </DisablePostsMetricsProvider> 157 - </DisableFollowedByMetricsProvider> 158 - </DisableFollowingMetricsProvider> 159 - </DisableFollowersMetricsProvider> 160 - </DisableReplyMetricsProvider> 161 - </DisableSavesMetricsProvider> 162 - </DisableQuotesMetricsProvider> 163 - </DisableRepostsMetricsProvider> 164 - </DisableLikesMetricsProvider> 165 - </DisableViaRepostNotificationProvider> 166 - </HideFeedsPromoTabProvider> 167 - </KawaiiProvider> 168 - </RepostCarouselProvider> 169 - </TrendingSettingsProvider> 170 - </SubtitlesProvider> 171 - </UsedStarterPacksProvider> 172 - </AutoplayProvider> 173 - </DisableHapticsProvider> 174 - </InAppBrowserProvider> 175 - </ImageCdnHostProvider> 176 - </HighQualityImagesProvider> 177 - </HiddenPostsProvider> 178 - </ExternalEmbedsProvider> 179 - </LargeAltBadgeProvider> 180 - </UseHandleInLinksProvider> 181 - </ShowLinkInHandleProvider> 182 - </NoDiscoverProvider> 183 - </PdsLabelProvider> 105 + <FaviconServiceProvider> 106 + <PdsLabelProvider> 107 + <NoDiscoverProvider> 108 + <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> 184 + </ShowLinkInHandleProvider> 185 + </NoDiscoverProvider> 186 + </PdsLabelProvider> 187 + </FaviconServiceProvider> 184 188 </DeerVerificationProvider> 185 189 </ConstellationInstanceProvider> 186 190 </ConstellationProvider>
+26 -119
src/state/queries/pds-label.ts
··· 1 1 import {useQuery} from '@tanstack/react-query' 2 2 3 + import {useFaviconService} from '#/state/preferences/favicon-service' 3 4 import {resolvePdsServiceUrl} from '#/state/queries/resolve-identity' 4 - import {IS_WEB} from '#/env' 5 5 6 6 const BSKY_PDS_HOSTNAMES = ['bsky.social', 'staging.bsky.dev'] 7 7 const BSKY_PDS_SUFFIX = '.bsky.network' ··· 27 27 } 28 28 } 29 29 30 - // Ordered from highest to lowest quality. The web path probes these via Image 31 - // (CORS-safe) and returns the first that successfully loads. The native path 32 - // uses these as a fallback chain when no <link> icon is found in the HTML. 33 - const ICON_CANDIDATE_PATHS = [ 34 - '/apple-touch-icon.png', // 180 × 180, very common 35 - '/apple-icon-180x180.png', 36 - '/favicon-256x256.png', 37 - '/favicon-96x96.png', 38 - '/favicon-32x32.png', 39 - '/favicon-16x16.png', 40 - '/favicon.ico', 41 - ] 42 - 43 - /** Returns the pixel size for a `sizes` attribute value like "180x180", or 0. */ 44 - function parseSizeAttr(sizes: string | null | undefined): number { 45 - if (!sizes) return 0 46 - const match = sizes.match(/(\d+)x\d+/i) 47 - return match ? parseInt(match[1], 10) : 0 48 - } 49 - 50 - /** Resolves an href found in a <link> tag to an absolute URL. */ 51 - function resolveHref(href: string, origin: string): string { 52 - if (href.startsWith('http')) return href 53 - if (href.startsWith('//')) return `https:${href}` 54 - if (href.startsWith('/')) return `${origin}${href}` 55 - return `${origin}/${href}` 56 - } 57 - 58 - async function getFaviconUrl(pdsUrl: string): Promise<string | undefined> { 59 - let origin = '' 60 - try { 61 - origin = new URL(pdsUrl).origin 62 - } catch { 63 - return undefined 64 - } 65 - 66 - if (IS_WEB) { 67 - // fetch() is blocked by CORS for third-party origins on web. 68 - // Probe candidate URLs in parallel using the Image constructor (CORS-safe). 69 - // Return whichever high-quality candidate loads first, in priority order. 70 - const results = await Promise.all( 71 - ICON_CANDIDATE_PATHS.map( 72 - path => 73 - new Promise<string | undefined>(resolve => { 74 - const url = `${origin}${path}` 75 - const img = new Image() 76 - img.onload = () => resolve(url) 77 - img.onerror = () => resolve(undefined) 78 - img.src = url 79 - }), 80 - ), 81 - ) 82 - // Return the first (highest-priority) candidate that loaded. 83 - return results.find(Boolean) 84 - } 85 - 86 - // Native path: parse the page HTML for all <link rel="icon"> / <link 87 - // rel="apple-touch-icon"> tags, pick the one with the largest declared size, 88 - // then fall back to probing the candidate paths in order. 89 - const htmlIconUrl = await fetch(origin, {headers: {Accept: 'text/html'}}) 90 - .then(async res => { 91 - if (!res.ok) return undefined 92 - const html = await res.text() 93 - 94 - // Collect every <link> tag that looks like an icon. 95 - const linkTagRe = /<link([^>]+)>/gi 96 - let best: {url: string; size: number} | undefined 97 - 98 - let tagMatch: RegExpExecArray | null 99 - while ((tagMatch = linkTagRe.exec(html)) !== null) { 100 - const attrs = tagMatch[1] 101 - const relMatch = attrs.match(/rel=["']([^"']+)["']/i) 102 - if (!relMatch) continue 103 - const rel = relMatch[1].toLowerCase() 104 - if (!rel.includes('icon')) continue 105 - 106 - const hrefMatch = 107 - attrs.match(/href=["']([^"']+)["']/i) || 108 - attrs.match(/href=([^\s>]+)/i) 109 - if (!hrefMatch) continue 110 - 111 - const sizesMatch = attrs.match(/sizes=["']([^"']+)["']/i) 112 - const size = parseSizeAttr(sizesMatch?.[1]) 113 - const url = resolveHref(hrefMatch[1], origin) 114 - 115 - // apple-touch-icon gets a size bonus so it beats a generic icon of the 116 - // same declared dimensions. 117 - const effectiveSize = rel.includes('apple-touch-icon') ? size + 1 : size 118 - 119 - if (!best || effectiveSize > best.size) { 120 - best = {url, size: effectiveSize} 121 - } 122 - } 123 - 124 - return best?.url 125 - }) 126 - .catch(() => undefined) 127 - 128 - if (htmlIconUrl) return htmlIconUrl 129 - 130 - // Fall back to probing known high-quality paths in order. 131 - for (const path of ICON_CANDIDATE_PATHS) { 132 - const url = `${origin}${path}` 133 - const ok = await fetch(url, {method: 'HEAD'}) 134 - .then(res => res.ok) 135 - .catch(() => false) 136 - if (ok) return url 137 - } 138 - 30 + function getFaviconUrl( 31 + pdsUrl: string, 32 + faviconService: string, 33 + ): string | undefined { 139 34 try { 140 35 const hostname = new URL(pdsUrl).hostname 141 - return `https://favicon.im/${hostname}?throw-error-on-404=true` 36 + // Replace the (pds) placeholder with the actual PDS domain 37 + return faviconService.replace('(pds)', hostname) 142 38 } catch { 143 39 return undefined 144 40 } ··· 152 48 queryKey: RQKEY(did ?? ''), 153 49 queryFn: async () => { 154 50 if (!did) return null 155 - const pdsUrl = await resolvePdsServiceUrl(did as `did:${string}`) 51 + const pdsUrl = await resolvePdsServiceUrl( 52 + did as `did:${string}:${string}`, 53 + ) 54 + if (!pdsUrl) return undefined 156 55 const isBsky = isBskyPdsUrl(pdsUrl) 157 56 const isBridged = isBridgedPdsUrl(pdsUrl) 158 57 return {pdsUrl, isBsky, isBridged} ··· 163 62 } 164 63 165 64 export const RQKEY_FAVICON_ROOT = 'pds-favicon' 166 - export const RQKEY_FAVICON = (pdsUrl: string) => [RQKEY_FAVICON_ROOT, pdsUrl] 65 + export const RQKEY_FAVICON = (pdsUrl: string, faviconService: string) => [ 66 + RQKEY_FAVICON_ROOT, 67 + pdsUrl, 68 + faviconService, 69 + ] 167 70 168 71 export function usePdsFaviconQuery(pdsUrl: string | undefined) { 72 + const faviconService = useFaviconService() 73 + const isEnabled = Boolean(pdsUrl && faviconService) 74 + const queryKey = isEnabled 75 + ? RQKEY_FAVICON(pdsUrl!, faviconService!) 76 + : ['pds-favicon-disabled'] 77 + 169 78 return useQuery({ 170 - queryKey: RQKEY_FAVICON(pdsUrl ?? ''), 171 - queryFn: async () => { 172 - if (!pdsUrl) return undefined 173 - return await getFaviconUrl(pdsUrl) 174 - }, 175 - enabled: !!pdsUrl, 79 + queryKey, 80 + queryFn: () => 81 + isEnabled ? getFaviconUrl(pdsUrl!, faviconService!) : undefined, 82 + enabled: isEnabled, 176 83 staleTime: 1000 * 60 * 60, // 1 hour 177 84 }) 178 85 }