import 'dart:async'; import 'dart:developer' as dev; import 'dart:io'; import 'dart:math'; import 'package:audio_waveforms/audio_waveforms.dart'; import 'package:flutter/material.dart'; import 'package:flutter_sound/flutter_sound.dart'; import 'package:get/get.dart'; import 'package:taafee_mobile/core/utils/pagination_list.dart'; import 'package:taafee_mobile/core/utils/rx_futures.dart'; import 'package:taafee_mobile/core/utils/utils.dart'; import 'package:taafee_mobile/features/chat/data_layer/model/chat_user.dart'; import 'package:taafee_mobile/features/chat/data_layer/model/room.dart'; import 'package:taafee_mobile/features/chat/data_layer/service/chat_resource.dart' as resource; // import 'package:path_provider/path_provider.dart'; import 'package:rx_future/rx_future.dart'; import 'package:permission_handler/permission_handler.dart'; import '../../../core/errors/custom_exception.dart'; import '../../../core/local_storage/local_storage.dart'; import '../../../core/network/socket/socket.dart'; import '../data_layer/model/message.dart'; import '../data_layer/service/chat_files_source.dart'; import '../data_layer/service/chat_listener.dart'; enum SocketConnectionState { connected, connecting, notConnected, error } class ChatController extends GetxController { //sources resource.ChatResource chatResource = resource.ChatResource(); ChatListener chatListener = ChatListener(); ChatFilesSource chatFilesSource = ChatFilesSource(); LocalStorage localStorage = LocalStorage(); //initialization Future init( {dynamic Function(MessageModel)? handler, required Function(dynamic) onSessionTerminated}) async { if (isInitialized) return; chatUser = ChatUser( id: localStorage.getChatUserId() ?? 0, name: localStorage.getUser()!.firstName); await socketInit(); receiveMessagesInRoom(); receiveMessages(handler: handler); listenForMessagesReading(); listenOnRoomState(); listenOnSessionTerminated(onSessionTerminated: onSessionTerminated); } //socket SocketIO io = Get.find(); bool isInitialized = false; Future socketInit() async { if (isInitialized) return; io.init(); io.setAuthentication(localStorage.getChatToken() ?? ""); io.setOnConnect(() { connectionState.update((val) { connectionState.value = SocketConnectionState.connected; }); connectionState.refresh(); dev.log("Connected Successfully"); }); io.setOnDisconnect(() { dev.log("Socket disconnected"); connectionState.update((val) { val = SocketConnectionState.notConnected; }); connectionState.refresh(); }); io.setOnConnecting(() { connectionState.update((val) { val = SocketConnectionState.connecting; }); connectionState.refresh(); dev.log("Connecting to socket"); }); io.setOnConnectionError(() { dev.log("Error Connecting to socket"); connectionState.update((val) { val = SocketConnectionState.error; }); connectionState.refresh(); throw GenericException( type: ExceptionType.connectionError, errorMessage: "You Have no Internet Connection", ); }); isInitialized = true; await io.connect(); } // user ChatUser chatUser = ChatUser.zero(); /// ------------------- connection -----------/// RxFuture serverState = RxFuture(null); Rx connectionState = SocketConnectionState.notConnected.obs; bool get shouldConnect => (connectionState.value == SocketConnectionState.error) | (connectionState.value == SocketConnectionState.notConnected); Future connectToServer({Function? onSuccess}) async { if (!shouldConnect) return; serverState.observe((_) async { return await io.boot(); }, onSuccess: ((p0) { onSuccess?.call(); })); } /// ------------------- Rooms ----------------/// RxFuture> rooms = RxFuture(Pagination()); int? get currentRoomId => currentRoom.value?.id; ChatUser? get currentChatUser => currentRoom.value!.user; Rx currentRoom = null.obs; //private void setCurrentRoom(Room room) { currentRoom = room.obs; currentRoom.refresh(); } Room? getRoomById(int? id) => id == null ? null : rooms.result.data.firstWhereOrNull((element) => element.id == id); // Future getRooms({int? specificPage}) async { // await connectToServer(); // await rooms.observe( // (value) async { // await value!.nextPage( // (currentPage) async { // return chatResource.getRooms(currentPage); // }, // place: InsertPlace.end, // ); // return value; // there is no way that this would be null, there is a default value in rooms declaration. // }, // multipleCallsBehavior: MultipleCallsBehavior.abortNew, // ); // } Future getRooms({String searchQuery = '', int? specificPage}) async { await connectToServer(); if (specificPage != null) { rooms.update((val) { val!.value.clear(); }); rooms.refresh(); } await rooms.observe( (value) async { await value!.nextPage( (currentPage) async { return chatResource.getRooms( searchQuery: searchQuery, currentPage: specificPage ?? currentPage); }, place: InsertPlace.end, ); return value; // there is no way that this would be null, there is a default value in rooms declaration. }, multipleCallsBehavior: MultipleCallsBehavior.abortNew, ); } void updateRoom() { int index = rooms.result.data .indexWhere((element) => (element.id == currentRoomId)); // rooms = Pagination( // data: rooms.result.data.insert(0, Room.copy(currentRoom.value!))); rooms.update((val) { if (index == -1) { val!.value.data.insert(0, Room.copy(currentRoom.value!)); index = 0; } if (currentRoom.value!.messages.data.isNotEmpty) { val!.value.data[index].lastMessage = currentRoom.value!.messages.data[0]; val.value.data[index].lastMessageDate = currentRoom.value!.messages.data[0].createdAt; } if (index != 0 && (val!.value.data[index].lastMessageDate.hour == DateTime.now().hour && val.value.data[index].lastMessageDate.day == DateTime.now().day && val.value.data[index].lastMessageDate.month == DateTime.now().month && val.value.data[index].lastMessageDate.year == DateTime.now().year)) { Room room = val.value.data[index]; val.value.data.removeAt(index); val.value.data.insert(0, room); } }); rooms.refresh(); } RxFuture createRoomState = RxFuture(null); void createRoom( {required int chatUserId, void Function(Room?)? onSuccess}) async { await createRoomState.observe((value) async { return await chatResource.createRoom(id: chatUserId); }, onSuccess: (room) async { await getRooms(); Room? roomById = getRoomById(room!.id); if (roomById != null) { room = roomById; } currentRoom = room.obs; // currentRoom.update((val) { // val = room; // }); // currentRoom.value = room; currentRoom.refresh(); onSuccess?.call(room); }, onError: (err) { if (err.toString() == 'email_already_exists') { // getRooms() // get the room that contains two users (chatUserId) // setCurrentRoom // } }); } RxFuture blockRoomState = RxFuture(null); void blockRoom( {void Function()? onSuccess, void Function(Object)? onError}) async { blockRoomState.observe((value) async { return await chatResource.blockRoom(id: currentRoomId!); }, onSuccess: (value) async { if (rooms.result.data.isEmpty) { await getRooms(); } int index = rooms.result.data .indexWhere((element) => element.id == currentRoomId); rooms.update((val) { val!.value.data[index].state = RoomState.blocked; }); currentRoom.update((val) { val!.state = RoomState.blocked; }); currentRoom.refresh(); rooms.refresh(); onSuccess?.call(); }, onError: (err) { onError?.call(err); }); } RxFuture unblockRoomState = RxFuture(null); void unblockRoom( {void Function()? onSuccess, void Function(Object)? onError}) async { blockRoomState.observe((value) async { return await chatResource.unblockRoom(id: currentRoomId!); }, onSuccess: (value) { int index = rooms.result.data .indexWhere((element) => element.id == currentRoomId); rooms.update((val) { val!.value.data[index].state = RoomState.active; }); currentRoom.update((val) { val!.state = RoomState.active; }); currentRoom.refresh(); rooms.refresh(); onSuccess?.call(); }, onError: (err) { onError?.call(err); }); } void listenOnRoomState() { listenOnRoomBlocked(); listenOnRoomBlocking(); listenOnRoomUnblocked(); // chatListener.updatedRoom((roomId, roomStatus) { // if (rooms.result.data.isNotEmpty) { // int index = rooms.result.data // .indexWhere((element) => element.id == currentRoomId); // rooms.update((val) { // val!.value.data[index].state = Room.getRoomStateByString(roomStatus)!; // }); // rooms.refresh(); // if (currentRoomId == roomId) { // currentRoom.update((val) { // val!.state = Room.getRoomStateByString(roomStatus)!; // }); // currentRoom.refresh(); // } // } // }); } void listenOnRoomUnblocked() { chatListener.unblockedRoom((roomId) { if (rooms.result.data.isNotEmpty) { int index = rooms.result.data .indexWhere((element) => element.id == currentRoomId); rooms.update((val) { val!.value.data[index].state = RoomState.active; }); rooms.refresh(); if (currentRoomId == roomId) { currentRoom.update((val) { val!.state = RoomState.active; }); currentRoom.refresh(); } } }); } void listenOnRoomBlocked() { chatListener.blockedRoom((roomId) { if (rooms.result.data.isNotEmpty) { int index = rooms.result.data .indexWhere((element) => element.id == currentRoomId); rooms.update((val) { val!.value.data[index].state = RoomState.blocking; }); rooms.refresh(); if (currentRoomId == roomId) { currentRoom.update((val) { val!.state = RoomState.blocking; }); currentRoom.refresh(); } } }); } void listenOnRoomBlocking() { chatListener.blockingRoom((roomId) { if (rooms.result.data.isNotEmpty) { int index = rooms.result.data .indexWhere((element) => element.id == currentRoomId); rooms.update((val) { val!.value.data[index].state = RoomState.blocked; }); rooms.refresh(); if (currentRoomId == roomId) { currentRoom.update((val) { val!.state = RoomState.blocked; }); currentRoom.refresh(); } } }); } void listenOnSessionTerminated( {required Function(dynamic) onSessionTerminated}) { chatListener.termminatedSession(onSessionTerminated); } //support RxFuture supportRoomState = RxFuture(null); Future checkSupportRoomStatus( {void Function(Room?)? onSuccess, void Function(Object)? onError}) async { supportRoomState.observe((value) async { return await chatResource.checkSupportRoomStatus(); }, onSuccess: (room) async { if (room == null) { Room newSupportRoom = await chatResource.createSupportRoom(); setCurrentRoom(newSupportRoom); //creat support room //set current room } else { //set current room setCurrentRoom(room); } onSuccess?.call(currentRoom.value); }, onError: (err) { onError?.call(err); }); } /// ------------- messages -------------- /// // getMessages RxFutures messagesState = RxFutures(); Future getCurrentRoomMessages({int? specificPage}) async { if (currentRoom.value == null || (currentRoom.value!.messagesStateId != null && messagesState.loading(currentRoom.value!.messagesStateId!))) return; String id = messagesState.init(RxFuture(null)); currentRoom.value!.messagesStateId = id; await messagesState.observe( id, (value) async { return await currentRoom.value!.messages.nextPage((currentPage) async { return await chatResource.getCurrentRoomMessages( roomId: currentRoomId!, page: specificPage ?? currentPage); }); }, onSuccess: (value) { currentRoom.refresh(); }, multipleCallsBehavior: MultipleCallsBehavior.abortNew, ); } //read Message RxFutures readMessageState = RxFutures(); void readMessage({required int messageId}) async { dev.log('i am reading message with id : $messageId'); String sendingStateId = readMessageState.init(RxFuture(null)); readMessageState.observe(sendingStateId, (value) async { await chatResource.readMessage(messageId: messageId); }, onSuccess: (value) { int index = currentRoom.value!.messages.data .indexWhere((element) => element.id == messageId); currentRoom.update((val) { val!.messages.data[index].readBy!.ids .insert(0, val.messages.data[index].user.id); }); currentRoom.refresh(); }); } void listenForMessagesReading() { chatListener.messagesReaded((messageId, userId) { if (currentRoom.value != null && currentRoom.value!.type == RoomType.private) { currentRoom.update((val) { int index = val!.messages.data .indexWhere((element) => element.id == messageId); val.messages.data[index].readBy!.ids.insert(0, userId); }); currentRoom.refresh(); } }); } // sendMessages RxFutures sendingMessageState = RxFutures(); RxBool isMessageLoading(String? sendingMessageStateId) { if (sendingMessageStateId != null) { return messagesState.loading(sendingMessageStateId).obs; } else { return false.obs; } } Future sendMessage(MessageModel message, {int? roomId, String? filePath, void Function(Object e)? onError}) async { if (roomId == null && currentRoom.value == null) { throw Exception( "You must either provide room id, or select a room to send message", ); } message.user = chatUser; message.userId = chatUser.id; if (shouldConnect) { try { await io.boot(); await getCurrentRoomMessages(specificPage: 1); } catch (err) { onError?.call(err); } } String sendingStateId = sendingMessageState.init(RxFuture(message)); message.sendingStateId = sendingStateId; await sendingMessageState.observe(sendingStateId, (_) async { message.sendingStateId = sendingStateId; // currentRoom.value!.messages.data.insert(0, MessageModel.copy(message)); if (message.type != MessageType.voice) { currentRoom.update((val) { val!.messages.data.insert(0, MessageModel.copy(message)); }); } if (message.type != MessageType.text) { await uploadFile(filePath: filePath!); if (uploadFileState.result != null) { int id = uploadFileState.result!; message.content = "$id"; } else { onError?.call(e); currentRoom.update((val) { val!.messages.data.removeAt(0); }); return message; } } if (message.type == MessageType.voice) { currentRoom.update((val) { // message.temporaryFile = null; val!.messages.data.insert(0, MessageModel.copy(message)); val.messages.data[0].content = '${uploadFileState.result}'; // val.messages.data[0].temporaryFile = null; }); currentRoom.refresh(); } return await chatResource.sendMessage( messageModel: message, roomId: currentRoomId!); }, onSuccess: (value) { currentRoom.update((val) { val!.messages.data[0].id = value.id; val.messages.data[0].sendingStateId = null; val.messages.data[0].content = value.content; val.messages.data[0].readBy = value.readBy; if (message.type != MessageType.voice) { val.messages.data[0].temporaryFile = null; } }); rooms.refresh(); }, onError: (err) { currentRoom.update((val) { val!.messages.data.removeAt(0); }); onError?.call(err); }); } // cancel sending void cancelSendingMessage(MessageModel message) { if (message.sendingStateId == null || !sendingMessageState.loading(message.sendingStateId!)) return; sendingMessageState.cancel(message.sendingStateId!); } // receive Messages void receiveMessagesInRoom() async { connectToServer(); chatListener.receiveMessage((message) { currentRoom.update((val) { val?.messages.data.insert(0, message); }); }); } void receiveMessages({dynamic Function(MessageModel)? handler}) async { connectToServer(); chatListener.receiveMessage((message) async { if (rooms.result.data.isEmpty) { await getRooms(); } if (rooms.result.data .lastIndexWhere((element) => element.id == message.roomId) == -1) { await getRooms(); } int index = rooms.result.data .lastIndexWhere((element) => element.id == message.roomId); if (index == -1) { rooms.update((val) { val!.value.data.insert( 0, Room( id: message.roomId, user: message.user, lastMessageDate: message.createdAt)); }); index = 0; } rooms.update((val) { val!.value.data[index].lastMessage = message; }); rooms.refresh(); //assign when user tap . // currentRoom.value = rooms.result.data // .firstWhere((element) => (element.id == message.roomId)); handler?.call(message); }); } // upload Files RxFuture uploadFileState = RxFuture(null); Future uploadFile({required String filePath}) async { await uploadFileState.observe((value) async { return await chatFilesSource.uploadFile( roomId: currentRoomId!, filePath: filePath); }); } // Chat Details scroll controller Rx scrollController = ScrollController().obs; void scrollControllerJump() { scrollController.update((val) { val!.jumpTo(0); }); scrollController.refresh(); } // keyBoard opening detection RxBool keyBoardOpened = false.obs; void setkeyBoardOpened(bool newValue) { keyBoardOpened.value = newValue; keyBoardOpened.refresh(); } //reply senario handling RxBool isReplying = false.obs; void toggleIsReplying() => isReplying.value = !isReplying.value; Rx replyModel = MessageModel.zero().obs; void updateReplyModel({ required MessageModel messageModel, }) { replyModel.value = messageModel; replyModel.refresh(); } //voice message FlutterSoundRecorder voiceRecorder = FlutterSoundRecorder(); File recordFile = File('record.wav'); bool isRecordReady = false; RxBool isRecording = false.obs; RxString recordingTime = '00:00'.obs; File? audioFile; toggleIsRecording() { if (isRecording.value == false) { recordingTime.value = '00:00'; recordingTime.refresh(); } isRecording.value = !isRecording.value; isRecording.refresh(); } Future getVoiceFile({required String id}) async { return await chatFilesSource.getVoiceFile(id); } void timer() { var startTime = DateTime.now(); Timer.periodic(const Duration(seconds: 1), (t) { var diff = DateTime.now().difference(startTime); recordingTime.value = '${diff.inHours == 0 ? '' : '${diff.inHours.toString().padLeft(2, "0")}:'}${(diff.inMinutes % 60).floor().toString().padLeft(2, "0")}:${(diff.inSeconds % 60).floor().toString().padLeft(2, '0')}'; recordingTime.refresh(); if (!voiceRecorder.isRecording) { t.cancel(); } }); } Future initRecorder() async { final status = await Permission.microphone.request(); if (status != PermissionStatus.granted) { throw 'dfaf'; } await Permission.storage.request(); await Permission.manageExternalStorage.request(); await voiceRecorder.openRecorder(); isRecordReady = true; voiceRecorder.setSubscriptionDuration(const Duration(microseconds: 500)); } Future record() async { if (!isRecordReady) return; await voiceRecorder.startRecorder( toFile: '${Utils.randomString(5)}${Random().nextInt(100000)}'); timer(); } Future cancelRecording() async { recordingTime.value = '00:00'; recordingTime.refresh(); await voiceRecorder.stopRecorder(); } Future stopRecording({void Function(Object)? onError}) async { recordingTime.value = '00:00'; recordingTime.refresh(); if (!isRecordReady) return; String? path = await voiceRecorder.stopRecorder(); audioFile = File(path!); await sendMessage( MessageModel( user: chatUser, userId: chatUser.id, roomId: currentRoomId!, content: '', createdAt: DateTime.now(), temporaryFile: File(audioFile!.path), type: MessageType.voice, ), filePath: audioFile!.path, roomId: currentRoomId, onError: (err) { onError?.call(err); }); } RxMap voiceFiles = {}.obs; Map> voicePlayers = {}; RxMap isPlayingMap = {}.obs; RxMap isLoadingMap = {}.obs; void addVoiceFile(String id, File file) { voiceFiles[id] = file; voiceFiles.refresh(); } Rx getVoicePlayer(String id) { if (voicePlayers[id] != null) { return voicePlayers[id]!; } voicePlayers[id] = PlayerController().obs; return voicePlayers[id]!; } void updateIsLoading(String id, bool newValue) { isLoadingMap[id] = newValue; isLoadingMap.refresh(); } void updateIsPlaying(String id, bool newValue) { isPlayingMap[id] = newValue; isPlayingMap.refresh(); } void clear() { isInitialized = false; connectionState = SocketConnectionState.notConnected.obs; voiceFiles.clear(); voicePlayers.clear(); isPlayingMap.clear(); isLoadingMap.clear(); voiceRecorder = FlutterSoundRecorder(); recordFile = File('record.wav'); isRecordReady = false; isRecording = false.obs; recordingTime = '00:00'.obs; isReplying = false.obs; replyModel = MessageModel.zero().obs; keyBoardOpened = false.obs; scrollController = ScrollController().obs; messagesState.clear(); rooms = RxFuture(Pagination()); currentRoom = null.obs; io.clearListeners(); } // RxBool isPlaying(String id) { // if (isPlayingMap[id] != null) { // return isPlayingMap[id]!; // } // isPlayingMap[id] = false.obs; // return isPlayingMap[id]!; // } // RxBool isLoading(String id) { // if (isLoadingMap[id] != null) { // return isLoadingMap[id]!; // } // isLoadingMap[id] = false.obs; // return isLoadingMap[id]!; // } } //dummy // RxList messages = [ // MessageModel( // roomId: 0, // userId: 0, // content: 'message,hi', // createdAt: DateTime.now().toString(), // user: ChatUser(id: 0, name: 'name'), // ), // MessageModel( // roomId: 0, // userId: 0, // content: 'message,hi', // user: ChatUser(id: 0, name: 'name'), // createdAt: DateTime.now().toString(), // ), // MessageModel( // user: ChatUser(id: 0, name: 'name'), // roomId: 0, // userId: 0, // content: 'message,hi', // createdAt: DateTime.now().toString(), // ), // MessageModel( // roomId: 0, // user: ChatUser(id: 0, name: 'name'), // userId: 0, // content: 'message,hi', // createdAt: DateTime.now().toString(), // ), // MessageModel( // user: ChatUser(id: 0, name: 'name'), // roomId: 0, // userId: 0, // content: 'message,hi', // direction: MessageDirection.received, // createdAt: DateTime.now().toString(), // ), // MessageModel( // user: ChatUser(id: 0, name: 'name'), // roomId: 0, // userId: 0, // content: 'message,hi', // direction: MessageDirection.received, // createdAt: DateTime.now().toString(), // ), // MessageModel( // user: ChatUser(id: 0, name: 'name'), // roomId: 0, // userId: 0, // content: 'message,hi', // direction: MessageDirection.received, // createdAt: DateTime.now().toString(), // ), // MessageModel( // user: ChatUser(id: 0, name: 'name'), // roomId: 0, // userId: 0, // content: 'message,hi', // direction: MessageDirection.received, // createdAt: DateTime.now().toString(), // ), // MessageModel( // user: ChatUser(id: 0, name: 'name'), // roomId: 0, // userId: 0, // content: 'message,hi', // direction: MessageDirection.received, // createdAt: DateTime.now().toString(), // ), // MessageModel( // user: ChatUser(id: 0, name: 'name'), // roomId: 0, // userId: 0, // content: 'message,hi', // direction: MessageDirection.received, // createdAt: DateTime.now().toString(), // ), // MessageModel( // user: ChatUser(id: 0, name: 'name'), // roomId: 0, // userId: 0, // content: 'message,hi', // createdAt: DateTime.now().toString(), // ), // MessageModel( // user: ChatUser(id: 0, name: 'name'), // roomId: 0, // userId: 0, // content: 'message,hi', // createdAt: DateTime.now().toString(), // direction: MessageDirection.received, // ), // MessageModel( // user: ChatUser(id: 0, name: 'name'), // roomId: 0, // userId: 0, // content: networkImageTest, // type: MessageType.image, // createdAt: DateTime.now().toString(), // direction: MessageDirection.received, // ), // MessageModel( // user: ChatUser(id: 0, name: 'name'), // roomId: 0, // userId: 0, // content: networkImageTest, // type: MessageType.image, // createdAt: DateTime.now().toString(), // ), // MessageModel( // user: ChatUser(id: 0, name: 'name'), // roomId: 0, // userId: 0, // content: 'message,hi', // repliedMessage: MessageModel( // user: ChatUser(id: 0, name: 'name'), // roomId: 0, // userId: 0, // content: networkImageTest, // type: MessageType.image, // createdAt: DateTime.now().toString(), // direction: MessageDirection.received, // ), // createdAt: DateTime.now().toString(), // ), // MessageModel( // user: ChatUser(id: 0, name: 'name'), // roomId: 0, // userId: 0, // content: 'message,hi', // repliedMessage: MessageModel( // user: ChatUser(id: 0, name: 'name'), // roomId: 0, // userId: 0, // content: 'message,hi', // createdAt: DateTime.now().toString(), // ), // createdAt: DateTime.now().toString(), // ), // MessageModel( // user: ChatUser(id: 0, name: 'name'), // roomId: 0, // userId: 0, // content: 'message,hi', // repliedMessage: MessageModel( // user: ChatUser(id: 0, name: 'name'), // roomId: 0, // userId: 0, // content: 'message,hi', // createdAt: DateTime.now().toString(), // direction: MessageDirection.received, // ), // createdAt: DateTime.now().toString(), // ), // ].obs;