import { Button } from '@/components/ui/button' import logger from '@/lib/logger' import { useNostr } from '@/providers/NostrProvider' import { useEffect, useState, useMemo, useRef } from 'react' import { useTranslation } from 'react-i18next' import { Trash2, RefreshCw, Database, X, Terminal, XCircle } from 'lucide-react' import { Input } from '@/components/ui/input' import client from '@/services/client.service' import indexedDb from '@/services/indexed-db.service' import postEditorCache from '@/services/post-editor-cache.service' import { StorageKey } from '@/constants' import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog' import { Drawer, DrawerContent, DrawerHeader, DrawerTitle, DrawerDescription } from '@/components/ui/drawer' import { useScreenSize } from '@/providers/ScreenSizeProvider' import { toast } from 'sonner' import { syncUserDeletionTombstones } from '@/lib/sync-user-deletions' import { useCacheBrowser } from '../../contexts/cache-browser-context' export default function InBrowserCacheSetting() { const { t } = useTranslation() const { isSmallScreen } = useScreenSize() const { pubkey, relayList, requestAccountNetworkHydrate } = useNostr() const { openBrowseCache } = useCacheBrowser() const [consoleLogs, setConsoleLogs] = useState; timestamp: number }>>([]) const [showConsoleLogs, setShowConsoleLogs] = useState(false) const [consoleLogSearch, setConsoleLogSearch] = useState('') const [consoleLogLevel, setConsoleLogLevel] = useState<'errors-warnings' | 'all'>('all') const [cacheRefreshBusy, setCacheRefreshBusy] = useState(false) const consoleLogRef = useRef; timestamp: number }>>([]) const handleClearCache = async () => { if (!confirm(t('Are you sure you want to clear all cached data? This will delete all stored events and settings from your browser.'))) { return } try { await indexedDb.clearAllCache() await indexedDb.clearPiperTtsCache() const cacheKeys = Object.values(StorageKey).filter(key => key.includes('CACHE') || key.includes('EVENT') || key.includes('FEED') || key.includes('NOTIFICATION') ) cacheKeys.forEach(key => { try { window.localStorage.removeItem(key) } catch (e) { logger.warn(`Failed to remove ${key} from localStorage`, e as Error) } }) if ('caches' in window) { try { const cacheNames = await caches.keys() const currentOrigin = window.location.origin const appCacheNames = [ 'nostr-images', 'satellite-images', 'external-images' ] const appCaches = cacheNames.filter(name => { if (appCacheNames.includes(name)) return true if (name.startsWith('workbox-') || name.startsWith('precache-')) return true if (name.includes(currentOrigin.replace(/https?:\/\//, '').split('/')[0])) return true return false }) await Promise.all(appCaches.map(name => caches.delete(name).catch(error => { logger.warn(`Failed to delete cache: ${name}`, { error }) }))) } catch (error) { logger.warn('Failed to clear some service worker caches', { error }) } } postEditorCache.clearAllPostCaches() client.clearInMemoryCaches() toast.success(t('Cache cleared successfully')) setTimeout(() => window.location.reload(), 1500) } catch (error) { logger.error('Failed to clear cache', { error }) toast.error(t('Failed to clear cache')) } } const handleRefreshCache = async () => { try { setCacheRefreshBusy(true) await indexedDb.forceDatabaseUpgrade() if (pubkey) { await requestAccountNetworkHydrate() await syncUserDeletionTombstones(pubkey, relayList) } toast.success(t('Cache refreshed successfully')) } catch (error) { logger.error('Failed to refresh cache', { error }) toast.error(t('Failed to refresh cache')) } finally { setCacheRefreshBusy(false) } } const handleClearServiceWorker = async () => { if (!confirm(t('Are you sure you want to unregister the service worker? This will clear this app\'s service worker caches and you will need to reload the page.'))) { return } try { const currentOrigin = window.location.origin let unregisteredCount = 0 let cacheClearedCount = 0 if (window.isSecureContext && 'serviceWorker' in navigator) { let registrations: readonly ServiceWorkerRegistration[] = [] try { registrations = await navigator.serviceWorker.getRegistrations() } catch (error) { logger.warn('Failed to get service worker registrations', { error }) } if (registrations.length > 0) { const unregisterPromises = registrations.map(async (registration) => { try { const scope = registration.scope if (scope.startsWith(currentOrigin)) { const result = await registration.unregister() if (result) unregisteredCount++ return result } return false } catch (error) { logger.warn('Failed to unregister a service worker', { error }) return false } }) await Promise.all(unregisterPromises) } } if ('caches' in window) { try { const cacheNames = await caches.keys() const appCacheNames = [ 'nostr-images', 'satellite-images', 'external-images' ] const appCaches = cacheNames.filter(name => { if (appCacheNames.includes(name)) return true if (name.startsWith('workbox-') || name.startsWith('precache-')) return true if (name.includes(currentOrigin.replace(/https?:\/\//, '').split('/')[0])) return true return false }) await Promise.all(appCaches.map(name => { cacheClearedCount++ return caches.delete(name).catch(error => { logger.warn(`Failed to delete cache: ${name}`, { error }) cacheClearedCount-- }) })) } catch (error) { logger.warn('Failed to clear some caches', { error }) } } if (unregisteredCount > 0 || cacheClearedCount > 0) { const message = unregisteredCount > 0 && cacheClearedCount > 0 ? t('Service worker unregistered and caches cleared. Please reload the page.') : unregisteredCount > 0 ? t('Service worker unregistered. Please reload the page.') : t('Service worker caches cleared. Please reload the page.') toast.success(message) setTimeout(() => window.location.reload(), 1000) } else { toast.info(t('No service workers or caches found for this app')) } } catch (error) { logger.error('Failed to unregister service worker', { error }) toast.error(t('Failed to unregister service worker: ') + (error instanceof Error ? error.message : String(error))) } } useEffect(() => { const originalLog = console.log const originalError = console.error const originalWarn = console.warn const originalInfo = console.info const captureLog = (type: string, ...args: any[]) => { let message = '' let formattedParts: Array<{ text: string; style?: string }> = [] if (args.length > 0 && typeof args[0] === 'string' && args[0].includes('%c')) { const formatString = args[0] const parts = formatString.split(/%c/g) formattedParts = [] for (let i = 0; i < parts.length; i++) { const text = parts[i] const style = i < args.length - 1 && typeof args[i + 1] === 'string' ? args[i + 1] : undefined formattedParts.push({ text, style }) } const remainingArgs = args.slice(parts.length) if (remainingArgs.length > 0) { const remainingText = remainingArgs.map(arg => { if (typeof arg === 'object') { try { return JSON.stringify(arg, null, 2) } catch { return String(arg) } } return String(arg) }).join(' ') if (formattedParts.length > 0) { formattedParts[formattedParts.length - 1].text += ' ' + remainingText } else { formattedParts.push({ text: remainingText }) } } message = formattedParts.map(p => p.text).join('') } else { message = args.map(arg => { if (typeof arg === 'object') { try { return JSON.stringify(arg, null, 2) } catch { return String(arg) } } return String(arg) }).join(' ') formattedParts = [{ text: message }] } const logEntry = { type, message, formattedParts, timestamp: Date.now() } consoleLogRef.current.push(logEntry) if (consoleLogRef.current.length > 1000) { consoleLogRef.current = consoleLogRef.current.slice(-1000) } if (showConsoleLogs) { setConsoleLogs([...consoleLogRef.current]) } } console.log = (...args: any[]) => { captureLog('log', ...args); originalLog.apply(console, args) } console.error = (...args: any[]) => { captureLog('error', ...args); originalError.apply(console, args) } console.warn = (...args: any[]) => { captureLog('warn', ...args); originalWarn.apply(console, args) } console.info = (...args: any[]) => { captureLog('info', ...args); originalInfo.apply(console, args) } return () => { console.log = originalLog console.error = originalError console.warn = originalWarn console.info = originalInfo } }, [showConsoleLogs]) const handleShowConsoleLogs = () => { setConsoleLogs([...consoleLogRef.current]) setShowConsoleLogs(true) setConsoleLogSearch('') setConsoleLogLevel('all') } const handleClearConsoleLogs = () => { consoleLogRef.current = [] setConsoleLogs([]) toast.success(t('Console logs cleared')) } const filteredConsoleLogs = useMemo(() => { let filtered = [...consoleLogs] if (consoleLogLevel === 'errors-warnings') { filtered = filtered.filter(log => log.type === 'error' || log.type === 'warn') } if (consoleLogSearch.trim()) { const query = consoleLogSearch.toLowerCase().trim() filtered = filtered.filter(log => log.message.toLowerCase().includes(query) || log.type.toLowerCase().includes(query) ) } return filtered }, [consoleLogs, consoleLogSearch, consoleLogLevel]) const renderConsoleLogList = () => filteredConsoleLogs.length === 0 ? (
{consoleLogs.length === 0 ? t('No console logs captured yet') : t('No logs match the current filters') }
) : ( filteredConsoleLogs.map((log, index) => (
{new Date(log.timestamp).toLocaleTimeString()} [{log.type}]
              {log.formattedParts ? (
                log.formattedParts.map((part, i) => {
                  if (part.style) {
                    const styleObj: Record = {}
                    part.style.split(';').forEach(rule => {
                      const trimmed = rule.trim()
                      if (trimmed) {
                        const colonIndex = trimmed.indexOf(':')
                        if (colonIndex > 0) {
                          const key = trimmed.substring(0, colonIndex).trim()
                          const value = trimmed.substring(colonIndex + 1).trim()
                          const camelKey = key.replace(/-([a-z])/g, (_, c) => c.toUpperCase())
                          styleObj[camelKey] = value
                        }
                      }
                    })
                    return {part.text}
                  }
                  return {part.text}
                })
              ) : (
                log.message
              )}
            
)) ) const consoleLogFilters = (
setConsoleLogSearch(e.target.value)} className="min-w-0 flex-1 basis-[min(100%,12rem)]" />
) return (
{t('Clear cached data stored in your browser, including IndexedDB events, localStorage settings, and service worker caches.')}
{t('refreshCacheButtonExplainer', { defaultValue: 'Refresh Cache runs an IndexedDB upgrade check, re-fetches your relay lists and profile-related events from the network (same work as the automatic startup sync), syncs kind-5 deletions into tombstones and removes deleted items from the local cache, then refreshes the store counts below.' })}
{isSmallScreen ? (
{t('Console Logs')} {t('View recent console logs for debugging')} ({filteredConsoleLogs.length} / {consoleLogs.length} {t('entries')})
{consoleLogFilters}
{renderConsoleLogList()}
) : (
{t('Console Logs')} {t('View recent console logs for debugging')} ({filteredConsoleLogs.length} / {consoleLogs.length} {t('entries')})
{consoleLogFilters}
{renderConsoleLogList()}
)}
) }