mirror of
https://github.com/dergigi/boris.git
synced 2026-01-08 09:24:42 +01:00
- Add web app manifest with proper metadata and icon support - Configure vite-plugin-pwa with injectManifest strategy - Migrate service worker to Workbox with precaching and runtime caching - Add runtime caching for cross-origin images (preserves existing behavior) - Add runtime caching for cross-origin article HTML for offline reading - Create PWA install hook and UI component in settings - Add online/offline status monitoring and toast notifications - Add service worker update notifications - Add placeholder PWA icons (192x192, 512x512, maskable variants) - Update HTML with manifest link and theme-color meta tag - Preserve existing relay/airplane mode functionality (WebSockets not intercepted) The app now passes PWA installability criteria while maintaining all existing offline functionality. Icons should be replaced with proper branded designs.
175 lines
6.2 KiB
TypeScript
175 lines
6.2 KiB
TypeScript
import React, { useState, useEffect, useRef } from 'react'
|
|
import { faTimes, faUndo } from '@fortawesome/free-solid-svg-icons'
|
|
import { RelayPool } from 'applesauce-relay'
|
|
import { UserSettings } from '../services/settingsService'
|
|
import IconButton from './IconButton'
|
|
import { loadFont } from '../utils/fontLoader'
|
|
import ReadingDisplaySettings from './Settings/ReadingDisplaySettings'
|
|
import LayoutNavigationSettings from './Settings/LayoutNavigationSettings'
|
|
import StartupPreferencesSettings from './Settings/StartupPreferencesSettings'
|
|
import ZapSettings from './Settings/ZapSettings'
|
|
import OfflineModeSettings from './Settings/OfflineModeSettings'
|
|
import RelaySettings from './Settings/RelaySettings'
|
|
import PWASettings from './Settings/PWASettings'
|
|
import { useRelayStatus } from '../hooks/useRelayStatus'
|
|
|
|
const DEFAULT_SETTINGS: UserSettings = {
|
|
collapseOnArticleOpen: true,
|
|
defaultViewMode: 'compact',
|
|
showHighlights: true,
|
|
sidebarCollapsed: true,
|
|
highlightsCollapsed: true,
|
|
readingFont: 'source-serif-4',
|
|
fontSize: 21,
|
|
highlightStyle: 'marker',
|
|
highlightColor: '#ffff00',
|
|
highlightColorNostrverse: '#9333ea',
|
|
highlightColorFriends: '#f97316',
|
|
highlightColorMine: '#ffff00',
|
|
defaultHighlightVisibilityNostrverse: true,
|
|
defaultHighlightVisibilityFriends: true,
|
|
defaultHighlightVisibilityMine: true,
|
|
zapSplitHighlighterWeight: 50,
|
|
zapSplitBorisWeight: 2.1,
|
|
zapSplitAuthorWeight: 50,
|
|
useLocalRelayAsCache: true,
|
|
rebroadcastToAllRelays: false,
|
|
}
|
|
|
|
interface SettingsProps {
|
|
settings: UserSettings
|
|
onSave: (settings: UserSettings) => Promise<void>
|
|
onClose: () => void
|
|
relayPool: RelayPool | null
|
|
}
|
|
|
|
const Settings: React.FC<SettingsProps> = ({ settings, onSave, onClose, relayPool }) => {
|
|
const [localSettings, setLocalSettings] = useState<UserSettings>(() => {
|
|
// Migrate old settings format to new weight-based format
|
|
const migrated = { ...settings }
|
|
const anySettings = migrated as Record<string, unknown>
|
|
if ('zapSplitPercentage' in anySettings && !('zapSplitHighlighterWeight' in migrated)) {
|
|
migrated.zapSplitHighlighterWeight = (anySettings.zapSplitPercentage as number) ?? 50
|
|
migrated.zapSplitAuthorWeight = 100 - ((anySettings.zapSplitPercentage as number) ?? 50)
|
|
}
|
|
if ('borisSupportPercentage' in anySettings && !('zapSplitBorisWeight' in migrated)) {
|
|
migrated.zapSplitBorisWeight = (anySettings.borisSupportPercentage as number) ?? 2.1
|
|
}
|
|
return migrated
|
|
})
|
|
const isInitialMount = useRef(true)
|
|
const saveTimeoutRef = useRef<number | null>(null)
|
|
const isLocallyUpdating = useRef(false)
|
|
|
|
// Poll for relay status updates
|
|
const relayStatuses = useRelayStatus({ relayPool })
|
|
|
|
useEffect(() => {
|
|
// Don't update from external settings if we're currently making local changes
|
|
if (isLocallyUpdating.current) {
|
|
return
|
|
}
|
|
|
|
const migrated = { ...settings }
|
|
const anySettings = migrated as Record<string, unknown>
|
|
if ('zapSplitPercentage' in anySettings && !('zapSplitHighlighterWeight' in migrated)) {
|
|
migrated.zapSplitHighlighterWeight = (anySettings.zapSplitPercentage as number) ?? 50
|
|
migrated.zapSplitAuthorWeight = 100 - ((anySettings.zapSplitPercentage as number) ?? 50)
|
|
}
|
|
if ('borisSupportPercentage' in anySettings && !('zapSplitBorisWeight' in migrated)) {
|
|
migrated.zapSplitBorisWeight = (anySettings.borisSupportPercentage as number) ?? 2.1
|
|
}
|
|
setLocalSettings(migrated)
|
|
}, [settings])
|
|
|
|
useEffect(() => {
|
|
// Preload all fonts for the dropdown
|
|
const fonts = ['inter', 'lora', 'merriweather', 'open-sans', 'roboto', 'source-serif-4', 'crimson-text', 'libre-baskerville', 'pt-serif']
|
|
fonts.forEach(font => {
|
|
loadFont(font).catch(err => console.warn('Failed to preload font:', font, err))
|
|
})
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
const fontToLoad = localSettings.readingFont || 'source-serif-4'
|
|
loadFont(fontToLoad).catch(err => console.warn('Failed to load preview font:', fontToLoad, err))
|
|
}, [localSettings.readingFont])
|
|
|
|
useEffect(() => {
|
|
if (isInitialMount.current) {
|
|
isInitialMount.current = false
|
|
return
|
|
}
|
|
|
|
// Mark that we're making local updates
|
|
isLocallyUpdating.current = true
|
|
|
|
// Clear any pending save
|
|
if (saveTimeoutRef.current) {
|
|
clearTimeout(saveTimeoutRef.current)
|
|
}
|
|
|
|
// Debounce the save to avoid rapid updates
|
|
saveTimeoutRef.current = setTimeout(() => {
|
|
onSave(localSettings).finally(() => {
|
|
// Allow external updates again after a short delay
|
|
setTimeout(() => {
|
|
isLocallyUpdating.current = false
|
|
}, 500)
|
|
})
|
|
}, 300)
|
|
|
|
return () => {
|
|
if (saveTimeoutRef.current) {
|
|
clearTimeout(saveTimeoutRef.current)
|
|
}
|
|
}
|
|
}, [localSettings, onSave])
|
|
|
|
const handleResetToDefaults = () => {
|
|
if (confirm('Reset all settings to defaults?')) {
|
|
setLocalSettings(DEFAULT_SETTINGS)
|
|
}
|
|
}
|
|
|
|
const handleUpdate = (updates: Partial<UserSettings>) => {
|
|
setLocalSettings({ ...localSettings, ...updates })
|
|
}
|
|
|
|
return (
|
|
<div className="settings-view">
|
|
<div className="settings-header">
|
|
<h2>Settings</h2>
|
|
<div className="settings-header-actions">
|
|
<IconButton
|
|
icon={faUndo}
|
|
onClick={handleResetToDefaults}
|
|
title="Reset to defaults"
|
|
ariaLabel="Reset to defaults"
|
|
variant="ghost"
|
|
/>
|
|
<IconButton
|
|
icon={faTimes}
|
|
onClick={onClose}
|
|
title="Close settings"
|
|
ariaLabel="Close settings"
|
|
variant="ghost"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="settings-content">
|
|
<ReadingDisplaySettings settings={localSettings} onUpdate={handleUpdate} />
|
|
<LayoutNavigationSettings settings={localSettings} onUpdate={handleUpdate} />
|
|
<StartupPreferencesSettings settings={localSettings} onUpdate={handleUpdate} />
|
|
<ZapSettings settings={localSettings} onUpdate={handleUpdate} />
|
|
<OfflineModeSettings settings={localSettings} onUpdate={handleUpdate} onClose={onClose} />
|
|
<RelaySettings relayStatuses={relayStatuses} onClose={onClose} />
|
|
<PWASettings />
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default Settings
|