Files
sf-app-platform/docs/chat-known-issues.md
JulianAlcala abdbc2bf2e release: 1.0.0+6 — chat notifications + videocall refactor
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
2026-05-08 07:20:40 -05:00

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.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).