tangled
alpha
login
or
join now
bas.sh
/
witchsky.app
forked from
jollywhoppers.com/witchsky.app
0
fork
atom
Bluesky app fork with some witchin' additions 💫
0
fork
atom
overview
issues
pulls
pipelines
fix(pds badge): favicon fetching
xan.lol
1 week ago
d264c697
719154b3
verified
This commit was signed with the committer's
known signature
.
xan.lol
SSH Key Fingerprint:
SHA256:7Zs+dcly5YqxBg7v8XsE1uPMYCobHKBw7CDiNxpmSrY=
+7
-112
2 changed files
expand all
collapse all
unified
split
src
components
PdsBadge.tsx
state
queries
pds-label.ts
+2
-3
src/components/PdsBadge.tsx
···
1
1
import {View} from 'react-native'
2
2
-
import {msg} from '@lingui/macro'
2
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
35
-
!!handle &&
36
36
-
(handle.endsWith('.bsky.social') || handle === 'bsky.social')
35
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
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
30
-
// Ordered from highest to lowest quality. The web path probes these via Image
31
31
-
// (CORS-safe) and returns the first that successfully loads. The native path
32
32
-
// uses these as a fallback chain when no <link> icon is found in the HTML.
33
33
-
const ICON_CANDIDATE_PATHS = [
34
34
-
'/apple-touch-icon.png', // 180 × 180, very common
35
35
-
'/apple-icon-180x180.png',
36
36
-
'/favicon-256x256.png',
37
37
-
'/favicon-96x96.png',
38
38
-
'/favicon-32x32.png',
39
39
-
'/favicon-16x16.png',
40
40
-
'/favicon.ico',
41
41
-
]
42
42
-
43
43
-
/** Returns the pixel size for a `sizes` attribute value like "180x180", or 0. */
44
44
-
function parseSizeAttr(sizes: string | null | undefined): number {
45
45
-
if (!sizes) return 0
46
46
-
const match = sizes.match(/(\d+)x\d+/i)
47
47
-
return match ? parseInt(match[1], 10) : 0
48
48
-
}
49
49
-
50
50
-
/** Resolves an href found in a <link> tag to an absolute URL. */
51
51
-
function resolveHref(href: string, origin: string): string {
52
52
-
if (href.startsWith('http')) return href
53
53
-
if (href.startsWith('//')) return `https:${href}`
54
54
-
if (href.startsWith('/')) return `${origin}${href}`
55
55
-
return `${origin}/${href}`
56
56
-
}
57
57
-
58
58
-
async function fetchFaviconUrl(pdsUrl: string): Promise<string | undefined> {
59
59
-
let origin = ''
29
29
+
function getFaviconUrl(pdsUrl: string): string | undefined {
60
30
try {
61
61
-
origin = new URL(pdsUrl).origin
31
31
+
const hostname = new URL(pdsUrl).hostname
32
32
+
return `https://favicon.im/${hostname}?throw-error-on-404=true`
62
33
} catch {
63
34
return undefined
64
35
}
65
65
-
66
66
-
if (IS_WEB) {
67
67
-
// fetch() is blocked by CORS for third-party origins on web.
68
68
-
// Probe candidate URLs in parallel using the Image constructor (CORS-safe).
69
69
-
// Return whichever high-quality candidate loads first, in priority order.
70
70
-
const results = await Promise.all(
71
71
-
ICON_CANDIDATE_PATHS.map(
72
72
-
path =>
73
73
-
new Promise<string | undefined>(resolve => {
74
74
-
const url = `${origin}${path}`
75
75
-
const img = new Image()
76
76
-
img.onload = () => resolve(url)
77
77
-
img.onerror = () => resolve(undefined)
78
78
-
img.src = url
79
79
-
}),
80
80
-
),
81
81
-
)
82
82
-
// Return the first (highest-priority) candidate that loaded.
83
83
-
return results.find(Boolean)
84
84
-
}
85
85
-
86
86
-
// Native path: parse the page HTML for all <link rel="icon"> / <link
87
87
-
// rel="apple-touch-icon"> tags, pick the one with the largest declared size,
88
88
-
// then fall back to probing the candidate paths in order.
89
89
-
const htmlIconUrl = await fetch(origin, {headers: {Accept: 'text/html'}})
90
90
-
.then(async res => {
91
91
-
if (!res.ok) return undefined
92
92
-
const html = await res.text()
93
93
-
94
94
-
// Collect every <link> tag that looks like an icon.
95
95
-
const linkTagRe = /<link([^>]+)>/gi
96
96
-
let best: {url: string; size: number} | undefined
97
97
-
98
98
-
let tagMatch: RegExpExecArray | null
99
99
-
while ((tagMatch = linkTagRe.exec(html)) !== null) {
100
100
-
const attrs = tagMatch[1]
101
101
-
const relMatch = attrs.match(/rel=["']([^"']+)["']/i)
102
102
-
if (!relMatch) continue
103
103
-
const rel = relMatch[1].toLowerCase()
104
104
-
if (!rel.includes('icon')) continue
105
105
-
106
106
-
const hrefMatch =
107
107
-
attrs.match(/href=["']([^"']+)["']/i) ||
108
108
-
attrs.match(/href=([^\s>]+)/i)
109
109
-
if (!hrefMatch) continue
110
110
-
111
111
-
const sizesMatch = attrs.match(/sizes=["']([^"']+)["']/i)
112
112
-
const size = parseSizeAttr(sizesMatch?.[1])
113
113
-
const url = resolveHref(hrefMatch[1], origin)
114
114
-
115
115
-
// apple-touch-icon gets a size bonus so it beats a generic icon of the
116
116
-
// same declared dimensions.
117
117
-
const effectiveSize = rel.includes('apple-touch-icon') ? size + 1 : size
118
118
-
119
119
-
if (!best || effectiveSize > best.size) {
120
120
-
best = {url, size: effectiveSize}
121
121
-
}
122
122
-
}
123
123
-
124
124
-
return best?.url
125
125
-
})
126
126
-
.catch(() => undefined)
127
127
-
128
128
-
if (htmlIconUrl) return htmlIconUrl
129
129
-
130
130
-
// Fall back to probing known high-quality paths in order.
131
131
-
for (const path of ICON_CANDIDATE_PATHS) {
132
132
-
const url = `${origin}${path}`
133
133
-
const ok = await fetch(url, {method: 'HEAD'})
134
134
-
.then(res => res.ok)
135
135
-
.catch(() => false)
136
136
-
if (ok) return url
137
137
-
}
138
138
-
139
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
166
-
queryFn: async () => {
62
62
+
queryFn: () => {
167
63
if (!pdsUrl) return undefined
168
168
-
return await fetchFaviconUrl(pdsUrl)
64
64
+
return getFaviconUrl(pdsUrl)
169
65
},
170
66
enabled: !!pdsUrl,
171
67
staleTime: 1000 * 60 * 60, // 1 hour