Phase 3 (security): - P3.1: Feature gate server-side enforcement (403 on disabled features) - P3.2: Unified auth error interceptor (401→logout) - P3.3: Store-level state isolation via StoreSnapshot contract (added message-list-tabs + task stores to snapshot/restore cycle) - P3.4: Push event bus extraction — email-store no longer imports calendar/contact/filter/file stores directly - P1.3: Auth localStorage AES-GCM encryption via custom Zustand adapter Phase 4 (polish): - P4.1: Offline write queue — pending operations in localStorage, auto-retry on reconnect, offline-queue-indicator banner - P4.2: Identity spoofing — fromOverrideEmail domain validation - P4.3: WebSocket push for Electron via main-process IPC bridge (ws package with Authorization headers)
50 lines
1.6 KiB
TypeScript
50 lines
1.6 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect, useCallback } from 'react';
|
|
import { useAuthStore } from '@/stores/auth-store';
|
|
import { useAccountStore } from '@/stores/account-store';
|
|
import {
|
|
getPendingOperationsCount,
|
|
onPendingCountChange,
|
|
processQueue,
|
|
} from '@/lib/offline-write-queue';
|
|
|
|
export function OfflineQueueIndicator() {
|
|
const [count, setCount] = useState(0);
|
|
const [processing, setProcessing] = useState(false);
|
|
const client = useAuthStore((s) => s.client);
|
|
const activeAccountId = useAccountStore((s) => s.activeAccountId);
|
|
|
|
useEffect(() => {
|
|
setCount(getPendingOperationsCount());
|
|
return onPendingCountChange(setCount);
|
|
}, []);
|
|
|
|
const handleRetry = useCallback(async () => {
|
|
if (!client || !activeAccountId) return;
|
|
setProcessing(true);
|
|
try {
|
|
await processQueue(client, activeAccountId);
|
|
} finally {
|
|
setProcessing(false);
|
|
}
|
|
}, [client, activeAccountId]);
|
|
|
|
if (count === 0) return null;
|
|
|
|
return (
|
|
<div className="flex items-center justify-between gap-2 bg-amber-50 border-b border-amber-200 px-4 py-1.5 text-sm dark:bg-amber-950 dark:border-amber-800">
|
|
<span className="text-amber-800 dark:text-amber-200">
|
|
{count} pending {count === 1 ? 'operation' : 'operations'} (offline)
|
|
</span>
|
|
<button
|
|
onClick={handleRetry}
|
|
disabled={processing || !client}
|
|
className="rounded bg-amber-200 px-2 py-0.5 text-xs font-medium text-amber-900 hover:bg-amber-300 disabled:opacity-50 dark:bg-amber-800 dark:text-amber-100 dark:hover:bg-amber-700"
|
|
>
|
|
{processing ? 'Retrying...' : 'Retry now'}
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|