From 4021eccd9f2486b3acf665621a2fbd45c98adfbf Mon Sep 17 00:00:00 2001 From: "Aamir.Muhammad" Date: Tue, 20 Dec 2022 09:11:51 +0300 Subject: [PATCH 01/12] Chat Error Handling --- lib/generated_plugin_registrant.dart | 4 +-- lib/provider/chat_provider_model.dart | 51 +++++++++++++++++++++++---- lib/ui/chat/chat_bubble.dart | 10 +++--- lib/ui/chat/chat_detailed_screen.dart | 4 +-- 4 files changed, 55 insertions(+), 14 deletions(-) diff --git a/lib/generated_plugin_registrant.dart b/lib/generated_plugin_registrant.dart index 288ba42..4a90695 100644 --- a/lib/generated_plugin_registrant.dart +++ b/lib/generated_plugin_registrant.dart @@ -16,7 +16,7 @@ import 'package:image_picker_for_web/image_picker_for_web.dart'; import 'package:shared_preferences_web/shared_preferences_web.dart'; import 'package:url_launcher_web/url_launcher_web.dart'; import 'package:video_player_web/video_player_web.dart'; -import 'package:wakelock_web/wakelock_web.dart'; +//import 'package:wakelock_web/wakelock_web.dart'; import 'package:flutter_web_plugins/flutter_web_plugins.dart'; @@ -32,6 +32,6 @@ void registerPlugins(Registrar registrar) { SharedPreferencesPlugin.registerWith(registrar); UrlLauncherPlugin.registerWith(registrar); VideoPlayerPlugin.registerWith(registrar); - WakelockWeb.registerWith(registrar); + // WakelockWeb.registerWith(registrar); registrar.registerMessageHandler(); } diff --git a/lib/provider/chat_provider_model.dart b/lib/provider/chat_provider_model.dart index 5f28b0b..91d3e7f 100644 --- a/lib/provider/chat_provider_model.dart +++ b/lib/provider/chat_provider_model.dart @@ -970,15 +970,15 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { //////// Audio Recoding Work //////////////////// - Future initAudio() async { + Future initAudio({required int receiverId}) async { appDirectory = await getApplicationDocumentsDirectory(); - path = "${appDirectory.path}/${AppState().chatDetails!.response!.id}-${DateTime.now().microsecondsSinceEpoch}.aac"; + path = "${appDirectory.path}/${AppState().chatDetails!.response!.id}-$receiverID-${DateTime.now().microsecondsSinceEpoch}.aac"; recorderController = RecorderController() ..androidEncoder = AndroidEncoder.aac ..androidOutputFormat = AndroidOutputFormat.mpeg4 ..iosEncoder = IosEncoder.kAudioFormatMPEG4AAC - ..sampleRate = 8000 - ..bitRate = 32000; + ..sampleRate = 6000 + ..bitRate = 18000; playerController = PlayerController(); } @@ -992,7 +992,6 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { void startRecoding() async { PermissionStatus status = await Permission.microphone.request(); - print(status); if (status.isDenied == true) { startRecoding(); } else { @@ -1014,6 +1013,15 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { }); } + void _pauseTimer() { + _timer?.cancel(); + _timer = Timer.periodic(const Duration(seconds: 1), (Timer t) { + _recodeDuration++; + // buildTimer(); + notifyListeners(); + }); + } + Future pauseRecoding() async { isPause = true; isPlaying = true; @@ -1025,6 +1033,7 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { await playerController.preparePlayer(file.path, 1.0); var tempDuration = _recodeDuration; _recodeDuration = tempDuration; + _pauseTimer(); _timer?.cancel(); notifyListeners(); } @@ -1077,11 +1086,41 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { void playRecoding() async { isPlaying = true; - await playerController.startPlayer(finishMode: FinishMode.stop); + await playerController.startPlayer(finishMode: FinishMode.pause); } void playOrPause() async { playerController.playerState == PlayerState.playing ? await playerController.pausePlayer() : playRecoding(); notifyListeners(); } + + void sendVoiceMessage(BuildContext context, {required int targetUserId, required int userStatus, required String userEmail, required String targetUserName}) async { + recorderController.pause(); + path = await recorderController.stop(false); + print(path); + File voiceFile = File(path!); + voiceFile.readAsBytesSync(); + _pauseTimer(); + _timer?.cancel(); + isPause = false; + isPlaying = false; + isRecoding = false; + Utils.showLoading(context); + dynamic value = await uploadAttachments(AppState().chatDetails!.response!.id.toString(), voiceFile); + logger.d(value); + String? ext = getFileExtension(voiceFile.path); + Utils.hideLoading(context); + sendChatToServer( + chatEventId: 2, + fileTypeId: getFileType(ext.toString()), + targetUserId: targetUserId, + targetUserName: targetUserName, + isAttachment: true, + chatReplyId: null, + isReply: false, + isImageLoaded: true, + image: voiceFile.readAsBytesSync()); + + notifyListeners(); + } } diff --git a/lib/ui/chat/chat_bubble.dart b/lib/ui/chat/chat_bubble.dart index 06afecf..ed20c24 100644 --- a/lib/ui/chat/chat_bubble.dart +++ b/lib/ui/chat/chat_bubble.dart @@ -328,15 +328,17 @@ class WaveBubble extends StatelessWidget { playerController: playerController, padding: EdgeInsets.zero, margin: EdgeInsets.zero, + enableSeekGesture: true, + density: 2, playerWaveStyle: const PlayerWaveStyle( fixedWaveColor: Colors.white, - liveWaveColor:MyColors.lightGreenColor, + liveWaveColor:MyColors.greenColor, showTop: true, showBottom: true, waveCap: StrokeCap.round, - seekLineThickness: 3, - visualizerHeight: 6, - backgroundColor: Colors.transparent + seekLineThickness: 2, + visualizerHeight: 5, + backgroundColor: Colors.transparent, ), ), ], diff --git a/lib/ui/chat/chat_detailed_screen.dart b/lib/ui/chat/chat_detailed_screen.dart index 5d3648e..605a922 100644 --- a/lib/ui/chat/chat_detailed_screen.dart +++ b/lib/ui/chat/chat_detailed_screen.dart @@ -80,7 +80,7 @@ class _ChatDetailScreenState extends State { loadMore: false, isNewChat: params!.isNewChat!, ); - data.initAudio(); + data.initAudio(receiverId: params!.chatUser!.id!); } return Scaffold( @@ -252,7 +252,7 @@ class _ChatDetailScreenState extends State { }), SvgPicture.asset("assets/icons/chat/chat_send_icon.svg", height: 26, width: 26) .onPress( - () => m.sendChatMessage(context, + () => m.sendVoiceMessage(context, targetUserId: params!.chatUser!.id!, userStatus: params!.chatUser!.userStatus ?? 0, userEmail: params!.chatUser!.email!, From f60394cc38146b6b513b9db86ee89604d95d6232 Mon Sep 17 00:00:00 2001 From: "Aamir.Muhammad" Date: Tue, 20 Dec 2022 10:18:07 +0300 Subject: [PATCH 02/12] chat voice message implementation --- lib/api/chat/chat_api_client.dart | 1 - lib/provider/chat_provider_model.dart | 80 +++++++++++++-------------- lib/ui/chat/chat_detailed_screen.dart | 32 +++++------ 3 files changed, 56 insertions(+), 57 deletions(-) diff --git a/lib/api/chat/chat_api_client.dart b/lib/api/chat/chat_api_client.dart index 409295f..2f08f2f 100644 --- a/lib/api/chat/chat_api_client.dart +++ b/lib/api/chat/chat_api_client.dart @@ -33,7 +33,6 @@ class ChatApiClient { }, ); if (!kReleaseMode) { - print("Status Code is ================" + response.statusCode.toString()); logger.i("res: " + response.body); } if (response.statusCode == 200) { diff --git a/lib/provider/chat_provider_model.dart b/lib/provider/chat_provider_model.dart index 91d3e7f..4e1d421 100644 --- a/lib/provider/chat_provider_model.dart +++ b/lib/provider/chat_provider_model.dart @@ -55,9 +55,6 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { Future getUserAutoLoginToken() async { userLoginToken.UserAutoLoginModel userLoginResponse = await ChatApiClient().getUserLoginToken(); - print("======================================= Chat Login Token Check ====================================="); - logger.d(userLoginResponse.toJson()); - print("======================================= Chat Login Token Check ====================================="); if (userLoginResponse.response != null) { AppState().setchatUserDetails = userLoginResponse; } else { @@ -677,7 +674,9 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { String? getFileExtension(String fileName) { try { - print("Ext: " + "." + fileName.split('.').last); + if (kDebugMode) { + print("ext: " + "." + fileName.split('.').last); + } return "." + fileName.split('.').last; } catch (e) { return null; @@ -886,8 +885,12 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { } else { await deleteFile(userID); Uint8List decodedBytes = base64Decode(encodedBytes); - Directory appDocumentsDirectory = await getApplicationDocumentsDirectory(); // 1 - late File imageFile = File("${appDocumentsDirectory.path}/$userID.jpg"); + Directory appDocumentsDirectory = await getApplicationDocumentsDirectory(); + String dirPath = '${appDocumentsDirectory.path}/chat_images'; + if (!await Directory(dirPath).exists()) { + await Directory(dirPath).create(); + } + late File imageFile = File("$dirPath/$userID.jpg"); imageFile.writeAsBytesSync(decodedBytes); return imageFile; } @@ -895,7 +898,8 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { Future deleteFile(String userID) async { Directory appDocumentsDirectory = await getApplicationDocumentsDirectory(); - late File imageFile = File('${appDocumentsDirectory.path}/$userID.jpg'); + String dirPath = '${appDocumentsDirectory.path}/chat_images'; + late File imageFile = File('$dirPath/$userID.jpg'); if (await imageFile.exists()) { await imageFile.delete(); } @@ -972,7 +976,11 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { Future initAudio({required int receiverId}) async { appDirectory = await getApplicationDocumentsDirectory(); - path = "${appDirectory.path}/${AppState().chatDetails!.response!.id}-$receiverID-${DateTime.now().microsecondsSinceEpoch}.aac"; + String dirPath = '${appDirectory.path}/chat_audios'; + if (!await Directory(dirPath).exists()) { + await Directory(dirPath).create(); + } + path = "$dirPath/${AppState().chatDetails!.response!.id}-$receiverID-${DateTime.now().microsecondsSinceEpoch}.aac"; recorderController = RecorderController() ..androidEncoder = AndroidEncoder.aac ..androidOutputFormat = AndroidOutputFormat.mpeg4 @@ -996,7 +1004,9 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { startRecoding(); } else { recorderController.reset(); + logger.d(recorderController.isRecording); await recorderController.record(path); + logger.d(recorderController.isRecording); _recodeDuration = 0; _startTimer(); isRecoding = !isRecoding; @@ -1013,27 +1023,16 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { }); } - void _pauseTimer() { - _timer?.cancel(); - _timer = Timer.periodic(const Duration(seconds: 1), (Timer t) { - _recodeDuration++; - // buildTimer(); - notifyListeners(); - }); - } - Future pauseRecoding() async { isPause = true; isPlaying = true; recorderController.pause(); path = await recorderController.stop(false); - print(path); File file = File(path!); file.readAsBytesSync(); await playerController.preparePlayer(file.path, 1.0); - var tempDuration = _recodeDuration; - _recodeDuration = tempDuration; - _pauseTimer(); + // var tempDuration = _recodeDuration; + // _recodeDuration = tempDuration; _timer?.cancel(); notifyListeners(); } @@ -1047,19 +1046,20 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { } Future deleteRecoding() async { - print(path); _recodeDuration = 0; _timer?.cancel(); - // path = await recorderController.stop(false); + // path = await recorderController.stop(true); recorderController.reset(); print(path); if (path != null && path!.isNotEmpty) { File delFile = File(path!); double fileSizeInKB = delFile.lengthSync() / 1024; double fileSizeInMB = fileSizeInKB / 1024; - debugPrint("Deleted file size: ${delFile.lengthSync()}"); - debugPrint("Deleted file size in KB: " + fileSizeInKB.toString()); - debugPrint("Deleted file size in MB: " + fileSizeInMB.toString()); + if (kDebugMode) { + debugPrint("Deleted file size: ${delFile.lengthSync()}"); + debugPrint("Deleted file size in KB: " + fileSizeInKB.toString()); + debugPrint("Deleted file size in MB: " + fileSizeInMB.toString()); + } if (await delFile.exists()) { delFile.delete(); } @@ -1095,12 +1095,13 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { } void sendVoiceMessage(BuildContext context, {required int targetUserId, required int userStatus, required String userEmail, required String targetUserName}) async { - recorderController.pause(); + //recorderController.pause(); path = await recorderController.stop(false); - print(path); + if (kDebugMode) { + print(path); + } File voiceFile = File(path!); voiceFile.readAsBytesSync(); - _pauseTimer(); _timer?.cancel(); isPause = false; isPlaying = false; @@ -1110,17 +1111,16 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { logger.d(value); String? ext = getFileExtension(voiceFile.path); Utils.hideLoading(context); - sendChatToServer( - chatEventId: 2, - fileTypeId: getFileType(ext.toString()), - targetUserId: targetUserId, - targetUserName: targetUserName, - isAttachment: true, - chatReplyId: null, - isReply: false, - isImageLoaded: true, - image: voiceFile.readAsBytesSync()); - + // sendChatToServer( + // chatEventId: 2, + // fileTypeId: getFileType(ext.toString()), + // targetUserId: targetUserId, + // targetUserName: targetUserName, + // isAttachment: true, + // chatReplyId: null, + // isReply: false, + // isImageLoaded: true, + // image: voiceFile.readAsBytesSync()); notifyListeners(); } } diff --git a/lib/ui/chat/chat_detailed_screen.dart b/lib/ui/chat/chat_detailed_screen.dart index 605a922..c10a8c6 100644 --- a/lib/ui/chat/chat_detailed_screen.dart +++ b/lib/ui/chat/chat_detailed_screen.dart @@ -234,22 +234,22 @@ class _ChatDetailScreenState extends State { ).paddingAll(10).onPress(() { m.deleteRecoding(); }), - if (m.isPause) - const Icon( - Icons.mic, - size: 26, - color: MyColors.lightGreenColor, - ).paddingOnly(right: 15).onPress(() { - m.resumeRecoding(); - }), - if (!m.isPause) - const Icon( - Icons.pause_circle_outline, - size: 26, - color: MyColors.lightGreenColor, - ).paddingOnly(right: 15).onPress(() { - m.pauseRecoding(); - }), + // if (m.isPause) + // const Icon( + // Icons.mic, + // size: 26, + // color: MyColors.lightGreenColor, + // ).paddingOnly(right: 15).onPress(() { + // m.resumeRecoding(); + // }), + // if (!m.isPause) + // const Icon( + // Icons.pause_circle_outline, + // size: 26, + // color: MyColors.lightGreenColor, + // ).paddingOnly(right: 15).onPress(() { + // m.pauseRecoding(); + // }), SvgPicture.asset("assets/icons/chat/chat_send_icon.svg", height: 26, width: 26) .onPress( () => m.sendVoiceMessage(context, From abd1fa93756ea164fae672f26dd07a26258f0433 Mon Sep 17 00:00:00 2001 From: "Aamir.Muhammad" Date: Wed, 21 Dec 2022 16:30:02 +0300 Subject: [PATCH 03/12] chat voice message implementation --- assets/icons/chat/aac.svg | 54 ++++ assets/icons/chat/mp3.svg | 57 ++++ lib/api/chat/chat_api_client.dart | 19 +- .../chat/get_single_user_chat_list_model.dart | 58 ++-- lib/provider/chat_provider_model.dart | 269 +++++++++++++++--- lib/ui/chat/chat_bubble.dart | 134 +++++++-- lib/ui/chat/chat_detailed_screen.dart | 8 +- 7 files changed, 503 insertions(+), 96 deletions(-) create mode 100644 assets/icons/chat/aac.svg create mode 100644 assets/icons/chat/mp3.svg diff --git a/assets/icons/chat/aac.svg b/assets/icons/chat/aac.svg new file mode 100644 index 0000000..61d50bb --- /dev/null +++ b/assets/icons/chat/aac.svg @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/chat/mp3.svg b/assets/icons/chat/mp3.svg new file mode 100644 index 0000000..ed8e31e --- /dev/null +++ b/assets/icons/chat/mp3.svg @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/api/chat/chat_api_client.dart b/lib/api/chat/chat_api_client.dart index 2f08f2f..46dcce7 100644 --- a/lib/api/chat/chat_api_client.dart +++ b/lib/api/chat/chat_api_client.dart @@ -96,6 +96,7 @@ class ChatApiClient { } return response; } catch (e) { + getSingleUserChatHistory(senderUID: senderUID, receiverUID: receiverUID, loadMore: loadMore, paginationVal: paginationVal); throw e; } } @@ -119,7 +120,6 @@ class ChatApiClient { if (!kReleaseMode) { logger.i("res: " + response.body); } - fav.FavoriteChatUser favoriteChatUser = fav.FavoriteChatUser.fromRawJson(response.body); return favoriteChatUser; } catch (e) { @@ -128,29 +128,28 @@ class ChatApiClient { } } - Future uploadMedia(String userId, File file) async { + Future uploadMedia(String userId, File file) async { + print("${ApiConsts.chatMediaImageUploadUrl}upload"); + print(AppState().chatDetails!.response!.token); dynamic request = MultipartRequest('POST', Uri.parse('${ApiConsts.chatMediaImageUploadUrl}upload')); request.fields.addAll({'userId': userId, 'fileSource': '1'}); request.files.add(await MultipartFile.fromPath('files', file.path)); request.headers.addAll({'Authorization': 'Bearer ${AppState().chatDetails!.response!.token}'}); StreamedResponse response = await request.send(); - if (!kReleaseMode) {} - return response; + String data = await response.stream.bytesToString(); + if (!kReleaseMode) { + logger.i("res: " + data); + } + return jsonDecode(data); } // Download File For Chat - Future downloadURL({required String fileName, required String fileTypeDescription}) async { - print(fileName); - print(fileTypeDescription); - print("${ApiConsts.chatMediaImageUploadUrl}download"); - print(AppState().chatDetails!.response!.token); Response response = await ApiClient().postJsonForResponse( "${ApiConsts.chatMediaImageUploadUrl}download", {"fileType": fileTypeDescription, "fileName": fileName, "fileSource": 1}, token: AppState().chatDetails!.response!.token, ); - Uint8List data = Uint8List.fromList(response.bodyBytes); return data; } diff --git a/lib/models/chat/get_single_user_chat_list_model.dart b/lib/models/chat/get_single_user_chat_list_model.dart index 246a515..c585af7 100644 --- a/lib/models/chat/get_single_user_chat_list_model.dart +++ b/lib/models/chat/get_single_user_chat_list_model.dart @@ -32,7 +32,8 @@ class SingleUserChatModel { this.userChatReplyResponse, this.isReplied, this.isImageLoaded, - this.image}); + this.image, + this.voice}); int? userChatHistoryId; int? userChatHistoryLineId; @@ -58,6 +59,7 @@ class SingleUserChatModel { bool? isReplied; bool? isImageLoaded; Uint8List? image; + Uint8List? voice; factory SingleUserChatModel.fromJson(Map json) => SingleUserChatModel( userChatHistoryId: json["userChatHistoryId"] == null ? null : json["userChatHistoryId"], @@ -83,7 +85,8 @@ class SingleUserChatModel { userChatReplyResponse: json["userChatReplyResponse"] == null ? null : UserChatReplyResponse.fromJson(json["userChatReplyResponse"]), isReplied: false, isImageLoaded: false, - image: null); + image: null, + voice: null); Map toJson() => { "userChatHistoryId": userChatHistoryId == null ? null : userChatHistoryId, @@ -143,19 +146,19 @@ class FileTypeResponse { } class UserChatReplyResponse { - UserChatReplyResponse({ - this.userChatHistoryId, - this.chatEventId, - this.contant, - this.contantNo, - this.fileTypeId, - this.createdDate, - this.targetUserId, - this.targetUserName, - this.fileTypeResponse, - this.isImageLoaded, - this.image, - }); + UserChatReplyResponse( + {this.userChatHistoryId, + this.chatEventId, + this.contant, + this.contantNo, + this.fileTypeId, + this.createdDate, + this.targetUserId, + this.targetUserName, + this.fileTypeResponse, + this.isImageLoaded, + this.image, + this.voice}); int? userChatHistoryId; int? chatEventId; @@ -168,19 +171,22 @@ class UserChatReplyResponse { FileTypeResponse? fileTypeResponse; bool? isImageLoaded; Uint8List? image; + Uint8List? voice; factory UserChatReplyResponse.fromJson(Map json) => UserChatReplyResponse( - userChatHistoryId: json["userChatHistoryId"] == null ? null : json["userChatHistoryId"], - chatEventId: json["chatEventId"] == null ? null : json["chatEventId"], - contant: json["contant"] == null ? null : json["contant"], - contantNo: json["contantNo"] == null ? null : json["contantNo"], - fileTypeId: json["fileTypeId"], - createdDate: json["createdDate"] == null ? null : DateTime.parse(json["createdDate"]), - targetUserId: json["targetUserId"] == null ? null : json["targetUserId"], - targetUserName: json["targetUserName"] == null ? null : json["targetUserName"], - fileTypeResponse: json["fileTypeResponse"] == null ? null : FileTypeResponse.fromJson(json["fileTypeResponse"]), - isImageLoaded: false, - image: null); + userChatHistoryId: json["userChatHistoryId"] == null ? null : json["userChatHistoryId"], + chatEventId: json["chatEventId"] == null ? null : json["chatEventId"], + contant: json["contant"] == null ? null : json["contant"], + contantNo: json["contantNo"] == null ? null : json["contantNo"], + fileTypeId: json["fileTypeId"], + createdDate: json["createdDate"] == null ? null : DateTime.parse(json["createdDate"]), + targetUserId: json["targetUserId"] == null ? null : json["targetUserId"], + targetUserName: json["targetUserName"] == null ? null : json["targetUserName"], + fileTypeResponse: json["fileTypeResponse"] == null ? null : FileTypeResponse.fromJson(json["fileTypeResponse"]), + isImageLoaded: false, + image: null, + voice: null, + ); Map toJson() => { "userChatHistoryId": userChatHistoryId == null ? null : userChatHistoryId, diff --git a/lib/provider/chat_provider_model.dart b/lib/provider/chat_provider_model.dart index 4e1d421..974bc30 100644 --- a/lib/provider/chat_provider_model.dart +++ b/lib/provider/chat_provider_model.dart @@ -85,12 +85,13 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { void registerEvents() { chatHubConnection.on("OnUpdateUserStatusAsync", changeStatus); // chatHubConnection.on("OnDeliveredChatUserAsync", onMsgReceived); - // hubConnection.on("OnSeenChatUserAsync", onChatSeen); + chatHubConnection.on("OnSubmitChatAsync", OnSubmitChatAsync); chatHubConnection.on("OnUserTypingAsync", onUserTyping); chatHubConnection.on("OnUserCountAsync", userCountAsync); - // hubConnection.on("OnUpdateUserChatHistoryWindowsAsync", updateChatHistoryWindow); + // chatHubConnection.on("OnUpdateUserChatHistoryWindowsAsync", updateChatHistoryWindow); chatHubConnection.on("OnGetUserChatHistoryNotDeliveredAsync", chatNotDelivered); chatHubConnection.on("OnUpdateUserChatHistoryStatusAsync", updateUserChatStatus); + print("Alll Registered"); } void getUserRecentChats() async { @@ -107,9 +108,7 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { if (favUList.response != null && recentChat.response != null) { favUsersList = favUList.response!; favUsersList.sort( - (ChatUser a, ChatUser b) => a.userName!.toLowerCase().compareTo( - b.userName!.toLowerCase(), - ), + (ChatUser a, ChatUser b) => a.userName!.toLowerCase().compareTo(b.userName!.toLowerCase()), ); for (dynamic user in recentChat.response!) { for (dynamic favUser in favUList.response!) { @@ -230,16 +229,15 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { Future uploadAttachments(String userId, File file) async { dynamic result; try { - StreamedResponse response = await ChatApiClient().uploadMedia(userId, file); - if (response.statusCode == 200) { - result = jsonDecode(await response.stream.bytesToString()); + Object? response = await ChatApiClient().uploadMedia(userId, file); + if (response != null) { + result = response; } else { result = []; } } catch (e) { throw e; } - return result; } @@ -365,6 +363,7 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { ChatUser( id: data.first.currentUserId, userName: data.first.currentUserName, + email: data.first.currentUserEmail, unreadMessageCount: 0, isImageLoading: false, image: chatImages!.first.profilePicture ?? "", @@ -404,6 +403,28 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { notifyListeners(); } + void OnSubmitChatAsync(List? parameters) { + logger.d(parameters); + List data = [], temp = []; + for (dynamic msg in parameters!) { + data = getSingleUserChatModel(jsonEncode(msg)); + temp = getSingleUserChatModel(jsonEncode(msg)); + data.first.targetUserId = temp.first.currentUserId; + data.first.targetUserName = temp.first.currentUserName; + data.first.targetUserEmail = temp.first.currentUserEmail; + data.first.currentUserId = temp.first.targetUserId; + data.first.currentUserName = temp.first.targetUserName; + data.first.currentUserEmail = temp.first.targetUserEmail; + } + if (isChatScreenActive && data.first.currentUserId == receiverID) { + int index = userChatHistory.indexWhere((SingleUserChatModel element) => element.userChatHistoryId == 0); + logger.d(index); + userChatHistory[index] = data.first; + } + + notifyListeners(); + } + void sort() { searchedChats!.sort( (ChatUser a, ChatUser b) => b.unreadMessageCount!.compareTo(a.unreadMessageCount!), @@ -454,6 +475,10 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { return 2; case ".rar": return 2; + case ".aac": + return 13; + case ".mp3": + return 14; default: return 0; } @@ -487,6 +512,10 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { return "application/octet-stream"; case ".rar": return "application/octet-stream"; + case ".aac": + return "audio/aac"; + case ".mp3": + return "audio/mp3"; default: return ""; } @@ -501,11 +530,13 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { required bool isAttachment, required bool isReply, Uint8List? image, + Uint8List? voice, required bool isImageLoaded}) async { Uuid uuid = const Uuid(); String contentNo = uuid.v4(); String msg = message.text; SingleUserChatModel data = SingleUserChatModel( + userChatHistoryId: 0, chatEventId: chatEventId, chatSource: 1, contant: msg, @@ -530,7 +561,10 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { : null, image: image, isImageLoaded: isImageLoaded, + voice: voice, ); + print("Model data---------------------------"); + logger.d(data.toJson()); userChatHistory.insert(0, data); isFileSelected = false; isMsgReply = false; @@ -569,9 +603,11 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { isReply: false, isImageLoaded: true, image: selectedFile.readAsBytesSync()); - } // normal attachemnt msg + } if (!isFileSelected && isMsgReply) { - print("Normal Text To Text Reply"); + if (kDebugMode) { + print("Normal Text To Text Reply"); + } if (message.text == null || message.text.isEmpty) { return; } @@ -723,6 +759,10 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { return "assets/icons/chat/zip.svg"; case ".rar": return "assets/icons/chat/zip.svg"; + case ".aac": + return "assets/icons/chat/aac.svg"; + case ".mp3": + return "assets/icons/chat/zip.mp3"; default: return "assets/images/thumb.svg"; } @@ -889,6 +929,7 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { String dirPath = '${appDocumentsDirectory.path}/chat_images'; if (!await Directory(dirPath).exists()) { await Directory(dirPath).create(); + await File('$dirPath/.nomedia').create(); } late File imageFile = File("$dirPath/$userID.jpg"); imageFile.writeAsBytesSync(decodedBytes); @@ -956,11 +997,10 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { } void userTypingInvoke({required int currentUser, required int reciptUser}) async { - logger.d([reciptUser, currentUser]); await chatHubConnection.invoke("UserTypingAsync", args: [reciptUser, currentUser]); } - // Audio Recoding Work +// Audio Recoding Work Timer? _timer; int _recodeDuration = 0; bool isRecoding = false; @@ -972,13 +1012,18 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { late RecorderController recorderController; late PlayerController playerController; - //////// Audio Recoding Work //////////////////// +//////// Audio Recoding Work //////////////////// Future initAudio({required int receiverId}) async { + // final dir = Directory((Platform.isAndroid + // ? await getExternalStorageDirectory() //FOR ANDROID + // : await getApplicationSupportDirectory() //FOR IOS + // )! appDirectory = await getApplicationDocumentsDirectory(); String dirPath = '${appDirectory.path}/chat_audios'; if (!await Directory(dirPath).exists()) { await Directory(dirPath).create(); + await File('$dirPath/.nomedia').create(); } path = "$dirPath/${AppState().chatDetails!.response!.id}-$receiverID-${DateTime.now().microsecondsSinceEpoch}.aac"; recorderController = RecorderController() @@ -986,6 +1031,7 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { ..androidOutputFormat = AndroidOutputFormat.mpeg4 ..iosEncoder = IosEncoder.kAudioFormatMPEG4AAC ..sampleRate = 6000 + ..updateFrequency = const Duration(milliseconds: 100) ..bitRate = 18000; playerController = PlayerController(); } @@ -1014,15 +1060,23 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { } } - void _startTimer() { + Future _startTimer() async { _timer?.cancel(); - _timer = Timer.periodic(const Duration(seconds: 1), (Timer t) { + _timer = Timer.periodic(const Duration(seconds: 1), (Timer t) async { _recodeDuration++; - buildTimer(); - notifyListeners(); + if (_recodeDuration <= 59) { + applyCounter(); + } else { + pauseRecoding(); + } }); } + void applyCounter() { + buildTimer(); + notifyListeners(); + } + Future pauseRecoding() async { isPause = true; isPlaying = true; @@ -1030,27 +1084,16 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { path = await recorderController.stop(false); File file = File(path!); file.readAsBytesSync(); + path = file.path; await playerController.preparePlayer(file.path, 1.0); - // var tempDuration = _recodeDuration; - // _recodeDuration = tempDuration; _timer?.cancel(); notifyListeners(); } - void resumeRecoding() { - isPause = false; - isPlaying = false; - isRecoding = true; - recorderController.record(path); - _startTimer(); - } - Future deleteRecoding() async { _recodeDuration = 0; _timer?.cancel(); - // path = await recorderController.stop(true); - recorderController.reset(); - print(path); + recorderController.stop(true); if (path != null && path!.isNotEmpty) { File delFile = File(path!); double fileSizeInKB = delFile.lengthSync() / 1024; @@ -1095,8 +1138,9 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { } void sendVoiceMessage(BuildContext context, {required int targetUserId, required int userStatus, required String userEmail, required String targetUserName}) async { - //recorderController.pause(); - path = await recorderController.stop(false); + if (!isPause) { + path = await recorderController.stop(false); + } if (kDebugMode) { print(path); } @@ -1110,17 +1154,156 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { dynamic value = await uploadAttachments(AppState().chatDetails!.response!.id.toString(), voiceFile); logger.d(value); String? ext = getFileExtension(voiceFile.path); + logger.d(voiceFile.path!.split("/").last); Utils.hideLoading(context); - // sendChatToServer( - // chatEventId: 2, - // fileTypeId: getFileType(ext.toString()), - // targetUserId: targetUserId, - // targetUserName: targetUserName, - // isAttachment: true, - // chatReplyId: null, - // isReply: false, - // isImageLoaded: true, - // image: voiceFile.readAsBytesSync()); + sendVoiceMessageToServer( + msgText: voiceFile.path!.split("/").last, + chatEventId: 2, + fileTypeId: getFileType(ext.toString()), + targetUserId: targetUserId, + targetUserName: targetUserName, + isVoiceAttached: true, + voice: voiceFile.readAsBytesSync(), + userEmail: userEmail, + userStatus: userStatus, + chatReplyId: null, + isAttachment: true, + isReply: false, + voicFile: voiceFile, + ); notifyListeners(); } + + Future sendVoiceMessageToServer( + {String? msgText, + int? chatEventId, + int? fileTypeId, + int? targetUserId, + String? targetUserName, + bool? isVoiceAttached, + Uint8List? voice, + String? userEmail, + int? userStatus, + bool? isReply, + bool? isAttachment, + int? chatReplyId, + File? voicFile}) async { + Uuid uuid = const Uuid(); + String contentNo = uuid.v4(); + String msg = msgText!; + SingleUserChatModel data = SingleUserChatModel( + chatEventId: chatEventId, + chatSource: 1, + contant: msg, + contantNo: contentNo, + conversationId: chatCID, + createdDate: DateTime.now(), + currentUserId: AppState().chatDetails!.response!.id, + currentUserName: AppState().chatDetails!.response!.userName, + targetUserId: targetUserId, + targetUserName: targetUserName, + isReplied: false, + fileTypeId: fileTypeId, + userChatReplyResponse: isReply! ? UserChatReplyResponse.fromJson(repliedMsg.first.toJson()) : null, + fileTypeResponse: isAttachment! + ? FileTypeResponse( + fileTypeId: fileTypeId, + fileTypeName: getFileExtension(voicFile!.path).toString(), + fileKind: "file", + fileName: msgText, + fileTypeDescription: getFileTypeDescription(getFileExtension(voicFile!.path).toString()), + ) + : null, + image: null, + isImageLoaded: false, + voice: voice, + ); + userChatHistory.insert(0, data); + notifyListeners(); + String chatData = + '{"contant":"$msg","contantNo":"$contentNo","chatEventId":$chatEventId,"fileTypeId": $fileTypeId,"currentUserId":${AppState().chatDetails!.response!.id},"chatSource":1,"userChatHistoryLineRequestList":[{"isSeen":false,"isDelivered":false,"targetUserId":$targetUserId,"targetUserStatus":1}],"chatReplyId":$chatReplyId,"conversationId":"$chatCID"}'; + await chatHubConnection.invoke("AddChatUserAsync", args: [json.decode(chatData)]); + + if (searchedChats != null) { + dynamic contain = searchedChats!.where((ChatUser element) => element.id == targetUserId); + if (contain.isEmpty) { + List emails = []; + emails.add(await EmailImageEncryption().encrypt(val: userEmail!)); + List chatImages = await ChatApiClient().getUsersImages(encryptedEmails: emails); + searchedChats!.add( + ChatUser( + id: targetUserId, + userName: targetUserName, + unreadMessageCount: 0, + email: userEmail, + isImageLoading: false, + image: chatImages.first.profilePicture ?? "", + isImageLoaded: true, + isTyping: false, + isFav: false, + userStatus: userStatus, + userLocalDownlaodedImage: await downloadImageLocal(chatImages.first.profilePicture, targetUserId.toString()), + ), + ); + notifyListeners(); + } + } else { + List emails = []; + emails.add(await EmailImageEncryption().encrypt(val: userEmail!)); + List chatImages = await ChatApiClient().getUsersImages(encryptedEmails: emails); + searchedChats!.add( + ChatUser( + id: targetUserId, + userName: targetUserName, + unreadMessageCount: 0, + email: userEmail, + isImageLoading: false, + image: chatImages.first.profilePicture ?? "", + isImageLoaded: true, + isTyping: false, + isFav: false, + userStatus: userStatus, + userLocalDownlaodedImage: await downloadImageLocal(chatImages.first.profilePicture, targetUserId.toString()), + ), + ); + notifyListeners(); + } + } + + void playVoice( + BuildContext context, { + required SingleUserChatModel data, + }) async { + Utils.showLoading(context); + Uint8List encodedString = await ChatApiClient().downloadURL(fileName: data.contant!, fileTypeDescription: getFileTypeDescription(data.fileTypeResponse!.fileTypeName ?? "")); + try { + String path = await downChatVoice(encodedString, data.fileTypeResponse!.fileTypeName ?? "", data); + logger.d(path); + File file = File(path!); + file.readAsBytesSync(); + Utils.hideLoading(context); + await playerController.preparePlayer(file.path, 1.0); + notifyListeners(); + playerController.startPlayer(finishMode: FinishMode.pause); + } catch (e) { + Utils.showToast("Cannot open file."); + } + } + + Future downChatVoice(Uint8List bytes, String ext, SingleUserChatModel data) async { + String dirPath = '${(await getApplicationDocumentsDirectory()).path}/chat_audios'; + if (!await Directory(dirPath).exists()) { + await Directory(dirPath).create(); + await File('$dirPath/.nomedia').create(); + } + File file = File("$dirPath/${data.currentUserId}-${data.targetUserId}-${DateTime.now().microsecondsSinceEpoch}." + ext); + await file.writeAsBytes(bytes); + return file.path; + } + +// data.scrollController.animateTo( +// data.scrollController.position.maxScrollExtent, +// duration: const Duration(milliseconds: 100), +// curve: Curves.easeOut, +// ); } diff --git a/lib/ui/chat/chat_bubble.dart b/lib/ui/chat/chat_bubble.dart index ed20c24..ea87450 100644 --- a/lib/ui/chat/chat_bubble.dart +++ b/lib/ui/chat/chat_bubble.dart @@ -20,8 +20,6 @@ import 'package:mohem_flutter_app/widgets/bottom_sheet.dart'; import 'package:open_file/open_file.dart'; import 'package:provider/provider.dart'; -// todo: @aamir use extension methods, and use correct widgets. - class ChatBubble extends StatelessWidget { ChatBubble({Key? key, required this.dateTime, required this.cItem}) : super(key: key); final String dateTime; @@ -102,7 +100,8 @@ class ChatBubble extends StatelessWidget { ], ), ), - ).paddingOnly(bottom: 7), + ).paddingOnly(bottom: 7).onPress(() { + }), if (fileTypeID == 12 || fileTypeID == 4 || fileTypeID == 3) ClipRRect( borderRadius: BorderRadius.circular(5.0), @@ -117,18 +116,23 @@ class ChatBubble extends StatelessWidget { ); }), ), - ).paddingOnly(bottom: 4) + ).paddingOnly(bottom: 4), + if (fileTypeID == 13) + currentWaveBubble(context).onPress(() { + data.playVoice(context, data: cItem); + }) else Row( children: [ if (fileTypeID == 1 || fileTypeID == 5 || fileTypeID == 7 || fileTypeID == 6 || fileTypeID == 8 - // || fileTypeID == 2 + // || fileTypeID == 2 ) SvgPicture.asset(data.getType(fileTypeName ?? ""), height: 30, width: 22, alignment: Alignment.center, fit: BoxFit.cover).paddingOnly(left: 0, right: 10), (cItem.contant ?? "").toText12().expanded, if (fileTypeID == 1 || fileTypeID == 5 || fileTypeID == 7 || fileTypeID == 6 || fileTypeID == 8 - //|| fileTypeID == 2 - ) const Icon(Icons.remove_red_eye, size: 16) + //|| fileTypeID == 2 + ) + const Icon(Icons.remove_red_eye, size: 16) ], ), Align( @@ -157,10 +161,7 @@ class ChatBubble extends StatelessWidget { transform: GradientRotation(.83), begin: Alignment.topRight, end: Alignment.bottomLeft, - colors: [ - MyColors.gradiantEndColor, - MyColors.gradiantStartColor, - ], + colors: [MyColors.gradiantEndColor, MyColors.gradiantStartColor], ), ), child: Column( @@ -203,7 +204,8 @@ class ChatBubble extends StatelessWidget { ], ), ), - ).paddingOnly(bottom: 7), + ).paddingOnly(bottom: 7).onPress(() { + }), if (fileTypeID == 12 || fileTypeID == 4 || fileTypeID == 3) ClipRRect( borderRadius: BorderRadius.circular(5.0), @@ -218,7 +220,11 @@ class ChatBubble extends StatelessWidget { ); }), ), - ).paddingOnly(bottom: 4) + ).paddingOnly(bottom: 4), + if (fileTypeID == 13) + recipetWaveBubble(context).onPress(() { + data.playVoice(context, data: cItem); + }) else Row( children: [ @@ -283,6 +289,102 @@ class ChatBubble extends StatelessWidget { ); } } + + Widget currentWaveBubble(BuildContext context) { + return Container( + margin: const EdgeInsets.all(0), + decoration: BoxDecoration( + border: Border( + left: BorderSide(width: 6, color: isCurrentUser ? MyColors.gradiantStartColor : MyColors.white), + ), + color: isCurrentUser ? MyColors.black.withOpacity(0.10) : MyColors.black.withOpacity(0.30), + // gradient: const LinearGradient( + // transform: GradientRotation(.83), + // begin: Alignment.topRight, + // end: Alignment.bottomLeft, + // colors: [ + // MyColors.gradiantEndColor, + // MyColors.gradiantStartColor, + // ], + // ), + ), + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + const Icon( + Icons.play_arrow, + color: MyColors.lightGreenColor, + ).paddingAll(10), + AudioFileWaveforms( + size: Size(MediaQuery.of(context).size.width * 0.3, 10), + playerController: data.playerController, + padding: EdgeInsets.zero, + margin: EdgeInsets.zero, + enableSeekGesture: true, + density: 1, + playerWaveStyle: const PlayerWaveStyle( + fixedWaveColor: Colors.white, + liveWaveColor: MyColors.greenColor, + showTop: true, + showBottom: true, + waveCap: StrokeCap.round, + seekLineThickness: 2, + visualizerHeight: 4, + backgroundColor: Colors.transparent, + ), + ).expanded, + ], + ), + ).circle(5); + } + + Widget recipetWaveBubble(BuildContext context) { + return Container( + margin: const EdgeInsets.all(0), + decoration: BoxDecoration( + border: Border( + left: BorderSide(width: 6, color: isCurrentUser ? MyColors.gradiantStartColor : MyColors.white), + ), + color: isCurrentUser ? MyColors.black.withOpacity(0.10) : MyColors.black.withOpacity(0.30), + // gradient: const LinearGradient( + // transform: GradientRotation(.83), + // begin: Alignment.topRight, + // end: Alignment.bottomLeft, + // colors: [ + // MyColors.gradiantEndColor, + // MyColors.gradiantStartColor, + // ], + // ), + ), + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + const Icon( + Icons.play_arrow, + color: MyColors.white, + ).paddingAll(10), + AudioFileWaveforms( + size: Size(MediaQuery.of(context).size.width * 0.3, 10), + playerController: data.playerController, + padding: EdgeInsets.zero, + margin: EdgeInsets.zero, + enableSeekGesture: true, + density: 1, + playerWaveStyle: const PlayerWaveStyle( + fixedWaveColor: Colors.white, + liveWaveColor: MyColors.greenColor, + showTop: true, + showBottom: true, + waveCap: StrokeCap.round, + seekLineThickness: 2, + visualizerHeight: 4, + backgroundColor: Colors.transparent, + ), + ).expanded, + ], + ), + ).circle(5); + } } class WaveBubble extends StatelessWidget { @@ -329,15 +431,15 @@ class WaveBubble extends StatelessWidget { padding: EdgeInsets.zero, margin: EdgeInsets.zero, enableSeekGesture: true, - density: 2, + density: 1, playerWaveStyle: const PlayerWaveStyle( fixedWaveColor: Colors.white, - liveWaveColor:MyColors.greenColor, + liveWaveColor: MyColors.greenColor, showTop: true, showBottom: true, waveCap: StrokeCap.round, seekLineThickness: 2, - visualizerHeight: 5, + visualizerHeight: 4, backgroundColor: Colors.transparent, ), ), diff --git a/lib/ui/chat/chat_detailed_screen.dart b/lib/ui/chat/chat_detailed_screen.dart index c10a8c6..1ec1bcf 100644 --- a/lib/ui/chat/chat_detailed_screen.dart +++ b/lib/ui/chat/chat_detailed_screen.dart @@ -144,7 +144,13 @@ class _ChatDetailScreenState extends State { ); }, ).onPress(() async { - if (m.userChatHistory[i].fileTypeResponse != null) { + logger.d(m.userChatHistory[i].toJson()); + if (m.userChatHistory[i].fileTypeResponse != null && m.userChatHistory[i].fileTypeId! == 1 || + m.userChatHistory[i].fileTypeId! == 5 || + m.userChatHistory[i].fileTypeId! == 7 || + m.userChatHistory[i].fileTypeId! == 6 || + m.userChatHistory[i].fileTypeId! == 8 || + m.userChatHistory[i].fileTypeId! == 2) { m.getChatMedia(context, fileTypeName: m.userChatHistory[i].fileTypeResponse!.fileTypeName ?? "", fileTypeID: m.userChatHistory[i].fileTypeId!, fileName: m.userChatHistory[i].contant!); } From 5d93451c13fe967b9ac66d590fbdbdb476f32b99 Mon Sep 17 00:00:00 2001 From: "Aamir.Muhammad" Date: Thu, 22 Dec 2022 09:41:12 +0300 Subject: [PATCH 04/12] chat voice message implementation --- lib/api/chat/chat_api_client.dart | 14 ++++++-- lib/provider/chat_provider_model.dart | 46 +++++++++++++++------------ lib/ui/chat/chat_bubble.dart | 6 ++-- lib/ui/chat/chat_detailed_screen.dart | 23 ++++++++------ 4 files changed, 53 insertions(+), 36 deletions(-) diff --git a/lib/api/chat/chat_api_client.dart b/lib/api/chat/chat_api_client.dart index 46dcce7..43e9f6c 100644 --- a/lib/api/chat/chat_api_client.dart +++ b/lib/api/chat/chat_api_client.dart @@ -57,6 +57,7 @@ class ChatApiClient { return List.from(json.decode(response.body).map((x) => ChatUser.fromJson(x))); } + //Get User Recent Chats Future getRecentChats() async { try { Response response = await ApiClient().getJsonForResponse( @@ -74,6 +75,7 @@ class ChatApiClient { } } + // Get Favorite Users Future getFavUsers() async { Response favRes = await ApiClient().getJsonForResponse( "${ApiConsts.chatFavUser}getFavUserById/${AppState().chatDetails!.response!.id}", @@ -85,6 +87,7 @@ class ChatApiClient { return ChatUserModel.fromJson(json.decode(favRes.body)); } + //Get User Chat History Future getSingleUserChatHistory({required int senderUID, required int receiverUID, required bool loadMore, bool isNewChat = false, required int paginationVal}) async { try { Response response = await ApiClient().getJsonForResponse( @@ -101,6 +104,7 @@ class ChatApiClient { } } +//Favorite Users Future favUser({required int userID, required int targetUserID}) async { Response response = await ApiClient().postJsonForResponse("${ApiConsts.chatFavUser}addFavUser", {"targetUserId": targetUserID, "userId": userID}, token: AppState().chatDetails!.response!.token); if (!kReleaseMode) { @@ -110,6 +114,7 @@ class ChatApiClient { return favoriteChatUser; } + //UnFavorite Users Future unFavUser({required int userID, required int targetUserID}) async { try { Response response = await ApiClient().postJsonForResponse( @@ -128,9 +133,13 @@ class ChatApiClient { } } +// Upload Chat Media Future uploadMedia(String userId, File file) async { - print("${ApiConsts.chatMediaImageUploadUrl}upload"); - print(AppState().chatDetails!.response!.token); + if (kDebugMode) { + print("${ApiConsts.chatMediaImageUploadUrl}upload"); + print(AppState().chatDetails!.response!.token); + } + dynamic request = MultipartRequest('POST', Uri.parse('${ApiConsts.chatMediaImageUploadUrl}upload')); request.fields.addAll({'userId': userId, 'fileSource': '1'}); request.files.add(await MultipartFile.fromPath('files', file.path)); @@ -154,6 +163,7 @@ class ChatApiClient { return data; } + //Get Chat Users & Favorite Images Future> getUsersImages({required List encryptedEmails}) async { List imagesData = []; Response response = await ApiClient().postJsonForResponse( diff --git a/lib/provider/chat_provider_model.dart b/lib/provider/chat_provider_model.dart index 974bc30..16bf327 100644 --- a/lib/provider/chat_provider_model.dart +++ b/lib/provider/chat_provider_model.dart @@ -68,7 +68,9 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { Future buildHubConnection() async { chatHubConnection = await getHubConnection(); await chatHubConnection.start(); - print("Startedddddddd"); + if (kDebugMode) { + logger.i("Hub Conn: Startedddddddd"); + } chatHubConnection.on("OnDeliveredChatUserAsync", onMsgReceived); chatHubConnection.on("OnGetChatConversationCount", onNewChatConversion); } @@ -91,7 +93,9 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { // chatHubConnection.on("OnUpdateUserChatHistoryWindowsAsync", updateChatHistoryWindow); chatHubConnection.on("OnGetUserChatHistoryNotDeliveredAsync", chatNotDelivered); chatHubConnection.on("OnUpdateUserChatHistoryStatusAsync", updateUserChatStatus); - print("Alll Registered"); + if (kDebugMode) { + logger.i("All listeners registered"); + } } void getUserRecentChats() async { @@ -279,7 +283,9 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { void updateChatHistoryWindow(List? args) { dynamic items = args!.toList(); - print("---------------------------------Update Chat History Windows Async -------------------------------------"); + if (kDebugMode) { + logger.i("---------------------------------Update Chat History Windows Async -------------------------------------"); + } logger.d(items); // for (var user in searchedChats!) { // if (user.id == items.first["id"]) { @@ -404,7 +410,7 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { } void OnSubmitChatAsync(List? parameters) { - logger.d(parameters); + logger.i(parameters); List data = [], temp = []; for (dynamic msg in parameters!) { data = getSingleUserChatModel(jsonEncode(msg)); @@ -563,8 +569,9 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { isImageLoaded: isImageLoaded, voice: voice, ); - print("Model data---------------------------"); - logger.d(data.toJson()); + if (kDebugMode) { + logger.i("model data: " + jsonEncode(data)); + } userChatHistory.insert(0, data); isFileSelected = false; isMsgReply = false; @@ -579,16 +586,19 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { void sendChatMessage(BuildContext context, {required int targetUserId, required int userStatus, required String userEmail, required String targetUserName}) async { if (!isFileSelected && !isMsgReply) { - print("Normal Text Msg"); + if (kDebugMode) { + print("Normal Text Msg"); + } if (message.text == null || message.text.isEmpty) { return; } sendChatToServer( chatEventId: 1, fileTypeId: null, targetUserId: targetUserId, targetUserName: targetUserName, isAttachment: false, chatReplyId: null, isReply: false, isImageLoaded: false, image: null); - } // normal Text msg + } if (isFileSelected && !isMsgReply) { - bool isImage = false; - print("Normal Attachment Msg"); + if (kDebugMode) { + logger.i("Normal Attachment Msg"); + } Utils.showLoading(context); dynamic value = await uploadAttachments(AppState().chatDetails!.response!.id.toString(), selectedFile); String? ext = getFileExtension(selectedFile.path); @@ -623,7 +633,9 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { image: repliedMsg.first.image); } // reply msg over image && normal if (isFileSelected && isMsgReply) { - print("Reply With File"); + if (kDebugMode) { + print("Reply With File"); + } Utils.showLoading(context); dynamic value = await uploadAttachments(AppState().chatDetails!.response!.id.toString(), selectedFile); String? ext = getFileExtension(selectedFile.path); @@ -711,7 +723,7 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { String? getFileExtension(String fileName) { try { if (kDebugMode) { - print("ext: " + "." + fileName.split('.').last); + logger.i("ext: " + "." + fileName.split('.').last); } return "." + fileName.split('.').last; } catch (e) { @@ -836,7 +848,6 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { } void clearAll() { - print("----------------- Disposed ---------------------------"); searchedChats = pChatHistory; search.clear(); isChatScreenActive = false; @@ -1050,9 +1061,7 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { startRecoding(); } else { recorderController.reset(); - logger.d(recorderController.isRecording); await recorderController.record(path); - logger.d(recorderController.isRecording); _recodeDuration = 0; _startTimer(); isRecoding = !isRecoding; @@ -1142,7 +1151,7 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { path = await recorderController.stop(false); } if (kDebugMode) { - print(path); + logger.i("path:" + path!); } File voiceFile = File(path!); voiceFile.readAsBytesSync(); @@ -1152,9 +1161,7 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { isRecoding = false; Utils.showLoading(context); dynamic value = await uploadAttachments(AppState().chatDetails!.response!.id.toString(), voiceFile); - logger.d(value); String? ext = getFileExtension(voiceFile.path); - logger.d(voiceFile.path!.split("/").last); Utils.hideLoading(context); sendVoiceMessageToServer( msgText: voiceFile.path!.split("/").last, @@ -1168,7 +1175,7 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { userStatus: userStatus, chatReplyId: null, isAttachment: true, - isReply: false, + isReply: isMsgReply, voicFile: voiceFile, ); notifyListeners(); @@ -1278,7 +1285,6 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { Uint8List encodedString = await ChatApiClient().downloadURL(fileName: data.contant!, fileTypeDescription: getFileTypeDescription(data.fileTypeResponse!.fileTypeName ?? "")); try { String path = await downChatVoice(encodedString, data.fileTypeResponse!.fileTypeName ?? "", data); - logger.d(path); File file = File(path!); file.readAsBytesSync(); Utils.hideLoading(context); diff --git a/lib/ui/chat/chat_bubble.dart b/lib/ui/chat/chat_bubble.dart index ea87450..bb67365 100644 --- a/lib/ui/chat/chat_bubble.dart +++ b/lib/ui/chat/chat_bubble.dart @@ -100,8 +100,7 @@ class ChatBubble extends StatelessWidget { ], ), ), - ).paddingOnly(bottom: 7).onPress(() { - }), + ).paddingOnly(bottom: 7).onPress(() {}), if (fileTypeID == 12 || fileTypeID == 4 || fileTypeID == 3) ClipRRect( borderRadius: BorderRadius.circular(5.0), @@ -204,8 +203,7 @@ class ChatBubble extends StatelessWidget { ], ), ), - ).paddingOnly(bottom: 7).onPress(() { - }), + ).paddingOnly(bottom: 7).onPress(() {}), if (fileTypeID == 12 || fileTypeID == 4 || fileTypeID == 3) ClipRRect( borderRadius: BorderRadius.circular(5.0), diff --git a/lib/ui/chat/chat_detailed_screen.dart b/lib/ui/chat/chat_detailed_screen.dart index 1ec1bcf..7b3a72b 100644 --- a/lib/ui/chat/chat_detailed_screen.dart +++ b/lib/ui/chat/chat_detailed_screen.dart @@ -130,7 +130,7 @@ class _ChatDetailScreenState extends State { reverse: true, itemCount: m.userChatHistory.length, padding: const EdgeInsets.all(21), - separatorBuilder: (cxt, index) => 8.height, + separatorBuilder: (BuildContext cxt, int index) => 8.height, itemBuilder: (BuildContext context, int i) { return SwipeTo( iconColor: MyColors.lightGreenColor, @@ -145,14 +145,17 @@ class _ChatDetailScreenState extends State { }, ).onPress(() async { logger.d(m.userChatHistory[i].toJson()); - if (m.userChatHistory[i].fileTypeResponse != null && m.userChatHistory[i].fileTypeId! == 1 || - m.userChatHistory[i].fileTypeId! == 5 || - m.userChatHistory[i].fileTypeId! == 7 || - m.userChatHistory[i].fileTypeId! == 6 || - m.userChatHistory[i].fileTypeId! == 8 || - m.userChatHistory[i].fileTypeId! == 2) { - m.getChatMedia(context, - fileTypeName: m.userChatHistory[i].fileTypeResponse!.fileTypeName ?? "", fileTypeID: m.userChatHistory[i].fileTypeId!, fileName: m.userChatHistory[i].contant!); + if (m.userChatHistory[i].fileTypeResponse != null && m.userChatHistory[i].fileTypeId != null) { + if (m.userChatHistory[i].fileTypeId! == 1 || + m.userChatHistory[i].fileTypeId! == 5 || + m.userChatHistory[i].fileTypeId! == 7 || + m.userChatHistory[i].fileTypeId! == 6 || + m.userChatHistory[i].fileTypeId! == 8 + // || m.userChatHistory[i].fileTypeId! == 2 + ) { + m.getChatMedia(context, + fileTypeName: m.userChatHistory[i].fileTypeResponse!.fileTypeName ?? "", fileTypeID: m.userChatHistory[i].fileTypeId!, fileName: m.userChatHistory[i].contant!); + } } }); }, @@ -312,7 +315,7 @@ class _ChatDetailScreenState extends State { () => m.selectImageToUpload(context), ), ).paddingOnly(right: 15), - Icon( + const Icon( Icons.mic, color: MyColors.lightGreenColor, ).paddingOnly(right: 15).onPress(() { From 47a697f352e4452729d15628399eaa5afaf56615 Mon Sep 17 00:00:00 2001 From: "Aamir.Muhammad" Date: Thu, 22 Dec 2022 11:13:26 +0300 Subject: [PATCH 05/12] chat voice message implementation --- .../chat/get_single_user_chat_list_model.dart | 8 ++++++-- lib/provider/chat_provider_model.dart | 6 +++++- lib/ui/chat/chat_bubble.dart | 14 ++++++++------ 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/lib/models/chat/get_single_user_chat_list_model.dart b/lib/models/chat/get_single_user_chat_list_model.dart index c585af7..7ca60a8 100644 --- a/lib/models/chat/get_single_user_chat_list_model.dart +++ b/lib/models/chat/get_single_user_chat_list_model.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'dart:typed_data'; +import 'package:audio_waveforms/audio_waveforms.dart'; import 'package:flutter/foundation.dart'; List singleUserChatModelFromJson(String str) => List.from(json.decode(str).map((x) => SingleUserChatModel.fromJson(x))); @@ -33,7 +34,8 @@ class SingleUserChatModel { this.isReplied, this.isImageLoaded, this.image, - this.voice}); + this.voice, + this.voiceController}); int? userChatHistoryId; int? userChatHistoryLineId; @@ -60,6 +62,7 @@ class SingleUserChatModel { bool? isImageLoaded; Uint8List? image; Uint8List? voice; + PlayerController? voiceController; factory SingleUserChatModel.fromJson(Map json) => SingleUserChatModel( userChatHistoryId: json["userChatHistoryId"] == null ? null : json["userChatHistoryId"], @@ -86,7 +89,8 @@ class SingleUserChatModel { isReplied: false, isImageLoaded: false, image: null, - voice: null); + voice: null, + voiceController: json["fileTypeId"] == 13 ? PlayerController() : null); Map toJson() => { "userChatHistoryId": userChatHistoryId == null ? null : userChatHistoryId, diff --git a/lib/provider/chat_provider_model.dart b/lib/provider/chat_provider_model.dart index 16bf327..5f7bf0e 100644 --- a/lib/provider/chat_provider_model.dart +++ b/lib/provider/chat_provider_model.dart @@ -1102,7 +1102,11 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { Future deleteRecoding() async { _recodeDuration = 0; _timer?.cancel(); - recorderController.stop(true); + if (path == null) { + path = await recorderController.stop(true); + } else { + await recorderController.stop(true); + } if (path != null && path!.isNotEmpty) { File delFile = File(path!); double fileSizeInKB = delFile.lengthSync() / 1024; diff --git a/lib/ui/chat/chat_bubble.dart b/lib/ui/chat/chat_bubble.dart index bb67365..9f0396a 100644 --- a/lib/ui/chat/chat_bubble.dart +++ b/lib/ui/chat/chat_bubble.dart @@ -117,7 +117,7 @@ class ChatBubble extends StatelessWidget { ), ).paddingOnly(bottom: 4), if (fileTypeID == 13) - currentWaveBubble(context).onPress(() { + currentWaveBubble(context, cItem).onPress(() { data.playVoice(context, data: cItem); }) else @@ -220,7 +220,7 @@ class ChatBubble extends StatelessWidget { ), ).paddingOnly(bottom: 4), if (fileTypeID == 13) - recipetWaveBubble(context).onPress(() { + recipetWaveBubble(context, cItem).onPress(() { data.playVoice(context, data: cItem); }) else @@ -288,7 +288,8 @@ class ChatBubble extends StatelessWidget { } } - Widget currentWaveBubble(BuildContext context) { + Widget currentWaveBubble(BuildContext context, SingleUserChatModel data) { + PlayerController cunController = PlayerController(); return Container( margin: const EdgeInsets.all(0), decoration: BoxDecoration( @@ -315,7 +316,7 @@ class ChatBubble extends StatelessWidget { ).paddingAll(10), AudioFileWaveforms( size: Size(MediaQuery.of(context).size.width * 0.3, 10), - playerController: data.playerController, + playerController: data.voiceController!, padding: EdgeInsets.zero, margin: EdgeInsets.zero, enableSeekGesture: true, @@ -336,7 +337,8 @@ class ChatBubble extends StatelessWidget { ).circle(5); } - Widget recipetWaveBubble(BuildContext context) { + Widget recipetWaveBubble(BuildContext context, SingleUserChatModel data) { + PlayerController repController = PlayerController(); return Container( margin: const EdgeInsets.all(0), decoration: BoxDecoration( @@ -363,7 +365,7 @@ class ChatBubble extends StatelessWidget { ).paddingAll(10), AudioFileWaveforms( size: Size(MediaQuery.of(context).size.width * 0.3, 10), - playerController: data.playerController, + playerController: data.voiceController!, padding: EdgeInsets.zero, margin: EdgeInsets.zero, enableSeekGesture: true, From 091abc68bfc748a4eadfc0d90c9c96aa9139c342 Mon Sep 17 00:00:00 2001 From: Sikander Saleem Date: Thu, 22 Dec 2022 12:03:54 +0300 Subject: [PATCH 06/12] IOS Fixes --- lib/classes/consts.dart | 4 ++-- lib/provider/chat_provider_model.dart | 11 ++++++++--- lib/ui/chat/chat_bubble.dart | 25 +++++++++++++++++-------- 3 files changed, 27 insertions(+), 13 deletions(-) diff --git a/lib/classes/consts.dart b/lib/classes/consts.dart index 52d0407..4a7d7fe 100644 --- a/lib/classes/consts.dart +++ b/lib/classes/consts.dart @@ -3,8 +3,8 @@ import 'package:mohem_flutter_app/ui/marathon/widgets/question_card.dart'; class ApiConsts { //static String baseUrl = "http://10.200.204.20:2801/"; // Local server // static String baseUrl = "https://erptstapp.srca.org.sa"; // SRCA server - static String baseUrl = "https://uat.hmgwebservices.com"; // UAT server - // static String baseUrl = "https://hmgwebservices.com"; // Live server + //static String baseUrl = "https://uat.hmgwebservices.com"; // UAT server + static String baseUrl = "https://hmgwebservices.com"; // Live server static String baseUrlServices = baseUrl + "/Services/"; // server // static String baseUrlServices = "https://api.cssynapses.com/tangheem/"; // Live server static String utilitiesRest = baseUrlServices + "Utilities.svc/REST/"; diff --git a/lib/provider/chat_provider_model.dart b/lib/provider/chat_provider_model.dart index 5f7bf0e..becf4dd 100644 --- a/lib/provider/chat_provider_model.dart +++ b/lib/provider/chat_provider_model.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; + import 'package:audio_waveforms/audio_waveforms.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/cupertino.dart'; @@ -11,7 +12,6 @@ import 'package:http/http.dart'; import 'package:just_audio/just_audio.dart' as JustAudio; import 'package:mohem_flutter_app/api/chat/chat_api_client.dart'; import 'package:mohem_flutter_app/app_state/app_state.dart'; -import 'package:mohem_flutter_app/classes/app_permissions.dart'; import 'package:mohem_flutter_app/classes/consts.dart'; import 'package:mohem_flutter_app/classes/encryption.dart'; import 'package:mohem_flutter_app/classes/utils.dart'; @@ -1292,14 +1292,19 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { File file = File(path!); file.readAsBytesSync(); Utils.hideLoading(context); - await playerController.preparePlayer(file.path, 1.0); + await data.voiceController!.preparePlayer(file.path, 1.0); + data.voiceController!.startPlayer(finishMode: FinishMode.pause); notifyListeners(); - playerController.startPlayer(finishMode: FinishMode.pause); } catch (e) { Utils.showToast("Cannot open file."); } } + void stopPlaying(BuildContext context, {required SingleUserChatModel data}) async { + data.voiceController!.stopPlayer(); + notifyListeners(); + } + Future downChatVoice(Uint8List bytes, String ext, SingleUserChatModel data) async { String dirPath = '${(await getApplicationDocumentsDirectory()).path}/chat_audios'; if (!await Directory(dirPath).exists()) { diff --git a/lib/ui/chat/chat_bubble.dart b/lib/ui/chat/chat_bubble.dart index 9f0396a..d6a6abe 100644 --- a/lib/ui/chat/chat_bubble.dart +++ b/lib/ui/chat/chat_bubble.dart @@ -118,7 +118,11 @@ class ChatBubble extends StatelessWidget { ).paddingOnly(bottom: 4), if (fileTypeID == 13) currentWaveBubble(context, cItem).onPress(() { - data.playVoice(context, data: cItem); + if(cItem.voiceController!.playerState == PlayerState.playing){ + data.stopPlaying(context, data: cItem); + }else{ + data.playVoice(context, data: cItem); + } }) else Row( @@ -203,7 +207,8 @@ class ChatBubble extends StatelessWidget { ], ), ), - ).paddingOnly(bottom: 7).onPress(() {}), + ).paddingOnly(bottom: 7).onPress(() { + }), if (fileTypeID == 12 || fileTypeID == 4 || fileTypeID == 3) ClipRRect( borderRadius: BorderRadius.circular(5.0), @@ -221,7 +226,12 @@ class ChatBubble extends StatelessWidget { ).paddingOnly(bottom: 4), if (fileTypeID == 13) recipetWaveBubble(context, cItem).onPress(() { - data.playVoice(context, data: cItem); + if(cItem.voiceController!.playerState == PlayerState.playing){ + data.stopPlaying(context, data: cItem); + }else{ + data.playVoice(context, data: cItem); + } + }) else Row( @@ -310,8 +320,8 @@ class ChatBubble extends StatelessWidget { child: Row( mainAxisSize: MainAxisSize.max, children: [ - const Icon( - Icons.play_arrow, + Icon( + data.voiceController!.playerState == PlayerState.playing ? Icons.stop_circle : Icons.play_arrow, color: MyColors.lightGreenColor, ).paddingAll(10), AudioFileWaveforms( @@ -338,7 +348,6 @@ class ChatBubble extends StatelessWidget { } Widget recipetWaveBubble(BuildContext context, SingleUserChatModel data) { - PlayerController repController = PlayerController(); return Container( margin: const EdgeInsets.all(0), decoration: BoxDecoration( @@ -359,8 +368,8 @@ class ChatBubble extends StatelessWidget { child: Row( mainAxisSize: MainAxisSize.max, children: [ - const Icon( - Icons.play_arrow, + Icon( + data.voiceController!.playerState == PlayerState.playing ? Icons.stop_circle : Icons.play_arrow, color: MyColors.white, ).paddingAll(10), AudioFileWaveforms( From 97dd82aff79986afef3c37430961b10a758e487b Mon Sep 17 00:00:00 2001 From: "Aamir.Muhammad" Date: Thu, 22 Dec 2022 14:39:11 +0300 Subject: [PATCH 07/12] chat voice message implementation --- lib/provider/chat_provider_model.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/provider/chat_provider_model.dart b/lib/provider/chat_provider_model.dart index becf4dd..d9de168 100644 --- a/lib/provider/chat_provider_model.dart +++ b/lib/provider/chat_provider_model.dart @@ -1301,7 +1301,7 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { } void stopPlaying(BuildContext context, {required SingleUserChatModel data}) async { - data.voiceController!.stopPlayer(); + await data.voiceController!.pausePlayer(); notifyListeners(); } From 5eec9dc07b27e7314fd909e52c25840f0db29d75 Mon Sep 17 00:00:00 2001 From: "Aamir.Muhammad" Date: Thu, 22 Dec 2022 15:18:02 +0300 Subject: [PATCH 08/12] chat voice message implementation --- lib/provider/chat_provider_model.dart | 38 +++++++++++++++++++-------- lib/ui/chat/chat_bubble.dart | 22 +++++++++------- 2 files changed, 39 insertions(+), 21 deletions(-) diff --git a/lib/provider/chat_provider_model.dart b/lib/provider/chat_provider_model.dart index d9de168..7ffc0e7 100644 --- a/lib/provider/chat_provider_model.dart +++ b/lib/provider/chat_provider_model.dart @@ -32,6 +32,7 @@ import 'package:uuid/uuid.dart'; class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { ScrollController scrollController = ScrollController(); + TextEditingController message = TextEditingController(); TextEditingController search = TextEditingController(); List userChatHistory = []; @@ -1180,7 +1181,7 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { chatReplyId: null, isAttachment: true, isReply: isMsgReply, - voicFile: voiceFile, + voiceFile: voiceFile, ); notifyListeners(); } @@ -1198,7 +1199,7 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { bool? isReply, bool? isAttachment, int? chatReplyId, - File? voicFile}) async { + File? voiceFile}) async { Uuid uuid = const Uuid(); String contentNo = uuid.v4(); String msg = msgText!; @@ -1219,10 +1220,10 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { fileTypeResponse: isAttachment! ? FileTypeResponse( fileTypeId: fileTypeId, - fileTypeName: getFileExtension(voicFile!.path).toString(), + fileTypeName: getFileExtension(voiceFile!.path).toString(), fileKind: "file", fileName: msgText, - fileTypeDescription: getFileTypeDescription(getFileExtension(voicFile!.path).toString()), + fileTypeDescription: getFileTypeDescription(getFileExtension(voiceFile!.path).toString()), ) : null, image: null, @@ -1293,7 +1294,7 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { file.readAsBytesSync(); Utils.hideLoading(context); await data.voiceController!.preparePlayer(file.path, 1.0); - data.voiceController!.startPlayer(finishMode: FinishMode.pause); + data.voiceController!.startPlayer(finishMode: FinishMode.stop); notifyListeners(); } catch (e) { Utils.showToast("Cannot open file."); @@ -1301,7 +1302,7 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { } void stopPlaying(BuildContext context, {required SingleUserChatModel data}) async { - await data.voiceController!.pausePlayer(); + await data.voiceController!.stopPlayer(); notifyListeners(); } @@ -1316,9 +1317,24 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { return file.path; } -// data.scrollController.animateTo( -// data.scrollController.position.maxScrollExtent, -// duration: const Duration(milliseconds: 100), -// curve: Curves.easeOut, -// ); + void scrollToMsg(SingleUserChatModel data) { + if (data.userChatReplyResponse != null && data.userChatReplyResponse!.userChatHistoryId != null) { + int index = userChatHistory.indexWhere((SingleUserChatModel element) => element.userChatHistoryId == data.userChatReplyResponse!.userChatHistoryId); + double contentSize = scrollController.position.viewportDimension + scrollController.position.maxScrollExtent; + double target = contentSize * index / userChatHistory.length; + scrollController.position.animateTo( + target, + duration: const Duration(seconds: 1), + curve: Curves.easeInOut, + ); + + // scrollController.scrollTo(index: 150, duration: Duration(seconds: 1)); + // scrollController.animateTo(offset, duration: duration, curve: curve); + } + // scrollController.animateTo( + // scrollController.position.maxScrollExtent, + // duration: const Duration(milliseconds: 100), + // curve: Curves.easeOut, + // ); + } } diff --git a/lib/ui/chat/chat_bubble.dart b/lib/ui/chat/chat_bubble.dart index d6a6abe..3b81d91 100644 --- a/lib/ui/chat/chat_bubble.dart +++ b/lib/ui/chat/chat_bubble.dart @@ -100,7 +100,9 @@ class ChatBubble extends StatelessWidget { ], ), ), - ).paddingOnly(bottom: 7).onPress(() {}), + ).paddingOnly(bottom: 7).onPress(() { + data.scrollToMsg(cItem); + }), if (fileTypeID == 12 || fileTypeID == 4 || fileTypeID == 3) ClipRRect( borderRadius: BorderRadius.circular(5.0), @@ -118,9 +120,9 @@ class ChatBubble extends StatelessWidget { ).paddingOnly(bottom: 4), if (fileTypeID == 13) currentWaveBubble(context, cItem).onPress(() { - if(cItem.voiceController!.playerState == PlayerState.playing){ + if (cItem.voiceController!.playerState == PlayerState.playing) { data.stopPlaying(context, data: cItem); - }else{ + } else { data.playVoice(context, data: cItem); } }) @@ -208,6 +210,7 @@ class ChatBubble extends StatelessWidget { ), ), ).paddingOnly(bottom: 7).onPress(() { + data.scrollToMsg(cItem); }), if (fileTypeID == 12 || fileTypeID == 4 || fileTypeID == 3) ClipRRect( @@ -226,12 +229,11 @@ class ChatBubble extends StatelessWidget { ).paddingOnly(bottom: 4), if (fileTypeID == 13) recipetWaveBubble(context, cItem).onPress(() { - if(cItem.voiceController!.playerState == PlayerState.playing){ + if (cItem.voiceController!.playerState == PlayerState.playing) { data.stopPlaying(context, data: cItem); - }else{ + } else { data.playVoice(context, data: cItem); } - }) else Row( @@ -320,8 +322,8 @@ class ChatBubble extends StatelessWidget { child: Row( mainAxisSize: MainAxisSize.max, children: [ - Icon( - data.voiceController!.playerState == PlayerState.playing ? Icons.stop_circle : Icons.play_arrow, + Icon( + data.voiceController!.playerState == PlayerState.playing ? Icons.stop_circle : Icons.play_arrow, color: MyColors.lightGreenColor, ).paddingAll(10), AudioFileWaveforms( @@ -368,8 +370,8 @@ class ChatBubble extends StatelessWidget { child: Row( mainAxisSize: MainAxisSize.max, children: [ - Icon( - data.voiceController!.playerState == PlayerState.playing ? Icons.stop_circle : Icons.play_arrow, + Icon( + data.voiceController!.playerState == PlayerState.playing ? Icons.stop_circle : Icons.play_arrow, color: MyColors.white, ).paddingAll(10), AudioFileWaveforms( From dd6b0c09029f3f5bc0af64f4fb94ef1517b660b0 Mon Sep 17 00:00:00 2001 From: "Aamir.Muhammad" Date: Thu, 22 Dec 2022 15:38:35 +0300 Subject: [PATCH 09/12] Scroll to Message --- lib/provider/chat_provider_model.dart | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/lib/provider/chat_provider_model.dart b/lib/provider/chat_provider_model.dart index 7ffc0e7..40e9327 100644 --- a/lib/provider/chat_provider_model.dart +++ b/lib/provider/chat_provider_model.dart @@ -1320,21 +1320,15 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { void scrollToMsg(SingleUserChatModel data) { if (data.userChatReplyResponse != null && data.userChatReplyResponse!.userChatHistoryId != null) { int index = userChatHistory.indexWhere((SingleUserChatModel element) => element.userChatHistoryId == data.userChatReplyResponse!.userChatHistoryId); - double contentSize = scrollController.position.viewportDimension + scrollController.position.maxScrollExtent; - double target = contentSize * index / userChatHistory.length; - scrollController.position.animateTo( - target, - duration: const Duration(seconds: 1), - curve: Curves.easeInOut, - ); - - // scrollController.scrollTo(index: 150, duration: Duration(seconds: 1)); - // scrollController.animateTo(offset, duration: duration, curve: curve); + if (index >= 1) { + double contentSize = scrollController.position.viewportDimension + scrollController.position.maxScrollExtent; + double target = contentSize * index / userChatHistory.length; + scrollController.position.animateTo( + target, + duration: const Duration(seconds: 1), + curve: Curves.easeInOut, + ); + } } - // scrollController.animateTo( - // scrollController.position.maxScrollExtent, - // duration: const Duration(milliseconds: 100), - // curve: Curves.easeOut, - // ); } } From fb3b3e8e4671839ab84b39b2eba236ac1842bf13 Mon Sep 17 00:00:00 2001 From: "Aamir.Muhammad" Date: Sun, 25 Dec 2022 17:12:06 +0300 Subject: [PATCH 10/12] Voice Chat Fixes & Audio Player Implementation --- .../chat/get_single_user_chat_list_model.dart | 10 +- lib/provider/chat_provider_model.dart | 498 +++++++++--------- lib/ui/chat/chat_bubble.dart | 168 +++--- lib/ui/chat/chat_detailed_screen.dart | 54 +- 4 files changed, 346 insertions(+), 384 deletions(-) diff --git a/lib/models/chat/get_single_user_chat_list_model.dart b/lib/models/chat/get_single_user_chat_list_model.dart index 7ca60a8..3722c09 100644 --- a/lib/models/chat/get_single_user_chat_list_model.dart +++ b/lib/models/chat/get_single_user_chat_list_model.dart @@ -1,8 +1,8 @@ import 'dart:convert'; +import 'dart:io'; import 'dart:typed_data'; - -import 'package:audio_waveforms/audio_waveforms.dart'; import 'package:flutter/foundation.dart'; +import 'package:just_audio/just_audio.dart'; List singleUserChatModelFromJson(String str) => List.from(json.decode(str).map((x) => SingleUserChatModel.fromJson(x))); @@ -61,8 +61,8 @@ class SingleUserChatModel { bool? isReplied; bool? isImageLoaded; Uint8List? image; - Uint8List? voice; - PlayerController? voiceController; + File? voice; + AudioPlayer? voiceController; factory SingleUserChatModel.fromJson(Map json) => SingleUserChatModel( userChatHistoryId: json["userChatHistoryId"] == null ? null : json["userChatHistoryId"], @@ -90,7 +90,7 @@ class SingleUserChatModel { isImageLoaded: false, image: null, voice: null, - voiceController: json["fileTypeId"] == 13 ? PlayerController() : null); + voiceController: json["fileTypeId"] == 13 ? AudioPlayer() : null); Map toJson() => { "userChatHistoryId": userChatHistoryId == null ? null : userChatHistoryId, diff --git a/lib/provider/chat_provider_model.dart b/lib/provider/chat_provider_model.dart index 40e9327..6383082 100644 --- a/lib/provider/chat_provider_model.dart +++ b/lib/provider/chat_provider_model.dart @@ -10,6 +10,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:http/http.dart'; import 'package:just_audio/just_audio.dart' as JustAudio; +import 'package:just_audio/just_audio.dart'; import 'package:mohem_flutter_app/api/chat/chat_api_client.dart'; import 'package:mohem_flutter_app/app_state/app_state.dart'; import 'package:mohem_flutter_app/classes/consts.dart'; @@ -35,22 +36,21 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { TextEditingController message = TextEditingController(); TextEditingController search = TextEditingController(); - List userChatHistory = []; + List userChatHistory = [], repliedMsg = []; List? pChatHistory, searchedChats; String chatCID = ''; bool isLoading = true; bool isChatScreenActive = false; int receiverID = 0; late File selectedFile; - bool isFileSelected = false; String sFileType = ""; - bool isMsgReply = false; - List repliedMsg = []; + List favUsersList = []; int paginationVal = 0; - bool currentUserTyping = false; int? cTypingUserId = 0; + bool isTextMsg = false, isReplyMsg = false, isAttachmentMsg = false, isVoiceMsg = false; + //Chat Home Page Counter int chatUConvCounter = 0; @@ -411,6 +411,9 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { } void OnSubmitChatAsync(List? parameters) { + print(isChatScreenActive); + print(receiverID); + print(isChatScreenActive); logger.i(parameters); List data = [], temp = []; for (dynamic msg in parameters!) { @@ -537,45 +540,56 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { required bool isAttachment, required bool isReply, Uint8List? image, - Uint8List? voice, - required bool isImageLoaded}) async { + required bool isImageLoaded, + String? userEmail, + int? userStatus, + File? voiceFile, + required bool isVoiceAttached}) async { Uuid uuid = const Uuid(); String contentNo = uuid.v4(); - String msg = message.text; + String msg; + if (isVoiceAttached) { + msg = voiceFile!.path.split("/").last; + } else { + msg = message.text; + } + logger.w(jsonEncode(repliedMsg)); SingleUserChatModel data = SingleUserChatModel( - userChatHistoryId: 0, - chatEventId: chatEventId, - chatSource: 1, - contant: msg, - contantNo: contentNo, - conversationId: chatCID, - createdDate: DateTime.now(), - currentUserId: AppState().chatDetails!.response!.id, - currentUserName: AppState().chatDetails!.response!.userName, - targetUserId: targetUserId, - targetUserName: targetUserName, - isReplied: false, - fileTypeId: fileTypeId, - userChatReplyResponse: isReply ? UserChatReplyResponse.fromJson(repliedMsg.first.toJson()) : null, - fileTypeResponse: isAttachment - ? FileTypeResponse( - fileTypeId: fileTypeId, - fileTypeName: getFileExtension(selectedFile.path).toString(), - fileKind: "file", - fileName: selectedFile.path.split("/").last, - fileTypeDescription: getFileTypeDescription(getFileExtension(selectedFile.path).toString()), - ) - : null, - image: image, - isImageLoaded: isImageLoaded, - voice: voice, - ); + userChatHistoryId: 0, + chatEventId: chatEventId, + chatSource: 1, + contant: msg, + contantNo: contentNo, + conversationId: chatCID, + createdDate: DateTime.now(), + currentUserId: AppState().chatDetails!.response!.id, + currentUserName: AppState().chatDetails!.response!.userName, + targetUserId: targetUserId, + targetUserName: targetUserName, + isReplied: false, + fileTypeId: fileTypeId, + userChatReplyResponse: isReply ? UserChatReplyResponse.fromJson(repliedMsg.first.toJson()) : null, + fileTypeResponse: isAttachment + ? FileTypeResponse( + fileTypeId: fileTypeId, + fileTypeName: isVoiceMsg ? getFileExtension(voiceFile!.path).toString() : getFileExtension(selectedFile.path).toString(), + fileKind: "file", + fileName: isVoiceMsg ? msg : selectedFile.path.split("/").last, + fileTypeDescription: isVoiceMsg ? getFileTypeDescription(getFileExtension(voiceFile!.path).toString()) : getFileTypeDescription(getFileExtension(selectedFile.path).toString()), + ) + : null, + image: image, + isImageLoaded: isImageLoaded, + voice: isVoiceMsg ? voiceFile! : null, + voiceController: isVoiceMsg ? AudioPlayer() : null); if (kDebugMode) { logger.i("model data: " + jsonEncode(data)); } userChatHistory.insert(0, data); - isFileSelected = false; - isMsgReply = false; + isTextMsg = false; + isReplyMsg = false; + isAttachmentMsg = false; + isVoiceMsg = false; sFileType = ""; message.clear(); notifyListeners(); @@ -586,20 +600,55 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { } void sendChatMessage(BuildContext context, {required int targetUserId, required int userStatus, required String userEmail, required String targetUserName}) async { - if (!isFileSelected && !isMsgReply) { - if (kDebugMode) { - print("Normal Text Msg"); + if (kDebugMode) { + print("====================== Values ============================"); + print("Is Text " + isTextMsg.toString()); + print("isReply " + isReplyMsg.toString()); + print("isAttachment " + isAttachmentMsg.toString()); + print("isVoice " + isVoiceMsg.toString()); + } + //Text + if (isTextMsg && !isAttachmentMsg && !isVoiceMsg && !isReplyMsg) { + logger.d("// Normal Text Message"); + if (message.text == null || message.text.isEmpty) { + return; } + sendChatToServer( + chatEventId: 1, + fileTypeId: null, + targetUserId: targetUserId, + targetUserName: targetUserName, + isAttachment: false, + chatReplyId: null, + isReply: false, + isImageLoaded: false, + image: null, + isVoiceAttached: false, + userEmail: userEmail, + userStatus: userStatus); + } else if (isTextMsg && !isAttachmentMsg && !isVoiceMsg && isReplyMsg) { + logger.d("// Text Message as Reply"); if (message.text == null || message.text.isEmpty) { return; } sendChatToServer( - chatEventId: 1, fileTypeId: null, targetUserId: targetUserId, targetUserName: targetUserName, isAttachment: false, chatReplyId: null, isReply: false, isImageLoaded: false, image: null); + chatEventId: 1, + fileTypeId: null, + targetUserId: targetUserId, + targetUserName: targetUserName, + chatReplyId: repliedMsg.first.userChatHistoryId, + isAttachment: false, + isReply: true, + isImageLoaded: repliedMsg.first.isImageLoaded!, + image: repliedMsg.first.image, + isVoiceAttached: false, + voiceFile: null, + userEmail: userEmail, + userStatus: userStatus); } - if (isFileSelected && !isMsgReply) { - if (kDebugMode) { - logger.i("Normal Attachment Msg"); - } + // Attachment + else if (!isTextMsg && isAttachmentMsg && !isVoiceMsg && !isReplyMsg) { + logger.d("// Normal Image Message"); Utils.showLoading(context); dynamic value = await uploadAttachments(AppState().chatDetails!.response!.id.toString(), selectedFile); String? ext = getFileExtension(selectedFile.path); @@ -613,46 +662,100 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { chatReplyId: null, isReply: false, isImageLoaded: true, - image: selectedFile.readAsBytesSync()); + image: selectedFile.readAsBytesSync(), + isVoiceAttached: false, + userEmail: userEmail, + userStatus: userStatus); + } else if (!isTextMsg && isAttachmentMsg && !isVoiceMsg && isReplyMsg) { + logger.d("// Image as Reply Msg"); + Utils.showLoading(context); + dynamic value = await uploadAttachments(AppState().chatDetails!.response!.id.toString(), selectedFile); + String? ext = getFileExtension(selectedFile.path); + Utils.hideLoading(context); + sendChatToServer( + chatEventId: 2, + fileTypeId: getFileType(ext.toString()), + targetUserId: targetUserId, + targetUserName: targetUserName, + isAttachment: true, + chatReplyId: repliedMsg.first.userChatHistoryId, + isReply: true, + isImageLoaded: true, + image: selectedFile.readAsBytesSync(), + isVoiceAttached: false, + userEmail: userEmail, + userStatus: userStatus); } - if (!isFileSelected && isMsgReply) { - if (kDebugMode) { - print("Normal Text To Text Reply"); + //Voice + + else if (!isTextMsg && !isAttachmentMsg && isVoiceMsg && !isReplyMsg) { + logger.d("// Normal Voice Message"); + + if (!isPause) { + path = await recorderController.stop(false); } - if (message.text == null || message.text.isEmpty) { - return; + if (kDebugMode) { + logger.i("path:" + path!); } + File voiceFile = File(path!); + voiceFile.readAsBytesSync(); + _timer?.cancel(); + isPause = false; + isPlaying = false; + isRecoding = false; + Utils.showLoading(context); + dynamic value = await uploadAttachments(AppState().chatDetails!.response!.id.toString(), voiceFile); + String? ext = getFileExtension(voiceFile.path); + Utils.hideLoading(context); sendChatToServer( - chatEventId: 1, - fileTypeId: null, + chatEventId: 2, + fileTypeId: getFileType(ext.toString()), targetUserId: targetUserId, targetUserName: targetUserName, - chatReplyId: repliedMsg.first.userChatHistoryId, - isAttachment: false, - isReply: true, - isImageLoaded: repliedMsg.first.isImageLoaded!, - image: repliedMsg.first.image); - } // reply msg over image && normal - if (isFileSelected && isMsgReply) { + chatReplyId: null, + isAttachment: true, + isReply: isReplyMsg, + isImageLoaded: false, + voiceFile: voiceFile, + isVoiceAttached: true, + userEmail: userEmail, + userStatus: userStatus); + notifyListeners(); + } else if (!isTextMsg && !isAttachmentMsg && isVoiceMsg && isReplyMsg) { + logger.d("// Voice as Reply Msg"); + + if (!isPause) { + path = await recorderController.stop(false); + } if (kDebugMode) { - print("Reply With File"); + logger.i("path:" + path!); } + File voiceFile = File(path!); + voiceFile.readAsBytesSync(); + _timer?.cancel(); + isPause = false; + isPlaying = false; + isRecoding = false; + Utils.showLoading(context); - dynamic value = await uploadAttachments(AppState().chatDetails!.response!.id.toString(), selectedFile); - String? ext = getFileExtension(selectedFile.path); + dynamic value = await uploadAttachments(AppState().chatDetails!.response!.id.toString(), voiceFile); + String? ext = getFileExtension(voiceFile.path); Utils.hideLoading(context); sendChatToServer( chatEventId: 2, fileTypeId: getFileType(ext.toString()), targetUserId: targetUserId, targetUserName: targetUserName, + chatReplyId: null, isAttachment: true, - chatReplyId: repliedMsg.first.userChatHistoryId, - isReply: true, - isImageLoaded: true, - image: selectedFile.readAsBytesSync()); + isReply: isReplyMsg, + isImageLoaded: false, + voiceFile: voiceFile, + isVoiceAttached: true, + userEmail: userEmail, + userStatus: userStatus); + notifyListeners(); } - if (searchedChats != null) { dynamic contain = searchedChats!.where((ChatUser element) => element.id == targetUserId); if (contain.isEmpty) { @@ -676,34 +779,36 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { ); notifyListeners(); } - } else { - List emails = []; - emails.add(await EmailImageEncryption().encrypt(val: userEmail)); - List chatImages = await ChatApiClient().getUsersImages(encryptedEmails: emails); - searchedChats!.add( - ChatUser( - id: targetUserId, - userName: targetUserName, - unreadMessageCount: 0, - email: userEmail, - isImageLoading: false, - image: chatImages.first.profilePicture ?? "", - isImageLoaded: true, - isTyping: false, - isFav: false, - userStatus: userStatus, - userLocalDownlaodedImage: await downloadImageLocal(chatImages.first.profilePicture, targetUserId.toString()), - ), - ); - notifyListeners(); } + // else { + // List emails = []; + // emails.add(await EmailImageEncryption().encrypt(val: userEmail)); + // List chatImages = await ChatApiClient().getUsersImages(encryptedEmails: emails); + // searchedChats!.add( + // ChatUser( + // id: targetUserId, + // userName: targetUserName, + // unreadMessageCount: 0, + // email: userEmail, + // isImageLoading: false, + // image: chatImages.first.profilePicture ?? "", + // isImageLoaded: true, + // isTyping: false, + // isFav: false, + // userStatus: userStatus, + // userLocalDownlaodedImage: await downloadImageLocal(chatImages.first.profilePicture, targetUserId.toString()), + // ), + // ); + // notifyListeners(); + // } } void selectImageToUpload(BuildContext context) { ImageOptions.showImageOptionsNew(context, true, (String image, File file) async { if (checkFileSize(file.path)) { selectedFile = file; - isFileSelected = true; + isAttachmentMsg = true; + isTextMsg = false; sFileType = getFileExtension(file.path)!; message.text = file.path.split("/").last; Navigator.of(context).pop(); @@ -715,7 +820,7 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { } void removeAttachment() { - isFileSelected = false; + isAttachmentMsg = false; sFileType = ""; message.text = ''; notifyListeners(); @@ -784,14 +889,14 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { void chatReply(SingleUserChatModel data) { repliedMsg = []; data.isReplied = true; - isMsgReply = true; + isReplyMsg = true; repliedMsg.add(data); notifyListeners(); } void closeMe() { repliedMsg = []; - isMsgReply = false; + isReplyMsg = false; notifyListeners(); } @@ -841,10 +946,12 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { receiverID = 0; paginationVal = 0; message.text = ''; - isFileSelected = false; + isAttachmentMsg = false; repliedMsg = []; sFileType = ""; - isMsgReply = false; + isReplyMsg = false; + isTextMsg = false; + isVoiceMsg = false; notifyListeners(); } @@ -855,7 +962,10 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { receiverID = 0; paginationVal = 0; message.text = ''; - isFileSelected = false; + isTextMsg = false; + isAttachmentMsg = false; + isVoiceMsg = false; + isReplyMsg = false; repliedMsg = []; sFileType = ""; } @@ -866,7 +976,10 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { receiverID = 0; paginationVal = 0; message.text = ''; - isFileSelected = false; + isTextMsg = false; + isAttachmentMsg = false; + isVoiceMsg = false; + isReplyMsg = false; repliedMsg = []; sFileType = ""; deleteData(); @@ -1052,6 +1165,7 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { isRecoding = false; isPlaying = false; isPause = false; + isVoiceMsg = false; recorderController.dispose(); playerController.dispose(); } @@ -1061,6 +1175,7 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { if (status.isDenied == true) { startRecoding(); } else { + isVoiceMsg = true; recorderController.reset(); await recorderController.record(path); _recodeDuration = 0; @@ -1123,6 +1238,7 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { isPause = false; isRecoding = false; isPlaying = false; + isVoiceMsg = false; notifyListeners(); } } @@ -1141,169 +1257,49 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { return numberStr; } - void playRecoding() async { - isPlaying = true; - await playerController.startPlayer(finishMode: FinishMode.pause); - } - - void playOrPause() async { - playerController.playerState == PlayerState.playing ? await playerController.pausePlayer() : playRecoding(); - notifyListeners(); - } - - void sendVoiceMessage(BuildContext context, {required int targetUserId, required int userStatus, required String userEmail, required String targetUserName}) async { - if (!isPause) { - path = await recorderController.stop(false); - } - if (kDebugMode) { - logger.i("path:" + path!); - } - File voiceFile = File(path!); - voiceFile.readAsBytesSync(); - _timer?.cancel(); - isPause = false; - isPlaying = false; - isRecoding = false; - Utils.showLoading(context); - dynamic value = await uploadAttachments(AppState().chatDetails!.response!.id.toString(), voiceFile); - String? ext = getFileExtension(voiceFile.path); - Utils.hideLoading(context); - sendVoiceMessageToServer( - msgText: voiceFile.path!.split("/").last, - chatEventId: 2, - fileTypeId: getFileType(ext.toString()), - targetUserId: targetUserId, - targetUserName: targetUserName, - isVoiceAttached: true, - voice: voiceFile.readAsBytesSync(), - userEmail: userEmail, - userStatus: userStatus, - chatReplyId: null, - isAttachment: true, - isReply: isMsgReply, - voiceFile: voiceFile, - ); - notifyListeners(); - } - - Future sendVoiceMessageToServer( - {String? msgText, - int? chatEventId, - int? fileTypeId, - int? targetUserId, - String? targetUserName, - bool? isVoiceAttached, - Uint8List? voice, - String? userEmail, - int? userStatus, - bool? isReply, - bool? isAttachment, - int? chatReplyId, - File? voiceFile}) async { - Uuid uuid = const Uuid(); - String contentNo = uuid.v4(); - String msg = msgText!; - SingleUserChatModel data = SingleUserChatModel( - chatEventId: chatEventId, - chatSource: 1, - contant: msg, - contantNo: contentNo, - conversationId: chatCID, - createdDate: DateTime.now(), - currentUserId: AppState().chatDetails!.response!.id, - currentUserName: AppState().chatDetails!.response!.userName, - targetUserId: targetUserId, - targetUserName: targetUserName, - isReplied: false, - fileTypeId: fileTypeId, - userChatReplyResponse: isReply! ? UserChatReplyResponse.fromJson(repliedMsg.first.toJson()) : null, - fileTypeResponse: isAttachment! - ? FileTypeResponse( - fileTypeId: fileTypeId, - fileTypeName: getFileExtension(voiceFile!.path).toString(), - fileKind: "file", - fileName: msgText, - fileTypeDescription: getFileTypeDescription(getFileExtension(voiceFile!.path).toString()), - ) - : null, - image: null, - isImageLoaded: false, - voice: voice, - ); - userChatHistory.insert(0, data); - notifyListeners(); - String chatData = - '{"contant":"$msg","contantNo":"$contentNo","chatEventId":$chatEventId,"fileTypeId": $fileTypeId,"currentUserId":${AppState().chatDetails!.response!.id},"chatSource":1,"userChatHistoryLineRequestList":[{"isSeen":false,"isDelivered":false,"targetUserId":$targetUserId,"targetUserStatus":1}],"chatReplyId":$chatReplyId,"conversationId":"$chatCID"}'; - await chatHubConnection.invoke("AddChatUserAsync", args: [json.decode(chatData)]); - - if (searchedChats != null) { - dynamic contain = searchedChats!.where((ChatUser element) => element.id == targetUserId); - if (contain.isEmpty) { - List emails = []; - emails.add(await EmailImageEncryption().encrypt(val: userEmail!)); - List chatImages = await ChatApiClient().getUsersImages(encryptedEmails: emails); - searchedChats!.add( - ChatUser( - id: targetUserId, - userName: targetUserName, - unreadMessageCount: 0, - email: userEmail, - isImageLoading: false, - image: chatImages.first.profilePicture ?? "", - isImageLoaded: true, - isTyping: false, - isFav: false, - userStatus: userStatus, - userLocalDownlaodedImage: await downloadImageLocal(chatImages.first.profilePicture, targetUserId.toString()), - ), - ); - notifyListeners(); - } - } else { - List emails = []; - emails.add(await EmailImageEncryption().encrypt(val: userEmail!)); - List chatImages = await ChatApiClient().getUsersImages(encryptedEmails: emails); - searchedChats!.add( - ChatUser( - id: targetUserId, - userName: targetUserName, - unreadMessageCount: 0, - email: userEmail, - isImageLoading: false, - image: chatImages.first.profilePicture ?? "", - isImageLoaded: true, - isTyping: false, - isFav: false, - userStatus: userStatus, - userLocalDownlaodedImage: await downloadImageLocal(chatImages.first.profilePicture, targetUserId.toString()), - ), - ); - notifyListeners(); - } - } + // void playRecoding() async { + // isPlaying = true; + // await playerController.startPlayer(finishMode: FinishMode.pause); + //} void playVoice( BuildContext context, { required SingleUserChatModel data, }) async { - Utils.showLoading(context); - Uint8List encodedString = await ChatApiClient().downloadURL(fileName: data.contant!, fileTypeDescription: getFileTypeDescription(data.fileTypeResponse!.fileTypeName ?? "")); - try { - String path = await downChatVoice(encodedString, data.fileTypeResponse!.fileTypeName ?? "", data); - File file = File(path!); - file.readAsBytesSync(); - Utils.hideLoading(context); - await data.voiceController!.preparePlayer(file.path, 1.0); - data.voiceController!.startPlayer(finishMode: FinishMode.stop); - notifyListeners(); - } catch (e) { - Utils.showToast("Cannot open file."); + if (data.voice != null && data.voice!.existsSync()) { + print("Heree"); + await data.voiceController!.setFilePath(data!.voice!.path); + await data.voiceController!.setLoopMode(LoopMode.off); + Duration? duration = await data.voiceController!.load(); + await data.voiceController!.seek(duration); + await data.voiceController!.play(); + } else { + Utils.showLoading(context); + Uint8List encodedString = await ChatApiClient().downloadURL(fileName: data.contant!, fileTypeDescription: getFileTypeDescription(data.fileTypeResponse!.fileTypeName ?? "")); + try { + String path = await downChatVoice(encodedString, data.fileTypeResponse!.fileTypeName ?? "", data); + File file = File(path!); + await file.readAsBytes(); + data.voice = file; + Duration? duration = await data.voiceController!.setFilePath(file.path); + await data.voiceController!.setLoopMode(LoopMode.off); + await data.voiceController!.seek(duration); + await data.voiceController!.setVolume(1.0); + await data.voiceController!.load(); + Utils.hideLoading(context); + await data.voiceController!.play(); + } catch (e) { + Utils.showToast("Cannot open file."); + } } } - void stopPlaying(BuildContext context, {required SingleUserChatModel data}) async { - await data.voiceController!.stopPlayer(); - notifyListeners(); + void pausePlaying(BuildContext context, {required SingleUserChatModel data}) async { + await data.voiceController!.pause(); + } + + void resumePlaying(BuildContext context, {required SingleUserChatModel data}) async { + await data.voiceController!.play(); } Future downChatVoice(Uint8List bytes, String ext, SingleUserChatModel data) async { diff --git a/lib/ui/chat/chat_bubble.dart b/lib/ui/chat/chat_bubble.dart index 3b81d91..b54dc3f 100644 --- a/lib/ui/chat/chat_bubble.dart +++ b/lib/ui/chat/chat_bubble.dart @@ -1,23 +1,18 @@ import 'dart:convert'; import 'dart:typed_data'; - -import 'package:audio_waveforms/audio_waveforms.dart'; +import 'package:audio_waveforms/audio_waveforms.dart' as awf; +import 'package:just_audio/just_audio.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:mohem_flutter_app/api/api_client.dart'; import 'package:mohem_flutter_app/api/chat/chat_api_client.dart'; import 'package:mohem_flutter_app/app_state/app_state.dart'; import 'package:mohem_flutter_app/classes/colors.dart'; -import 'package:mohem_flutter_app/classes/utils.dart'; import 'package:mohem_flutter_app/extensions/int_extensions.dart'; import 'package:mohem_flutter_app/extensions/string_extensions.dart'; import 'package:mohem_flutter_app/extensions/widget_extensions.dart'; -import 'package:mohem_flutter_app/main.dart'; import 'package:mohem_flutter_app/models/chat/get_single_user_chat_list_model.dart'; import 'package:mohem_flutter_app/provider/chat_provider_model.dart'; import 'package:mohem_flutter_app/ui/chat/chat_full_image_preview.dart'; -import 'package:mohem_flutter_app/widgets/bottom_sheet.dart'; -import 'package:open_file/open_file.dart'; import 'package:provider/provider.dart'; class ChatBubble extends StatelessWidget { @@ -29,7 +24,7 @@ class ChatBubble extends StatelessWidget { bool isReplied = false; int? fileTypeID; String? fileTypeName; - late ChatProviderModel data; + late ChatProviderModel provider; String? fileTypeDescription; bool isDelivered = false; @@ -52,7 +47,7 @@ class ChatBubble extends StatelessWidget { Size windowSize = MediaQuery.of(context).size; screenOffset = Offset(windowSize.width / 2, windowSize.height / 2); makeAssign(); - data = Provider.of(context, listen: false); + provider = Provider.of(context, listen: false); return isCurrentUser ? currentUser(context) : receiptUser(context); } @@ -101,7 +96,7 @@ class ChatBubble extends StatelessWidget { ), ), ).paddingOnly(bottom: 7).onPress(() { - data.scrollToMsg(cItem); + provider.scrollToMsg(cItem); }), if (fileTypeID == 12 || fileTypeID == 4 || fileTypeID == 3) ClipRRect( @@ -119,20 +114,14 @@ class ChatBubble extends StatelessWidget { ), ).paddingOnly(bottom: 4), if (fileTypeID == 13) - currentWaveBubble(context, cItem).onPress(() { - if (cItem.voiceController!.playerState == PlayerState.playing) { - data.stopPlaying(context, data: cItem); - } else { - data.playVoice(context, data: cItem); - } - }) + currentWaveBubble(context, cItem) else Row( children: [ if (fileTypeID == 1 || fileTypeID == 5 || fileTypeID == 7 || fileTypeID == 6 || fileTypeID == 8 // || fileTypeID == 2 ) - SvgPicture.asset(data.getType(fileTypeName ?? ""), height: 30, width: 22, alignment: Alignment.center, fit: BoxFit.cover).paddingOnly(left: 0, right: 10), + SvgPicture.asset(provider.getType(fileTypeName ?? ""), height: 30, width: 22, alignment: Alignment.center, fit: BoxFit.cover).paddingOnly(left: 0, right: 10), (cItem.contant ?? "").toText12().expanded, if (fileTypeID == 1 || fileTypeID == 5 || fileTypeID == 7 || fileTypeID == 6 || fileTypeID == 8 //|| fileTypeID == 2 @@ -210,7 +199,7 @@ class ChatBubble extends StatelessWidget { ), ), ).paddingOnly(bottom: 7).onPress(() { - data.scrollToMsg(cItem); + provider.scrollToMsg(cItem); }), if (fileTypeID == 12 || fileTypeID == 4 || fileTypeID == 3) ClipRRect( @@ -228,20 +217,14 @@ class ChatBubble extends StatelessWidget { ), ).paddingOnly(bottom: 4), if (fileTypeID == 13) - recipetWaveBubble(context, cItem).onPress(() { - if (cItem.voiceController!.playerState == PlayerState.playing) { - data.stopPlaying(context, data: cItem); - } else { - data.playVoice(context, data: cItem); - } - }) + recipetWaveBubble(context, cItem) else Row( children: [ if (fileTypeID == 1 || fileTypeID == 5 || fileTypeID == 7 || fileTypeID == 6 || fileTypeID == 8 // || fileTypeID == 2 ) - SvgPicture.asset(data.getType(fileTypeName ?? ""), height: 30, width: 22, alignment: Alignment.center, fit: BoxFit.cover).paddingOnly(left: 0, right: 10), + SvgPicture.asset(provider.getType(fileTypeName ?? ""), height: 30, width: 22, alignment: Alignment.center, fit: BoxFit.cover).paddingOnly(left: 0, right: 10), (cItem.contant ?? "").toText12(color: Colors.white).expanded, if (fileTypeID == 1 || fileTypeID == 5 || fileTypeID == 7 || fileTypeID == 6 || fileTypeID == 8 //|| fileTypeID == 2 @@ -261,8 +244,6 @@ class ChatBubble extends StatelessWidget { } Widget showImage({required bool isReplyPreview, required String fileName, required String fileTypeDescription}) { - if (isReplyPreview) {} - if (cItem.isImageLoaded! && cItem.image != null) { return Image.memory( cItem.image!, @@ -301,7 +282,6 @@ class ChatBubble extends StatelessWidget { } Widget currentWaveBubble(BuildContext context, SingleUserChatModel data) { - PlayerController cunController = PlayerController(); return Container( margin: const EdgeInsets.all(0), decoration: BoxDecoration( @@ -309,40 +289,18 @@ class ChatBubble extends StatelessWidget { left: BorderSide(width: 6, color: isCurrentUser ? MyColors.gradiantStartColor : MyColors.white), ), color: isCurrentUser ? MyColors.black.withOpacity(0.10) : MyColors.black.withOpacity(0.30), - // gradient: const LinearGradient( - // transform: GradientRotation(.83), - // begin: Alignment.topRight, - // end: Alignment.bottomLeft, - // colors: [ - // MyColors.gradiantEndColor, - // MyColors.gradiantStartColor, - // ], - // ), ), child: Row( - mainAxisSize: MainAxisSize.max, children: [ - Icon( - data.voiceController!.playerState == PlayerState.playing ? Icons.stop_circle : Icons.play_arrow, - color: MyColors.lightGreenColor, - ).paddingAll(10), - AudioFileWaveforms( - size: Size(MediaQuery.of(context).size.width * 0.3, 10), - playerController: data.voiceController!, - padding: EdgeInsets.zero, - margin: EdgeInsets.zero, - enableSeekGesture: true, - density: 1, - playerWaveStyle: const PlayerWaveStyle( - fixedWaveColor: Colors.white, - liveWaveColor: MyColors.greenColor, - showTop: true, - showBottom: true, - waveCap: StrokeCap.round, - seekLineThickness: 2, - visualizerHeight: 4, - backgroundColor: Colors.transparent, - ), + getPlayer(player: data.voiceController!, modelData: data), + Slider( + activeColor: Colors.white, + inactiveColor: Colors.grey, + value: 0.toDouble(), + max: 50.toDouble(), + onChanged: (double value) { + // Add code to track the music duration. + }, ).expanded, ], ), @@ -357,49 +315,71 @@ class ChatBubble extends StatelessWidget { left: BorderSide(width: 6, color: isCurrentUser ? MyColors.gradiantStartColor : MyColors.white), ), color: isCurrentUser ? MyColors.black.withOpacity(0.10) : MyColors.black.withOpacity(0.30), - // gradient: const LinearGradient( - // transform: GradientRotation(.83), - // begin: Alignment.topRight, - // end: Alignment.bottomLeft, - // colors: [ - // MyColors.gradiantEndColor, - // MyColors.gradiantStartColor, - // ], - // ), ), child: Row( mainAxisSize: MainAxisSize.max, children: [ - Icon( - data.voiceController!.playerState == PlayerState.playing ? Icons.stop_circle : Icons.play_arrow, - color: MyColors.white, - ).paddingAll(10), - AudioFileWaveforms( - size: Size(MediaQuery.of(context).size.width * 0.3, 10), - playerController: data.voiceController!, - padding: EdgeInsets.zero, - margin: EdgeInsets.zero, - enableSeekGesture: true, - density: 1, - playerWaveStyle: const PlayerWaveStyle( - fixedWaveColor: Colors.white, - liveWaveColor: MyColors.greenColor, - showTop: true, - showBottom: true, - waveCap: StrokeCap.round, - seekLineThickness: 2, - visualizerHeight: 4, - backgroundColor: Colors.transparent, - ), + getPlayer(player: data.voiceController!, modelData: data), + Slider( + activeColor: Colors.white, + inactiveColor: Colors.grey, + value: 0.toDouble(), + max: 50.toDouble(), + onChanged: (double value) { + // Add code to track the music duration. + }, ).expanded, ], ), ).circle(5); } + + Widget getPlayer({required AudioPlayer player, required SingleUserChatModel modelData}) { + return StreamBuilder( + stream: player!.playerStateStream, + builder: (BuildContext context, AsyncSnapshot snapshot) { + PlayerState? playerState = snapshot.data; + ProcessingState? processingState = playerState?.processingState; + bool? playing = playerState?.playing; + if (processingState == ProcessingState.loading || processingState == ProcessingState.buffering) { + return Container( + margin: const EdgeInsets.all(8.0), + width: 30.0, + height: 30.0, + child: const CircularProgressIndicator(), + ); + } else if (playing != true) { + return Icon( + Icons.play_arrow, + size: 30, + color: MyColors.lightGreenColor, + ).onPress(() { + provider.playVoice(context, data: modelData); + }); + } else if (processingState != ProcessingState.completed) { + return Icon( + Icons.pause, + size: 30, + color: MyColors.lightGreenColor, + ).onPress(() { + provider.pausePlaying(context, data: modelData); + }); + } else { + return Icon( + Icons.replay, + size: 30, + color: MyColors.lightGreenColor, + ).onPress(() { + player!.seek(Duration.zero); + }); + } + }, + ); + } } class WaveBubble extends StatelessWidget { - final PlayerController playerController; + final awf.PlayerController playerController; final VoidCallback onTap; final bool isPlaying; @@ -436,14 +416,14 @@ class WaveBubble extends StatelessWidget { splashColor: Colors.transparent, highlightColor: Colors.transparent, ), - AudioFileWaveforms( + awf.AudioFileWaveforms( size: Size(MediaQuery.of(context).size.width / 2, 10), playerController: playerController, padding: EdgeInsets.zero, margin: EdgeInsets.zero, enableSeekGesture: true, density: 1, - playerWaveStyle: const PlayerWaveStyle( + playerWaveStyle: const awf.PlayerWaveStyle( fixedWaveColor: Colors.white, liveWaveColor: MyColors.greenColor, showTop: true, diff --git a/lib/ui/chat/chat_detailed_screen.dart b/lib/ui/chat/chat_detailed_screen.dart index 7b3a72b..c4c1eed 100644 --- a/lib/ui/chat/chat_detailed_screen.dart +++ b/lib/ui/chat/chat_detailed_screen.dart @@ -144,7 +144,7 @@ class _ChatDetailScreenState extends State { ); }, ).onPress(() async { - logger.d(m.userChatHistory[i].toJson()); + logger.w(m.userChatHistory[i].toJson()); if (m.userChatHistory[i].fileTypeResponse != null && m.userChatHistory[i].fileTypeId != null) { if (m.userChatHistory[i].fileTypeId! == 1 || m.userChatHistory[i].fileTypeId! == 5 || @@ -161,7 +161,7 @@ class _ChatDetailScreenState extends State { }, ), ).expanded, - if (m.isMsgReply) + if (m.isReplyMsg) SizedBox( height: 82, child: Row( @@ -183,7 +183,7 @@ class _ChatDetailScreenState extends State { ], ).expanded, 12.width, - if (m.isMsgReply && m.repliedMsg.isNotEmpty) showReplyImage(m.repliedMsg, m), + if (m.isReplyMsg && m.repliedMsg.isNotEmpty) showReplyImage(m.repliedMsg, m), 12.width, const Icon(Icons.cancel, size: 23, color: MyColors.grey7BColor).onPress(m.closeMe), ], @@ -192,12 +192,9 @@ class _ChatDetailScreenState extends State { ], ), ), - if (m.isFileSelected && m.sFileType == ".png" || m.sFileType == ".jpeg" || m.sFileType == ".jpg") + if (m.isAttachmentMsg && m.sFileType == ".png" || m.sFileType == ".jpeg" || m.sFileType == ".jpg") SizedBox(height: 200, width: double.infinity, child: Image.file(m.selectedFile, fit: BoxFit.cover)).paddingOnly(left: 21, right: 21, top: 21), - const Divider( - height: 1, - color: MyColors.lightGreyEFColor, - ), + const Divider(height: 1, color: MyColors.lightGreyEFColor), if (m.isRecoding) Column( children: [ @@ -206,12 +203,11 @@ class _ChatDetailScreenState extends State { Text(m.buildTimer()).paddingAll(10), if (m.isRecoding && m.isPlaying) WaveBubble( - playerController: m.playerController, - onTap: () { - m.playOrPause(); - }, - isPlaying: m.playerController.playerState == PlayerState.playing) - .expanded + playerController: m.playerController, + isPlaying: m.playerController.playerState == PlayerState.playing, + onTap: () { + }, + ).expanded else AudioWaveforms( waveStyle: const WaveStyle( @@ -243,25 +239,9 @@ class _ChatDetailScreenState extends State { ).paddingAll(10).onPress(() { m.deleteRecoding(); }), - // if (m.isPause) - // const Icon( - // Icons.mic, - // size: 26, - // color: MyColors.lightGreenColor, - // ).paddingOnly(right: 15).onPress(() { - // m.resumeRecoding(); - // }), - // if (!m.isPause) - // const Icon( - // Icons.pause_circle_outline, - // size: 26, - // color: MyColors.lightGreenColor, - // ).paddingOnly(right: 15).onPress(() { - // m.pauseRecoding(); - // }), SvgPicture.asset("assets/icons/chat/chat_send_icon.svg", height: 26, width: 26) .onPress( - () => m.sendVoiceMessage(context, + () => m.sendChatMessage(context, targetUserId: params!.chatUser!.id!, userStatus: params!.chatUser!.userStatus ?? 0, userEmail: params!.chatUser!.email!, @@ -278,8 +258,8 @@ class _ChatDetailScreenState extends State { TextField( controller: m.message, decoration: InputDecoration( - hintText: m.isFileSelected ? m.selectedFile.path.split("/").last : LocaleKeys.typeheretoreply.tr(), - hintStyle: TextStyle(color: m.isFileSelected ? MyColors.darkTextColor : MyColors.grey98Color, fontSize: 14), + hintText: m.isAttachmentMsg ? m.selectedFile.path.split("/").last : LocaleKeys.typeheretoreply.tr(), + hintStyle: TextStyle(color: m.isAttachmentMsg ? MyColors.darkTextColor : MyColors.grey98Color, fontSize: 14), border: InputBorder.none, focusedBorder: InputBorder.none, enabledBorder: InputBorder.none, @@ -297,7 +277,13 @@ class _ChatDetailScreenState extends State { ? SvgPicture.asset(m.getType(m.sFileType), height: 30, width: 22, alignment: Alignment.center, fit: BoxFit.cover).paddingOnly(left: 21, right: 15) : null, ), - onChanged: (val) { + onChanged: (String val) { + print(val.length); + if (val.isNotEmpty) { + m.isTextMsg = true; + } else { + m.isTextMsg = false; + } m.userTypingInvoke(currentUser: AppState().chatDetails!.response!.id!, reciptUser: params!.chatUser!.id!); }, ).expanded, From 09c401a157958d902bdef9e9fdd2aa168a96533f Mon Sep 17 00:00:00 2001 From: "Aamir.Muhammad" Date: Mon, 26 Dec 2022 11:08:28 +0300 Subject: [PATCH 11/12] Voice Chat Fixes & Audio Player Implementation --- lib/provider/chat_provider_model.dart | 47 +----- lib/ui/chat/chat_bubble.dart | 221 +++++++++++++------------- lib/ui/chat/chat_detailed_screen.dart | 4 +- lib/ui/chat/chat_home.dart | 2 + lib/ui/chat/common.dart | 189 ++++++++++++++++++++++ pubspec.yaml | 4 +- 6 files changed, 306 insertions(+), 161 deletions(-) create mode 100644 lib/ui/chat/common.dart diff --git a/lib/provider/chat_provider_model.dart b/lib/provider/chat_provider_model.dart index 6383082..7cb7b96 100644 --- a/lib/provider/chat_provider_model.dart +++ b/lib/provider/chat_provider_model.dart @@ -552,8 +552,8 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { msg = voiceFile!.path.split("/").last; } else { msg = message.text; + logger.w(msg); } - logger.w(jsonEncode(repliedMsg)); SingleUserChatModel data = SingleUserChatModel( userChatHistoryId: 0, chatEventId: chatEventId, @@ -1257,51 +1257,6 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { return numberStr; } - // void playRecoding() async { - // isPlaying = true; - // await playerController.startPlayer(finishMode: FinishMode.pause); - //} - - void playVoice( - BuildContext context, { - required SingleUserChatModel data, - }) async { - if (data.voice != null && data.voice!.existsSync()) { - print("Heree"); - await data.voiceController!.setFilePath(data!.voice!.path); - await data.voiceController!.setLoopMode(LoopMode.off); - Duration? duration = await data.voiceController!.load(); - await data.voiceController!.seek(duration); - await data.voiceController!.play(); - } else { - Utils.showLoading(context); - Uint8List encodedString = await ChatApiClient().downloadURL(fileName: data.contant!, fileTypeDescription: getFileTypeDescription(data.fileTypeResponse!.fileTypeName ?? "")); - try { - String path = await downChatVoice(encodedString, data.fileTypeResponse!.fileTypeName ?? "", data); - File file = File(path!); - await file.readAsBytes(); - data.voice = file; - Duration? duration = await data.voiceController!.setFilePath(file.path); - await data.voiceController!.setLoopMode(LoopMode.off); - await data.voiceController!.seek(duration); - await data.voiceController!.setVolume(1.0); - await data.voiceController!.load(); - Utils.hideLoading(context); - await data.voiceController!.play(); - } catch (e) { - Utils.showToast("Cannot open file."); - } - } - } - - void pausePlaying(BuildContext context, {required SingleUserChatModel data}) async { - await data.voiceController!.pause(); - } - - void resumePlaying(BuildContext context, {required SingleUserChatModel data}) async { - await data.voiceController!.play(); - } - Future downChatVoice(Uint8List bytes, String ext, SingleUserChatModel data) async { String dirPath = '${(await getApplicationDocumentsDirectory()).path}/chat_audios'; if (!await Directory(dirPath).exists()) { diff --git a/lib/ui/chat/chat_bubble.dart b/lib/ui/chat/chat_bubble.dart index b54dc3f..824ab48 100644 --- a/lib/ui/chat/chat_bubble.dart +++ b/lib/ui/chat/chat_bubble.dart @@ -1,34 +1,48 @@ import 'dart:convert'; +import 'dart:io'; import 'dart:typed_data'; +import 'package:flutter/services.dart'; import 'package:audio_waveforms/audio_waveforms.dart' as awf; -import 'package:just_audio/just_audio.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:mohem_flutter_app/api/chat/chat_api_client.dart'; import 'package:mohem_flutter_app/app_state/app_state.dart'; import 'package:mohem_flutter_app/classes/colors.dart'; +import 'package:mohem_flutter_app/classes/utils.dart'; import 'package:mohem_flutter_app/extensions/int_extensions.dart'; import 'package:mohem_flutter_app/extensions/string_extensions.dart'; import 'package:mohem_flutter_app/extensions/widget_extensions.dart'; import 'package:mohem_flutter_app/models/chat/get_single_user_chat_list_model.dart'; import 'package:mohem_flutter_app/provider/chat_provider_model.dart'; import 'package:mohem_flutter_app/ui/chat/chat_full_image_preview.dart'; +import 'package:mohem_flutter_app/ui/chat/common.dart'; import 'package:provider/provider.dart'; +import 'package:rxdart/rxdart.dart'; +import 'package:just_audio/just_audio.dart'; class ChatBubble extends StatelessWidget { ChatBubble({Key? key, required this.dateTime, required this.cItem}) : super(key: key); final String dateTime; final SingleUserChatModel cItem; + bool isCurrentUser = false; + bool isSeen = false; + bool isReplied = false; + int? fileTypeID; + String? fileTypeName; + late ChatProviderModel provider; String? fileTypeDescription; + bool isDelivered = false; + String userName = ''; + late Offset screenOffset; void makeAssign() { @@ -42,6 +56,51 @@ class ChatBubble extends StatelessWidget { userName = AppState().chatDetails!.response!.userName == cItem.currentUserName.toString() ? "You" : cItem.currentUserName.toString(); } + void playVoice( + BuildContext context, { + required SingleUserChatModel data, + }) async { + if (data.voice != null && data.voice!.existsSync()) { + await data.voiceController!.setFilePath(data!.voice!.path); + await data.voiceController!.setLoopMode(LoopMode.off); + Duration? duration = await data.voiceController!.load(); + await data.voiceController!.seek(duration); + await data.voiceController!.play(); + } else { + Utils.showLoading(context); + Uint8List encodedString = await ChatApiClient().downloadURL(fileName: data.contant!, fileTypeDescription: provider.getFileTypeDescription(data.fileTypeResponse!.fileTypeName ?? "")); + try { + String path = await provider.downChatVoice(encodedString, data.fileTypeResponse!.fileTypeName ?? "", data); + File file = File(path!); + await file.readAsBytes(); + data.voice = file; + Duration? duration = await data.voiceController!.setFilePath(file.path); + await data.voiceController!.setLoopMode(LoopMode.off); + await data.voiceController!.seek(duration); + await data.voiceController!.setVolume(1.0); + await data.voiceController!.load(); + Utils.hideLoading(context); + await data.voiceController!.play(); + } catch (e) { + Utils.showToast("Cannot open file."); + } + } + } + + void pausePlaying(BuildContext context, {required SingleUserChatModel data}) async { + await data.voiceController!.pause(); + } + + void rePlay(BuildContext context, {required SingleUserChatModel data}) async { + if (data.voice != null && data.voice!.existsSync()) { + await data.voiceController!.seek(Duration.zero); + await data.voiceController!.play(); + } + } + + Stream get _positionDataStream => Rx.combineLatest3(cItem.voiceController!.positionStream, cItem.voiceController!.bufferedPositionStream, + cItem.voiceController!.durationStream, (Duration position, Duration bufferedPosition, Duration? duration) => PositionData(position, bufferedPosition, duration ?? Duration.zero)); + @override Widget build(BuildContext context) { Size windowSize = MediaQuery.of(context).size; @@ -77,21 +136,18 @@ class ChatBubble extends StatelessWidget { .paddingOnly(right: 5, top: 5, bottom: 8, left: 5), ], ).expanded, - if (cItem.userChatReplyResponse != null && cItem.userChatReplyResponse!.fileTypeId == 12 || - cItem.userChatReplyResponse!.fileTypeId == 3 || - cItem.userChatReplyResponse!.fileTypeId == 4) - ClipRRect( - borderRadius: BorderRadius.circular(8.0), - child: SizedBox( - height: 32, - width: 32, - child: showImage( - isReplyPreview: true, + if (cItem.userChatReplyResponse != null) + if (cItem.userChatReplyResponse!.fileTypeId == 12 || cItem.userChatReplyResponse!.fileTypeId == 3 || cItem.userChatReplyResponse!.fileTypeId == 4) + ClipRRect( + borderRadius: BorderRadius.circular(8.0), + child: SizedBox( + height: 32, + width: 32, + child: showImage( + isReplyPreview: false, fileName: cItem.userChatReplyResponse!.contant!, - fileTypeDescription: cItem.userChatReplyResponse!.fileTypeResponse!.fileTypeDescription ?? "image/jpg") - .paddingOnly(left: 10, right: 10, bottom: 16, top: 16), - ), - ), + fileTypeDescription: cItem.userChatReplyResponse!.fileTypeResponse!.fileTypeDescription ?? "image/jpg")), + ).paddingOnly(left: 10, right: 10, bottom: 16, top: 16), ], ), ), @@ -113,7 +169,7 @@ class ChatBubble extends StatelessWidget { }), ), ).paddingOnly(bottom: 4), - if (fileTypeID == 13) + if (fileTypeID == 13 && cItem.voiceController != null) currentWaveBubble(context, cItem) else Row( @@ -181,20 +237,19 @@ class ChatBubble extends StatelessWidget { .paddingOnly(right: 5, top: 5, bottom: 8, left: 5), ], ).expanded, - if (cItem.userChatReplyResponse != null && cItem.userChatReplyResponse!.fileTypeId == 12 || - cItem.userChatReplyResponse!.fileTypeId == 3 || - cItem.userChatReplyResponse!.fileTypeId == 4) - ClipRRect( - borderRadius: BorderRadius.circular(8.0), - child: SizedBox( - height: 32, - width: 32, - child: showImage( - isReplyPreview: true, - fileName: cItem.userChatReplyResponse!.contant!, - fileTypeDescription: cItem.userChatReplyResponse!.fileTypeResponse!.fileTypeDescription ?? "image/jpg"), - ), - ).paddingOnly(left: 10, right: 10, bottom: 16, top: 16) + if (cItem.userChatReplyResponse != null) + if (cItem.userChatReplyResponse!.fileTypeId == 12 || cItem.userChatReplyResponse!.fileTypeId == 3 || cItem.userChatReplyResponse!.fileTypeId == 4) + ClipRRect( + borderRadius: BorderRadius.circular(8.0), + child: SizedBox( + height: 32, + width: 32, + child: showImage( + isReplyPreview: true, + fileName: cItem.userChatReplyResponse!.contant!, + fileTypeDescription: cItem.userChatReplyResponse!.fileTypeResponse!.fileTypeDescription ?? "image/jpg"), + ), + ).paddingOnly(left: 10, right: 10, bottom: 16, top: 16) ], ), ), @@ -216,7 +271,7 @@ class ChatBubble extends StatelessWidget { }), ), ).paddingOnly(bottom: 4), - if (fileTypeID == 13) + if (fileTypeID == 13 && cItem.voiceController != null) recipetWaveBubble(context, cItem) else Row( @@ -293,15 +348,18 @@ class ChatBubble extends StatelessWidget { child: Row( children: [ getPlayer(player: data.voiceController!, modelData: data), - Slider( - activeColor: Colors.white, - inactiveColor: Colors.grey, - value: 0.toDouble(), - max: 50.toDouble(), - onChanged: (double value) { - // Add code to track the music duration. + StreamBuilder( + stream: _positionDataStream, + builder: (BuildContext context, AsyncSnapshot snapshot) { + PositionData? positionData = snapshot.data; + return SeekBar( + duration: positionData?.duration ?? Duration.zero, + position: positionData?.position ?? Duration.zero, + bufferedPosition: positionData?.bufferedPosition ?? Duration.zero, + onChangeEnd: data.voiceController!.seek, + ).expanded; }, - ).expanded, + ), ], ), ).circle(5); @@ -320,15 +378,18 @@ class ChatBubble extends StatelessWidget { mainAxisSize: MainAxisSize.max, children: [ getPlayer(player: data.voiceController!, modelData: data), - Slider( - activeColor: Colors.white, - inactiveColor: Colors.grey, - value: 0.toDouble(), - max: 50.toDouble(), - onChanged: (double value) { - // Add code to track the music duration. + StreamBuilder( + stream: _positionDataStream, + builder: (BuildContext context, AsyncSnapshot snapshot) { + PositionData? positionData = snapshot.data; + return SeekBar( + duration: positionData?.duration ?? Duration.zero, + position: positionData?.position ?? Duration.zero, + bufferedPosition: positionData?.bufferedPosition ?? Duration.zero, + onChangeEnd: data.voiceController!.seek, + ).expanded; }, - ).expanded, + ), ], ), ).circle(5); @@ -354,7 +415,7 @@ class ChatBubble extends StatelessWidget { size: 30, color: MyColors.lightGreenColor, ).onPress(() { - provider.playVoice(context, data: modelData); + playVoice(context, data: modelData); }); } else if (processingState != ProcessingState.completed) { return Icon( @@ -362,7 +423,7 @@ class ChatBubble extends StatelessWidget { size: 30, color: MyColors.lightGreenColor, ).onPress(() { - provider.pausePlaying(context, data: modelData); + pausePlaying(context, data: modelData); }); } else { return Icon( @@ -370,7 +431,7 @@ class ChatBubble extends StatelessWidget { size: 30, color: MyColors.lightGreenColor, ).onPress(() { - player!.seek(Duration.zero); + rePlay(context, data: modelData); }); } }, @@ -378,64 +439,4 @@ class ChatBubble extends StatelessWidget { } } -class WaveBubble extends StatelessWidget { - final awf.PlayerController playerController; - final VoidCallback onTap; - final bool isPlaying; - - const WaveBubble({ - Key? key, - required this.playerController, - required this.onTap, - required this.isPlaying, - }) : super(key: key); - @override - Widget build(BuildContext context) { - return Container( - margin: const EdgeInsets.all(10), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(30), - gradient: const LinearGradient( - transform: GradientRotation(.83), - begin: Alignment.topRight, - end: Alignment.bottomLeft, - colors: [ - MyColors.gradiantEndColor, - MyColors.gradiantStartColor, - ], - ), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - onPressed: onTap, - icon: Icon(isPlaying ? Icons.stop : Icons.play_arrow), - color: Colors.white, - splashColor: Colors.transparent, - highlightColor: Colors.transparent, - ), - awf.AudioFileWaveforms( - size: Size(MediaQuery.of(context).size.width / 2, 10), - playerController: playerController, - padding: EdgeInsets.zero, - margin: EdgeInsets.zero, - enableSeekGesture: true, - density: 1, - playerWaveStyle: const awf.PlayerWaveStyle( - fixedWaveColor: Colors.white, - liveWaveColor: MyColors.greenColor, - showTop: true, - showBottom: true, - waveCap: StrokeCap.round, - seekLineThickness: 2, - visualizerHeight: 4, - backgroundColor: Colors.transparent, - ), - ), - ], - ), - ); - } -} diff --git a/lib/ui/chat/chat_detailed_screen.dart b/lib/ui/chat/chat_detailed_screen.dart index c4c1eed..dcbda22 100644 --- a/lib/ui/chat/chat_detailed_screen.dart +++ b/lib/ui/chat/chat_detailed_screen.dart @@ -1,6 +1,4 @@ import 'dart:async'; -import 'dart:convert'; - import 'package:audio_waveforms/audio_waveforms.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; @@ -18,7 +16,7 @@ import 'package:mohem_flutter_app/models/chat/get_single_user_chat_list_model.da import 'package:mohem_flutter_app/provider/chat_provider_model.dart'; import 'package:mohem_flutter_app/ui/chat/call/chat_outgoing_call_screen.dart'; import 'package:mohem_flutter_app/ui/chat/chat_bubble.dart'; -import 'package:mohem_flutter_app/widgets/app_bar_widget.dart'; +import 'package:mohem_flutter_app/ui/chat/common.dart'; import 'package:mohem_flutter_app/widgets/chat_app_bar_widge.dart'; import 'package:mohem_flutter_app/widgets/shimmer/dashboard_shimmer_widget.dart'; import 'package:provider/provider.dart'; diff --git a/lib/ui/chat/chat_home.dart b/lib/ui/chat/chat_home.dart index af243e5..8973f6b 100644 --- a/lib/ui/chat/chat_home.dart +++ b/lib/ui/chat/chat_home.dart @@ -81,6 +81,7 @@ class _ChatHomeState extends State { children: [ myTab(LocaleKeys.mychats.tr(), 0), myTab(LocaleKeys.favorite.tr(), 1), + myTab("My Team", 2), ], ), ), @@ -95,6 +96,7 @@ class _ChatHomeState extends State { children: [ ChatHomeScreen(), ChatFavoriteUsersScreen(), + ChatFavoriteUsersScreen(), ], ).expanded, ], diff --git a/lib/ui/chat/common.dart b/lib/ui/chat/common.dart new file mode 100644 index 0000000..e0cb4d0 --- /dev/null +++ b/lib/ui/chat/common.dart @@ -0,0 +1,189 @@ +import 'dart:math'; +import 'package:audio_waveforms/audio_waveforms.dart'; +import 'package:flutter/material.dart'; +import 'package:mohem_flutter_app/classes/colors.dart'; + +class SeekBar extends StatefulWidget { + final Duration duration; + final Duration position; + final Duration bufferedPosition; + final ValueChanged? onChanged; + final ValueChanged? onChangeEnd; + + const SeekBar({ + Key? key, + required this.duration, + required this.position, + required this.bufferedPosition, + this.onChanged, + this.onChangeEnd, + }) : super(key: key); + + @override + SeekBarState createState() => SeekBarState(); +} + +class SeekBarState extends State { + double? _dragValue; + late SliderThemeData _sliderThemeData; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _sliderThemeData = SliderTheme.of(context).copyWith( + // trackHeight: 2.0, + thumbColor: MyColors.lightGreenColor, + activeTrackColor: MyColors.lightGreenColor, + inactiveTrackColor: MyColors.grey57Color.withOpacity(0.4), + ); + } + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + SliderTheme( + data: _sliderThemeData.copyWith( + thumbShape: HiddenThumbComponentShape(), + ), + child: ExcludeSemantics( + child: Slider( + min: 0.0, + max: widget.duration.inMilliseconds.toDouble(), + value: min(widget.bufferedPosition.inMilliseconds.toDouble(), widget.duration.inMilliseconds.toDouble()), + onChanged: (value) { + setState(() { + _dragValue = value; + }); + if (widget.onChanged != null) { + widget.onChanged!(Duration(milliseconds: value.round())); + } + }, + onChangeEnd: (value) { + if (widget.onChangeEnd != null) { + widget.onChangeEnd!(Duration(milliseconds: value.round())); + } + _dragValue = null; + }, + ), + ), + ), + SliderTheme( + data: _sliderThemeData.copyWith( + inactiveTrackColor: Colors.transparent, + ), + child: Slider( + min: 0.0, + max: widget.duration.inMilliseconds.toDouble(), + value: min(_dragValue ?? widget.position.inMilliseconds.toDouble(), widget.duration.inMilliseconds.toDouble()), + onChanged: (value) { + setState(() { + _dragValue = value; + }); + if (widget.onChanged != null) { + widget.onChanged!(Duration(milliseconds: value.round())); + } + }, + onChangeEnd: (value) { + if (widget.onChangeEnd != null) { + widget.onChangeEnd!(Duration(milliseconds: value.round())); + } + _dragValue = null; + }, + ), + ), + ], + ); + } +} + +class PositionData { + final Duration position; + final Duration bufferedPosition; + final Duration duration; + + PositionData(this.position, this.bufferedPosition, this.duration); +} + +class HiddenThumbComponentShape extends SliderComponentShape { + @override + Size getPreferredSize(bool isEnabled, bool isDiscrete) => Size.zero; + + @override + void paint( + PaintingContext context, + Offset center, { + required Animation activationAnimation, + required Animation enableAnimation, + required bool isDiscrete, + required TextPainter labelPainter, + required RenderBox parentBox, + required SliderThemeData sliderTheme, + required TextDirection textDirection, + required double value, + required double textScaleFactor, + required Size sizeWithOverflow, + }) {} +} + +class WaveBubble extends StatelessWidget { + final PlayerController playerController; + final VoidCallback onTap; + final bool isPlaying; + + const WaveBubble({ + Key? key, + required this.playerController, + required this.onTap, + required this.isPlaying, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.all(10), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(30), + gradient: const LinearGradient( + transform: GradientRotation(.83), + begin: Alignment.topRight, + end: Alignment.bottomLeft, + colors: [ + MyColors.gradiantEndColor, + MyColors.gradiantStartColor, + ], + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + onPressed: onTap, + icon: Icon(isPlaying ? Icons.stop : Icons.play_arrow), + color: Colors.white, + splashColor: Colors.transparent, + highlightColor: Colors.transparent, + ), + AudioFileWaveforms( + size: Size(MediaQuery.of(context).size.width / 2, 10), + playerController: playerController, + padding: EdgeInsets.zero, + margin: EdgeInsets.zero, + enableSeekGesture: true, + density: 1, + playerWaveStyle: const PlayerWaveStyle( + fixedWaveColor: Colors.white, + liveWaveColor: MyColors.greenColor, + showTop: true, + showBottom: true, + waveCap: StrokeCap.round, + seekLineThickness: 2, + visualizerHeight: 4, + backgroundColor: Colors.transparent, + ), + ), + ], + ), + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 2f3fffa..e2092b0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -94,9 +94,8 @@ dependencies: camera: ^0.10.0+4 #Chat Voice Message Recoding & Play -# record: ^4.4.3 audio_waveforms: ^0.1.5+1 -# animated_text_kit: ^4.2.2 + rxdart: ^0.27.7 #Encryption flutter_des: ^2.1.0 @@ -106,6 +105,7 @@ dependencies: safe_device: ^1.1.2 flutter_layout_grid: ^2.0.1 + dev_dependencies: flutter_test: sdk: flutter From 8c3745b7473b89efe67f6ccb9768bc7c3a2ea8b0 Mon Sep 17 00:00:00 2001 From: "Aamir.Muhammad" Date: Mon, 26 Dec 2022 14:58:02 +0300 Subject: [PATCH 12/12] Voice Chat Fixes & Audio Player Implementation --- lib/app_state/app_state.dart | 25 ++- lib/classes/consts.dart | 4 +- lib/provider/chat_provider_model.dart | 132 ++++++++++----- lib/provider/dashboard_provider_model.dart | 5 +- lib/ui/chat/chat_home.dart | 6 +- lib/ui/chat/my_team_screen.dart | 155 ++++++++++++++++++ .../search_employee_bottom_sheet.dart | 2 +- 7 files changed, 278 insertions(+), 51 deletions(-) create mode 100644 lib/ui/chat/my_team_screen.dart diff --git a/lib/app_state/app_state.dart b/lib/app_state/app_state.dart index 7d175b5..b354a43 100644 --- a/lib/app_state/app_state.dart +++ b/lib/app_state/app_state.dart @@ -5,6 +5,7 @@ import 'package:mohem_flutter_app/models/chat/get_user_login_token_model.dart'; import 'package:mohem_flutter_app/models/itg_forms_models/request_detail_model.dart'; import 'package:mohem_flutter_app/models/member_information_list_model.dart'; import 'package:mohem_flutter_app/models/member_login_list_model.dart'; +import 'package:mohem_flutter_app/models/my_team/get_employee_subordinates_list.dart'; import 'package:mohem_flutter_app/models/post_params_model.dart'; import 'package:mohem_flutter_app/models/privilege_list_model.dart'; import 'package:mohem_flutter_app/models/worklist_response_model.dart'; @@ -38,7 +39,6 @@ class AppState { String? get getForgetPasswordTokenID => forgetPasswordTokenID; - //Wifi info String? _mohemmWifiSSID; @@ -52,13 +52,13 @@ class AppState { String? get getMohemmWifiPassword => _mohemmWifiPassword; - String? _marathonToken ; + String? _marathonToken; set setMarathonToken(String token) => _marathonToken = token; String? get getMarathonToken => _marathonToken; - String? _projectID ; + String? _projectID; set setMarathonProjectId(String token) => _projectID = token; @@ -145,7 +145,24 @@ class AppState { UserAutoLoginModel? get getchatUserDetails => chatDetails; - String? _base64ImageEmp = "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"; + String? _base64ImageEmp = + "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"; String? get getBase64ImageEmp => _base64ImageEmp; + + bool _empStatusIsManager = false; + + bool get getempStatusIsManager => _empStatusIsManager; + + set setempStatusIsManager(bool empStatusIsManager) { + _empStatusIsManager = empStatusIsManager; + } + + List _employeeSubordinatesList = []; + + List get getemployeeSubordinatesList => _employeeSubordinatesList; + + set setemployeeSubordinatesList(List employeeSubordinatesList) { + _employeeSubordinatesList = employeeSubordinatesList; + } } diff --git a/lib/classes/consts.dart b/lib/classes/consts.dart index 4a7d7fe..52d0407 100644 --- a/lib/classes/consts.dart +++ b/lib/classes/consts.dart @@ -3,8 +3,8 @@ import 'package:mohem_flutter_app/ui/marathon/widgets/question_card.dart'; class ApiConsts { //static String baseUrl = "http://10.200.204.20:2801/"; // Local server // static String baseUrl = "https://erptstapp.srca.org.sa"; // SRCA server - //static String baseUrl = "https://uat.hmgwebservices.com"; // UAT server - static String baseUrl = "https://hmgwebservices.com"; // Live server + static String baseUrl = "https://uat.hmgwebservices.com"; // UAT server + // static String baseUrl = "https://hmgwebservices.com"; // Live server static String baseUrlServices = baseUrl + "/Services/"; // server // static String baseUrlServices = "https://api.cssynapses.com/tangheem/"; // Live server static String utilitiesRest = baseUrlServices + "Utilities.svc/REST/"; diff --git a/lib/provider/chat_provider_model.dart b/lib/provider/chat_provider_model.dart index 4dc55f6..53b71ff 100644 --- a/lib/provider/chat_provider_model.dart +++ b/lib/provider/chat_provider_model.dart @@ -12,6 +12,7 @@ import 'package:http/http.dart'; import 'package:just_audio/just_audio.dart' as JustAudio; import 'package:just_audio/just_audio.dart'; import 'package:mohem_flutter_app/api/chat/chat_api_client.dart'; +import 'package:mohem_flutter_app/api/my_team/my_team_api_client.dart'; import 'package:mohem_flutter_app/app_state/app_state.dart'; import 'package:mohem_flutter_app/classes/consts.dart'; import 'package:mohem_flutter_app/classes/encryption.dart'; @@ -22,6 +23,7 @@ import 'package:mohem_flutter_app/models/chat/get_search_user_chat_model.dart'; import 'package:mohem_flutter_app/models/chat/get_single_user_chat_list_model.dart'; import 'package:mohem_flutter_app/models/chat/get_user_login_token_model.dart' as userLoginToken; import 'package:mohem_flutter_app/models/chat/make_user_favotire_unfavorite_chat_model.dart' as fav; +import 'package:mohem_flutter_app/models/my_team/get_employee_subordinates_list.dart'; import 'package:mohem_flutter_app/ui/landing/dashboard_screen.dart'; import 'package:mohem_flutter_app/widgets/image_picker.dart'; import 'package:open_file/open_file.dart'; @@ -48,9 +50,22 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { List favUsersList = []; int paginationVal = 0; int? cTypingUserId = 0; - bool isTextMsg = false, isReplyMsg = false, isAttachmentMsg = false, isVoiceMsg = false; + // Audio Recoding Work + Timer? _timer; + int _recodeDuration = 0; + bool isRecoding = false; + bool isPause = false; + bool isPlaying = false; + String? path; + String? musicFile; + late Directory appDirectory; + late RecorderController recorderController; + late PlayerController playerController; + List getEmployeeSubordinatesList = []; + List teamMembersList = []; + //Chat Home Page Counter int chatUConvCounter = 0; @@ -67,6 +82,7 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { } Future buildHubConnection() async { + chatHubConnection = await getHubConnection(); await chatHubConnection.start(); if (kDebugMode) { @@ -183,34 +199,32 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { } void markRead(List data, int receiverID) { - if (data != null) { - for (SingleUserChatModel element in data!) { - if (AppState().chatDetails!.response!.id! == element.targetUserId) { - if (element.isSeen != null) { - if (!element.isSeen!) { - element.isSeen = true; - dynamic data = [ - { - "userChatHistoryId": element.userChatHistoryId, - "TargetUserId": element.currentUserId == receiverID ? element.currentUserId : element.targetUserId, - "isDelivered": true, - "isSeen": true, - } - ]; - updateUserChatHistoryStatusAsync(data); - notifyListeners(); - } + for (SingleUserChatModel element in data!) { + if (AppState().chatDetails!.response!.id! == element.targetUserId) { + if (element.isSeen != null) { + if (!element.isSeen!) { + element.isSeen = true; + dynamic data = [ + { + "userChatHistoryId": element.userChatHistoryId, + "TargetUserId": element.currentUserId == receiverID ? element.currentUserId : element.targetUserId, + "isDelivered": true, + "isSeen": true, + } + ]; + updateUserChatHistoryStatusAsync(data); + notifyListeners(); } - for (ChatUser element in searchedChats!) { - if (element.id == receiverID) { - element.unreadMessageCount = 0; - chatUConvCounter = 0; - } + } + for (ChatUser element in searchedChats!) { + if (element.id == receiverID) { + element.unreadMessageCount = 0; + chatUConvCounter = 0; } } } - notifyListeners(); } + notifyListeners(); } void updateUserChatHistoryStatusAsync(List data) { @@ -610,7 +624,7 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { //Text if (isTextMsg && !isAttachmentMsg && !isVoiceMsg && !isReplyMsg) { logger.d("// Normal Text Message"); - if (message.text == null || message.text.isEmpty) { + if (message.text.isEmpty) { return; } sendChatToServer( @@ -628,7 +642,7 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { userStatus: userStatus); } else if (isTextMsg && !isAttachmentMsg && !isVoiceMsg && isReplyMsg) { logger.d("// Text Message as Reply"); - if (message.text == null || message.text.isEmpty) { + if (message.text.isEmpty) { return; } sendChatToServer( @@ -992,7 +1006,7 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { void deleteData() { List exists = [], unique = []; - if(searchedChats != null) exists.addAll(searchedChats!); + if (searchedChats != null) exists.addAll(searchedChats!); exists.addAll(favUsersList!); Map profileMap = {}; for (ChatUser item in exists) { @@ -1125,18 +1139,6 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { await chatHubConnection.invoke("UserTypingAsync", args: [reciptUser, currentUser]); } -// Audio Recoding Work - Timer? _timer; - int _recodeDuration = 0; - bool isRecoding = false; - bool isPause = false; - bool isPlaying = false; - String? path; - String? musicFile; - late Directory appDirectory; - late RecorderController recorderController; - late PlayerController playerController; - //////// Audio Recoding Work //////////////////// Future initAudio({required int receiverId}) async { @@ -1282,4 +1284,56 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { } } } + + Future getTeamMembers() async { + teamMembersList = []; + isLoading = true; + if (AppState().getemployeeSubordinatesList.isNotEmpty) { + print("=============== In App State ====================="); + getEmployeeSubordinatesList = AppState().getemployeeSubordinatesList; + for (GetEmployeeSubordinatesList element in getEmployeeSubordinatesList) { + print(element.eMPLOYEEEMAILADDRESS); + teamMembersList.add( + ChatUser( + id: int.parse(element.eMPLOYEENUMBER!), + email: element.eMPLOYEEEMAILADDRESS, + userName: element.eMPLOYEEDISPLAYNAME, + phone: element.eMPLOYEEMOBILENUMBER, + userStatus: 0, + unreadMessageCount: 0, + isFav: false, + isTyping: false, + isImageLoading: false, + image: element.eMPLOYEEIMAGE ?? "", + isImageLoaded: true, + userLocalDownlaodedImage: await downloadImageLocal(element.eMPLOYEEIMAGE ?? "", element.eMPLOYEENUMBER!), + ), + ); + } + } else { + getEmployeeSubordinatesList = await MyTeamApiClient().getEmployeeSubordinates("", "", ""); + AppState().setemployeeSubordinatesList = getEmployeeSubordinatesList; + for (GetEmployeeSubordinatesList element in getEmployeeSubordinatesList) { + print(element.eMPLOYEEEMAILADDRESS); + teamMembersList.add( + ChatUser( + id: int.parse(element.eMPLOYEENUMBER!), + email: element.eMPLOYEEEMAILADDRESS, + userName: element.eMPLOYEEDISPLAYNAME, + phone: element.eMPLOYEEMOBILENUMBER, + userStatus: 0, + unreadMessageCount: 0, + isFav: false, + isTyping: false, + isImageLoading: false, + image: element.eMPLOYEEIMAGE ?? "", + isImageLoaded: true, + userLocalDownlaodedImage: await downloadImageLocal(element.eMPLOYEEIMAGE ?? "", element.eMPLOYEENUMBER!), + ), + ); + } + } + isLoading = false; + notifyListeners(); + } } diff --git a/lib/provider/dashboard_provider_model.dart b/lib/provider/dashboard_provider_model.dart index c1a186a..0c313b8 100644 --- a/lib/provider/dashboard_provider_model.dart +++ b/lib/provider/dashboard_provider_model.dart @@ -3,6 +3,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:mohem_flutter_app/api/dashboard_api_client.dart'; import 'package:mohem_flutter_app/api/offers_and_discounts_api_client.dart'; +import 'package:mohem_flutter_app/app_state/app_state.dart'; import 'package:mohem_flutter_app/classes/utils.dart'; import 'package:mohem_flutter_app/config/routes.dart'; import 'package:mohem_flutter_app/generated/locale_keys.g.dart'; @@ -35,8 +36,6 @@ class DashboardProviderModel with ChangeNotifier, DiagnosticableTreeMixin { bool isWorkListLoading = true; int workListCounter = 0; - - //Misssing Swipe bool isMissingSwipeLoading = true; int missingSwipeCounter = 0; @@ -94,7 +93,6 @@ class DashboardProviderModel with ChangeNotifier, DiagnosticableTreeMixin { accrualList = null; leaveBalanceAccrual = null; - ticketBalance = 0; isServicesMenusLoading = true; homeMenus = null; @@ -215,6 +213,7 @@ class DashboardProviderModel with ChangeNotifier, DiagnosticableTreeMixin { } List findMyTeam = menuList.where((element) => element.menuType == "M").toList(); if (findMyTeam.isNotEmpty) { + AppState().setempStatusIsManager = true; drawerMenuItemList.insert(2, DrawerMenuItem("assets/images/drawer/my_team.svg", LocaleKeys.myTeamMembers.tr(), AppRoutes.myTeam)); } } catch (ex) { diff --git a/lib/ui/chat/chat_home.dart b/lib/ui/chat/chat_home.dart index 8973f6b..49908dd 100644 --- a/lib/ui/chat/chat_home.dart +++ b/lib/ui/chat/chat_home.dart @@ -1,5 +1,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:mohem_flutter_app/app_state/app_state.dart'; import 'package:mohem_flutter_app/classes/colors.dart'; import 'package:mohem_flutter_app/extensions/int_extensions.dart'; import 'package:mohem_flutter_app/extensions/string_extensions.dart'; @@ -8,6 +9,7 @@ import 'package:mohem_flutter_app/generated/locale_keys.g.dart'; import 'package:mohem_flutter_app/provider/chat_provider_model.dart'; import 'package:mohem_flutter_app/ui/chat/chat_home_screen.dart'; import 'package:mohem_flutter_app/ui/chat/favorite_users_screen.dart'; +import 'package:mohem_flutter_app/ui/chat/my_team_screen.dart'; import 'package:mohem_flutter_app/ui/landing/dashboard_screen.dart'; import 'package:mohem_flutter_app/widgets/app_bar_widget.dart'; import 'package:provider/provider.dart'; @@ -81,7 +83,7 @@ class _ChatHomeState extends State { children: [ myTab(LocaleKeys.mychats.tr(), 0), myTab(LocaleKeys.favorite.tr(), 1), - myTab("My Team", 2), + AppState().getempStatusIsManager ? myTab("My Team", 2) : const SizedBox(), ], ), ), @@ -96,7 +98,7 @@ class _ChatHomeState extends State { children: [ ChatHomeScreen(), ChatFavoriteUsersScreen(), - ChatFavoriteUsersScreen(), + AppState().getempStatusIsManager ? const MyTeamScreen() : const SizedBox(), ], ).expanded, ], diff --git a/lib/ui/chat/my_team_screen.dart b/lib/ui/chat/my_team_screen.dart new file mode 100644 index 0000000..6b68a58 --- /dev/null +++ b/lib/ui/chat/my_team_screen.dart @@ -0,0 +1,155 @@ +import 'dart:convert'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:mohem_flutter_app/provider/chat_provider_model.dart'; +import 'package:mohem_flutter_app/app_state/app_state.dart'; +import 'package:mohem_flutter_app/classes/colors.dart'; +import 'package:mohem_flutter_app/classes/utils.dart'; +import 'package:mohem_flutter_app/config/routes.dart'; +import 'package:mohem_flutter_app/extensions/string_extensions.dart'; +import 'package:mohem_flutter_app/extensions/widget_extensions.dart'; +import 'package:mohem_flutter_app/ui/chat/chat_detailed_screen.dart'; +import 'package:mohem_flutter_app/widgets/shimmer/dashboard_shimmer_widget.dart'; +import 'package:provider/provider.dart'; + +class MyTeamScreen extends StatefulWidget { + const MyTeamScreen({Key? key}) : super(key: key); + + @override + State createState() => _MyTeamScreenState(); +} + +class _MyTeamScreenState extends State { + late ChatProviderModel provider; + + @override + void initState() { + super.initState(); + provider = Provider.of(context, listen: false); + loadMembers(); + } + + + void loadMembers(){ + provider.getTeamMembers(); + } + + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: MyColors.white, + body: Consumer( + builder: (BuildContext context, ChatProviderModel m, Widget? child) { + if (m.isLoading) { + return ChatHomeShimmer( + isDetailedScreen: false, + ); + } else { + return m.teamMembersList != null && m.teamMembersList.isNotEmpty + ? ListView.separated( + itemCount: m.teamMembersList!.length, + shrinkWrap: true, + physics: const ClampingScrollPhysics(), + padding: const EdgeInsets.only(bottom: 80.0), + itemBuilder: (BuildContext context, int index) { + return SizedBox( + height: 55, + child: Row( + children: [ + Stack( + children: [ + if (m.teamMembersList![index].isImageLoading!) + const SizedBox( + height: 48, + width: 48, + ).toShimmer().circle(30), + if (!m.teamMembersList![index].isImageLoading! && m.teamMembersList![index].userLocalDownlaodedImage == null) + SvgPicture.asset( + "assets/images/user.svg", + height: 48, + width: 48, + ), + if (!m.teamMembersList![index].isImageLoading! && m.teamMembersList![index].userLocalDownlaodedImage != null) + Container( + width: 48.0, + height: 48.0, + decoration: BoxDecoration( + shape: BoxShape.circle, + image: DecorationImage( + fit: BoxFit.cover, + image: FileImage(m.teamMembersList![index].userLocalDownlaodedImage!), + ), + ), + ), + Positioned( + right: 5, + bottom: 1, + child: Container( + width: 10, + height: 10, + decoration: BoxDecoration( + color: m.teamMembersList![index].userStatus == 1 ? MyColors.green2DColor : Colors.red, + ), + ).circle(10), + ) + ], + ), + Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + (m.teamMembersList![index].userName!.replaceFirst(".", " ").capitalizeFirstofEach ?? "").toText14(color: MyColors.darkTextColor).paddingOnly(left: 11, top: 13), + ], + ).expanded, + // SizedBox( + // width: 60, + // child: Row( + // crossAxisAlignment: CrossAxisAlignment.center, + // mainAxisAlignment: MainAxisAlignment.end, + // mainAxisSize: MainAxisSize.max, + // children: [ + // Icon( + // m.teamMembersList![index].isFav! ? Icons.star : Icons.star_border, + // color: m.teamMembersList![index].isFav! ? MyColors.yellowColor : MyColors.grey35Color, + // ).onPress(() { + // if (m.teamMembersList![index].isFav!) { + // m.unFavoriteUser( + // userID: AppState().chatDetails!.response!.id!, + // targetUserID: m.teamMembersList![index].id!, + // ); + // } + // }).center, + // ], + // ), + // ), + ], + ), + ).onPress(() { + print(jsonEncode(m.teamMembersList[index])); + // Navigator.pushNamed( + // context, + // AppRoutes.chatDetailed, + // arguments: ChatDetailedScreenParams(m.teamMembersList![index], true), + // ).then( + // (Object? value) { + // m.clearSelections(); + // }, + // ); + }); + }, + separatorBuilder: (BuildContext context, int index) => const Divider(color: MyColors.lightGreyE5Color).paddingOnly(left: 70), + ).paddingAll(21) + : Column( + children: [ + Utils.getNoDataWidget(context).expanded, + ], + ); + } + }, + ), + ); + } +} diff --git a/lib/widgets/bottom_sheets/search_employee_bottom_sheet.dart b/lib/widgets/bottom_sheets/search_employee_bottom_sheet.dart index 77501bc..488b50f 100644 --- a/lib/widgets/bottom_sheets/search_employee_bottom_sheet.dart +++ b/lib/widgets/bottom_sheets/search_employee_bottom_sheet.dart @@ -94,7 +94,7 @@ class _SearchEmployeeBottomSheetState extends State { searchText, int.parse(AppState().chatDetails!.response!.id.toString()), ); - chatUsersList!.removeWhere((element) => element.id == AppState().chatDetails!.response!.id); + chatUsersList!.removeWhere((ChatUser element) => element.id == AppState().chatDetails!.response!.id); Utils.hideLoading(context); setState(() {}); } catch (e) {