| .. | .. |
|---|
| 93 | 93 | // Load persisted state BEFORE connecting |
|---|
| 94 | 94 | final prefs = await SharedPreferences.getInstance(); |
|---|
| 95 | 95 | _lastSeq = prefs.getInt('lastSeq') ?? 0; |
|---|
| 96 | | - // Restore persisted unread counts |
|---|
| 96 | + // Restore persisted unread counts (kept loaded so any pre-resume read code |
|---|
| 97 | + // sees a valid state), but the app is foregrounding right now → user is |
|---|
| 98 | + // here, so everything is "seen." Clear immediately so a stale 15 doesn't |
|---|
| 99 | + // re-paint the badge on next background. |
|---|
| 97 | 100 | final savedUnreads = prefs.getString('unreadCounts'); |
|---|
| 98 | 101 | if (savedUnreads != null && mounted) { |
|---|
| 99 | 102 | try { |
|---|
| .. | .. |
|---|
| 103 | 106 | } catch (_) {} |
|---|
| 104 | 107 | } |
|---|
| 105 | 108 | _unreadCountsLoaded = true; |
|---|
| 109 | + _clearAllUnreads(); |
|---|
| 106 | 110 | // Restore saved session order and active session |
|---|
| 107 | 111 | _cachedSessionOrder = prefs.getStringList('sessionOrder'); |
|---|
| 108 | 112 | final savedSessionId = prefs.getString('activeSessionId'); |
|---|
| .. | .. |
|---|
| 156 | 160 | if (_ws != null && !_ws!.isConnected) { |
|---|
| 157 | 161 | _ws!.connect(); |
|---|
| 158 | 162 | } |
|---|
| 159 | | - // Don't update badge here — provider might not have loaded persisted counts yet. |
|---|
| 160 | | - // Native applicationDidBecomeActive reads correct value from UserDefaults. |
|---|
| 163 | + // App is foregrounded → user is here → everything previously unread is |
|---|
| 164 | + // now considered seen. Clear so we don't re-paint a stale badge when the |
|---|
| 165 | + // user backgrounds the app without having explicitly switched sessions. |
|---|
| 166 | + _clearAllUnreads(); |
|---|
| 161 | 167 | } else if (state == AppLifecycleState.paused && _unreadCountsLoaded) { |
|---|
| 162 | 168 | // Set badge to total unread count when going to background |
|---|
| 163 | 169 | _updateBadgeFromUnreads(); |
|---|
| .. | .. |
|---|
| 167 | 173 | void _updateBadgeFromUnreads() { |
|---|
| 168 | 174 | final counts = ref.read(unreadCountsProvider); |
|---|
| 169 | 175 | _persistUnreadCounts(counts); |
|---|
| 176 | + } |
|---|
| 177 | + |
|---|
| 178 | + /// Clear in-memory + persisted unread counts and remove the icon badge. |
|---|
| 179 | + /// Called on cold-start (after load) and on resume — the app being open |
|---|
| 180 | + /// means the user has effectively seen what was pending. |
|---|
| 181 | + void _clearAllUnreads() { |
|---|
| 182 | + if (!mounted) return; |
|---|
| 183 | + ref.read(unreadCountsProvider.notifier).state = <String, int>{}; |
|---|
| 184 | + SharedPreferences.getInstance().then((prefs) { |
|---|
| 185 | + prefs.setString('unreadCounts', jsonEncode(<String, int>{})); |
|---|
| 186 | + prefs.setInt('badgeCount', 0); |
|---|
| 187 | + }); |
|---|
| 188 | + PushService.clearBadge(); |
|---|
| 170 | 189 | } |
|---|
| 171 | 190 | |
|---|
| 172 | 191 | // ignore: unused_field |
|---|
| .. | .. |
|---|
| 214 | 233 | } |
|---|
| 215 | 234 | }; |
|---|
| 216 | 235 | _ws!.onMessage = _handleMessage; |
|---|
| 236 | + _ws!.onDebugStateRequest = _handleDebugStateRequest; |
|---|
| 217 | 237 | _ws!.onOpen = () { |
|---|
| 218 | 238 | _sessionReady = false; // Gate messages until sessions arrive |
|---|
| 219 | 239 | _pendingMessages.clear(); |
|---|
| .. | .. |
|---|
| 522 | 542 | } |
|---|
| 523 | 543 | } |
|---|
| 524 | 544 | |
|---|
| 545 | + /// Respond to a pailot_debug_state request from the server. |
|---|
| 546 | + /// Reads the in-memory session list and active session from providers |
|---|
| 547 | + /// and publishes exactly what the app is currently rendering. |
|---|
| 548 | + void _handleDebugStateRequest(String requestId) { |
|---|
| 549 | + final sessions = ref.read(sessionsProvider); |
|---|
| 550 | + final activeSessionId = ref.read(activeSessionIdProvider); |
|---|
| 551 | + |
|---|
| 552 | + final sessionPayloads = sessions.map((s) => { |
|---|
| 553 | + 'sessionId': s.id, |
|---|
| 554 | + 'index': s.index, |
|---|
| 555 | + 'displayedName': s.name, // exactly what is shown in the drawer |
|---|
| 556 | + 'type': s.type, |
|---|
| 557 | + if (s.kind != null) 'kind': s.kind, |
|---|
| 558 | + 'isActive': s.id == activeSessionId, |
|---|
| 559 | + }).toList(); |
|---|
| 560 | + |
|---|
| 561 | + _ws?.publishDebugStateResponse( |
|---|
| 562 | + requestId: requestId, |
|---|
| 563 | + activeSessionId: activeSessionId, |
|---|
| 564 | + sessions: sessionPayloads, |
|---|
| 565 | + platform: Platform.operatingSystem, |
|---|
| 566 | + ); |
|---|
| 567 | + } |
|---|
| 568 | + |
|---|
| 525 | 569 | void _handleIncomingMessage(Map<String, dynamic> msg) { |
|---|
| 526 | 570 | final sessionId = msg['sessionId'] as String?; |
|---|
| 527 | 571 | final content = msg['content'] as String? ?? |
|---|