feat: Implement messaging functionality with conversation and chat screens, and enhance agent detail views.
This commit is contained in:
149
lib/features/messaging/data/messaging_repository.dart
Normal file
149
lib/features/messaging/data/messaging_repository.dart
Normal file
@@ -0,0 +1,149 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
import 'package:real_estate_mobile/core/constants/api_constants.dart';
|
||||
import 'package:real_estate_mobile/core/network/api_client.dart';
|
||||
import 'package:real_estate_mobile/core/network/api_exceptions.dart';
|
||||
import 'package:real_estate_mobile/features/messaging/data/models/messaging_models.dart';
|
||||
|
||||
final _log = Logger(printer: PrettyPrinter(methodCount: 0));
|
||||
|
||||
class MessagingRepository {
|
||||
final Dio _dio = ApiClient.instance.dio;
|
||||
|
||||
/// GET /messages/conversations
|
||||
Future<List<Conversation>> getConversations() async {
|
||||
try {
|
||||
final response = await _dio.get(ApiConstants.conversations);
|
||||
final data = response.data['data'] as List<dynamic>;
|
||||
return data
|
||||
.map((e) => Conversation.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
} on DioException catch (e) {
|
||||
if (e.error is ApiException) throw e.error as ApiException;
|
||||
throw ApiException(
|
||||
message: e.message ?? 'Failed to fetch conversations',
|
||||
statusCode: e.response?.statusCode,
|
||||
);
|
||||
} catch (e, stack) {
|
||||
_log.e('Conversation parsing error', error: e, stackTrace: stack);
|
||||
throw ApiException(message: 'Failed to parse conversations: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// POST /messages/conversations
|
||||
Future<Conversation> startConversation(String agentProfileId) async {
|
||||
try {
|
||||
final response = await _dio.post(
|
||||
ApiConstants.conversations,
|
||||
data: {'agentProfileId': agentProfileId},
|
||||
);
|
||||
return Conversation.fromJson(
|
||||
response.data['data'] as Map<String, dynamic>);
|
||||
} on DioException catch (e) {
|
||||
if (e.error is ApiException) throw e.error as ApiException;
|
||||
throw ApiException(
|
||||
message: e.message ?? 'Failed to start conversation',
|
||||
statusCode: e.response?.statusCode,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// GET /messages/conversations/:id
|
||||
Future<Conversation> getConversation(String id) async {
|
||||
try {
|
||||
final response = await _dio.get('${ApiConstants.conversations}/$id');
|
||||
return Conversation.fromJson(
|
||||
response.data['data'] as Map<String, dynamic>);
|
||||
} on DioException catch (e) {
|
||||
if (e.error is ApiException) throw e.error as ApiException;
|
||||
throw ApiException(
|
||||
message: e.message ?? 'Failed to fetch conversation',
|
||||
statusCode: e.response?.statusCode,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// GET /messages/conversations/:id/messages?page=&limit=
|
||||
Future<({List<ChatMessage> messages, PaginationInfo pagination})> getMessages(
|
||||
String conversationId, {
|
||||
int page = 1,
|
||||
int limit = 50,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _dio.get(
|
||||
'${ApiConstants.conversations}/$conversationId/messages',
|
||||
queryParameters: {'page': page, 'limit': limit},
|
||||
);
|
||||
final data = response.data['data'] as Map<String, dynamic>;
|
||||
final messages = (data['messages'] as List<dynamic>)
|
||||
.map((e) => ChatMessage.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
final pagination = PaginationInfo.fromJson(
|
||||
data['pagination'] as Map<String, dynamic>);
|
||||
return (messages: messages, pagination: pagination);
|
||||
} on DioException catch (e) {
|
||||
if (e.error is ApiException) throw e.error as ApiException;
|
||||
throw ApiException(
|
||||
message: e.message ?? 'Failed to fetch messages',
|
||||
statusCode: e.response?.statusCode,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// POST /messages/conversations/:id/messages
|
||||
Future<ChatMessage> sendMessage(
|
||||
String conversationId, {
|
||||
required String content,
|
||||
MessageType type = MessageType.text,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _dio.post(
|
||||
'${ApiConstants.conversations}/$conversationId/messages',
|
||||
data: {
|
||||
'content': content,
|
||||
'messageType': type.toApiString(),
|
||||
},
|
||||
);
|
||||
return ChatMessage.fromJson(
|
||||
response.data['data'] as Map<String, dynamic>);
|
||||
} on DioException catch (e) {
|
||||
if (e.error is ApiException) throw e.error as ApiException;
|
||||
throw ApiException(
|
||||
message: e.message ?? 'Failed to send message',
|
||||
statusCode: e.response?.statusCode,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// PATCH /messages/conversations/:id/read
|
||||
Future<void> markAsRead(String conversationId) async {
|
||||
try {
|
||||
await _dio.patch('${ApiConstants.conversations}/$conversationId/read');
|
||||
} on DioException catch (e) {
|
||||
if (e.error is ApiException) throw e.error as ApiException;
|
||||
throw ApiException(
|
||||
message: e.message ?? 'Failed to mark as read',
|
||||
statusCode: e.response?.statusCode,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// GET /messages/unread-count
|
||||
Future<int> getUnreadCount() async {
|
||||
try {
|
||||
final response = await _dio.get(ApiConstants.unreadCount);
|
||||
return response.data['data']['unreadCount'] as int? ?? 0;
|
||||
} on DioException catch (e) {
|
||||
if (e.error is ApiException) throw e.error as ApiException;
|
||||
throw ApiException(
|
||||
message: e.message ?? 'Failed to fetch unread count',
|
||||
statusCode: e.response?.statusCode,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final messagingRepositoryProvider = Provider<MessagingRepository>((ref) {
|
||||
return MessagingRepository();
|
||||
});
|
||||
3
lib/features/messaging/data/models/conversation.dart
Normal file
3
lib/features/messaging/data/models/conversation.dart
Normal file
@@ -0,0 +1,3 @@
|
||||
// Re-export from the combined messaging models file.
|
||||
export 'package:real_estate_mobile/features/messaging/data/models/messaging_models.dart'
|
||||
show OtherParty, Conversation;
|
||||
3
lib/features/messaging/data/models/message.dart
Normal file
3
lib/features/messaging/data/models/message.dart
Normal file
@@ -0,0 +1,3 @@
|
||||
// Re-export from the combined messaging models file.
|
||||
export 'package:real_estate_mobile/features/messaging/data/models/messaging_models.dart'
|
||||
show MessageType, MessageStatus, MessageSender, SenderProfile, ChatMessage, PaginationInfo;
|
||||
281
lib/features/messaging/data/models/messaging_models.dart
Normal file
281
lib/features/messaging/data/models/messaging_models.dart
Normal file
@@ -0,0 +1,281 @@
|
||||
enum MessageType {
|
||||
text,
|
||||
file,
|
||||
image,
|
||||
system;
|
||||
|
||||
static MessageType fromString(String? value) {
|
||||
switch (value?.toUpperCase()) {
|
||||
case 'FILE':
|
||||
return MessageType.file;
|
||||
case 'IMAGE':
|
||||
return MessageType.image;
|
||||
case 'SYSTEM':
|
||||
return MessageType.system;
|
||||
default:
|
||||
return MessageType.text;
|
||||
}
|
||||
}
|
||||
|
||||
String toApiString() => name.toUpperCase();
|
||||
}
|
||||
|
||||
enum MessageStatus {
|
||||
sent,
|
||||
delivered,
|
||||
read;
|
||||
|
||||
static MessageStatus fromString(String? value) {
|
||||
switch (value?.toUpperCase()) {
|
||||
case 'DELIVERED':
|
||||
return MessageStatus.delivered;
|
||||
case 'READ':
|
||||
return MessageStatus.read;
|
||||
default:
|
||||
return MessageStatus.sent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class OtherParty {
|
||||
final String id;
|
||||
final String? userId;
|
||||
final String name;
|
||||
final String? avatar;
|
||||
final String? headline;
|
||||
final bool isOnline;
|
||||
final String? lastSeenAt;
|
||||
|
||||
const OtherParty({
|
||||
required this.id,
|
||||
this.userId,
|
||||
required this.name,
|
||||
this.avatar,
|
||||
this.headline,
|
||||
this.isOnline = false,
|
||||
this.lastSeenAt,
|
||||
});
|
||||
|
||||
factory OtherParty.fromJson(Map<String, dynamic> json) {
|
||||
return OtherParty(
|
||||
id: json['id'] as String,
|
||||
userId: json['userId'] as String?,
|
||||
name: json['name'] as String? ?? '',
|
||||
avatar: json['avatar'] as String?,
|
||||
headline: json['headline'] as String?,
|
||||
isOnline: json['isOnline'] as bool? ?? false,
|
||||
lastSeenAt: json['lastSeenAt'] as String?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Conversation {
|
||||
final String id;
|
||||
final String userId;
|
||||
final String agentProfileId;
|
||||
final String? lastMessageAt;
|
||||
final String? lastMessageText;
|
||||
final int userUnreadCount;
|
||||
final int agentUnreadCount;
|
||||
final int unreadCount;
|
||||
final String createdAt;
|
||||
final String updatedAt;
|
||||
final OtherParty otherParty;
|
||||
final List<ChatMessage>? messages;
|
||||
|
||||
const Conversation({
|
||||
required this.id,
|
||||
required this.userId,
|
||||
required this.agentProfileId,
|
||||
this.lastMessageAt,
|
||||
this.lastMessageText,
|
||||
this.userUnreadCount = 0,
|
||||
this.agentUnreadCount = 0,
|
||||
this.unreadCount = 0,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
required this.otherParty,
|
||||
this.messages,
|
||||
});
|
||||
|
||||
factory Conversation.fromJson(Map<String, dynamic> json) {
|
||||
return Conversation(
|
||||
id: json['id'] as String,
|
||||
userId: json['userId'] as String,
|
||||
agentProfileId: json['agentProfileId'] as String,
|
||||
lastMessageAt: json['lastMessageAt'] as String?,
|
||||
lastMessageText: json['lastMessageText'] as String?,
|
||||
userUnreadCount: json['userUnreadCount'] as int? ?? 0,
|
||||
agentUnreadCount: json['agentUnreadCount'] as int? ?? 0,
|
||||
unreadCount: json['unreadCount'] as int? ?? 0,
|
||||
createdAt: json['createdAt'] as String,
|
||||
updatedAt: json['updatedAt'] as String,
|
||||
otherParty:
|
||||
OtherParty.fromJson(json['otherParty'] as Map<String, dynamic>),
|
||||
messages: (json['messages'] as List<dynamic>?)
|
||||
?.map((e) => ChatMessage.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Conversation copyWith({
|
||||
int? unreadCount,
|
||||
String? lastMessageAt,
|
||||
String? lastMessageText,
|
||||
OtherParty? otherParty,
|
||||
List<ChatMessage>? messages,
|
||||
}) {
|
||||
return Conversation(
|
||||
id: id,
|
||||
userId: userId,
|
||||
agentProfileId: agentProfileId,
|
||||
lastMessageAt: lastMessageAt ?? this.lastMessageAt,
|
||||
lastMessageText: lastMessageText ?? this.lastMessageText,
|
||||
userUnreadCount: userUnreadCount,
|
||||
agentUnreadCount: agentUnreadCount,
|
||||
unreadCount: unreadCount ?? this.unreadCount,
|
||||
createdAt: createdAt,
|
||||
updatedAt: updatedAt,
|
||||
otherParty: otherParty ?? this.otherParty,
|
||||
messages: messages ?? this.messages,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SenderProfile {
|
||||
final String? firstName;
|
||||
final String? lastName;
|
||||
final String? avatar;
|
||||
|
||||
const SenderProfile({this.firstName, this.lastName, this.avatar});
|
||||
|
||||
factory SenderProfile.fromJson(Map<String, dynamic> json) {
|
||||
return SenderProfile(
|
||||
firstName: json['firstName'] as String?,
|
||||
lastName: json['lastName'] as String?,
|
||||
avatar: json['avatar'] as String?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MessageSender {
|
||||
final String id;
|
||||
final String role;
|
||||
final SenderProfile? userProfile;
|
||||
final SenderProfile? agentProfile;
|
||||
|
||||
const MessageSender({
|
||||
required this.id,
|
||||
required this.role,
|
||||
this.userProfile,
|
||||
this.agentProfile,
|
||||
});
|
||||
|
||||
String get displayName {
|
||||
final profile = userProfile ?? agentProfile;
|
||||
if (profile == null) return 'Unknown';
|
||||
return '${profile.firstName ?? ''} ${profile.lastName ?? ''}'.trim();
|
||||
}
|
||||
|
||||
String? get avatar => userProfile?.avatar ?? agentProfile?.avatar;
|
||||
|
||||
factory MessageSender.fromJson(Map<String, dynamic> json) {
|
||||
return MessageSender(
|
||||
id: json['id'] as String,
|
||||
role: json['role'] as String? ?? '',
|
||||
userProfile: json['userProfile'] != null
|
||||
? SenderProfile.fromJson(
|
||||
json['userProfile'] as Map<String, dynamic>)
|
||||
: null,
|
||||
agentProfile: json['agentProfile'] != null
|
||||
? SenderProfile.fromJson(
|
||||
json['agentProfile'] as Map<String, dynamic>)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ChatMessage {
|
||||
final String id;
|
||||
final String conversationId;
|
||||
final String senderId;
|
||||
final String content;
|
||||
final MessageType messageType;
|
||||
final String? fileUrl;
|
||||
final String? fileName;
|
||||
final int? fileSize;
|
||||
final String? mimeType;
|
||||
final MessageStatus status;
|
||||
final String? deliveredAt;
|
||||
final String? readAt;
|
||||
final String createdAt;
|
||||
final String updatedAt;
|
||||
final MessageSender sender;
|
||||
|
||||
const ChatMessage({
|
||||
required this.id,
|
||||
required this.conversationId,
|
||||
required this.senderId,
|
||||
required this.content,
|
||||
this.messageType = MessageType.text,
|
||||
this.fileUrl,
|
||||
this.fileName,
|
||||
this.fileSize,
|
||||
this.mimeType,
|
||||
this.status = MessageStatus.sent,
|
||||
this.deliveredAt,
|
||||
this.readAt,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
required this.sender,
|
||||
});
|
||||
|
||||
bool isMine(String currentUserId) => senderId == currentUserId;
|
||||
|
||||
factory ChatMessage.fromJson(Map<String, dynamic> json) {
|
||||
return ChatMessage(
|
||||
id: json['id'] as String,
|
||||
conversationId: json['conversationId'] as String,
|
||||
senderId: json['senderId'] as String,
|
||||
content: json['content'] as String? ?? '',
|
||||
messageType: MessageType.fromString(json['messageType'] as String?),
|
||||
fileUrl: json['fileUrl'] as String?,
|
||||
fileName: json['fileName'] as String?,
|
||||
fileSize: json['fileSize'] as int?,
|
||||
mimeType: json['mimeType'] as String?,
|
||||
status: MessageStatus.fromString(json['status'] as String?),
|
||||
deliveredAt: json['deliveredAt'] as String?,
|
||||
readAt: json['readAt'] as String?,
|
||||
createdAt: json['createdAt'] as String,
|
||||
updatedAt: json['updatedAt'] as String,
|
||||
sender: json['sender'] != null
|
||||
? MessageSender.fromJson(json['sender'] as Map<String, dynamic>)
|
||||
: MessageSender(id: json['senderId'] as String, role: ''),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PaginationInfo {
|
||||
final int page;
|
||||
final int limit;
|
||||
final int total;
|
||||
final int pages;
|
||||
|
||||
const PaginationInfo({
|
||||
required this.page,
|
||||
required this.limit,
|
||||
required this.total,
|
||||
required this.pages,
|
||||
});
|
||||
|
||||
bool get hasMore => page < pages;
|
||||
|
||||
factory PaginationInfo.fromJson(Map<String, dynamic> json) {
|
||||
return PaginationInfo(
|
||||
page: json['page'] as int? ?? 1,
|
||||
limit: json['limit'] as int? ?? 50,
|
||||
total: json['total'] as int? ?? 0,
|
||||
pages: json['pages'] as int? ?? 1,
|
||||
);
|
||||
}
|
||||
}
|
||||
204
lib/features/messaging/data/socket_service.dart
Normal file
204
lib/features/messaging/data/socket_service.dart
Normal file
@@ -0,0 +1,204 @@
|
||||
import 'dart:async';
|
||||
import 'package:logger/logger.dart';
|
||||
import 'package:real_estate_mobile/config/app_config.dart';
|
||||
import 'package:real_estate_mobile/core/storage/secure_storage.dart';
|
||||
import 'package:real_estate_mobile/features/messaging/data/models/message.dart';
|
||||
|
||||
// We'll use a simple WebSocket approach for now since socket_io_client needs to be added
|
||||
// This will be a placeholder that uses REST polling until socket_io_client is added
|
||||
import 'package:socket_io_client/socket_io_client.dart' as io;
|
||||
|
||||
final _log = Logger(printer: PrettyPrinter(methodCount: 0));
|
||||
|
||||
class SocketService {
|
||||
static final SocketService _instance = SocketService._();
|
||||
factory SocketService() => _instance;
|
||||
SocketService._();
|
||||
|
||||
io.Socket? _socket;
|
||||
bool _isConnected = false;
|
||||
String? _currentToken;
|
||||
|
||||
// Stream controllers for events
|
||||
final _messageController = StreamController<ChatMessage>.broadcast();
|
||||
final _typingStartController = StreamController<Map<String, dynamic>>.broadcast();
|
||||
final _typingStopController = StreamController<Map<String, dynamic>>.broadcast();
|
||||
final _statusController = StreamController<Map<String, dynamic>>.broadcast();
|
||||
final _readController = StreamController<Map<String, dynamic>>.broadcast();
|
||||
final _connectionController = StreamController<bool>.broadcast();
|
||||
|
||||
Stream<ChatMessage> get onNewMessage => _messageController.stream;
|
||||
Stream<Map<String, dynamic>> get onTypingStart => _typingStartController.stream;
|
||||
Stream<Map<String, dynamic>> get onTypingStop => _typingStopController.stream;
|
||||
Stream<Map<String, dynamic>> get onStatusChange => _statusController.stream;
|
||||
Stream<Map<String, dynamic>> get onMessagesRead => _readController.stream;
|
||||
Stream<bool> get onConnectionChange => _connectionController.stream;
|
||||
bool get isConnected => _isConnected;
|
||||
|
||||
Future<void> connect() async {
|
||||
final token = await SecureStorage.getAccessToken();
|
||||
if (token == null) {
|
||||
_log.w('No access token for socket connection');
|
||||
return;
|
||||
}
|
||||
|
||||
if (_socket?.connected == true && _currentToken == token) return;
|
||||
|
||||
_currentToken = token;
|
||||
|
||||
// Strip /api/v1 from base URL for socket connection
|
||||
final apiUrl = AppConfig.apiBaseUrl;
|
||||
final baseUrl = apiUrl.replaceAll(RegExp(r'/api/v1/?$'), '');
|
||||
|
||||
_socket?.disconnect();
|
||||
_socket?.dispose();
|
||||
|
||||
_socket = io.io(baseUrl, io.OptionBuilder()
|
||||
.setTransports(['websocket', 'polling'])
|
||||
.setAuth({'token': token})
|
||||
.disableAutoConnect()
|
||||
.enableReconnection()
|
||||
.setReconnectionAttempts(5)
|
||||
.setReconnectionDelay(1000)
|
||||
.setReconnectionDelayMax(5000)
|
||||
.build(),
|
||||
);
|
||||
|
||||
_socket!.onConnect((_) {
|
||||
_log.i('Socket connected: ${_socket!.id}');
|
||||
_isConnected = true;
|
||||
_connectionController.add(true);
|
||||
});
|
||||
|
||||
_socket!.onDisconnect((reason) {
|
||||
_log.w('Socket disconnected: $reason');
|
||||
_isConnected = false;
|
||||
_connectionController.add(false);
|
||||
|
||||
if (reason == 'io server disconnect') {
|
||||
_log.w('Server rejected connection - token likely expired');
|
||||
_reconnectWithFreshToken();
|
||||
}
|
||||
});
|
||||
|
||||
_socket!.onConnectError((error) {
|
||||
_log.e('Socket connect error: $error');
|
||||
_isConnected = false;
|
||||
_connectionController.add(false);
|
||||
});
|
||||
|
||||
// Listen for events
|
||||
_socket!.on('new_message', (data) {
|
||||
try {
|
||||
final message = ChatMessage.fromJson(data as Map<String, dynamic>);
|
||||
_messageController.add(message);
|
||||
} catch (e) {
|
||||
_log.e('Error parsing new_message: $e');
|
||||
}
|
||||
});
|
||||
|
||||
_socket!.on('typing_start', (data) {
|
||||
_typingStartController.add(Map<String, dynamic>.from(data as Map));
|
||||
});
|
||||
|
||||
_socket!.on('typing_stop', (data) {
|
||||
_typingStopController.add(Map<String, dynamic>.from(data as Map));
|
||||
});
|
||||
|
||||
_socket!.on('user_status_change', (data) {
|
||||
_statusController.add(Map<String, dynamic>.from(data as Map));
|
||||
});
|
||||
|
||||
_socket!.on('messages_read', (data) {
|
||||
_readController.add(Map<String, dynamic>.from(data as Map));
|
||||
});
|
||||
|
||||
_socket!.connect();
|
||||
}
|
||||
|
||||
Future<void> _reconnectWithFreshToken() async {
|
||||
final token = await SecureStorage.getAccessToken();
|
||||
if (token != null && token != _currentToken) {
|
||||
_currentToken = token;
|
||||
_socket?.io.options?['auth'] = {'token': token};
|
||||
_socket?.connect();
|
||||
}
|
||||
}
|
||||
|
||||
void disconnect() {
|
||||
_socket?.disconnect();
|
||||
_socket?.dispose();
|
||||
_socket = null;
|
||||
_isConnected = false;
|
||||
_currentToken = null;
|
||||
}
|
||||
|
||||
Future<void> joinConversation(String conversationId) async {
|
||||
if (_socket == null || !_isConnected) return;
|
||||
final completer = Completer<void>();
|
||||
_socket!.emitWithAck('join_conversation', {'conversationId': conversationId},
|
||||
ack: (data) => completer.complete(),
|
||||
);
|
||||
await completer.future.timeout(
|
||||
const Duration(seconds: 10),
|
||||
onTimeout: () => _log.w('join_conversation timeout'),
|
||||
);
|
||||
}
|
||||
|
||||
void leaveConversation(String conversationId) {
|
||||
_socket?.emit('leave_conversation', {'conversationId': conversationId});
|
||||
}
|
||||
|
||||
Future<ChatMessage?> sendMessage(
|
||||
String conversationId,
|
||||
Map<String, dynamic> messageData,
|
||||
) async {
|
||||
if (_socket == null || !_isConnected) return null;
|
||||
|
||||
final completer = Completer<ChatMessage?>();
|
||||
_socket!.emitWithAck('send_message', {
|
||||
'conversationId': conversationId,
|
||||
'message': messageData,
|
||||
}, ack: (data) {
|
||||
try {
|
||||
final response = data as Map<String, dynamic>;
|
||||
if (response['success'] == true && response['message'] != null) {
|
||||
completer.complete(
|
||||
ChatMessage.fromJson(response['message'] as Map<String, dynamic>),
|
||||
);
|
||||
} else {
|
||||
completer.complete(null);
|
||||
}
|
||||
} catch (e) {
|
||||
completer.complete(null);
|
||||
}
|
||||
});
|
||||
|
||||
return completer.future.timeout(
|
||||
const Duration(seconds: 10),
|
||||
onTimeout: () => null,
|
||||
);
|
||||
}
|
||||
|
||||
void startTyping(String conversationId) {
|
||||
_socket?.emit('typing_start', {'conversationId': conversationId});
|
||||
}
|
||||
|
||||
void stopTyping(String conversationId) {
|
||||
_socket?.emit('typing_stop', {'conversationId': conversationId});
|
||||
}
|
||||
|
||||
void markAsRead(String conversationId) {
|
||||
_socket?.emit('mark_read', {'conversationId': conversationId});
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
disconnect();
|
||||
_messageController.close();
|
||||
_typingStartController.close();
|
||||
_typingStopController.close();
|
||||
_statusController.close();
|
||||
_readController.close();
|
||||
_connectionController.close();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user