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:
@@ -30,6 +30,7 @@ class ChatConnectedData extends ChatData {
|
||||
final bool sessionExpired;
|
||||
final bool sessionPaused;
|
||||
final bool sessionClosing;
|
||||
final bool goodbyeSubmitted;
|
||||
final Map<String, dynamic>? extensionResponse;
|
||||
|
||||
const ChatConnectedData({
|
||||
@@ -39,6 +40,7 @@ class ChatConnectedData extends ChatData {
|
||||
this.sessionExpired = false,
|
||||
this.sessionPaused = false,
|
||||
this.sessionClosing = false,
|
||||
this.goodbyeSubmitted = false,
|
||||
this.extensionResponse,
|
||||
});
|
||||
|
||||
@@ -49,6 +51,7 @@ class ChatConnectedData extends ChatData {
|
||||
bool? sessionExpired,
|
||||
bool? sessionPaused,
|
||||
bool? sessionClosing,
|
||||
bool? goodbyeSubmitted,
|
||||
Map<String, dynamic>? extensionResponse,
|
||||
}) {
|
||||
return ChatConnectedData(
|
||||
@@ -58,6 +61,7 @@ class ChatConnectedData extends ChatData {
|
||||
sessionExpired: sessionExpired ?? this.sessionExpired,
|
||||
sessionPaused: sessionPaused ?? this.sessionPaused,
|
||||
sessionClosing: sessionClosing ?? this.sessionClosing,
|
||||
goodbyeSubmitted: goodbyeSubmitted ?? this.goodbyeSubmitted,
|
||||
extensionResponse: extensionResponse ?? this.extensionResponse,
|
||||
);
|
||||
}
|
||||
@@ -150,8 +154,12 @@ class Chat extends _$Chat {
|
||||
state = current.copyWith(sessionExpired: true);
|
||||
return;
|
||||
}
|
||||
final goodbyeSubmittedByMe = data?['goodbye_submitted_by_me'] as bool? ?? false;
|
||||
state = current.copyWith(
|
||||
sessionClosing: status == SessionStatus.closing,
|
||||
sessionPaused: status == SessionStatus.extending,
|
||||
sessionExpired: false,
|
||||
goodbyeSubmitted: goodbyeSubmittedByMe,
|
||||
);
|
||||
} catch (e) {
|
||||
// ignore: avoid_print
|
||||
@@ -175,6 +183,7 @@ class Chat extends _$Chat {
|
||||
}
|
||||
|
||||
final isClosing = sessionStatus == SessionStatus.closing;
|
||||
final goodbyeSubmittedByMe = sessionData?['goodbye_submitted_by_me'] as bool? ?? false;
|
||||
|
||||
final response = await _apiClient.get('/api/shared/chat/$sessionId/messages');
|
||||
final messagesData = response['data'] as List<dynamic>;
|
||||
@@ -215,6 +224,7 @@ class Chat extends _$Chat {
|
||||
state = ChatConnectedData(
|
||||
messages: messages,
|
||||
sessionClosing: isClosing,
|
||||
goodbyeSubmitted: goodbyeSubmittedByMe,
|
||||
);
|
||||
} catch (e) {
|
||||
state = const ChatErrorData('Gagal terhubung ke chat.');
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user