feat: Implement messaging functionality with conversation and chat screens, and enhance agent detail views.
This commit is contained in:
@@ -0,0 +1,299 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
import 'package:real_estate_mobile/features/auth/presentation/providers/auth_provider.dart';
|
||||
import 'package:real_estate_mobile/features/messaging/data/models/messaging_models.dart';
|
||||
import 'package:real_estate_mobile/features/messaging/data/messaging_repository.dart';
|
||||
|
||||
final _log = Logger(printer: PrettyPrinter(methodCount: 0));
|
||||
|
||||
class MessagingState {
|
||||
final List<Conversation> conversations;
|
||||
final Map<String, List<ChatMessage>> messages;
|
||||
final Map<String, PaginationInfo> pagination;
|
||||
final Set<String> typingUsers;
|
||||
final bool isLoadingConversations;
|
||||
final bool isLoadingMessages;
|
||||
final bool isSending;
|
||||
final String? error;
|
||||
final String? activeConversationId;
|
||||
final int unreadCount;
|
||||
|
||||
const MessagingState({
|
||||
this.conversations = const [],
|
||||
this.messages = const {},
|
||||
this.pagination = const {},
|
||||
this.typingUsers = const {},
|
||||
this.isLoadingConversations = false,
|
||||
this.isLoadingMessages = false,
|
||||
this.isSending = false,
|
||||
this.error,
|
||||
this.activeConversationId,
|
||||
this.unreadCount = 0,
|
||||
});
|
||||
|
||||
MessagingState copyWith({
|
||||
List<Conversation>? conversations,
|
||||
Map<String, List<ChatMessage>>? messages,
|
||||
Map<String, PaginationInfo>? pagination,
|
||||
Set<String>? typingUsers,
|
||||
bool? isLoadingConversations,
|
||||
bool? isLoadingMessages,
|
||||
bool? isSending,
|
||||
String? error,
|
||||
String? activeConversationId,
|
||||
int? unreadCount,
|
||||
bool clearError = false,
|
||||
bool clearActiveConversation = false,
|
||||
}) {
|
||||
return MessagingState(
|
||||
conversations: conversations ?? this.conversations,
|
||||
messages: messages ?? this.messages,
|
||||
pagination: pagination ?? this.pagination,
|
||||
typingUsers: typingUsers ?? this.typingUsers,
|
||||
isLoadingConversations:
|
||||
isLoadingConversations ?? this.isLoadingConversations,
|
||||
isLoadingMessages: isLoadingMessages ?? this.isLoadingMessages,
|
||||
isSending: isSending ?? this.isSending,
|
||||
error: clearError ? null : (error ?? this.error),
|
||||
activeConversationId: clearActiveConversation
|
||||
? null
|
||||
: (activeConversationId ?? this.activeConversationId),
|
||||
unreadCount: unreadCount ?? this.unreadCount,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MessagingNotifier extends StateNotifier<MessagingState> {
|
||||
final MessagingRepository _repository;
|
||||
final Ref _ref;
|
||||
|
||||
MessagingNotifier(this._repository, this._ref)
|
||||
: super(const MessagingState());
|
||||
|
||||
String? get _currentUserId => _ref.read(authProvider).user?.id;
|
||||
|
||||
Future<void> loadConversations() async {
|
||||
state = state.copyWith(isLoadingConversations: true, clearError: true);
|
||||
try {
|
||||
final result = await _repository.getConversations();
|
||||
state = state.copyWith(
|
||||
conversations: result,
|
||||
isLoadingConversations: false,
|
||||
);
|
||||
} catch (e, stack) {
|
||||
_log.e('Failed to load conversations', error: e, stackTrace: stack);
|
||||
state = state.copyWith(
|
||||
isLoadingConversations: false,
|
||||
error: 'Failed to load conversations',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loadMessages(
|
||||
String conversationId, {
|
||||
bool loadMore = false,
|
||||
}) async {
|
||||
if (loadMore) {
|
||||
final existing = state.pagination[conversationId];
|
||||
if (existing != null && !existing.hasMore) return;
|
||||
}
|
||||
|
||||
state = state.copyWith(isLoadingMessages: true, clearError: true);
|
||||
|
||||
try {
|
||||
final nextPage =
|
||||
loadMore ? ((state.pagination[conversationId]?.page ?? 0) + 1) : 1;
|
||||
|
||||
final result = await _repository.getMessages(
|
||||
conversationId,
|
||||
page: nextPage,
|
||||
);
|
||||
|
||||
final newMessages = result.messages;
|
||||
final paginationInfo = result.pagination;
|
||||
|
||||
final existingMessages =
|
||||
loadMore ? (state.messages[conversationId] ?? []) : <ChatMessage>[];
|
||||
final mergedMessages =
|
||||
loadMore ? [...existingMessages, ...newMessages] : newMessages;
|
||||
|
||||
final updatedMessages =
|
||||
Map<String, List<ChatMessage>>.from(state.messages)
|
||||
..[conversationId] = mergedMessages;
|
||||
|
||||
final updatedPagination =
|
||||
Map<String, PaginationInfo>.from(state.pagination)
|
||||
..[conversationId] = paginationInfo;
|
||||
|
||||
state = state.copyWith(
|
||||
messages: updatedMessages,
|
||||
pagination: updatedPagination,
|
||||
isLoadingMessages: false,
|
||||
);
|
||||
} catch (_) {
|
||||
state = state.copyWith(
|
||||
isLoadingMessages: false,
|
||||
error: 'Failed to load messages',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> sendMessage(String conversationId, String content) async {
|
||||
final currentUserId = _currentUserId;
|
||||
if (currentUserId == null) return;
|
||||
|
||||
final tempId = 'temp_${DateTime.now().millisecondsSinceEpoch}';
|
||||
final now = DateTime.now().toIso8601String();
|
||||
|
||||
final tempMessage = ChatMessage(
|
||||
id: tempId,
|
||||
conversationId: conversationId,
|
||||
senderId: currentUserId,
|
||||
content: content,
|
||||
messageType: MessageType.text,
|
||||
status: MessageStatus.sent,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
sender: MessageSender(
|
||||
id: currentUserId,
|
||||
role: 'user',
|
||||
),
|
||||
);
|
||||
|
||||
// Add optimistic message at the beginning (newest-first)
|
||||
final currentMessages =
|
||||
List<ChatMessage>.from(state.messages[conversationId] ?? []);
|
||||
currentMessages.insert(0, tempMessage);
|
||||
|
||||
final updatedMessages =
|
||||
Map<String, List<ChatMessage>>.from(state.messages)
|
||||
..[conversationId] = currentMessages;
|
||||
|
||||
state = state.copyWith(
|
||||
messages: updatedMessages,
|
||||
isSending: true,
|
||||
clearError: true,
|
||||
);
|
||||
|
||||
try {
|
||||
final realMessage = await _repository.sendMessage(
|
||||
conversationId,
|
||||
content: content,
|
||||
);
|
||||
|
||||
final messagesAfterSend =
|
||||
List<ChatMessage>.from(state.messages[conversationId] ?? []);
|
||||
final tempIndex = messagesAfterSend.indexWhere((m) => m.id == tempId);
|
||||
if (tempIndex != -1) {
|
||||
messagesAfterSend[tempIndex] = realMessage;
|
||||
}
|
||||
|
||||
final finalMessages =
|
||||
Map<String, List<ChatMessage>>.from(state.messages)
|
||||
..[conversationId] = messagesAfterSend;
|
||||
|
||||
// Update conversation's last message
|
||||
final updatedConversations = state.conversations.map((c) {
|
||||
if (c.id == conversationId) {
|
||||
return c.copyWith(
|
||||
lastMessageText: realMessage.content,
|
||||
lastMessageAt: realMessage.createdAt,
|
||||
);
|
||||
}
|
||||
return c;
|
||||
}).toList();
|
||||
|
||||
state = state.copyWith(
|
||||
messages: finalMessages,
|
||||
conversations: updatedConversations,
|
||||
isSending: false,
|
||||
);
|
||||
} catch (_) {
|
||||
final messagesAfterError =
|
||||
List<ChatMessage>.from(state.messages[conversationId] ?? []);
|
||||
messagesAfterError.removeWhere((m) => m.id == tempId);
|
||||
|
||||
final errorMessages =
|
||||
Map<String, List<ChatMessage>>.from(state.messages)
|
||||
..[conversationId] = messagesAfterError;
|
||||
|
||||
state = state.copyWith(
|
||||
messages: errorMessages,
|
||||
isSending: false,
|
||||
error: 'Failed to send message',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> markAsRead(String conversationId) async {
|
||||
try {
|
||||
await _repository.markAsRead(conversationId);
|
||||
|
||||
final updatedConversations = state.conversations.map((c) {
|
||||
if (c.id == conversationId) {
|
||||
return c.copyWith(unreadCount: 0);
|
||||
}
|
||||
return c;
|
||||
}).toList();
|
||||
|
||||
final totalUnread = updatedConversations.fold<int>(
|
||||
0,
|
||||
(sum, c) => sum + c.unreadCount,
|
||||
);
|
||||
|
||||
state = state.copyWith(
|
||||
conversations: updatedConversations,
|
||||
unreadCount: totalUnread,
|
||||
);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
void setActiveConversation(String? conversationId) {
|
||||
if (conversationId == null) {
|
||||
state = state.copyWith(clearActiveConversation: true);
|
||||
} else {
|
||||
state = state.copyWith(activeConversationId: conversationId);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loadUnreadCount() async {
|
||||
try {
|
||||
final count = await _repository.getUnreadCount();
|
||||
state = state.copyWith(unreadCount: count);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
Future<String?> startConversation(String agentProfileId) async {
|
||||
state = state.copyWith(isLoadingConversations: true, clearError: true);
|
||||
try {
|
||||
final conversation =
|
||||
await _repository.startConversation(agentProfileId);
|
||||
|
||||
final exists = state.conversations.any((c) => c.id == conversation.id);
|
||||
final updatedConversations = exists
|
||||
? state.conversations
|
||||
: [conversation, ...state.conversations];
|
||||
|
||||
state = state.copyWith(
|
||||
conversations: updatedConversations,
|
||||
isLoadingConversations: false,
|
||||
);
|
||||
return conversation.id;
|
||||
} catch (_) {
|
||||
state = state.copyWith(
|
||||
isLoadingConversations: false,
|
||||
error: 'Failed to start conversation',
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final messagingProvider =
|
||||
StateNotifierProvider<MessagingNotifier, MessagingState>((ref) {
|
||||
return MessagingNotifier(ref.watch(messagingRepositoryProvider), ref);
|
||||
});
|
||||
|
||||
final currentUserIdProvider = Provider<String?>((ref) {
|
||||
return ref.watch(authProvider).user?.id;
|
||||
});
|
||||
Reference in New Issue
Block a user