Chat notifications (production-ready) - ChatDeeplinkService: resolves client chat ID from incoming push/WS payloads using the (senderId, chatId) matrix and switches selectedDeviceProvider when the payload references a different watch (multi-device deeplink) - IncomingChatResolver in domain layer with full unit coverage of the 4-case matrix - ChatContext provider (sealed: outsideChat / list / conversation) wired into ChatListScreen and ChatConversationScreen via initState/dispose to enable WhatsApp-style suppression - notifications_init refactor: foreground CHAT_MESSAGE notifications are suppressed on chat list and matching conversation; tap navigation goes through ChatDeeplinkService - ChatSyncService.subscribeToReconnect: reconciles from REST when the WebSocket comes back from a disconnect (recovers messages missed in background) - 5s message-id dedup window in the WS listener (mitigates server-side duplicate chat-message-received events) - Reconcile from remote on conversation mount (covers cached controller from background) - AppBar refresh button + inverted pull-to-refresh in the conversation (overscroll either edge of the reverse list) WS event parser fix - chat-message-received normalises to chat_message_received; parser now accepts both that and chat_message so the conversation reactively refreshes when a watch sends a message Chat application layer - Split the conversation controller into services: chat_send_service, chat_sync_service, chat_participants_service, chat_permission_flow_service, chat_media_cleanup_service - chat_conversation_config centralises page size, polling interval, dedup window - chat_bubble_shell extracted; input bar split into smaller widgets - emoji picker sheet + emoji blocking input formatter + watch_emoji_catalog - Multipart upload header race fixed via synchronized.Lock around the shared Dio instance Videocall (carryover from earlier work in this branch) - Application services: incoming, outgoing, session - Domain entities: VideocallIncomingArgs, VideocallRoom, VideocallUserId, parseDeviceIdFromRoom helper - Views split: idle, incoming, active call, group call - Widgets: picture_in_picture_video, remote_or_fallback_video, video_call_header - videocall_config centralises timeouts, ringing duration, battery threshold - Incoming via push (channel mode) with full-screen notification + ringtone - Hangup-on-remote-left moved to controller; redirect on participant update documented - treezor_token_interceptor: distinguish session expiry from operation-denied 401s Localization & misc - New keys for chat conversation, refresh, errors across en/es/de/fr/it/pt - Location map: dispose ref-after-unmount fix; route history layer cleanup - Legacy device view model: position update event handling - AndroidManifest: notification channel + permissions for incoming-call full-screen intent
6.8 KiB
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_<userId>_<deviceIdentificator>para conversación 1:1.family_<delegationId>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<String?> 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.darttest/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).