- Remove 60+ debug print statements from services and screens - Sanitize error messages to not expose internal API details - Remove stack trace exposure in beacon_scan_screen - Delete unused order_home_screen.dart - Remove unused ChatScreen route from app_router - Fix widget_test.dart to compile - Remove unused foundation.dart import from menu_browse_screen Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
267 lines
6.3 KiB
Dart
267 lines
6.3 KiB
Dart
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<ChatMessage> _messageController =
|
|
StreamController<ChatMessage>.broadcast();
|
|
final StreamController<TypingEvent> _typingController =
|
|
StreamController<TypingEvent>.broadcast();
|
|
final StreamController<ChatEvent> _eventController =
|
|
StreamController<ChatEvent>.broadcast();
|
|
|
|
bool _isConnected = false;
|
|
|
|
/// Stream of incoming messages
|
|
Stream<ChatMessage> get messages => _messageController.stream;
|
|
|
|
/// Stream of typing events
|
|
Stream<TypingEvent> get typingEvents => _typingController.stream;
|
|
|
|
/// Stream of chat events (user joined/left, chat ended)
|
|
Stream<ChatEvent> 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<bool> 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<bool>();
|
|
|
|
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<String, dynamic>);
|
|
_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<int> 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<void> 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,
|
|
});
|
|
}
|