feat: configurable favicon service URL (#659)

This commit is contained in:
Alex Gleason
2025-11-14 05:28:10 -03:00
committed by GitHub
parent e544c0a801
commit f8cca5522f
28 changed files with 151 additions and 20 deletions

15
package-lock.json generated
View File

@@ -75,6 +75,7 @@
"tailwind-merge": "^2.5.5",
"tailwindcss-animate": "^1.0.7",
"tippy.js": "^6.3.7",
"uri-templates": "^0.2.0",
"vaul": "^1.1.2",
"yet-another-react-lightbox": "^3.21.7",
"zod": "^3.24.1"
@@ -84,6 +85,7 @@
"@types/node": "^22.10.2",
"@types/react": "^18.3.17",
"@types/react-dom": "^18.3.5",
"@types/uri-templates": "^0.1.34",
"@vitejs/plugin-react": "^4.3.4",
"autoprefixer": "^10.4.20",
"eslint": "^9.17.0",
@@ -5422,6 +5424,13 @@
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="
},
"node_modules/@types/uri-templates": {
"version": "0.1.34",
"resolved": "https://registry.npmjs.org/@types/uri-templates/-/uri-templates-0.1.34.tgz",
"integrity": "sha512-13v4r/Op3iEO1y6FvEHQjrUNnrNyO67SigdFC9n80sVfsrM2AWJRNYbE1pBs4/p87I7z1J979JGeLAo3rM1L/Q==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/use-sync-external-store": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
@@ -12278,6 +12287,12 @@
"punycode": "^2.1.0"
}
},
"node_modules/uri-templates": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/uri-templates/-/uri-templates-0.2.0.tgz",
"integrity": "sha512-EWkjYEN0L6KOfEoOH6Wj4ghQqU7eBZMJqRHQnxQAq+dSEzRPClkWjf8557HkWQXF6BrAUoLSAyy9i3RVTliaNg==",
"license": "http://geraintluff.github.io/tv4/LICENSE.txt"
},
"node_modules/use-callback-ref": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz",

View File

@@ -85,6 +85,7 @@
"tailwind-merge": "^2.5.5",
"tailwindcss-animate": "^1.0.7",
"tippy.js": "^6.3.7",
"uri-templates": "^0.2.0",
"vaul": "^1.1.2",
"yet-another-react-lightbox": "^3.21.7",
"zod": "^3.24.1"
@@ -94,6 +95,7 @@
"@types/node": "^22.10.2",
"@types/react": "^18.3.17",
"@types/react-dom": "^18.3.5",
"@types/uri-templates": "^0.1.34",
"@vitejs/plugin-react": "^4.3.4",
"autoprefixer": "^10.4.20",
"eslint": "^9.17.0",

View File

@@ -1,4 +1,6 @@
import { faviconUrl } from '@/lib/faviconUrl'
import { cn } from '@/lib/utils'
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
import { useState } from 'react'
export function Favicon({
@@ -10,15 +12,18 @@ export function Favicon({
className?: string
fallback?: React.ReactNode
}) {
const { faviconUrlTemplate } = useContentPolicy()
const [loading, setLoading] = useState(true)
const [error, setError] = useState(false)
if (error) return fallback
const url = faviconUrl(faviconUrlTemplate, `https://${domain}`)
return (
<div className={cn('relative', className)}>
{loading && <div className={cn('absolute inset-0', className)}>{fallback}</div>}
<img
src={`https://${domain}/favicon.ico`}
src={url}
alt={domain}
className={cn('absolute inset-0', loading && 'opacity-0', className)}
onError={() => setError(true)}

View File

@@ -6,6 +6,7 @@ import {
toGeneralSettings,
toPostSettings,
toRelaySettings,
toSystemSettings,
toTranslation,
toWallet
} from '@/lib/link'
@@ -15,6 +16,7 @@ import { useNostr } from '@/providers/NostrProvider'
import {
Check,
ChevronRight,
Cog,
Copy,
Info,
KeyRound,
@@ -141,6 +143,13 @@ export default function Settings() {
</div>
</SettingItem>
</AboutInfoDialog>
<SettingItem className="clickable" onClick={() => push(toSystemSettings())}>
<div className="flex items-center gap-4">
<Cog />
<div>{t('System')}</div>
</div>
<ChevronRight />
</SettingItem>
<div className="px-4 mt-4">
<Donation />
</div>

View File

@@ -39,6 +39,7 @@ export const StorageKey = {
SIDEBAR_COLLAPSE: 'sidebarCollapse',
PRIMARY_COLOR: 'primaryColor',
ENABLE_SINGLE_COLUMN_LAYOUT: 'enableSingleColumnLayout',
FAVICON_URL_TEMPLATE: 'faviconUrlTemplate',
MEDIA_UPLOAD_SERVICE: 'mediaUploadService', // deprecated
HIDE_UNTRUSTED_EVENTS: 'hideUntrustedEvents', // deprecated
ACCOUNT_RELAY_LIST_EVENT_MAP: 'accountRelayListEventMap', // deprecated
@@ -130,6 +131,8 @@ export const DEFAULT_NOSTRCONNECT_RELAY = [
'wss://relay.primal.net/'
]
export const DEFAULT_FAVICON_URL_TEMPLATE = 'https://{hostname}/favicon.ico'
export const POLL_TYPE = {
MULTIPLE_CHOICE: 'multiplechoice',
SINGLE_CHOICE: 'singlechoice'

View File

@@ -531,6 +531,7 @@ export default {
Close: 'إغلاق',
'Failed to get invite code from relay': 'فشل الحصول على رمز الدعوة من المرحل',
'Failed to get invite code': 'فشل الحصول على رمز الدعوة',
'Invite code copied to clipboard': 'تم نسخ رمز الدعوة إلى الحافظة'
'Invite code copied to clipboard': 'تم نسخ رمز الدعوة إلى الحافظة',
'Favicon URL': 'رابط الأيقونة المفضلة'
}
}

View File

@@ -547,6 +547,7 @@ export default {
Close: 'Schließen',
'Failed to get invite code from relay': 'Fehler beim Abrufen des Einladungscodes vom Relay',
'Failed to get invite code': 'Fehler beim Abrufen des Einladungscodes',
'Invite code copied to clipboard': 'Einladungscode in die Zwischenablage kopiert'
'Invite code copied to clipboard': 'Einladungscode in die Zwischenablage kopiert',
'Favicon URL': 'Favicon-URL'
}
}

View File

@@ -532,6 +532,7 @@ export default {
Close: 'Close',
'Failed to get invite code from relay': 'Failed to get invite code from relay',
'Failed to get invite code': 'Failed to get invite code',
'Invite code copied to clipboard': 'Invite code copied to clipboard'
'Invite code copied to clipboard': 'Invite code copied to clipboard',
'Favicon URL': 'Favicon URL'
}
}

View File

@@ -541,6 +541,7 @@ export default {
Close: 'Cerrar',
'Failed to get invite code from relay': 'Error al obtener código de invitación del relay',
'Failed to get invite code': 'Error al obtener código de invitación',
'Invite code copied to clipboard': 'Código de invitación copiado al portapapeles'
'Invite code copied to clipboard': 'Código de invitación copiado al portapapeles',
'Favicon URL': 'URL del Favicon'
}
}

View File

@@ -536,6 +536,7 @@ export default {
Close: 'بستن',
'Failed to get invite code from relay': 'دریافت کد دعوت از رله ناموفق بود',
'Failed to get invite code': 'دریافت کد دعوت ناموفق بود',
'Invite code copied to clipboard': 'کد دعوت در کلیپ‌بورد کپی شد'
'Invite code copied to clipboard': 'کد دعوت در کلیپ‌بورد کپی شد',
'Favicon URL': 'آدرس نماد سایت'
}
}

View File

@@ -546,6 +546,7 @@ export default {
Close: 'Fermer',
'Failed to get invite code from relay': "Échec de l'obtention du code d'invitation du relay",
'Failed to get invite code': "Échec de l'obtention du code d'invitation",
'Invite code copied to clipboard': "Code d'invitation copié dans le presse-papiers"
'Invite code copied to clipboard': "Code d'invitation copié dans le presse-papiers",
'Favicon URL': 'URL du Favicon'
}
}

View File

@@ -538,6 +538,7 @@ export default {
Close: 'बंद करें',
'Failed to get invite code from relay': 'रिले से निमंत्रण कोड प्राप्त करने में विफल',
'Failed to get invite code': 'निमंत्रण कोड प्राप्त करने में विफल',
'Invite code copied to clipboard': 'निमंत्रण कोड क्लिपबोर्ड पर कॉपी किया गया'
'Invite code copied to clipboard': 'निमंत्रण कोड क्लिपबोर्ड पर कॉपी किया गया',
'Favicon URL': 'फ़ेविकॉन URL'
}
}

View File

@@ -533,6 +533,7 @@ export default {
Close: 'Bezárás',
'Failed to get invite code from relay': 'Nem sikerült lekérni a meghívókódot a relay-től',
'Failed to get invite code': 'Nem sikerült lekérni a meghívókódot',
'Invite code copied to clipboard': 'Meghívókód vágólapra másolva'
'Invite code copied to clipboard': 'Meghívókód vágólapra másolva',
'Favicon URL': 'Favicon URL'
}
}

View File

@@ -541,6 +541,7 @@ export default {
Close: 'Chiudi',
'Failed to get invite code from relay': 'Impossibile ottenere il codice di invito dal relay',
'Failed to get invite code': 'Impossibile ottenere il codice di invito',
'Invite code copied to clipboard': 'Codice di invito copiato negli appunti'
'Invite code copied to clipboard': 'Codice di invito copiato negli appunti',
'Favicon URL': 'URL Favicon'
}
}

View File

@@ -535,6 +535,7 @@ export default {
Close: '閉じる',
'Failed to get invite code from relay': 'リレーから招待コードの取得に失敗しました',
'Failed to get invite code': '招待コードの取得に失敗しました',
'Invite code copied to clipboard': '招待コードをクリップボードにコピーしました'
'Invite code copied to clipboard': '招待コードをクリップボードにコピーしました',
'Favicon URL': 'ファビコンURL'
}
}

View File

@@ -535,6 +535,7 @@ export default {
Close: '닫기',
'Failed to get invite code from relay': '릴레이에서 초대 코드 가져오기 실패',
'Failed to get invite code': '초대 코드 가져오기 실패',
'Invite code copied to clipboard': '초대 코드가 클립보드에 복사되었습니다'
'Invite code copied to clipboard': '초대 코드가 클립보드에 복사되었습니다',
'Favicon URL': '파비콘 URL'
}
}

View File

@@ -541,6 +541,7 @@ export default {
Close: 'Zamknij',
'Failed to get invite code from relay': 'Nie udało się uzyskać kodu zaproszenia z przekaźnika',
'Failed to get invite code': 'Nie udało się uzyskać kodu zaproszenia',
'Invite code copied to clipboard': 'Kod zaproszenia skopiowany do schowka'
'Invite code copied to clipboard': 'Kod zaproszenia skopiowany do schowka',
'Favicon URL': 'URL Favicon'
}
}

View File

@@ -538,6 +538,7 @@ export default {
Close: 'Fechar',
'Failed to get invite code from relay': 'Falha ao obter código de convite do relay',
'Failed to get invite code': 'Falha ao obter código de convite',
'Invite code copied to clipboard': 'Código de convite copiado para a área de transferência'
'Invite code copied to clipboard': 'Código de convite copiado para a área de transferência',
'Favicon URL': 'URL do Favicon'
}
}

View File

@@ -541,6 +541,7 @@ export default {
Close: 'Fechar',
'Failed to get invite code from relay': 'Falha ao obter código de convite do relay',
'Failed to get invite code': 'Falha ao obter código de convite',
'Invite code copied to clipboard': 'Código de convite copiado para a área de transferência'
'Invite code copied to clipboard': 'Código de convite copiado para a área de transferência',
'Favicon URL': 'URL do Favicon'
}
}

View File

@@ -543,6 +543,7 @@ export default {
Close: 'Закрыть',
'Failed to get invite code from relay': 'Не удалось получить код приглашения от релея',
'Failed to get invite code': 'Не удалось получить код приглашения',
'Invite code copied to clipboard': 'Код приглашения скопирован в буфер обмена'
'Invite code copied to clipboard': 'Код приглашения скопирован в буфер обмена',
'Favicon URL': 'URL фавикона'
}
}

View File

@@ -529,6 +529,7 @@ export default {
Close: 'ปิด',
'Failed to get invite code from relay': 'ไม่สามารถรับรหัสเชิญจากรีเลย์',
'Failed to get invite code': 'ไม่สามารถรับรหัสเชิญ',
'Invite code copied to clipboard': 'คัดลอกรหัสเชิญไปยังคลิปบอร์ดแล้ว'
'Invite code copied to clipboard': 'คัดลอกรหัสเชิญไปยังคลิปบอร์ดแล้ว',
'Favicon URL': 'URL ไอคอน'
}
}

View File

@@ -526,6 +526,7 @@ export default {
Close: '关闭',
'Failed to get invite code from relay': '从中继器获取邀请码失败',
'Failed to get invite code': '获取邀请码失败',
'Invite code copied to clipboard': '邀请码已复制到剪贴板'
'Invite code copied to clipboard': '邀请码已复制到剪贴板',
'Favicon URL': '网站图标 URL'
}
}

19
src/lib/faviconUrl.ts Normal file
View File

@@ -0,0 +1,19 @@
import UriTemplate from 'uri-templates'
export function faviconUrl(template: string, url: string | URL): string {
const u = new URL(url)
return UriTemplate(template).fill({
href: u.href,
origin: u.origin,
protocol: u.protocol,
username: u.username,
password: u.password,
host: u.host,
hostname: u.hostname,
port: u.port,
pathname: u.pathname,
hash: u.hash,
search: u.search
})
}

View File

@@ -72,6 +72,7 @@ export const toGeneralSettings = () => '/settings/general'
export const toAppearanceSettings = () => '/settings/appearance'
export const toTranslation = () => '/settings/translation'
export const toEmojiPackSettings = () => '/settings/emoji-packs'
export const toSystemSettings = () => '/settings/system'
export const toProfileEditor = () => '/profile-editor'
export const toRelay = (url: string) => `/relays/${encodeURIComponent(url)}`
export const toRelayReviews = (url: string) => `/relays/${encodeURIComponent(url)}/reviews`

View File

@@ -0,0 +1,33 @@
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { DEFAULT_FAVICON_URL_TEMPLATE } from '@/constants'
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
import { forwardRef } from 'react'
import { useTranslation } from 'react-i18next'
const SystemSettingsPage = forwardRef(({ index }: { index?: number }, ref) => {
const { t } = useTranslation()
const { faviconUrlTemplate, setFaviconUrlTemplate } = useContentPolicy()
return (
<SecondaryPageLayout ref={ref} index={index} title={t('System')}>
<div className="space-y-4 mt-3">
<div className="px-4 space-y-2">
<Label htmlFor="favicon-url" className="text-base font-normal">
{t('Favicon URL')}
</Label>
<Input
id="favicon-url"
type="text"
value={faviconUrlTemplate}
onChange={(e) => setFaviconUrlTemplate(e.target.value)}
placeholder={DEFAULT_FAVICON_URL_TEMPLATE}
/>
</div>
</div>
</SecondaryPageLayout>
)
})
SystemSettingsPage.displayName = 'SystemSettingsPage'
export default SystemSettingsPage

View File

@@ -16,6 +16,9 @@ type TContentPolicyContext = {
autoLoadMedia: boolean
mediaAutoLoadPolicy: TMediaAutoLoadPolicy
setMediaAutoLoadPolicy: (policy: TMediaAutoLoadPolicy) => void
faviconUrlTemplate: string
setFaviconUrlTemplate: (template: string) => void
}
const ContentPolicyContext = createContext<TContentPolicyContext | undefined>(undefined)
@@ -35,6 +38,7 @@ export function ContentPolicyProvider({ children }: { children: React.ReactNode
storage.getHideContentMentioningMutedUsers()
)
const [mediaAutoLoadPolicy, setMediaAutoLoadPolicy] = useState(storage.getMediaAutoLoadPolicy())
const [faviconUrlTemplate, setFaviconUrlTemplate] = useState(storage.getFaviconUrlTemplate())
const [connectionType, setConnectionType] = useState((navigator as any).connection?.type)
useEffect(() => {
@@ -83,6 +87,11 @@ export function ContentPolicyProvider({ children }: { children: React.ReactNode
setMediaAutoLoadPolicy(policy)
}
const updateFaviconUrlTemplate = (template: string) => {
storage.setFaviconUrlTemplate(template)
setFaviconUrlTemplate(template)
}
return (
<ContentPolicyContext.Provider
value={{
@@ -94,7 +103,9 @@ export function ContentPolicyProvider({ children }: { children: React.ReactNode
setHideContentMentioningMutedUsers: updateHideContentMentioningMutedUsers,
autoLoadMedia,
mediaAutoLoadPolicy,
setMediaAutoLoadPolicy: updateMediaAutoLoadPolicy
setMediaAutoLoadPolicy: updateMediaAutoLoadPolicy,
faviconUrlTemplate,
setFaviconUrlTemplate: updateFaviconUrlTemplate
}}
>
{children}

View File

@@ -17,6 +17,7 @@ import RelaySettingsPage from '@/pages/secondary/RelaySettingsPage'
import RizfulPage from '@/pages/secondary/RizfulPage'
import SearchPage from '@/pages/secondary/SearchPage'
import SettingsPage from '@/pages/secondary/SettingsPage'
import SystemSettingsPage from '@/pages/secondary/SystemSettingsPage'
import TranslationPage from '@/pages/secondary/TranslationPage'
import WalletPage from '@/pages/secondary/WalletPage'
import { match } from 'path-to-regexp'
@@ -41,6 +42,7 @@ const SECONDARY_ROUTE_CONFIGS = [
{ path: '/settings/appearance', element: <AppearanceSettingsPage /> },
{ path: '/settings/translation', element: <TranslationPage /> },
{ path: '/settings/emoji-packs', element: <EmojiPackSettingsPage /> },
{ path: '/settings/system', element: <SystemSettingsPage /> },
{ path: '/profile-editor', element: <ProfileEditorPage /> },
{ path: '/mutes', element: <MuteListPage /> },
{ path: '/rizful', element: <RizfulPage /> },

View File

@@ -1,4 +1,5 @@
import {
DEFAULT_FAVICON_URL_TEMPLATE,
DEFAULT_NIP_96_SERVICE,
ExtendedKind,
MEDIA_AUTO_LOAD_POLICY,
@@ -52,6 +53,7 @@ class LocalStorageService {
private sidebarCollapse: boolean = false
private primaryColor: TPrimaryColor = 'DEFAULT'
private enableSingleColumnLayout: boolean = true
private faviconUrlTemplate: string = DEFAULT_FAVICON_URL_TEMPLATE
constructor() {
if (!LocalStorageService.instance) {
@@ -205,6 +207,9 @@ class LocalStorageService {
this.enableSingleColumnLayout =
window.localStorage.getItem(StorageKey.ENABLE_SINGLE_COLUMN_LAYOUT) !== 'false'
this.faviconUrlTemplate =
window.localStorage.getItem(StorageKey.FAVICON_URL_TEMPLATE) ?? DEFAULT_FAVICON_URL_TEMPLATE
// Clean up deprecated data
window.localStorage.removeItem(StorageKey.ACCOUNT_PROFILE_EVENT_MAP)
window.localStorage.removeItem(StorageKey.ACCOUNT_FOLLOW_LIST_EVENT_MAP)
@@ -515,6 +520,15 @@ class LocalStorageService {
this.enableSingleColumnLayout = enable
window.localStorage.setItem(StorageKey.ENABLE_SINGLE_COLUMN_LAYOUT, enable.toString())
}
getFaviconUrlTemplate() {
return this.faviconUrlTemplate
}
setFaviconUrlTemplate(template: string) {
this.faviconUrlTemplate = template
window.localStorage.setItem(StorageKey.FAVICON_URL_TEMPLATE, template)
}
}
const instance = new LocalStorageService()