import 'dart:async'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../domain/entities/chat.dart'; import '../../domain/entities/message.dart'; import '../../domain/repositories/chat_repository.dart'; import 'chat_event.dart'; import 'chat_state.dart'; class ChatBloc extends Bloc { final ChatRepository chatRepository; StreamSubscription? _messageSubscription; StreamSubscription? _typingSubscription; ChatBloc({required this.chatRepository}) : super(const ChatState.initial()) { on(_onStarted); on(_onChatsLoaded); on(_onChatSelected); on(_onMessagesRequested); on(_onMessageReceived); on(_onMessageSent); on(_onFavoritesRequested); on(_onTypingUpdated); on(_onSendTypingStatus); on(_onCacheCleared); _messageSubscription = chatRepository.messageStream.listen((message) { add(ChatEvent.messageReceived(message)); }); _typingSubscription = chatRepository.typingStream.listen((data) { add(ChatEvent.typingUpdated(data['chatId'], data['userId'], data['isTyping'])); }); } Future _onStarted(ChatEventStarted event, Emitter emit) async { // ignore: avoid_print print('[DEBUG] ChatBloc._onStarted for user: ${event.userId}'); // Start SignalR in background chatRepository.initSignalR().catchError((e) { // Log or handle SignalR init error }); // Immediately trigger chats loading add(ChatEvent.chatsLoaded(currentUserId: event.userId)); } Future _onChatsLoaded(ChatEventChatsLoaded event, Emitter emit) async { emit(const ChatState.loading()); final result = await chatRepository.getChats(currentUserId: event.currentUserId); result.fold( (error) => emit(ChatState.error(error.message ?? 'Failed to load chats')), (chats) => emit(ChatState.chatsLoaded(chats)), ); } Future _onChatSelected(ChatEventChatSelected event, Emitter emit) async { final currentChatId = state.maybeMap( chatSelected: (s) => s.chat.id, messagesLoaded: (s) => s.chat.id, orElse: () => null, ); if (currentChatId == event.chatId) { // Already in this chat, just refresh messages final messagesResult = await chatRepository.getMessages(event.chatId); messagesResult.fold( (error) => null, (messages) { final currentChat = state.maybeMap( chatSelected: (s) => s.chat, messagesLoaded: (s) => s.chat, orElse: () => null, ); if (currentChat != null) { emit(ChatState.messagesLoaded(currentChat, messages, typingUsers: state.typingUsers)); } }, ); return; } emit(ChatState.loading(typingUsers: state.typingUsers)); final chatResult = await chatRepository.getChatById(event.chatId); final messagesResult = await chatRepository.getMessages(event.chatId); chatResult.fold( (error) => emit(ChatState.error(error.message ?? 'Failed to load chat', typingUsers: state.typingUsers)), (chat) { messagesResult.fold( (error) => emit(ChatState.chatSelected(chat, [], typingUsers: state.typingUsers)), (messages) => emit(ChatState.chatSelected(chat, messages, typingUsers: state.typingUsers)), ); }, ); } Future _onMessagesRequested(ChatEventMessagesRequested event, Emitter emit) async { final currentChat = state.maybeMap( chatSelected: (s) => s.chat, messagesLoaded: (s) => s.chat, orElse: () => null, ); // Only fetch if it's the current chat or we are in chats list final result = await chatRepository.getMessages(event.chatId); result.fold( (error) => null, // Don't emit error state for background updates to avoid UI flickering (messages) { if (currentChat != null && currentChat.id == event.chatId) { // If we are in the chat, update messages and switch to messagesLoaded (stable state) emit(ChatState.messagesLoaded(currentChat, messages, typingUsers: state.typingUsers)); } // If we are in the list, the list will update via local cache anyway or on next fetch }, ); } Future _onMessageReceived(ChatEventMessageReceived event, Emitter emit) async { final message = event.message; final currentChat = state.maybeMap( chatSelected: (s) => s.chat, messagesLoaded: (s) => s.chat, orElse: () => null, ); if (currentChat != null && currentChat.id == message.chatId) { final List currentMessages = state.maybeMap( chatSelected: (s) => s.messages, messagesLoaded: (s) => s.messages, orElse: () => [], ); // Check for duplicates if (currentMessages.any((m) => m.id == message.id)) return; final updatedMessages = List.from(currentMessages)..insert(0, message); // Ensure sorting if needed (usually messages are already in order from stream) emit(ChatState.messagesLoaded(currentChat, updatedMessages, typingUsers: state.typingUsers)); } // Also trigger chats refresh if we are in chatsLoaded state to update last message/unread state.maybeMap( chatsLoaded: (_) => add(ChatEvent.chatsLoaded(currentUserId: currentChat?.participants?.firstWhere((p) => p.id != '').id)), // userId logic might need to be better orElse: () => null, ); } Future _onMessageSent(ChatEventMessageSent event, Emitter emit) async { final result = await chatRepository.sendMessage(event.chatId, event.content); result.fold( (error) => emit(ChatState.error(error.message ?? 'Failed to send message', typingUsers: state.typingUsers)), (_) { // Optimistically or via refresh add(ChatEvent.messagesRequested(event.chatId)); }, ); } Future _onFavoritesRequested(ChatEventFavoritesRequested event, Emitter emit) async { emit(ChatState.loading(typingUsers: state.typingUsers)); final result = await chatRepository.getOrCreateFavorites(); result.fold( (error) => emit(ChatState.error(error.message ?? 'Failed to open Favorites', typingUsers: state.typingUsers)), (chat) => add(ChatEvent.chatSelected(chat.id)), ); } Future _onTypingUpdated(ChatEventTypingUpdated event, Emitter emit) async { final Map> newTypingUsers = Map.from(state.typingUsers); final Set chatTypingUsers = Set.from(newTypingUsers[event.chatId] ?? {}); if (event.isTyping) { chatTypingUsers.add(event.userId); } else { chatTypingUsers.remove(event.userId); } if (chatTypingUsers.isEmpty) { newTypingUsers.remove(event.chatId); } else { newTypingUsers[event.chatId] = chatTypingUsers; } state.maybeMap( chatSelected: (s) { // If it's the current chat, transition to stable state if (s.chat.id == event.chatId) { emit(ChatState.messagesLoaded(s.chat, s.messages, typingUsers: newTypingUsers)); } else { emit(s.copyWith(typingUsers: newTypingUsers)); } }, messagesLoaded: (s) => emit(s.copyWith(typingUsers: newTypingUsers)), chatsLoaded: (s) => emit(s.copyWith(typingUsers: newTypingUsers)), orElse: () => emit(state.copyWith(typingUsers: newTypingUsers)), ); } Future _onSendTypingStatus(ChatEventSendTypingStatus event, Emitter emit) async { await chatRepository.sendTypingStatus(event.chatId, event.isTyping); } Future _onCacheCleared(ChatEventCacheCleared event, Emitter emit) async { // ignore: avoid_print print('[DEBUG] ChatBloc._onCacheCleared START'); await chatRepository.clearCache(); // ignore: avoid_print print('[DEBUG] ChatBloc._onCacheCleared END'); emit(const ChatState.initial()); } @override Future close() { _messageSubscription?.cancel(); _typingSubscription?.cancel(); chatRepository.disposeSignalR(); return super.close(); } }