From 6e87e9b6dacfe47f101fafe7180c47ad79e46937 Mon Sep 17 00:00:00 2001 From: Ramadhan Sjamsani Date: Mon, 1 Jun 2026 22:26:57 +0800 Subject: [PATCH] fix(chat): render message timestamps in device-local time Live chat bubbles read createdAt.hour/.minute directly, but server created_at (UTC, ISO-Z) was parsed without .toLocal() while optimistic sends used DateTime.now() (local). On any non-UTC device, your own messages showed local time and received/history messages showed UTC within the same conversation. Add .toLocal() at the history-load + incoming-WS parse sites in both apps so bubbles match the optimistic path and the transcript view. Session timer math was already tz-safe (Dart .difference uses absolute instants). Co-Authored-By: Claude Opus 4.8 (1M context) --- client_app/lib/core/chat/chat_notifier.dart | 7 +++++-- mitra_app/lib/core/chat/mitra_chat_notifier.dart | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/client_app/lib/core/chat/chat_notifier.dart b/client_app/lib/core/chat/chat_notifier.dart index 046cc18..ebd3660 100644 --- a/client_app/lib/core/chat/chat_notifier.dart +++ b/client_app/lib/core/chat/chat_notifier.dart @@ -245,7 +245,9 @@ class Chat extends _$Chat { content: m['content'] as String, type: m['type'] as String? ?? MessageType.text, status: m['status'] as String? ?? MessageStatus.sent, - createdAt: DateTime.parse(m['created_at'] as String), + // Server sends UTC (ISO-8601 with Z); render in device-local time so + // bubbles match optimistic sends (DateTime.now()) + the transcript view. + createdAt: DateTime.parse(m['created_at'] as String).toLocal(), )).toList(); final token = ref.read(authBridgeProvider).accessToken; @@ -351,7 +353,8 @@ class Chat extends _$Chat { content: data['content'] as String, type: data['message_type'] as String? ?? MessageType.text, status: MessageStatus.sent, - createdAt: DateTime.parse(data['created_at'] as String), + // UTC from server → device-local for display (see history-load note). + createdAt: DateTime.parse(data['created_at'] as String).toLocal(), ); state = current.copyWith(messages: [...current.messages, msg]); markDelivered([msg.id]); diff --git a/mitra_app/lib/core/chat/mitra_chat_notifier.dart b/mitra_app/lib/core/chat/mitra_chat_notifier.dart index 989db26..a66d157 100644 --- a/mitra_app/lib/core/chat/mitra_chat_notifier.dart +++ b/mitra_app/lib/core/chat/mitra_chat_notifier.dart @@ -214,7 +214,9 @@ class MitraChat extends _$MitraChat { content: m['content'] as String, type: m['type'] as String? ?? MessageType.text, status: m['status'] as String? ?? MessageStatus.sent, - createdAt: DateTime.parse(m['created_at'] as String), + // Server sends UTC (ISO-8601 with Z); render in device-local time so + // bubbles match optimistic sends (DateTime.now()) + the transcript view. + createdAt: DateTime.parse(m['created_at'] as String).toLocal(), )).toList(); final token = ref.read(authBridgeProvider).accessToken; @@ -329,7 +331,8 @@ class MitraChat extends _$MitraChat { content: data['content'] as String, type: data['message_type'] as String? ?? MessageType.text, status: MessageStatus.sent, - createdAt: DateTime.parse(data['created_at'] as String), + // UTC from server → device-local for display (see history-load note). + createdAt: DateTime.parse(data['created_at'] as String).toLocal(), ); state = current.copyWith(messages: [...current.messages, msg]); markDelivered([msg.id]);