mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-08 06:19:05 +08:00
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
f172a5464d
commit
a088023869
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 { InfoCircleOutlined, SearchOutlined, SyncOutlined, TranslationOutlined } from '@ant-design/icons'
|
||||||
|
import Favicon from '@renderer/components/Icons/FallbackFavicon'
|
||||||
import { HStack } from '@renderer/components/Layout'
|
import { HStack } from '@renderer/components/Layout'
|
||||||
import { getModelUniqId } from '@renderer/services/ModelService'
|
import { getModelUniqId } from '@renderer/services/ModelService'
|
||||||
import { Message, Model } from '@renderer/types'
|
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) => (
|
{message.metadata.tavily.results.map((result, index) => (
|
||||||
<HStack key={result.url} style={{ alignItems: 'center', gap: 8 }}>
|
<HStack key={result.url} style={{ alignItems: 'center', gap: 8 }}>
|
||||||
<span style={{ fontSize: 13, color: 'var(--color-text-2)' }}>{index + 1}.</span>
|
<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">
|
<CitationLink href={result.url} target="_blank" rel="noopener noreferrer">
|
||||||
{result.title}
|
{result.title}
|
||||||
</CitationLink>
|
</CitationLink>
|
||||||
@ -214,11 +215,4 @@ const SearchingText = styled.div`
|
|||||||
color: var(--color-text-1);
|
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)
|
export default React.memo(MessageContent)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user