Bluesky app fork with some witchin' additions 💫

fix(pds badge): favicon fetching

xan.lol d264c697 719154b3

verified
+7 -112
+2 -3
src/components/PdsBadge.tsx
··· 1 1 import {View} from 'react-native' 2 - import {msg} from '@lingui/macro' 2 + import {msg} from '@lingui/core/macro' 3 3 import {useLingui} from '@lingui/react' 4 4 5 5 import { ··· 32 32 ) 33 33 34 34 const isBskyHandle = 35 - !!handle && 36 - (handle.endsWith('.bsky.social') || handle === 'bsky.social') 35 + !!handle && (handle.endsWith('.bsky.social') || handle === 'bsky.social') 37 36 38 37 if (!enabled) return null 39 38 if (isLoading) return <PdsBadgeLoading size={size} isBsky={isBskyHandle} />
+5 -109
src/state/queries/pds-label.ts
··· 1 1 import {useQuery} from '@tanstack/react-query' 2 2 3 3 import {resolvePdsServiceUrl} from '#/state/queries/resolve-identity' 4 - import {IS_WEB} from '#/env' 5 4 6 5 const BSKY_PDS_HOSTNAMES = ['bsky.social', 'staging.bsky.dev'] 7 6 const BSKY_PDS_SUFFIX = '.bsky.network' ··· 27 26 } 28 27 } 29 28 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 fetchFaviconUrl(pdsUrl: string): Promise<string | undefined> { 59 - let origin = '' 29 + function getFaviconUrl(pdsUrl: string): string | undefined { 60 30 try { 61 - origin = new URL(pdsUrl).origin 31 + const hostname = new URL(pdsUrl).hostname 32 + return `https://favicon.im/${hostname}?throw-error-on-404=true` 62 33 } catch { 63 34 return undefined 64 35 } 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 - 139 - return undefined 140 36 } 141 37 142 38 export const RQKEY_ROOT = 'pds-label' ··· 163 59 export function usePdsFaviconQuery(pdsUrl: string | undefined) { 164 60 return useQuery({ 165 61 queryKey: RQKEY_FAVICON(pdsUrl ?? ''), 166 - queryFn: async () => { 62 + queryFn: () => { 167 63 if (!pdsUrl) return undefined 168 - return await fetchFaviconUrl(pdsUrl) 64 + return getFaviconUrl(pdsUrl) 169 65 }, 170 66 enabled: !!pdsUrl, 171 67 staleTime: 1000 * 60 * 60, // 1 hour