Mitra chat: real customer name + ticking timer + goodbye pill

Four small fixes on the mitra chat screen, all surfacing through the
chat connected-state.

1. AppBar customer name. The hardcoded "Customer" only ever came from
   the FCM-tap navigation fallback (notification_service:
   `extra: {'customerName': 'Customer'}`); the popup-overlay path passes
   the real name but FCM had no way to know it. /chat/:sessionId/info
   already returns `customer_display_name` — propagate it into
   MitraChatConnectedData and read in the AppBar via .select. Falls
   back to the route arg for the brief connecting window.

2. SISA WAKTU stuck at --:--. The pill watches a remaining-seconds
   provider that's only updated by backend WS frames. Backend only
   fires session_timer at 3-min + 1-min warnings + expiry, so the pill
   sat at --:-- for the first ~7 minutes of a 10-min chat. Added a
   local 1s ticker in the notifier that drives the provider against
   expires_at (also pulled from /info). WS warning frames still
   overwrite normally on top.

3. Pesan Penutup textbox. Replaced the rounded-rect OutlineInputBorder
   field with a fixed-height Container pill whose border matches the
   previous "shadow" tone (HaloTokens.border). Pill borderRadius is
   the full 100 (was 12).

4. Goodbye textbox text was top-aligned because maxLines: 3 +
   OutlineInputBorder left vertical alignment to InputDecoration's
   built-in padding. Switched to maxLines: 1 + textAlignVertical:
   center + isCollapsed: true inside the fixed-height container —
   text now sits on the vertical center.

Bonus: the goodbye subhead "Tuliskan pesan terakhirmu untuk Customer"
also picked up the real name ("…untuk Andi Pratama").

Verified end-to-end on emulator-5556 (TestMitra-1501 + customer
"Andi Pratama"): AppBar shows Andi Pratama, SISA WAKTU ticks (04:57 →
04:35 across screenshots), goodbye pill renders with centered hint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-21 21:09:00 +08:00
parent a8c20d929e
commit 82c9b1eee8
2 changed files with 105 additions and 19 deletions

View File

@@ -61,6 +61,12 @@ class MitraChatConnectedData extends MitraChatData {
final List<String> topics;
// Phase 4 — voice-call mode badge in header. `chat` is the default (no pill).
final SessionMode mode;
// Customer display name + session deadline pulled from /info on connect.
// AppBar reads displayName (falls back to route-arg "Customer" when null);
// the timer pill ticks down against expiresAt via a local 1s ticker so it
// doesn't sit at --:-- waiting for a server warning frame.
final String? customerDisplayName;
final DateTime? expiresAt;
const MitraChatConnectedData({
required this.messages,
@@ -73,6 +79,8 @@ class MitraChatConnectedData extends MitraChatData {
this.topicSensitivity = TopicSensitivity.regular,
this.topics = const [],
this.mode = SessionMode.chat,
this.customerDisplayName,
this.expiresAt,
});
MitraChatConnectedData copyWith({
@@ -87,6 +95,8 @@ class MitraChatConnectedData extends MitraChatData {
TopicSensitivity? topicSensitivity,
List<String>? topics,
SessionMode? mode,
String? customerDisplayName,
DateTime? expiresAt,
}) {
return MitraChatConnectedData(
messages: messages ?? this.messages,
@@ -99,6 +109,8 @@ class MitraChatConnectedData extends MitraChatData {
topicSensitivity: topicSensitivity ?? this.topicSensitivity,
topics: topics ?? this.topics,
mode: mode ?? this.mode,
customerDisplayName: customerDisplayName ?? this.customerDisplayName,
expiresAt: expiresAt ?? this.expiresAt,
);
}
}
@@ -143,6 +155,11 @@ class MitraChat extends _$MitraChat {
WebSocketChannel? _channel;
StreamSubscription? _wsSubscription;
Timer? _typingTimer;
// Local 1s ticker that updates mitraChatRemainingSecondsProvider against
// the session's expires_at. Backend's session_timer WS frames only fire
// at 3min + 1min warnings + expiry — without this ticker the pill stays
// at --:-- for most of the session.
Timer? _countdownTimer;
// Survives `disconnect()` so a later `didChangeAppLifecycleState(resumed)`
// can re-issue `connect(sessionId)` with the right session — disconnect()
// resets `state` to MitraChatInitialData, which is otherwise the only
@@ -178,6 +195,9 @@ class MitraChat extends _$MitraChat {
final espTopics = rawTopics is List
? rawTopics.whereType<String>().toList(growable: false)
: const <String>[];
final customerDisplayName = sessionData?['customer_display_name'] as String?;
final expiresAtRaw = sessionData?['expires_at'] as String?;
final expiresAt = expiresAtRaw == null ? null : DateTime.tryParse(expiresAtRaw);
// If the customer requested an extension while we were off-WS (e.g.
// mitra on Home, app backgrounded, FCM tap cold-started us here),
@@ -230,7 +250,13 @@ class MitraChat extends _$MitraChat {
topics: espTopics,
mode: sessionMode,
extensionRequest: pendingExt,
customerDisplayName: customerDisplayName,
expiresAt: expiresAt,
);
// Kick off the 1s ticker if we have a deadline. Updates the auto-dispose
// remaining-seconds provider used by the timer pill.
if (expiresAt != null) _startCountdownTicker(expiresAt);
} catch (e) {
state = const MitraChatErrorData('Gagal terhubung ke chat.');
}
@@ -413,5 +439,23 @@ class MitraChat extends _$MitraChat {
_channel = null;
_typingTimer?.cancel();
_typingTimer = null;
_countdownTimer?.cancel();
_countdownTimer = null;
}
/// Drives the remaining-seconds provider every second against [deadline].
/// Backend session_timer WS frames still arrive (3min/1min warnings,
/// expiry) — they overwrite via the existing _onMessageReceived handler.
/// On extension accept the connect() flow re-runs (sessionData refreshes)
/// and the new deadline restarts the ticker.
void _startCountdownTicker(DateTime deadline) {
_countdownTimer?.cancel();
void tick() {
final remaining = deadline.difference(DateTime.now()).inSeconds;
final clamped = remaining < 0 ? 0 : remaining;
ref.read(mitraChatRemainingSecondsProvider.notifier).update(clamped);
}
tick(); // initial paint — don't wait a second for the first value
_countdownTimer = Timer.periodic(const Duration(seconds: 1), (_) => tick());
}
}