fix
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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 (_) {}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user