mirror of
https://github.com/dergigi/boris.git
synced 2026-02-16 12:34:41 +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.
236 lines
7.3 KiB
TypeScript
236 lines
7.3 KiB
TypeScript
import { useState, useEffect } from 'react'
|
|
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
|
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
|
import { faSpinner } from '@fortawesome/free-solid-svg-icons'
|
|
import { EventStoreProvider, AccountsProvider, Hooks } from 'applesauce-react'
|
|
import { EventStore } from 'applesauce-core'
|
|
import { AccountManager } from 'applesauce-accounts'
|
|
import { registerCommonAccountTypes } from 'applesauce-accounts/accounts'
|
|
import { RelayPool } from 'applesauce-relay'
|
|
import { createAddressLoader } from 'applesauce-loaders/loaders'
|
|
import Bookmarks from './components/Bookmarks'
|
|
import Toast from './components/Toast'
|
|
import { useToast } from './hooks/useToast'
|
|
import { useOnlineStatus } from './hooks/useOnlineStatus'
|
|
import { RELAYS } from './config/relays'
|
|
|
|
const DEFAULT_ARTICLE = import.meta.env.VITE_DEFAULT_ARTICLE_NADDR ||
|
|
'naddr1qvzqqqr4gupzqmjxss3dld622uu8q25gywum9qtg4w4cv4064jmg20xsac2aam5nqqxnzd3cxqmrzv3exgmr2wfesgsmew'
|
|
|
|
// AppRoutes component that has access to hooks
|
|
function AppRoutes({
|
|
relayPool,
|
|
showToast
|
|
}: {
|
|
relayPool: RelayPool
|
|
showToast: (message: string) => void
|
|
}) {
|
|
const accountManager = Hooks.useAccountManager()
|
|
|
|
const handleLogout = () => {
|
|
accountManager.clearActive()
|
|
showToast('Logged out successfully')
|
|
}
|
|
|
|
return (
|
|
<Routes>
|
|
<Route
|
|
path="/a/:naddr"
|
|
element={
|
|
<Bookmarks
|
|
relayPool={relayPool}
|
|
onLogout={handleLogout}
|
|
/>
|
|
}
|
|
/>
|
|
<Route
|
|
path="/r/*"
|
|
element={
|
|
<Bookmarks
|
|
relayPool={relayPool}
|
|
onLogout={handleLogout}
|
|
/>
|
|
}
|
|
/>
|
|
<Route
|
|
path="/settings"
|
|
element={
|
|
<Bookmarks
|
|
relayPool={relayPool}
|
|
onLogout={handleLogout}
|
|
/>
|
|
}
|
|
/>
|
|
<Route
|
|
path="/explore"
|
|
element={
|
|
<Bookmarks
|
|
relayPool={relayPool}
|
|
onLogout={handleLogout}
|
|
/>
|
|
}
|
|
/>
|
|
<Route
|
|
path="/me"
|
|
element={
|
|
<Bookmarks
|
|
relayPool={relayPool}
|
|
onLogout={handleLogout}
|
|
/>
|
|
}
|
|
/>
|
|
<Route path="/" element={<Navigate to={`/a/${DEFAULT_ARTICLE}`} replace />} />
|
|
</Routes>
|
|
)
|
|
}
|
|
|
|
function App() {
|
|
const [eventStore, setEventStore] = useState<EventStore | null>(null)
|
|
const [accountManager, setAccountManager] = useState<AccountManager | null>(null)
|
|
const [relayPool, setRelayPool] = useState<RelayPool | null>(null)
|
|
const { toastMessage, toastType, showToast, clearToast } = useToast()
|
|
const isOnline = useOnlineStatus()
|
|
|
|
useEffect(() => {
|
|
const initializeApp = async () => {
|
|
// Initialize event store, account manager, and relay pool
|
|
const store = new EventStore()
|
|
const accounts = new AccountManager()
|
|
|
|
// Register common account types (needed for deserialization)
|
|
registerCommonAccountTypes(accounts)
|
|
|
|
// Load persisted accounts from localStorage
|
|
try {
|
|
const json = JSON.parse(localStorage.getItem('accounts') || '[]')
|
|
await accounts.fromJSON(json)
|
|
console.log('Loaded', accounts.accounts.length, 'accounts from storage')
|
|
|
|
// Load active account from storage
|
|
const activeId = localStorage.getItem('active')
|
|
if (activeId && accounts.getAccount(activeId)) {
|
|
accounts.setActive(activeId)
|
|
console.log('Restored active account:', activeId)
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to load accounts from storage:', err)
|
|
}
|
|
|
|
// Subscribe to accounts changes and persist to localStorage
|
|
const accountsSub = accounts.accounts$.subscribe(() => {
|
|
localStorage.setItem('accounts', JSON.stringify(accounts.toJSON()))
|
|
})
|
|
|
|
// Subscribe to active account changes and persist to localStorage
|
|
const activeSub = accounts.active$.subscribe((account) => {
|
|
if (account) {
|
|
localStorage.setItem('active', account.id)
|
|
} else {
|
|
localStorage.removeItem('active')
|
|
}
|
|
})
|
|
|
|
const pool = new RelayPool()
|
|
|
|
// Create a relay group for better event deduplication and management
|
|
pool.group(RELAYS)
|
|
console.log('Created relay group with', RELAYS.length, 'relays (including local)')
|
|
console.log('Relay URLs:', RELAYS)
|
|
|
|
// Keep all relay connections alive indefinitely by creating a persistent subscription
|
|
// This prevents disconnection when no other subscriptions are active
|
|
// Create a minimal subscription that never completes to keep connections alive
|
|
const keepAliveSub = pool.subscription(RELAYS, { kinds: [0], limit: 0 }).subscribe({
|
|
next: () => {}, // No-op, we don't care about events
|
|
error: (err) => console.warn('Keep-alive subscription error:', err)
|
|
})
|
|
console.log('🔗 Created keep-alive subscription for', RELAYS.length, 'relay(s)')
|
|
|
|
// Store subscription for cleanup
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
;(pool as any)._keepAliveSubscription = keepAliveSub
|
|
|
|
// Attach address/replaceable loaders so ProfileModel can fetch profiles
|
|
const addressLoader = createAddressLoader(pool, {
|
|
eventStore: store,
|
|
lookupRelays: RELAYS
|
|
})
|
|
store.addressableLoader = addressLoader
|
|
store.replaceableLoader = addressLoader
|
|
|
|
setEventStore(store)
|
|
setAccountManager(accounts)
|
|
setRelayPool(pool)
|
|
|
|
// Cleanup function
|
|
return () => {
|
|
accountsSub.unsubscribe()
|
|
activeSub.unsubscribe()
|
|
// Clean up keep-alive subscription if it exists
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
if ((pool as any)._keepAliveSubscription) {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
(pool as any)._keepAliveSubscription.unsubscribe()
|
|
}
|
|
}
|
|
}
|
|
|
|
let cleanup: (() => void) | undefined
|
|
initializeApp().then((fn) => {
|
|
cleanup = fn
|
|
})
|
|
|
|
return () => {
|
|
if (cleanup) cleanup()
|
|
}
|
|
}, [])
|
|
|
|
// Monitor online/offline status
|
|
useEffect(() => {
|
|
if (!isOnline) {
|
|
showToast('You are offline. Some features may be limited.')
|
|
}
|
|
}, [isOnline, showToast])
|
|
|
|
// Listen for service worker updates
|
|
useEffect(() => {
|
|
const handleSWUpdate = () => {
|
|
showToast('New version available! Refresh to update.')
|
|
}
|
|
|
|
window.addEventListener('sw-update-available', handleSWUpdate)
|
|
return () => {
|
|
window.removeEventListener('sw-update-available', handleSWUpdate)
|
|
}
|
|
}, [showToast])
|
|
|
|
if (!eventStore || !accountManager || !relayPool) {
|
|
return (
|
|
<div className="loading">
|
|
<FontAwesomeIcon icon={faSpinner} spin />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<EventStoreProvider eventStore={eventStore}>
|
|
<AccountsProvider manager={accountManager}>
|
|
<BrowserRouter>
|
|
<div className="app">
|
|
<AppRoutes relayPool={relayPool} showToast={showToast} />
|
|
</div>
|
|
</BrowserRouter>
|
|
{toastMessage && (
|
|
<Toast
|
|
message={toastMessage}
|
|
type={toastType}
|
|
onClose={clearToast}
|
|
/>
|
|
)}
|
|
</AccountsProvider>
|
|
</EventStoreProvider>
|
|
)
|
|
}
|
|
|
|
export default App
|