diff --git a/lib/features/messaging/data/socket_service.dart b/lib/features/messaging/data/socket_service.dart index c81f6fe..dcfcbd1 100644 --- a/lib/features/messaging/data/socket_service.dart +++ b/lib/features/messaging/data/socket_service.dart @@ -84,26 +84,34 @@ class SocketService { _log.i('Socket connected: ${_socket!.id}'); _isConnected = 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) { _log.w('Socket disconnected: $reason'); + final wasConnected = _isConnected; _isConnected = false; _connectionController.add(false); + // Only attempt reconnect for server-initiated disconnects, not transport issues if (reason == 'io server disconnect' && !_intentionalDisconnect) { - if (_reconnectAttempts < _maxReconnectAttempts) { - _reconnectAttempts++; - _log.w('Server rejected connection (attempt $_reconnectAttempts/$_maxReconnectAttempts) - refreshing token...'); - _reconnectWithFreshToken(); - } else { - _log.e('Max reconnect attempts reached - giving up. Check backend logs for rejection reason.'); - _reconnectAttempts = 0; // Reset for future manual reconnects + // If we were stably connected (not a connect-then-immediate-disconnect), + // reset the counter — this is a new disconnection event + if (wasConnected) { + // Check: was the connection stable? (connected for more than 2 seconds) + // If not, it's a repeated rejection — increment counter } + + 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 _reconnectWithFreshToken() async { // Wait with exponential backoff - final delay = Duration(seconds: _reconnectAttempts * 2); - _log.i('Waiting ${delay.inSeconds}s before reconnect attempt...'); + final delay = Duration(seconds: _reconnectAttempts * 2 + 1); + _log.i('Waiting ${delay.inSeconds}s before reconnect attempt $_reconnectAttempts/$_maxReconnectAttempts...'); await Future.delayed(delay); - final token = await SecureStorage.getAccessToken(); - if (token == null) { - _log.w('No access token available for socket reconnection'); + // Try to get a fresh token + try { + 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; } - // If token hasn't changed, try triggering a refresh via a lightweight API call - if (token == _currentToken) { - _log.i('Token unchanged, triggering refresh via /auth/me...'); - try { - final dio = ApiClient.instance.dio; - await dio.get('/auth/me'); - final refreshedToken = await SecureStorage.getAccessToken(); - if (refreshedToken == null || refreshedToken == _currentToken) { - _log.w('Token refresh did not produce a new token'); - // 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 - } + _log.i('Reconnecting socket with token...'); + _currentToken = freshToken; + + // Update auth on existing socket and reconnect (don't recreate) + if (_socket != null) { + _socket!.io.options?['auth'] = {'token': freshToken}; + _socket!.connect(); + } else { + await connect(); } - - // 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. diff --git a/lib/features/notifications/presentation/providers/notification_provider.dart b/lib/features/notifications/presentation/providers/notification_provider.dart index 2bf00f5..9e92d7b 100644 --- a/lib/features/notifications/presentation/providers/notification_provider.dart +++ b/lib/features/notifications/presentation/providers/notification_provider.dart @@ -42,6 +42,16 @@ class UnreadCountNotifier extends StateNotifier { } } + /// 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). void stopPolling() { _timer?.cancel(); @@ -164,18 +174,24 @@ class NotificationListNotifier extends StateNotifier { return n; }).toList(), ); - _ref.read(unreadCountProvider.notifier).refresh(); + // Decrement count immediately, then sync with server + final countNotifier = _ref.read(unreadCountProvider.notifier); + countNotifier.decrement(); } catch (_) {} } Future markAllAsRead() async { try { + // Set count to 0 immediately for instant UI feedback + _ref.read(unreadCountProvider.notifier).setZero(); await _repo.markAllAsRead(); state = state.copyWith( notifications: 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(); - } catch (_) {} + } } } diff --git a/lib/features/notifications/presentation/screens/notifications_screen.dart b/lib/features/notifications/presentation/screens/notifications_screen.dart index 537dc47..4dcf4f3 100644 --- a/lib/features/notifications/presentation/screens/notifications_screen.dart +++ b/lib/features/notifications/presentation/screens/notifications_screen.dart @@ -318,7 +318,7 @@ class _NotificationsScreenState extends ConsumerState { ), const SizedBox(height: 2), Text( - notification.description, + _formatDescription(notification.description), style: TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, @@ -425,6 +425,18 @@ class _NotificationsScreenState extends ConsumerState { } } + 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) { final now = DateTime.now(); final diff = now.difference(dateTime);