# Chat — Known issues y deuda Resumen actualizado al 2026-05-08 tras el refactor production-ready. ## Backend — bloqueantes coordinados ### 1. Push routing por chatId **Estado:** pendiente coordinar con backend. El backend usa la heurística `phone.endsWith(chatId)` para enrutar pushes. Eso no funciona con los chatIds del cliente, que son: - `1to1__` para conversación 1:1. - `family_` para grupo familiar. **Síntoma:** los pushes de mensajes propios reconciliados por el otro extremo no llegan al teléfono. **Acción:** pedir al backend que (a) acepte chatIds tipados, o (b) extraiga el `deviceIdentificator` del chatId 1:1 para el match. ### 2. `pushData.chatId` ausente **Estado:** pendiente coordinar con backend. El push de chat trae `pushData` sin `chatId` explícito. El handler del cliente (`apps/mobile_app/lib/core/notifications_init.dart`) ya tiene `TODO(push-chatid)` apuntando a esto: hoy si el deep-link no encuentra `chatId`, cae a la lista de chats en lugar de la conversación. **Acción:** backend debe incluir `chatId` en el `pushData` de `CHAT_MESSAGE`. ### 3. Tope de tamaño y mime types **Estado:** documentación pendiente. El cliente reacciona a `413 Payload Too Large` y `415 Unsupported Media Type` mapeándolos a `ChatConversationErrorEvent.fileTooLarge` / `fileUnsupported`. Pero **no validamos antes de subir** porque no conocemos los límites del backend. **Acción:** confirmar tamaño máximo (imagen/audio) y mime types soportados. Una vez confirmado, validar client-side antes del upload para evitar el round trip al servidor. ## Cliente — deuda conocida ### 4. Group fan-out: IDs por device **Estado:** workaround productivo, deuda lógica. Hoy `ChatSendService` envía un mensaje por device en el grupo familiar. El **primer device** usa el `messageId` del optimistic; los **demás** generan UUIDs nuevos. En backend quedan N mensajes con IDs distintos para un solo "send" lógico del usuario. **Riesgo:** en retry, los UUIDs 2..N se regeneran, creando duplicados en backend. Si el backend no dedupe por contenido, el grupo familiar verá mensajes duplicados. **Mitigación actual:** retry desde el optimistic mismo conserva el mensaje primario (que ya tenía un ID estable), pero los secundarios sí se duplican si el primer intento dejó algunos OK y otros KO. **Solución correcta:** backend debe aceptar un `bulkSend` que toma un solo `messageId` lógico + lista de devices target. O el cliente persiste los IDs secundarios en el optimistic state para no regenerarlos. ### 5. `selectedDevice` snapshot al abrir el chat **Estado:** mitigado, comportamiento aceptable. `build()` del controller toma snapshot de `selectedDeviceProvider` en `ChatConversationKind.oneToOne`. Si el usuario cambia de dispositivo después de abrir el chat, los **mensajes ya cargados** quedan congelados (correcto: son la conversación con el device anterior). **Ya arreglado:** los **envíos** sí re-leen `selectedDeviceProvider` justo antes de mandar, vía `ChatParticipantsService.resolveForSend`. Polling y WebSocket sub se mantienen contra el device snapshot. **Caveat:** si cambia el device durante el chat, el polling no refleja mensajes del nuevo device hasta que el chat se reabra. Esperado. ### 6. Audio recording mínimo de 1 segundo `ChatConversationConfig.minAudioRecordingMs = 1000`. Audios más cortos se descartan con un dialog "audio muy corto". Si querés cambiar el umbral, modificar el config — está centralizado. ### 7. WebSocket + polling híbrido El controller mantiene **ambos**: WebSocket para reconciliación inmediata cuando llega evento, y polling cada 4s **solo cuando hay mensajes pendientes** (no `delivered`). Polling se apaga automáticamente al confirmar entrega o tras 3 errores consecutivos. Esto es defensa en profundidad: si el WebSocket cae mientras hay un mensaje pendiente, el polling lo captura. Trade-off: ligeramente más tráfico cuando hay pendientes. ### 8. `ref.listen` en build de `ChatAudioPlayerController` `chat_audio_player_controller.dart:31` usa `ref.listen` dentro de `build()`. Va contra la regla "no `ref.listen` en build de notifier", pero es caso justificado: necesita callback de transición (cuando otro audio empieza, pausar el actual). Con `ref.watch` puro no se puede observar transiciones. Si la regla se vuelve más estricta, mover el listen a un método `start()` que la screen invoca explícitamente. ## Estructura post-refactor ``` chat/ ├── core/ │ ├── data/ │ │ ├── datasource/ │ │ │ └── chat_remote_datasource_impl.dart # Dio inyectado, no GetIt │ │ ├── models/ │ │ ├── repositories/ │ │ └── utils/chat_image_compressor.dart │ ├── domain/ │ │ ├── entities/, enums/, repositories/ │ │ └── services/ # ChatIdResolver, ChatPermissions, WatchEmojiCatalog │ └── providers/ └── features/ ├── chat_conversation/ │ ├── application/ # NUEVO — service layer │ │ ├── chat_send_service.dart # fan-out, retry, aggregate │ │ ├── chat_sync_service.dart # fetch, polling, WS │ │ ├── chat_participants_service.dart # 1to1 vs family │ │ ├── chat_permission_flow_service.dart # tracking + decision │ │ └── chat_media_cleanup_service.dart # orphan files │ ├── config/ │ │ └── chat_conversation_config.dart # 8 magic numbers centralizados │ ├── presentation/ │ │ ├── chat_conversation_screen.dart │ │ ├── providers/ │ │ │ ├── chat_conversation_controller.dart # 622 LOC (-19%) │ │ │ ├── chat_audio_player_controller.dart │ │ │ └── chat_recorder_controller.dart │ │ └── widgets/ │ │ ├── chat_bubble_shell.dart # NUEVO — shared shell+footer │ │ ├── chat_input_bar.dart # split en _Composer + _SendOrRecordAction │ │ ├── chat_message_bubble.dart # _TextBubble usa shell │ │ ├── chat_audio_bubble.dart # usa shell │ │ └── chat_image_bubble.dart # usa shell + cache existsSync │ └── chat_conversation_builder.dart └── chat_list/ ``` ## Tests - `test/core/data/repositories/chat_repository_impl_test.dart` - `test/core/domain/services/chat_id_resolver_test.dart` **Pendiente añadir:** - Tests del controller (send happy path, send error, retry, polling lifecycle). - Tests de `ChatSendService` (fan-out aggregate, retry semantics). - Tests de `ChatPermissionFlowService` (tracking + decision).