347 lines
11 KiB
TypeScript
347 lines
11 KiB
TypeScript
import { useState, useEffect, useCallback } from 'react'
|
||
import { nostrAuthService } from '@/lib/nostrAuth'
|
||
import { getLastSyncDate, setLastSyncDate as setLastSyncDateStorage, getCurrentTimestamp, calculateDaysBetween } from '@/lib/syncStorage'
|
||
import { MIN_EVENT_DATE } from '@/lib/platformConfig'
|
||
import { objectCache } from '@/lib/objectCache'
|
||
import { t } from '@/lib/i18n'
|
||
import { useSyncProgress } from '@/lib/hooks/useSyncProgress'
|
||
|
||
export function SyncProgressBar(): React.ReactElement | null {
|
||
const [lastSyncDate, setLastSyncDate] = useState<number | null>(null)
|
||
const [totalDays, setTotalDays] = useState<number>(0)
|
||
const [isInitialized, setIsInitialized] = useState(false)
|
||
const [connectionState, setConnectionState] = useState<{ connected: boolean; pubkey: string | null }>({ connected: false, pubkey: null })
|
||
const [error, setError] = useState<string | null>(null)
|
||
|
||
const loadSyncStatus = useCallback(async (): Promise<void> => {
|
||
try {
|
||
const state = nostrAuthService.getState()
|
||
if (!state.connected || !state.pubkey) {
|
||
return
|
||
}
|
||
|
||
const storedLastSyncDate = await getLastSyncDate()
|
||
const currentTimestamp = getCurrentTimestamp()
|
||
const days = calculateDaysBetween(storedLastSyncDate, currentTimestamp)
|
||
|
||
setLastSyncDate(storedLastSyncDate)
|
||
setTotalDays(days)
|
||
} catch (loadError) {
|
||
console.error('Error loading sync status:', loadError)
|
||
}
|
||
}, [])
|
||
|
||
const { syncProgress, isSyncing, startMonitoring, stopMonitoring } = useSyncProgress({
|
||
onComplete: loadSyncStatus,
|
||
})
|
||
|
||
useEffect(() => {
|
||
// Check connection state
|
||
const checkConnection = (): void => {
|
||
const state = nostrAuthService.getState()
|
||
console.warn('[SyncProgressBar] Initial connection check:', { connected: state.connected, pubkey: state.pubkey })
|
||
setConnectionState({ connected: state.connected ?? false, pubkey: state.pubkey ?? null })
|
||
setIsInitialized(true)
|
||
}
|
||
|
||
// Initial check
|
||
checkConnection()
|
||
|
||
// Listen to connection changes
|
||
const unsubscribe = nostrAuthService.subscribe((state) => {
|
||
console.warn('[SyncProgressBar] Connection state changed:', { connected: state.connected, pubkey: state.pubkey })
|
||
setConnectionState({ connected: state.connected ?? false, pubkey: state.pubkey ?? null })
|
||
})
|
||
|
||
return () => {
|
||
unsubscribe()
|
||
}
|
||
}, [])
|
||
|
||
useEffect(() => {
|
||
console.warn('[SyncProgressBar] Effect triggered:', { isInitialized, connected: connectionState.connected, pubkey: connectionState.pubkey, isSyncing })
|
||
|
||
if (!isInitialized) {
|
||
console.warn('[SyncProgressBar] Not initialized yet')
|
||
return
|
||
}
|
||
|
||
if (!connectionState.connected) {
|
||
console.warn('[SyncProgressBar] Not connected')
|
||
return
|
||
}
|
||
|
||
if (!connectionState.pubkey) {
|
||
console.warn('[SyncProgressBar] No pubkey')
|
||
return
|
||
}
|
||
|
||
void runAutoSyncCheck({
|
||
connection: { connected: connectionState.connected, pubkey: connectionState.pubkey },
|
||
isSyncing,
|
||
loadSyncStatus,
|
||
startMonitoring,
|
||
stopMonitoring,
|
||
setError,
|
||
})
|
||
}, [isInitialized, connectionState.connected, connectionState.pubkey, isSyncing, loadSyncStatus, startMonitoring, stopMonitoring])
|
||
|
||
async function resynchronize(): Promise<void> {
|
||
try {
|
||
const state = nostrAuthService.getState()
|
||
if (!state.connected || !state.pubkey) {
|
||
return
|
||
}
|
||
|
||
// Clear cache for user content (but keep other data)
|
||
await Promise.all([
|
||
objectCache.clear('author'),
|
||
objectCache.clear('series'),
|
||
objectCache.clear('publication'),
|
||
objectCache.clear('review'),
|
||
objectCache.clear('purchase'),
|
||
objectCache.clear('sponsoring'),
|
||
objectCache.clear('review_tip'),
|
||
])
|
||
|
||
// Reset last sync date to force full resync
|
||
await setLastSyncDateStorage(MIN_EVENT_DATE)
|
||
|
||
// Reload sync status
|
||
await loadSyncStatus()
|
||
|
||
// Start full resynchronization via Service Worker
|
||
if (state.pubkey !== null) {
|
||
const { swClient } = await import('@/lib/swClient')
|
||
const isReady = await swClient.isReady()
|
||
if (isReady) {
|
||
await swClient.startUserSync(state.pubkey)
|
||
startMonitoring()
|
||
} else {
|
||
stopMonitoring()
|
||
}
|
||
}
|
||
} catch (resyncError) {
|
||
console.error('Error resynchronizing:', resyncError)
|
||
stopMonitoring()
|
||
}
|
||
}
|
||
|
||
// Don't show if not initialized or not connected
|
||
if (!isInitialized || !connectionState.connected || !connectionState.pubkey) {
|
||
return null
|
||
}
|
||
|
||
// Check if sync is recently completed (within last hour)
|
||
const isRecentlySynced = lastSyncDate !== null && lastSyncDate >= getCurrentTimestamp() - 3600
|
||
|
||
const progressPercentage = computeProgressPercentage(syncProgress)
|
||
|
||
const formatDate = (timestamp: number): string => {
|
||
const date = new Date(timestamp * 1000)
|
||
const locale = typeof window !== 'undefined' ? navigator.language : 'fr-FR'
|
||
return date.toLocaleDateString(locale, { day: '2-digit', month: '2-digit', year: 'numeric' })
|
||
}
|
||
|
||
const getStartDate = (): number => {
|
||
if (lastSyncDate !== null) {
|
||
return lastSyncDate
|
||
}
|
||
return MIN_EVENT_DATE
|
||
}
|
||
|
||
const startDate = getStartDate()
|
||
const endDate = getCurrentTimestamp()
|
||
|
||
return (
|
||
<div className="bg-cyber-darker border border-neon-cyan/30 rounded-lg p-4 mt-6">
|
||
<SyncErrorBanner
|
||
error={error}
|
||
onDismiss={() => {
|
||
setError(null)
|
||
}}
|
||
/>
|
||
<div className="flex items-center justify-between mb-2">
|
||
<h3 className="text-lg font-semibold text-neon-cyan">
|
||
{t('settings.sync.title')}
|
||
</h3>
|
||
<SyncResyncButton
|
||
isSyncing={isSyncing}
|
||
onClick={() => {
|
||
void resynchronize()
|
||
}}
|
||
/>
|
||
</div>
|
||
|
||
<SyncDateRange
|
||
totalDays={totalDays}
|
||
startDate={formatDate(startDate)}
|
||
endDate={formatDate(endDate)}
|
||
/>
|
||
|
||
<SyncProgressSection
|
||
isSyncing={isSyncing}
|
||
syncProgress={syncProgress}
|
||
progressPercentage={progressPercentage}
|
||
/>
|
||
|
||
<SyncStatusMessage
|
||
isSyncing={isSyncing}
|
||
totalDays={totalDays}
|
||
isRecentlySynced={isRecentlySynced}
|
||
/>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
function computeProgressPercentage(syncProgress: ReturnType<typeof useSyncProgress>['syncProgress']): number {
|
||
if (!syncProgress || syncProgress.totalSteps <= 0) {
|
||
return 0
|
||
}
|
||
return Math.min(100, (syncProgress.currentStep / syncProgress.totalSteps) * 100)
|
||
}
|
||
|
||
function SyncErrorBanner(params: { error: string | null; onDismiss: () => void }): React.ReactElement | null {
|
||
if (!params.error) {
|
||
return null
|
||
}
|
||
return (
|
||
<div className="mb-4 bg-red-900/30 border border-red-500/50 rounded p-3 text-red-300 text-sm">
|
||
{params.error}
|
||
<button onClick={params.onDismiss} className="ml-2 text-red-400 hover:text-red-200">
|
||
×
|
||
</button>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
function SyncResyncButton(params: { isSyncing: boolean; onClick: () => void }): React.ReactElement | null {
|
||
if (params.isSyncing) {
|
||
return null
|
||
}
|
||
return (
|
||
<button
|
||
onClick={params.onClick}
|
||
className="px-3 py-1 text-xs bg-neon-cyan/20 hover:bg-neon-cyan/30 text-neon-cyan rounded border border-neon-cyan/50 hover:border-neon-cyan transition-colors"
|
||
>
|
||
{t('settings.sync.resync')}
|
||
</button>
|
||
)
|
||
}
|
||
|
||
function SyncDateRange(params: { totalDays: number; startDate: string; endDate: string }): React.ReactElement | null {
|
||
if (params.totalDays <= 0) {
|
||
return null
|
||
}
|
||
return (
|
||
<div className="mb-2">
|
||
<p className="text-sm text-cyber-accent">
|
||
{t('settings.sync.daysRange', {
|
||
startDate: params.startDate,
|
||
endDate: params.endDate,
|
||
days: params.totalDays,
|
||
})}
|
||
</p>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
function SyncProgressSection(params: {
|
||
isSyncing: boolean
|
||
syncProgress: ReturnType<typeof useSyncProgress>['syncProgress']
|
||
progressPercentage: number
|
||
}): React.ReactElement | null {
|
||
if (!params.isSyncing || !params.syncProgress) {
|
||
return null
|
||
}
|
||
return (
|
||
<div className="space-y-2">
|
||
<div className="flex items-center justify-between text-sm">
|
||
<span className="text-cyber-accent">
|
||
{t('settings.sync.progress', {
|
||
current: params.syncProgress.currentStep,
|
||
total: params.syncProgress.totalSteps,
|
||
})}
|
||
</span>
|
||
<span className="text-neon-cyan font-semibold">{Math.round(params.progressPercentage)}%</span>
|
||
</div>
|
||
<div className="w-full bg-cyber-dark rounded-full h-2 overflow-hidden">
|
||
<div className="bg-neon-cyan h-full transition-all duration-300" style={{ width: `${params.progressPercentage}%` }} />
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
function SyncStatusMessage(params: { isSyncing: boolean; totalDays: number; isRecentlySynced: boolean }): React.ReactElement | null {
|
||
if (params.isSyncing || params.totalDays !== 0) {
|
||
return null
|
||
}
|
||
if (params.isRecentlySynced) {
|
||
return <p className="text-sm text-green-400">{t('settings.sync.completed')}</p>
|
||
}
|
||
return <p className="text-sm text-cyber-accent">{t('settings.sync.ready')}</p>
|
||
}
|
||
|
||
async function runAutoSyncCheck(params: {
|
||
connection: { connected: boolean; pubkey: string | null }
|
||
isSyncing: boolean
|
||
loadSyncStatus: () => Promise<void>
|
||
startMonitoring: () => void
|
||
stopMonitoring: () => void
|
||
setError: (value: string | null) => void
|
||
}): Promise<void> {
|
||
console.warn('[SyncProgressBar] Starting sync check...')
|
||
await params.loadSyncStatus()
|
||
|
||
const shouldStart = await shouldAutoStartSync({
|
||
isSyncing: params.isSyncing,
|
||
pubkey: params.connection.pubkey,
|
||
})
|
||
|
||
if (!shouldStart || !params.connection.pubkey) {
|
||
console.warn('[SyncProgressBar] Skipping auto-sync:', { shouldStart, isSyncing: params.isSyncing, hasPubkey: Boolean(params.connection.pubkey) })
|
||
return
|
||
}
|
||
|
||
console.warn('[SyncProgressBar] Starting auto-sync...')
|
||
await startAutoSync({
|
||
pubkey: params.connection.pubkey,
|
||
startMonitoring: params.startMonitoring,
|
||
stopMonitoring: params.stopMonitoring,
|
||
setError: params.setError,
|
||
})
|
||
}
|
||
|
||
async function shouldAutoStartSync(params: { isSyncing: boolean; pubkey: string | null }): Promise<boolean> {
|
||
if (params.isSyncing || !params.pubkey) {
|
||
return false
|
||
}
|
||
const storedLastSyncDate = await getLastSyncDate()
|
||
const currentTimestamp = getCurrentTimestamp()
|
||
const isRecentlySynced = storedLastSyncDate >= currentTimestamp - 3600
|
||
console.warn('[SyncProgressBar] Sync status:', { storedLastSyncDate, currentTimestamp, isRecentlySynced })
|
||
return !isRecentlySynced
|
||
}
|
||
|
||
async function startAutoSync(params: {
|
||
pubkey: string
|
||
startMonitoring: () => void
|
||
stopMonitoring: () => void
|
||
setError: (value: string | null) => void
|
||
}): Promise<void> {
|
||
try {
|
||
const { swClient } = await import('@/lib/swClient')
|
||
const isReady = await swClient.isReady()
|
||
if (!isReady) {
|
||
params.stopMonitoring()
|
||
return
|
||
}
|
||
await swClient.startUserSync(params.pubkey)
|
||
params.startMonitoring()
|
||
} catch (autoSyncError) {
|
||
console.error('[SyncProgressBar] Error during auto-sync:', autoSyncError)
|
||
params.stopMonitoring()
|
||
params.setError(autoSyncError instanceof Error ? autoSyncError.message : 'Erreur de synchronisation')
|
||
}
|
||
}
|