Bluesky app fork with some witchin' additions 馃挮
0
fork

Configure Feed

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

at main 178 lines 5.6 kB view raw
1import {useQuery} from '@tanstack/react-query' 2 3import {resolvePdsServiceUrl} from '#/state/queries/resolve-identity' 4import {IS_WEB} from '#/env' 5 6const BSKY_PDS_HOSTNAMES = ['bsky.social', 'staging.bsky.dev'] 7const BSKY_PDS_SUFFIX = '.bsky.network' 8const BRIDGY_FED_HOSTNAME = 'atproto.brid.gy' 9 10export function isBskyPdsUrl(url: string): boolean { 11 try { 12 const hostname = new URL(url).hostname 13 return ( 14 BSKY_PDS_HOSTNAMES.includes(hostname) || 15 hostname.endsWith(BSKY_PDS_SUFFIX) 16 ) 17 } catch { 18 return false 19 } 20} 21 22export function isBridgedPdsUrl(url: string): boolean { 23 try { 24 return new URL(url).hostname === BRIDGY_FED_HOSTNAME 25 } catch { 26 return false 27 } 28} 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. 33const 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. */ 44function 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. */ 51function 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 58async 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 139 try { 140 const hostname = new URL(pdsUrl).hostname 141 return `https://favicon.im/${hostname}?throw-error-on-404=true` 142 } catch { 143 return undefined 144 } 145} 146 147export const RQKEY_ROOT = 'pds-label' 148export const RQKEY = (did: string) => [RQKEY_ROOT, did] 149 150export function usePdsLabelQuery(did: string | undefined) { 151 return useQuery({ 152 queryKey: RQKEY(did ?? ''), 153 queryFn: async () => { 154 if (!did) return null 155 const pdsUrl = await resolvePdsServiceUrl(did as `did:${string}`) 156 const isBsky = isBskyPdsUrl(pdsUrl) 157 const isBridged = isBridgedPdsUrl(pdsUrl) 158 return {pdsUrl, isBsky, isBridged} 159 }, 160 enabled: !!did, 161 staleTime: 1000 * 60 * 60, // 1 hour 162 }) 163} 164 165export const RQKEY_FAVICON_ROOT = 'pds-favicon' 166export const RQKEY_FAVICON = (pdsUrl: string) => [RQKEY_FAVICON_ROOT, pdsUrl] 167 168export function usePdsFaviconQuery(pdsUrl: string | undefined) { 169 return useQuery({ 170 queryKey: RQKEY_FAVICON(pdsUrl ?? ''), 171 queryFn: async () => { 172 if (!pdsUrl) return undefined 173 return await getFaviconUrl(pdsUrl) 174 }, 175 enabled: !!pdsUrl, 176 staleTime: 1000 * 60 * 60, // 1 hour 177 }) 178}