| .. | .. |
|---|
| 207 | 207 | final sessionId = msg['sessionId'] as String?; |
|---|
| 208 | 208 | if (sessionId != null) _incrementUnread(sessionId); |
|---|
| 209 | 209 | case 'catch_up': |
|---|
| 210 | | - // Only update lastSeq — don't replay messages. |
|---|
| 211 | | - // Local storage already has our messages. Replaying causes duplicates |
|---|
| 212 | | - // and overwrites user messages. New messages arriving while offline |
|---|
| 213 | | - // are stored via _storeForSession from the DIRECT broadcast path. |
|---|
| 214 | 210 | final serverSeq = msg['serverSeq'] as int?; |
|---|
| 215 | 211 | if (serverSeq != null && serverSeq > _lastSeq) { |
|---|
| 216 | 212 | _lastSeq = serverSeq; |
|---|
| 217 | 213 | _saveLastSeq(); |
|---|
| 218 | 214 | } |
|---|
| 215 | + // Merge catch_up messages: only add messages not already in local storage. |
|---|
| 216 | + // We check by content match against existing messages to avoid duplicates |
|---|
| 217 | + // while still picking up messages that arrived while the app was backgrounded. |
|---|
| 218 | + final catchUpMsgs = msg['messages'] as List<dynamic>?; |
|---|
| 219 | + if (catchUpMsgs != null && catchUpMsgs.isNotEmpty) { |
|---|
| 220 | + _isCatchingUp = true; |
|---|
| 221 | + final existing = ref.read(messagesProvider); |
|---|
| 222 | + final existingContents = existing |
|---|
| 223 | + .where((m) => m.role == MessageRole.assistant) |
|---|
| 224 | + .map((m) => m.content) |
|---|
| 225 | + .toSet(); |
|---|
| 226 | + for (final m in catchUpMsgs) { |
|---|
| 227 | + final content = (m as Map<String, dynamic>)['content'] as String? ?? ''; |
|---|
| 228 | + // Skip if we already have this message locally |
|---|
| 229 | + if (content.isNotEmpty && existingContents.contains(content)) continue; |
|---|
| 230 | + _handleMessage(m); |
|---|
| 231 | + if (content.isNotEmpty) existingContents.add(content); |
|---|
| 232 | + } |
|---|
| 233 | + _isCatchingUp = false; |
|---|
| 234 | + } |
|---|
| 219 | 235 | case 'pong': |
|---|
| 220 | 236 | break; // heartbeat response, ignore |
|---|
| 221 | 237 | case 'delete_message': |
|---|