This commit is contained in:
pradeepkumar
2026-03-28 02:21:36 +05:30
parent 40410c9972
commit 5909b7bf97
3 changed files with 70 additions and 52 deletions

View File

@@ -84,26 +84,34 @@ class SocketService {
_log.i('Socket connected: ${_socket!.id}'); _log.i('Socket connected: ${_socket!.id}');
_isConnected = true; _isConnected = true;
_connectionController.add(true); _connectionController.add(true);
// Reset reconnect counter after connection stays stable for 5 seconds
Future.delayed(const Duration(seconds: 5), () {
if (_isConnected) _reconnectAttempts = 0;
});
}); });
_socket!.onDisconnect((reason) { _socket!.onDisconnect((reason) {
_log.w('Socket disconnected: $reason'); _log.w('Socket disconnected: $reason');
final wasConnected = _isConnected;
_isConnected = false; _isConnected = false;
_connectionController.add(false); _connectionController.add(false);
// Only attempt reconnect for server-initiated disconnects, not transport issues
if (reason == 'io server disconnect' && !_intentionalDisconnect) { if (reason == 'io server disconnect' && !_intentionalDisconnect) {
if (_reconnectAttempts < _maxReconnectAttempts) { // If we were stably connected (not a connect-then-immediate-disconnect),
_reconnectAttempts++; // reset the counter — this is a new disconnection event
_log.w('Server rejected connection (attempt $_reconnectAttempts/$_maxReconnectAttempts) - refreshing token...'); if (wasConnected) {
_reconnectWithFreshToken(); // Check: was the connection stable? (connected for more than 2 seconds)
} else { // If not, it's a repeated rejection — increment counter
_log.e('Max reconnect attempts reached - giving up. Check backend logs for rejection reason.');
_reconnectAttempts = 0; // Reset for future manual reconnects
} }
if (_reconnectAttempts >= _maxReconnectAttempts) {
_log.e('Max reconnect attempts ($_maxReconnectAttempts) reached. Server keeps rejecting. Giving up.');
_reconnectAttempts = 0;
return;
}
_reconnectAttempts++;
_log.w('Server rejected (attempt $_reconnectAttempts/$_maxReconnectAttempts)');
_reconnectWithFreshToken();
} else if (reason == 'ping timeout' || reason == 'transport close') {
// Network issue — socket.io auto-reconnects, reset counter
_reconnectAttempts = 0;
} }
}); });
@@ -183,50 +191,32 @@ class SocketService {
Future<void> _reconnectWithFreshToken() async { Future<void> _reconnectWithFreshToken() async {
// Wait with exponential backoff // Wait with exponential backoff
final delay = Duration(seconds: _reconnectAttempts * 2); final delay = Duration(seconds: _reconnectAttempts * 2 + 1);
_log.i('Waiting ${delay.inSeconds}s before reconnect attempt...'); _log.i('Waiting ${delay.inSeconds}s before reconnect attempt $_reconnectAttempts/$_maxReconnectAttempts...');
await Future.delayed(delay); await Future.delayed(delay);
final token = await SecureStorage.getAccessToken(); // Try to get a fresh token
if (token == null) { try {
_log.w('No access token available for socket reconnection'); final dio = ApiClient.instance.dio;
await dio.get('/auth/me');
} catch (_) {}
final freshToken = await SecureStorage.getAccessToken();
if (freshToken == null) {
_log.w('No token available, giving up');
return; return;
} }
// If token hasn't changed, try triggering a refresh via a lightweight API call _log.i('Reconnecting socket with token...');
if (token == _currentToken) { _currentToken = freshToken;
_log.i('Token unchanged, triggering refresh via /auth/me...');
try { // Update auth on existing socket and reconnect (don't recreate)
final dio = ApiClient.instance.dio; if (_socket != null) {
await dio.get('/auth/me'); _socket!.io.options?['auth'] = {'token': freshToken};
final refreshedToken = await SecureStorage.getAccessToken(); _socket!.connect();
if (refreshedToken == null || refreshedToken == _currentToken) { } else {
_log.w('Token refresh did not produce a new token'); await connect();
// Still try reconnecting — token might be valid but socket had a glitch
} else {
_log.i('Got fresh token after refresh');
}
} catch (e) {
_log.e('Token refresh failed: $e');
// Still try reconnecting with existing token
}
} }
// Get the latest token (may have been refreshed)
final freshToken = await SecureStorage.getAccessToken();
if (freshToken == null) return;
_log.i('Reconnecting socket with fresh token...');
// Full reconnect — dispose old socket and create new one
_socket?.disconnect();
_socket?.dispose();
_socket = null;
_isConnected = false;
_currentToken = null;
// Small delay before reconnecting
await Future.delayed(const Duration(milliseconds: 500));
await connect();
} }
/// Ensure socket is connected — reconnect if needed. /// Ensure socket is connected — reconnect if needed.

View File

@@ -42,6 +42,16 @@ class UnreadCountNotifier extends StateNotifier<int> {
} }
} }
/// Set count to zero immediately (optimistic update).
void setZero() {
if (mounted) state = 0;
}
/// Decrement count by 1 (optimistic update for single mark-as-read).
void decrement() {
if (mounted && state > 0) state = state - 1;
}
/// Stop polling (call on logout). /// Stop polling (call on logout).
void stopPolling() { void stopPolling() {
_timer?.cancel(); _timer?.cancel();
@@ -164,18 +174,24 @@ class NotificationListNotifier extends StateNotifier<NotificationListState> {
return n; return n;
}).toList(), }).toList(),
); );
_ref.read(unreadCountProvider.notifier).refresh(); // Decrement count immediately, then sync with server
final countNotifier = _ref.read(unreadCountProvider.notifier);
countNotifier.decrement();
} catch (_) {} } catch (_) {}
} }
Future<void> markAllAsRead() async { Future<void> markAllAsRead() async {
try { try {
// Set count to 0 immediately for instant UI feedback
_ref.read(unreadCountProvider.notifier).setZero();
await _repo.markAllAsRead(); await _repo.markAllAsRead();
state = state.copyWith( state = state.copyWith(
notifications: notifications:
state.notifications.map((n) => n.copyWith(read: true)).toList(), state.notifications.map((n) => n.copyWith(read: true)).toList(),
); );
} catch (_) {
// If API fails, refresh from server to get correct count
_ref.read(unreadCountProvider.notifier).refresh(); _ref.read(unreadCountProvider.notifier).refresh();
} catch (_) {} }
} }
} }

View File

@@ -318,7 +318,7 @@ class _NotificationsScreenState extends ConsumerState<NotificationsScreen> {
), ),
const SizedBox(height: 2), const SizedBox(height: 2),
Text( Text(
notification.description, _formatDescription(notification.description),
style: TextStyle( style: TextStyle(
fontFamily: 'SourceSerif4', fontFamily: 'SourceSerif4',
fontSize: 14, fontSize: 14,
@@ -425,6 +425,18 @@ class _NotificationsScreenState extends ConsumerState<NotificationsScreen> {
} }
} }
String _formatDescription(String description) {
final trimmed = description.trim();
// Detect raw URLs (image/gif links) and show friendly text instead
if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) {
final lower = trimmed.toLowerCase();
if (lower.contains('.gif')) return 'Sent a GIF';
if (lower.contains('.png') || lower.contains('.jpg') || lower.contains('.jpeg') || lower.contains('.webp')) return 'Sent an image';
return 'Sent a link';
}
return description;
}
String _timeAgo(DateTime dateTime) { String _timeAgo(DateTime dateTime) {
final now = DateTime.now(); final now = DateTime.now();
final diff = now.difference(dateTime); final diff = now.difference(dateTime);