diff --git a/package-lock.json b/package-lock.json index 7a5a448..d771e73 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index ee6e1ea..7b01ce2 100644 --- a/package.json +++ b/package.json @@ -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", @@ -108,4 +110,4 @@ "vite": "^6.0.3", "vite-plugin-pwa": "^0.21.1" } -} \ No newline at end of file +} diff --git a/src/components/Favicon/index.tsx b/src/components/Favicon/index.tsx index 0dfe366..0fafecc 100644 --- a/src/components/Favicon/index.tsx +++ b/src/components/Favicon/index.tsx @@ -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 (
{loading &&
{fallback}
} {domain} setError(true)} diff --git a/src/components/Settings/index.tsx b/src/components/Settings/index.tsx index cc13475..7943a50 100644 --- a/src/components/Settings/index.tsx +++ b/src/components/Settings/index.tsx @@ -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() {
+ push(toSystemSettings())}> +
+ +
{t('System')}
+
+ +
diff --git a/src/constants.ts b/src/constants.ts index 0a951b0..40076a9 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -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' diff --git a/src/i18n/locales/ar.ts b/src/i18n/locales/ar.ts index 4517114..f866f4f 100644 --- a/src/i18n/locales/ar.ts +++ b/src/i18n/locales/ar.ts @@ -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': 'رابط الأيقونة المفضلة' } } diff --git a/src/i18n/locales/de.ts b/src/i18n/locales/de.ts index 919314e..ca8720a 100644 --- a/src/i18n/locales/de.ts +++ b/src/i18n/locales/de.ts @@ -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' } } diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index bd1d9e4..6b6f93c 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -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' } } diff --git a/src/i18n/locales/es.ts b/src/i18n/locales/es.ts index e92161d..421288e 100644 --- a/src/i18n/locales/es.ts +++ b/src/i18n/locales/es.ts @@ -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' } } diff --git a/src/i18n/locales/fa.ts b/src/i18n/locales/fa.ts index 562666e..7762659 100644 --- a/src/i18n/locales/fa.ts +++ b/src/i18n/locales/fa.ts @@ -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': 'آدرس نماد سایت' } } diff --git a/src/i18n/locales/fr.ts b/src/i18n/locales/fr.ts index 9867ef6..b1ef862 100644 --- a/src/i18n/locales/fr.ts +++ b/src/i18n/locales/fr.ts @@ -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' } } diff --git a/src/i18n/locales/hi.ts b/src/i18n/locales/hi.ts index 2c8ff3e..0e70d60 100644 --- a/src/i18n/locales/hi.ts +++ b/src/i18n/locales/hi.ts @@ -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' } } diff --git a/src/i18n/locales/hu.ts b/src/i18n/locales/hu.ts index 1621b09..dd4457e 100644 --- a/src/i18n/locales/hu.ts +++ b/src/i18n/locales/hu.ts @@ -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' } } diff --git a/src/i18n/locales/it.ts b/src/i18n/locales/it.ts index a040524..10ac08f 100644 --- a/src/i18n/locales/it.ts +++ b/src/i18n/locales/it.ts @@ -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' } } diff --git a/src/i18n/locales/ja.ts b/src/i18n/locales/ja.ts index fd8b3ba..eed9853 100644 --- a/src/i18n/locales/ja.ts +++ b/src/i18n/locales/ja.ts @@ -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' } } diff --git a/src/i18n/locales/ko.ts b/src/i18n/locales/ko.ts index 361218a..499f7f9 100644 --- a/src/i18n/locales/ko.ts +++ b/src/i18n/locales/ko.ts @@ -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' } } diff --git a/src/i18n/locales/pl.ts b/src/i18n/locales/pl.ts index a213795..1181c03 100644 --- a/src/i18n/locales/pl.ts +++ b/src/i18n/locales/pl.ts @@ -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' } } diff --git a/src/i18n/locales/pt-BR.ts b/src/i18n/locales/pt-BR.ts index 42695f0..881f229 100644 --- a/src/i18n/locales/pt-BR.ts +++ b/src/i18n/locales/pt-BR.ts @@ -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' } } diff --git a/src/i18n/locales/pt-PT.ts b/src/i18n/locales/pt-PT.ts index e962e12..f643deb 100644 --- a/src/i18n/locales/pt-PT.ts +++ b/src/i18n/locales/pt-PT.ts @@ -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' } } diff --git a/src/i18n/locales/ru.ts b/src/i18n/locales/ru.ts index 401059d..b530d18 100644 --- a/src/i18n/locales/ru.ts +++ b/src/i18n/locales/ru.ts @@ -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 фавикона' } } diff --git a/src/i18n/locales/th.ts b/src/i18n/locales/th.ts index 5724bd3..d9502e5 100644 --- a/src/i18n/locales/th.ts +++ b/src/i18n/locales/th.ts @@ -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 ไอคอน' } } diff --git a/src/i18n/locales/zh.ts b/src/i18n/locales/zh.ts index b3925c2..7e2bfd0 100644 --- a/src/i18n/locales/zh.ts +++ b/src/i18n/locales/zh.ts @@ -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' } } diff --git a/src/lib/faviconUrl.ts b/src/lib/faviconUrl.ts new file mode 100644 index 0000000..ef69c3e --- /dev/null +++ b/src/lib/faviconUrl.ts @@ -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 + }) +} diff --git a/src/lib/link.ts b/src/lib/link.ts index 473fb47..8c1a2af 100644 --- a/src/lib/link.ts +++ b/src/lib/link.ts @@ -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` diff --git a/src/pages/secondary/SystemSettingsPage/index.tsx b/src/pages/secondary/SystemSettingsPage/index.tsx new file mode 100644 index 0000000..9a71add --- /dev/null +++ b/src/pages/secondary/SystemSettingsPage/index.tsx @@ -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 ( + +
+
+ + setFaviconUrlTemplate(e.target.value)} + placeholder={DEFAULT_FAVICON_URL_TEMPLATE} + /> +
+
+
+ ) +}) +SystemSettingsPage.displayName = 'SystemSettingsPage' +export default SystemSettingsPage diff --git a/src/providers/ContentPolicyProvider.tsx b/src/providers/ContentPolicyProvider.tsx index a51a4a3..3c25a43 100644 --- a/src/providers/ContentPolicyProvider.tsx +++ b/src/providers/ContentPolicyProvider.tsx @@ -16,6 +16,9 @@ type TContentPolicyContext = { autoLoadMedia: boolean mediaAutoLoadPolicy: TMediaAutoLoadPolicy setMediaAutoLoadPolicy: (policy: TMediaAutoLoadPolicy) => void + + faviconUrlTemplate: string + setFaviconUrlTemplate: (template: string) => void } const ContentPolicyContext = createContext(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 ( {children} diff --git a/src/routes/secondary.tsx b/src/routes/secondary.tsx index eb3283e..af788ee 100644 --- a/src/routes/secondary.tsx +++ b/src/routes/secondary.tsx @@ -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: }, { path: '/settings/translation', element: }, { path: '/settings/emoji-packs', element: }, + { path: '/settings/system', element: }, { path: '/profile-editor', element: }, { path: '/mutes', element: }, { path: '/rizful', element: }, diff --git a/src/services/local-storage.service.ts b/src/services/local-storage.service.ts index 2075916..a3d2e11 100644 --- a/src/services/local-storage.service.ts +++ b/src/services/local-storage.service.ts @@ -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()