Phase 3: closing-overlay fix + goodbye-composer dedupe

Customer chat refreshSessionStatus now clears sessionExpired carryover so the
goodbye composer renders correctly when re-opening a closing session from
history. Backend /api/shared/chat/:id/info returns goodbye_submitted_by_me;
both apps suppress the composer for the side that has already submitted and
render an awaiting-banner view instead.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-27 13:43:19 +08:00
parent 05ab1e10df
commit 6801001b64
6 changed files with 157 additions and 10 deletions

View File

@@ -233,15 +233,23 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
// we mounted directly into a `closing` session (e.g. opened from history).
// The chatProvider listener can't catch this case because it only fires on
// transitions, not the current state at mount time.
final shouldShowGoodbye = closureState is ClosureShowGoodbyeData ||
closureState is ClosureSubmittingData ||
(state.sessionClosing &&
!state.sessionExpired &&
closureState is! ClosureCompleteData);
// Suppress when the customer has already submitted their goodbye — the
// session can stay in `closing` while waiting for the mitra to submit
// their own message or for the 5-min grace timer to auto-complete.
final shouldShowGoodbye = !state.goodbyeSubmitted &&
(closureState is ClosureShowGoodbyeData ||
closureState is ClosureSubmittingData ||
(state.sessionClosing &&
!state.sessionExpired &&
closureState is! ClosureCompleteData));
if (shouldShowGoodbye) {
return _buildGoodbyeView(closureState);
}
if (state.sessionClosing && state.goodbyeSubmitted) {
return _buildAwaitingMitraGoodbyeView(state);
}
if (state.sessionPaused) {
return _buildPausedView();
}
@@ -496,4 +504,54 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
),
);
}
Widget _buildAwaitingMitraGoodbyeView(ChatConnectedData state) {
return Stack(
children: [
Positioned.fill(
child: Container(
color: _kBgTint,
child: Image.asset(
'assets/images/chat_pattern.png',
repeat: ImageRepeat.repeat,
fit: BoxFit.none,
),
),
),
Column(
children: [
Container(
width: double.infinity,
color: _kEndedBannerColor,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: const Row(
children: [
Icon(Icons.hourglass_top, color: _kEndedBannerText, size: 20),
SizedBox(width: 8),
Expanded(
child: Text(
'Pesan penutupmu sudah terkirim. Menunggu Bestie...',
style: TextStyle(color: _kEndedBannerText, fontWeight: FontWeight.w600),
),
),
],
),
),
Expanded(
child: ListView.builder(
controller: _scrollController,
padding: const EdgeInsets.all(16),
itemCount: state.messages.length,
itemBuilder: (context, index) {
final msg = state.messages[index];
final isMe = msg.senderType == UserType.customer;
return _buildMessageBubble(msg, isMe);
},
),
),
],
),
],
);
}
}