fix: favicon can't load error (#2426)
* feat: Implement robust favicon loading with fallback mechanisms * refactor: Improve favicon loading state and use Promise Method * refactor: Extract FallbackFavicon into a separate component * feat: Add Splitbee favicon service to fallback favicon URLs
This commit is contained in:
parent
50e2dd0ec0
commit
b2ebbc1e30
134
src/renderer/src/components/Icons/FallbackFavicon.tsx
Normal file
134
src/renderer/src/components/Icons/FallbackFavicon.tsx
Normal file
@ -0,0 +1,134 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
// FallbackFavicon component that tries multiple favicon sources
|
||||
interface FallbackFaviconProps {
|
||||
hostname: string
|
||||
alt: string
|
||||
}
|
||||
|
||||
const FallbackFavicon: React.FC<FallbackFaviconProps> = ({ hostname, alt }) => {
|
||||
type FaviconState =
|
||||
| { status: 'idle' }
|
||||
| { status: 'loading' }
|
||||
| { status: 'failed' }
|
||||
| { status: 'loaded'; src: string }
|
||||
|
||||
const [faviconState, setFaviconState] = useState<FaviconState>({ status: 'idle' })
|
||||
|
||||
useEffect(() => {
|
||||
// Reset state when hostname changes
|
||||
setFaviconState({ status: 'loading' })
|
||||
|
||||
// Generate all possible favicon URLs
|
||||
const faviconUrls = [
|
||||
`https://favicon.splitbee.io/?url=${hostname}`,
|
||||
`https://${hostname}/favicon.ico`,
|
||||
`https://icon.horse/icon/${hostname}`,
|
||||
`https://favicon.cccyun.cc/${hostname}`,
|
||||
`https://favicon.im/${hostname}`,
|
||||
`https://www.google.com/s2/favicons?domain=${hostname}`
|
||||
]
|
||||
|
||||
// Main controller to abort all requests when needed
|
||||
const controller = new AbortController()
|
||||
const { signal } = controller
|
||||
|
||||
// Create a promise for each favicon URL
|
||||
const faviconPromises = faviconUrls.map((url) =>
|
||||
fetch(url, {
|
||||
method: 'HEAD',
|
||||
signal,
|
||||
credentials: 'omit'
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.ok) {
|
||||
return url
|
||||
}
|
||||
throw new Error(`Failed to fetch ${url}`)
|
||||
})
|
||||
.catch((error) => {
|
||||
// Rethrow aborted errors but silence other failures
|
||||
if (error.name === 'AbortError') {
|
||||
throw error
|
||||
}
|
||||
console.debug(`Failed to fetch favicon from ${url}:`, error)
|
||||
return null // Return null for failed requests
|
||||
})
|
||||
)
|
||||
|
||||
// Create a timeout promise
|
||||
const timeoutPromise = new Promise<string>((resolve) => {
|
||||
const timer = setTimeout(() => {
|
||||
resolve(faviconUrls[0]) // Default to first URL after timeout
|
||||
}, 2000)
|
||||
|
||||
// Clear timeout if signal is aborted
|
||||
signal.addEventListener('abort', () => clearTimeout(timer))
|
||||
})
|
||||
|
||||
// Use Promise.race to get the first successful result
|
||||
Promise.race([
|
||||
// Filter out failed requests (null results)
|
||||
Promise.any(faviconPromises)
|
||||
.then((result) => result || faviconUrls[0]) // Ensure we always have a string, not null
|
||||
.catch(() => faviconUrls[0]),
|
||||
timeoutPromise
|
||||
])
|
||||
.then((url) => {
|
||||
setFaviconState({ status: 'loaded', src: url })
|
||||
})
|
||||
.catch((error) => {
|
||||
console.debug('All favicon requests failed:', error)
|
||||
setFaviconState({ status: 'loaded', src: faviconUrls[0] })
|
||||
})
|
||||
|
||||
// Cleanup function
|
||||
return () => {
|
||||
controller.abort()
|
||||
}
|
||||
}, [hostname]) // Only depend on hostname
|
||||
|
||||
const handleError = () => {
|
||||
setFaviconState({ status: 'failed' })
|
||||
}
|
||||
|
||||
// Render based on current state
|
||||
if (faviconState.status === 'failed') {
|
||||
return <FaviconPlaceholder>{hostname.charAt(0).toUpperCase()}</FaviconPlaceholder>
|
||||
}
|
||||
|
||||
if (faviconState.status === 'loaded') {
|
||||
return <Favicon src={faviconState.src} alt={alt} onError={handleError} />
|
||||
}
|
||||
|
||||
return <FaviconLoading />
|
||||
}
|
||||
|
||||
const FaviconLoading = styled.div`
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 4px;
|
||||
background-color: var(--color-background-mute);
|
||||
`
|
||||
|
||||
const FaviconPlaceholder = styled.div`
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 4px;
|
||||
background-color: var(--color-primary-1);
|
||||
color: var(--color-primary-6);
|
||||
font-size: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
`
|
||||
const Favicon = styled.img`
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 4px;
|
||||
background-color: var(--color-background-mute);
|
||||
`
|
||||
|
||||
export default FallbackFavicon
|
||||
@ -1,4 +1,5 @@
|
||||
import { InfoCircleOutlined, SearchOutlined, SyncOutlined, TranslationOutlined } from '@ant-design/icons'
|
||||
import Favicon from '@renderer/components/Icons/FallbackFavicon'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import { getModelUniqId } from '@renderer/services/ModelService'
|
||||
import { Message, Model } from '@renderer/types'
|
||||
@ -135,7 +136,7 @@ const MessageContent: React.FC<Props> = ({ message: _message, model }) => {
|
||||
{message.metadata.tavily.results.map((result, index) => (
|
||||
<HStack key={result.url} style={{ alignItems: 'center', gap: 8 }}>
|
||||
<span style={{ fontSize: 13, color: 'var(--color-text-2)' }}>{index + 1}.</span>
|
||||
<Favicon src={`https://favicon.splitbee.io/?url=${new URL(result.url).hostname}`} alt={result.title} />
|
||||
<Favicon hostname={new URL(result.url).hostname} alt={result.title} />
|
||||
<CitationLink href={result.url} target="_blank" rel="noopener noreferrer">
|
||||
{result.title}
|
||||
</CitationLink>
|
||||
@ -214,11 +215,4 @@ const SearchingText = styled.div`
|
||||
color: var(--color-text-1);
|
||||
`
|
||||
|
||||
const Favicon = styled.img`
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 4px;
|
||||
background-color: var(--color-background-mute);
|
||||
`
|
||||
|
||||
export default React.memo(MessageContent)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user