fix: improve chat message deduplication, add conversation refresh logic, and update UI components for consistency
This commit is contained in:
@@ -117,10 +117,8 @@ class _AboutCta {
|
|||||||
|
|
||||||
// ── Defaults ──
|
// ── Defaults ──
|
||||||
|
|
||||||
const _defaultStats = [
|
// Empty defaults — only CMS-provided content should render (matches web behavior)
|
||||||
_AboutStatItem(value: '15k+', label: 'Verified Agents'),
|
const _defaultStats = <_AboutStatItem>[];
|
||||||
_AboutStatItem(value: '98%', label: 'Customer Satisfaction'),
|
|
||||||
];
|
|
||||||
|
|
||||||
const _defaultFeatures = [
|
const _defaultFeatures = [
|
||||||
_AboutFeatureItem(
|
_AboutFeatureItem(
|
||||||
|
|||||||
@@ -1651,12 +1651,13 @@ class _AgentEditProfileScreenState
|
|||||||
|
|
||||||
await showModalBottomSheet<void>(
|
await showModalBottomSheet<void>(
|
||||||
context: context,
|
context: context,
|
||||||
|
useRootNavigator: true,
|
||||||
isScrollControlled: true,
|
isScrollControlled: true,
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: Colors.white,
|
||||||
shape: const RoundedRectangleBorder(
|
shape: const RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||||
),
|
),
|
||||||
builder: (ctx) {
|
builder: (sheetCtx) {
|
||||||
return StatefulBuilder(
|
return StatefulBuilder(
|
||||||
builder: (ctx, setSheetState) {
|
builder: (ctx, setSheetState) {
|
||||||
final filtered = options.where((o) {
|
final filtered = options.where((o) {
|
||||||
@@ -1754,7 +1755,7 @@ class _AgentEditProfileScreenState
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: OutlinedButton(
|
child: OutlinedButton(
|
||||||
onPressed: () =>
|
onPressed: () =>
|
||||||
Navigator.pop(ctx),
|
Navigator.of(ctx).pop(),
|
||||||
style: OutlinedButton.styleFrom(
|
style: OutlinedButton.styleFrom(
|
||||||
padding:
|
padding:
|
||||||
const EdgeInsets.symmetric(
|
const EdgeInsets.symmetric(
|
||||||
@@ -1772,7 +1773,7 @@ class _AgentEditProfileScreenState
|
|||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
onChanged(workingSelected);
|
onChanged(workingSelected);
|
||||||
Navigator.pop(ctx);
|
Navigator.of(ctx).pop();
|
||||||
},
|
},
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
|
|||||||
@@ -585,6 +585,24 @@ class MessagingNotifier extends StateNotifier<MessagingState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Refetch a single conversation — used when opening chat to get fresh
|
||||||
|
/// presence (isOnline/lastSeenAt) since the list may be stale. Mirrors
|
||||||
|
/// web's selectConversation which calls getConversation on open.
|
||||||
|
Future<void> refreshConversation(String conversationId) async {
|
||||||
|
try {
|
||||||
|
final fresh = await _repository.getConversation(conversationId);
|
||||||
|
if (!mounted) return;
|
||||||
|
final updated = state.conversations.map((c) {
|
||||||
|
return c.id == conversationId
|
||||||
|
? c.copyWith(otherParty: fresh.otherParty)
|
||||||
|
: c;
|
||||||
|
}).toList();
|
||||||
|
state = state.copyWith(conversations: updated);
|
||||||
|
} catch (_) {
|
||||||
|
// Non-fatal — existing cached data remains in place
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> loadUnreadCount() async {
|
Future<void> loadUnreadCount() async {
|
||||||
try {
|
try {
|
||||||
final count = await _repository.getUnreadCount();
|
final count = await _repository.getUnreadCount();
|
||||||
|
|||||||
@@ -54,6 +54,9 @@ class _ChatScreenState extends ConsumerState<ChatScreen>
|
|||||||
final notifier = ref.read(messagingProvider.notifier);
|
final notifier = ref.read(messagingProvider.notifier);
|
||||||
notifier.setActiveConversation(widget.conversationId);
|
notifier.setActiveConversation(widget.conversationId);
|
||||||
notifier.loadMessages(widget.conversationId);
|
notifier.loadMessages(widget.conversationId);
|
||||||
|
// Refetch conversation to get fresh presence (isOnline/lastSeenAt) —
|
||||||
|
// the list snapshot may be stale. Matches web's selectConversation flow.
|
||||||
|
notifier.refreshConversation(widget.conversationId);
|
||||||
// Initialize mute/star state from conversation data
|
// Initialize mute/star state from conversation data
|
||||||
final conversations = ref.read(messagingProvider).conversations;
|
final conversations = ref.read(messagingProvider).conversations;
|
||||||
final conv = conversations.where((c) => c.id == widget.conversationId).firstOrNull;
|
final conv = conversations.where((c) => c.id == widget.conversationId).firstOrNull;
|
||||||
@@ -120,6 +123,9 @@ class _ChatScreenState extends ConsumerState<ChatScreen>
|
|||||||
});
|
});
|
||||||
// Refresh messages to catch anything missed while in background
|
// Refresh messages to catch anything missed while in background
|
||||||
ref.read(messagingProvider.notifier).loadMessages(widget.conversationId);
|
ref.read(messagingProvider.notifier).loadMessages(widget.conversationId);
|
||||||
|
// Refresh conversation to pick up any presence changes that happened
|
||||||
|
// while the app was backgrounded (socket status events were missed)
|
||||||
|
ref.read(messagingProvider.notifier).refreshConversation(widget.conversationId);
|
||||||
// Now that user is actively viewing, mark as read
|
// Now that user is actively viewing, mark as read
|
||||||
ref.read(messagingProvider.notifier).markAsRead(widget.conversationId);
|
ref.read(messagingProvider.notifier).markAsRead(widget.conversationId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,6 +77,9 @@ class SupportChatNotifier extends StateNotifier<SupportChatState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Track message IDs we've sent via REST to avoid socket echo duplicates
|
||||||
|
final Set<String> _sentMessageIds = {};
|
||||||
|
|
||||||
Future<void> sendMessage(String content) async {
|
Future<void> sendMessage(String content) async {
|
||||||
final chat = state.chat;
|
final chat = state.chat;
|
||||||
if (chat == null || content.trim().isEmpty) return;
|
if (chat == null || content.trim().isEmpty) return;
|
||||||
@@ -85,6 +88,7 @@ class SupportChatNotifier extends StateNotifier<SupportChatState> {
|
|||||||
state = state.copyWith(isSending: true);
|
state = state.copyWith(isSending: true);
|
||||||
try {
|
try {
|
||||||
final message = await _repo.sendMessage(chat.id, content.trim());
|
final message = await _repo.sendMessage(chat.id, content.trim());
|
||||||
|
_sentMessageIds.add(message.id);
|
||||||
// Dedup — socket broadcast may have already added this message
|
// Dedup — socket broadcast may have already added this message
|
||||||
if (!state.messages.any((m) => m.id == message.id)) {
|
if (!state.messages.any((m) => m.id == message.id)) {
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
@@ -121,7 +125,9 @@ class SupportChatNotifier extends StateNotifier<SupportChatState> {
|
|||||||
|
|
||||||
void addIncomingMessage(SupportMessage message) {
|
void addIncomingMessage(SupportMessage message) {
|
||||||
if (state.chat == null || message.chatId != state.chat!.id) return;
|
if (state.chat == null || message.chatId != state.chat!.id) return;
|
||||||
|
// Skip if already in list OR if we sent this via REST (socket echo)
|
||||||
if (state.messages.any((m) => m.id == message.id)) return;
|
if (state.messages.any((m) => m.id == message.id)) return;
|
||||||
|
if (_sentMessageIds.contains(message.id)) return;
|
||||||
|
|
||||||
state = state.copyWith(messages: [...state.messages, message]);
|
state = state.copyWith(messages: [...state.messages, message]);
|
||||||
_repo.markAsRead(state.chat!.id);
|
_repo.markAsRead(state.chat!.id);
|
||||||
|
|||||||
@@ -333,7 +333,17 @@ class _SupportChatScreenState extends ConsumerState<SupportChatScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildMessageArea(SupportChatState state, String currentUserId) {
|
Widget _buildMessageArea(SupportChatState state, String currentUserId) {
|
||||||
final groups = _groupByDate(state.messages);
|
// Filter out any messages with empty content (phantom/blank cards)
|
||||||
|
// and deduplicate by ID (socket echo + REST response race condition)
|
||||||
|
final seen = <String>{};
|
||||||
|
final cleanMessages = <SupportMessage>[];
|
||||||
|
for (final msg in state.messages) {
|
||||||
|
if (msg.content.trim().isEmpty) continue;
|
||||||
|
if (seen.contains(msg.id)) continue;
|
||||||
|
seen.add(msg.id);
|
||||||
|
cleanMessages.add(msg);
|
||||||
|
}
|
||||||
|
final groups = _groupByDate(cleanMessages);
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
color: const Color(0xFFF9F9F9),
|
color: const Color(0xFFF9F9F9),
|
||||||
|
|||||||
Reference in New Issue
Block a user