169 lines
4.8 KiB
Dart
169 lines
4.8 KiB
Dart
import 'dart:async';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/widgets.dart';
|
|
import 'auth_service.dart';
|
|
import 'chat_backup_manager.dart';
|
|
import 'secure_chat_service.dart';
|
|
import 'simple_e2ee_service.dart';
|
|
import 'local_message_store.dart';
|
|
|
|
class SyncManager with WidgetsBindingObserver {
|
|
final SecureChatService _secureChatService;
|
|
final AuthService _authService;
|
|
final SimpleE2EEService _e2ee = SimpleE2EEService();
|
|
final LocalMessageStore _localStore = LocalMessageStore.instance;
|
|
final Duration _resumeThreshold;
|
|
final Duration _syncInterval;
|
|
|
|
Timer? _timer;
|
|
StreamSubscription<AuthState>? _authSub;
|
|
DateTime? _lastSyncAt;
|
|
bool _syncInProgress = false;
|
|
bool _initialized = false;
|
|
bool _hydrating = false;
|
|
|
|
SyncManager({
|
|
required SecureChatService secureChatService,
|
|
required AuthService authService,
|
|
Duration resumeThreshold = const Duration(minutes: 2),
|
|
Duration syncInterval = const Duration(minutes: 5),
|
|
}) : _secureChatService = secureChatService,
|
|
_authService = authService,
|
|
_resumeThreshold = resumeThreshold,
|
|
_syncInterval = syncInterval;
|
|
|
|
void init() {
|
|
if (_initialized) return;
|
|
_initialized = true;
|
|
|
|
WidgetsBinding.instance.addObserver(this);
|
|
_authSub = _authService.authStateChanges.listen(_handleAuthChange);
|
|
|
|
if (_authService.isAuthenticated) {
|
|
_startTimer();
|
|
// Defer heavy sync work so the UI thread isn't blocked
|
|
Future.delayed(const Duration(milliseconds: 500), () {
|
|
if (!_initialized) return;
|
|
if (kDebugMode) debugPrint('[SyncManager] Deferred startup sync starting');
|
|
// On web, skip eager chat sync — Hive/IndexedDB is too slow to open
|
|
// at startup (~130s). Chat loads lazily when user opens a conversation.
|
|
if (!kIsWeb) {
|
|
unawaited(ensureHistoryLoaded());
|
|
_triggerSync(reason: 'startup');
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
void dispose() {
|
|
if (!_initialized) return;
|
|
_initialized = false;
|
|
WidgetsBinding.instance.removeObserver(this);
|
|
_authSub?.cancel();
|
|
_authSub = null;
|
|
_stopTimer();
|
|
}
|
|
|
|
@override
|
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
|
if (state == AppLifecycleState.resumed) {
|
|
if (_authService.isAuthenticated) {
|
|
_startTimer();
|
|
_syncIfStale();
|
|
}
|
|
} else if (state == AppLifecycleState.paused) {
|
|
_stopTimer();
|
|
// Trigger backup when app goes to background
|
|
ChatBackupManager.instance.triggerBackupIfNeeded();
|
|
} else if (state == AppLifecycleState.inactive ||
|
|
state == AppLifecycleState.detached) {
|
|
_stopTimer();
|
|
}
|
|
}
|
|
|
|
void _handleAuthChange(AuthState state) {
|
|
if (state.event == AuthChangeEvent.signedIn ||
|
|
state.event == AuthChangeEvent.tokenRefreshed) {
|
|
_startTimer();
|
|
if (!kIsWeb) {
|
|
unawaited(ensureHistoryLoaded());
|
|
_triggerSync(reason: 'auth');
|
|
}
|
|
} else if (state.event == AuthChangeEvent.signedOut) {
|
|
_lastSyncAt = null;
|
|
_stopTimer();
|
|
}
|
|
}
|
|
|
|
void _startTimer() {
|
|
_timer?.cancel();
|
|
_timer = Timer.periodic(_syncInterval, (_) {
|
|
if (_authService.isAuthenticated && !kIsWeb) {
|
|
_triggerSync(reason: 'timer');
|
|
}
|
|
});
|
|
}
|
|
|
|
void _stopTimer() {
|
|
_timer?.cancel();
|
|
_timer = null;
|
|
}
|
|
|
|
void _syncIfStale() {
|
|
if (_lastSyncAt == null ||
|
|
DateTime.now().difference(_lastSyncAt!) > _resumeThreshold) {
|
|
_triggerSync(reason: 'resume');
|
|
}
|
|
}
|
|
|
|
Future<void> _triggerSync({required String reason}) async {
|
|
if (!_authService.isAuthenticated || _syncInProgress) return;
|
|
_syncInProgress = true;
|
|
try {
|
|
|
|
await _e2ee.initialize();
|
|
if (!_e2ee.isReady) {
|
|
return;
|
|
}
|
|
|
|
// On web, skip eager hydration — Hive/IndexedDB startup is too slow.
|
|
if (!kIsWeb) {
|
|
await ensureHistoryLoaded();
|
|
}
|
|
|
|
await _secureChatService.syncAllConversations(force: true);
|
|
_lastSyncAt = DateTime.now();
|
|
|
|
// Schedule auto-backup after successful sync
|
|
ChatBackupManager.instance.scheduleBackup();
|
|
} catch (e) {
|
|
} finally {
|
|
_syncInProgress = false;
|
|
}
|
|
}
|
|
|
|
Future<void> ensureHistoryLoaded() async {
|
|
if (!_authService.isAuthenticated || _hydrating) return;
|
|
_hydrating = true;
|
|
try {
|
|
await _e2ee.initialize();
|
|
if (!_e2ee.isReady) {
|
|
return;
|
|
}
|
|
|
|
final conversations = await _secureChatService.getConversations();
|
|
for (final conv in conversations) {
|
|
final isEmpty =
|
|
(await _localStore.getMessageIdsForConversation(conv.id)).isEmpty;
|
|
if (isEmpty) {
|
|
await _secureChatService.fetchAndDecryptHistory(conv.id, limit: 50);
|
|
}
|
|
await _secureChatService.startLiveListener(conv.id);
|
|
}
|
|
} catch (e) {
|
|
} finally {
|
|
_hydrating = false;
|
|
}
|
|
}
|
|
}
|