import 'dart:async'; import 'package:socket_io_client/socket_io_client.dart' as io; import '../models/chat_message.dart'; import 'api.dart'; /// Service for managing real-time chat via WebSocket class ChatService { static const String _wsBaseUrl = 'https://app.payfrit.com:3001'; io.Socket? _socket; int? _currentTaskId; String? _userType; final StreamController _messageController = StreamController.broadcast(); final StreamController _typingController = StreamController.broadcast(); final StreamController _eventController = StreamController.broadcast(); bool _isConnected = false; /// Stream of incoming messages Stream get messages => _messageController.stream; /// Stream of typing events Stream get typingEvents => _typingController.stream; /// Stream of chat events (user joined/left, chat ended) Stream get events => _eventController.stream; /// Whether the socket is currently connected bool get isConnected => _isConnected; /// Current task ID int? get currentTaskId => _currentTaskId; /// Connect to chat for a specific task Future connect({ required int taskId, required String userToken, required String userType, }) async { // Disconnect if already connected to a different task if (_socket != null) { await disconnect(); } _currentTaskId = taskId; _userType = userType; final completer = Completer(); try { _socket = io.io( _wsBaseUrl, io.OptionBuilder() .setTransports(['websocket']) .disableAutoConnect() .build(), ); _socket!.onConnect((_) { _isConnected = true; // Join the chat room _socket!.emit('join-chat', { 'taskId': taskId, 'userToken': userToken, 'userType': userType, }); }); _socket!.on('joined', (data) { _eventController.add(ChatEvent( type: ChatEventType.joined, data: data, )); if (!completer.isCompleted) { completer.complete(true); } }); _socket!.on('error', (data) { _eventController.add(ChatEvent( type: ChatEventType.error, message: data['message'] ?? 'Unknown error', )); if (!completer.isCompleted) { completer.complete(false); } }); _socket!.on('new-message', (data) { final message = ChatMessage.fromJson(data as Map); _messageController.add(message); }); _socket!.on('user-typing', (data) { _typingController.add(TypingEvent( userType: data['userType'] ?? '', userName: data['userName'] ?? '', isTyping: data['isTyping'] ?? false, )); }); _socket!.on('user-joined', (data) { _eventController.add(ChatEvent( type: ChatEventType.userJoined, data: data, )); }); _socket!.on('user-left', (data) { _eventController.add(ChatEvent( type: ChatEventType.userLeft, data: data, )); }); _socket!.on('chat-ended', (data) { _eventController.add(ChatEvent( type: ChatEventType.chatEnded, message: data['message'] ?? 'Chat has ended', )); }); _socket!.onDisconnect((_) { _isConnected = false; _eventController.add(ChatEvent( type: ChatEventType.disconnected, )); }); _socket!.onConnectError((error) { _isConnected = false; if (!completer.isCompleted) { completer.complete(false); } }); _socket!.connect(); // Timeout after 10 seconds return completer.future.timeout( const Duration(seconds: 10), onTimeout: () => false, ); } catch (e) { return false; } } /// Send a message via WebSocket /// Returns true if message was sent, false if not connected bool sendMessage(String text) { if (_socket == null || !_isConnected || _currentTaskId == null) { return false; } _socket!.emit('send-message', { 'taskId': _currentTaskId, 'message': text, }); return true; } /// Send a message via HTTP (fallback) Future sendMessageHttp({ required int taskId, required String message, int? userId, String? senderType, }) async { return Api.sendChatMessage( taskId: taskId, message: message, userId: userId, senderType: senderType, ); } /// Notify that user is typing void setTyping(bool isTyping) { if (_socket == null || !_isConnected || _currentTaskId == null) return; _socket!.emit('typing', { 'taskId': _currentTaskId, 'isTyping': isTyping, }); } /// Close the chat (only workers can do this) void closeChat() { if (_socket == null || !_isConnected || _currentTaskId == null) return; _socket!.emit('chat-closed', { 'taskId': _currentTaskId, }); } /// Leave the chat room void leaveChat() { if (_socket == null || _currentTaskId == null) return; _socket!.emit('leave-chat', { 'taskId': _currentTaskId, }); } /// Disconnect from WebSocket Future disconnect() async { if (_socket != null) { leaveChat(); _socket!.disconnect(); _socket!.dispose(); _socket = null; } _isConnected = false; _currentTaskId = null; _userType = null; } /// Dispose the service and clean up streams void dispose() { disconnect(); _messageController.close(); _typingController.close(); _eventController.close(); } } /// Event for typing indicators class TypingEvent { final String userType; final String userName; final bool isTyping; const TypingEvent({ required this.userType, required this.userName, required this.isTyping, }); } /// Event type for chat state changes enum ChatEventType { joined, userJoined, userLeft, chatEnded, disconnected, error, } /// Event for chat state changes class ChatEvent { final ChatEventType type; final String? message; final dynamic data; const ChatEvent({ required this.type, this.message, this.data, }); }