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
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 151 additions and 20 deletions

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()