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

feat: widen scope of icons allowed to be used (highest to lowest quality)

ewancroft.uk 0ab07a04 b010079e

verified
+97 -35
+97 -35
src/state/queries/pds-label.ts
··· 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 + 30 58 async function fetchFaviconUrl(pdsUrl: string): Promise<string | undefined> { 31 59 let origin = '' 32 60 try { ··· 37 65 38 66 if (IS_WEB) { 39 67 // fetch() is blocked by CORS for third-party origins on web. 40 - // Use the browser Image constructor instead — it loads cross-origin without CORS. 41 - // Only resolve with the URL once the image confirms it loaded. 42 - return new Promise<string | undefined>(resolve => { 43 - const url = `${origin}/favicon.ico` 44 - const img = new Image() 45 - img.onload = () => resolve(url) 46 - img.onerror = () => resolve(undefined) 47 - img.src = url 48 - }) 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) 49 84 } 50 85 51 - const [linkIconUrl, faviconIcoUrl] = await Promise.all([ 52 - fetch(origin, {headers: {Accept: 'text/html'}}) 53 - .then(async res => { 54 - if (!res.ok) return undefined 55 - const html = await res.text() 56 - // Match <link rel="icon"> or <link rel="shortcut icon"> in either attribute order 57 - const match = 58 - html.match( 59 - /<link[^>]+rel=["'][^"']*\bicon\b[^"']*["'][^>]*href=["']([^"']+)["']/i, 60 - ) || 61 - html.match( 62 - /<link[^>]+href=["']([^"']+)["'][^>]*rel=["'][^"']*\bicon\b[^"']*["']/i, 63 - ) 64 - if (!match) return undefined 65 - const href = match[1] 66 - if (href.startsWith('http')) return href 67 - if (href.startsWith('//')) return `https:${href}` 68 - if (href.startsWith('/')) return `${origin}${href}` 69 - return `${origin}/${href}` 70 - }) 71 - .catch(() => undefined), 72 - fetch(`${origin}/favicon.ico`) 73 - .then(res => (res.ok ? `${origin}/favicon.ico` : undefined)) 74 - .catch(() => undefined), 75 - ]) 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 76 105 77 - return faviconIcoUrl ?? linkIconUrl 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 78 140 } 79 141 80 142 export const RQKEY_ROOT = 'pds-label'