diff --git a/assets/icons/chat/chat.svg b/assets/icons/chat/chat.svg new file mode 100644 index 0000000..29d4471 --- /dev/null +++ b/assets/icons/chat/chat.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/chat/chat_active_icon.svg b/assets/icons/chat/chat_active_icon.svg new file mode 100644 index 0000000..958d830 --- /dev/null +++ b/assets/icons/chat/chat_active_icon.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/assets/icons/chat/chat_send_icon.svg b/assets/icons/chat/chat_send_icon.svg new file mode 100644 index 0000000..6f1de72 --- /dev/null +++ b/assets/icons/chat/chat_send_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/langs/ar-SA.json b/assets/langs/ar-SA.json index e17edaa..00f18f3 100644 --- a/assets/langs/ar-SA.json +++ b/assets/langs/ar-SA.json @@ -394,7 +394,7 @@ "uploadFromGallery": "تحميل من\nملفات الجهاز", "name": "الأسم", "email": "ايميل", - "noHistoryAvailable":"لايوجد سجل بيانات سابقة ", + "noHistoryAvailable": "لايوجد سجل بيانات سابقة ", "purchaseRequisition": "طلب شراء", "moveOrder": "طلب تغيير", "humanResource": "الموارد البشريه", @@ -405,7 +405,7 @@ "addFavoriteList": "هل تريد اضافة {name} لقائمة المفضله", "feedbackUserExperience": "هذا للحصول على تعليقات حول تجربة المستخدم", "rateUI": ".1 كيف تريد تقييم التطبيق", - "submitSurvey":"ارسال الاستبيان", + "submitSurvey": "ارسال الاستبيان", "typeHere": "اكتب هنا", "info_detail": "تفاصيل المعلومات", "amount_detail": "تفاصيل المبلغ", @@ -451,5 +451,8 @@ "female": "Hello girl :) {}" } }, - "reset_locale": "Reset Language" + "reset_locale": "Reset Language", + "chat": "دردشة", + "mychats": "دردشاتي", + "createNewChat": "Create New Chat" } \ No newline at end of file diff --git a/assets/langs/en-US.json b/assets/langs/en-US.json index d16cfa1..6bb5972 100644 --- a/assets/langs/en-US.json +++ b/assets/langs/en-US.json @@ -451,5 +451,8 @@ "female": "Hello girl :) {}" } }, - "reset_locale": "Reset Language" + "reset_locale": "Reset Language", + "chat": "Chat", + "mychats": "My Chats", + "createNewChat": "Create New Chat" } \ No newline at end of file diff --git a/lib/api/chat/chat_provider_model.dart b/lib/api/chat/chat_provider_model.dart new file mode 100644 index 0000000..d4b49f8 --- /dev/null +++ b/lib/api/chat/chat_provider_model.dart @@ -0,0 +1,213 @@ +import 'dart:convert'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:http/http.dart'; +import 'package:logging/logging.dart'; +import 'package:mohem_flutter_app/api/api_client.dart'; +import 'package:mohem_flutter_app/classes/consts.dart'; +import 'package:mohem_flutter_app/classes/utils.dart'; +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:signalr_netcore/hub_connection.dart'; +import 'package:signalr_netcore/signalr_client.dart'; +import 'package:logger/logger.dart' as L; + +class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { + List userChatHistory = []; + List? pChatHistory, searchedChats; + late HubConnection hubConnection; + L.Logger logger = L.Logger(); + TextEditingController message = TextEditingController(); + ScrollController scrollController = ScrollController(); + static String token = + "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJuYW1laWQiOiI0MjA2MiIsImVtYWlsIjoiYWFtaXIubXVoYW1tYWRAY2xvdWRzb2x1dGlvbnMuY29tLnNhIiwiaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93cy8yMDA4LzA2L2lkZW50aXR5L2NsYWltcy91c2VyZGF0YSI6ImFhbWlyLm11aGFtbWFkIiwiaHR0cDovL3NjaGVtYXMueG1sc29hcC5vcmcvd3MvMjAwNS8wNS9pZGVudGl0eS9jbGFpbXMvbW9iaWxlcGhvbmUiOiI5NjY1MzA4OTYwMTgiLCJuYmYiOjE2NjU5MjA2NDEsImV4cCI6MTY2NjAwNzA0MSwiaWF0IjoxNjY1OTIwNjQxfQ.70tXWdpXtQ20PNBO3WF9ScWNWSyECpFfrW7_iuOmNfWmA63PCZzlTO0E6I3q3K9Kg2CWvOT9-dSDLjlRuXuC2w"; + + bool isLoading = true; + + void getChatMemberFromSearch(String sName, int cUserId) async { + isLoading = true; + notifyListeners(); + Response response = await ApiClient().getJsonForResponse( + "${ApiConsts.chatSearchMember}$sName/$cUserId", + token: token, + ); + isLoading = false; + notifyListeners(); + } + + void getUserRecentChats() async { + Response response = await ApiClient().getJsonForResponse( + "${ApiConsts.chatServerBaseApiUrl}${ApiConsts.chatRecentUrl}", + token: token, + ); + ChatUserModel recentChat = userToList(response.body); + pChatHistory = recentChat.response; + searchedChats = pChatHistory; + isLoading = false; + notifyListeners(); + } + + void getSingleUserChatHistory({required String senderUID, required int receiverUID, required String pagination}) async { + isLoading = true; + Response response = await ApiClient().getJsonForResponse( + "${ApiConsts.chatServerBaseApiUrl}${ApiConsts.chatSingleUserHistoryUrl}/$senderUID/$receiverUID/$pagination", + token: token, + ); + userChatHistory = getSingleUserChatintoModel(response.body); + isLoading = false; + logger.d(jsonEncode(userChatHistory)); + notifyListeners(); + } + + List getSingleUserChatintoModel(String str) => List.from(json.decode(str).map((x) => SingleUserChatModel.fromJson(x))); + + ChatUserModel userToList(String str) => ChatUserModel.fromJson(json.decode(str)); + + void buildHubConnection() async { + HttpConnectionOptions httpOp = HttpConnectionOptions(skipNegotiation: false, logMessageContent: true); + hubConnection = await HubConnectionBuilder() + .withUrl(ApiConsts.chatHubConnectionUrl + "?UserId=42062&source=Web&access_token=$token", options: httpOp) + .withAutomaticReconnect(retryDelays: [2000, 5000, 10000, 20000]) + .configureLogging(Logger("Logs Enabled")) + .build(); + hubConnection.onclose( + ({Exception? error}) { + logger.d(error); + }, + ); + hubConnection.onreconnecting( + ({Exception? error}) { + logger.d(error); + logger.d("Reconnecting"); + }, + ); + hubConnection.onreconnected( + ({String? connectionId}) { + logger.d("Reconnected"); + }, + ); + if (hubConnection.state != HubConnectionState.Connected) { + await hubConnection.start(); + hubConnection.on("OnUpdateUserStatusAsync", changeStatus); + hubConnection.on("OnDeliveredChatUserAsync", onMsgReceived); + // hubConnection.on("OnUserTypingAsync", onUserTyping); + //hubConnection.on("OnUserTypingAsync", changeTypingStatus); + } else { + hubConnection.on("OnUpdateUserStatusAsync", changeStatus); + hubConnection.on("OnDeliveredChatUserAsync", onMsgReceived); + // hubConnection.on("OnUserTypingAsync", onUserTyping); + //hubConnection.on("OnUserTypingAsync", changeTypingStatus); + } + isLoading = false; + notifyListeners(); + } + + void changeStatus(List? args) { + List items = args!.toList(); + for (var user in searchedChats!) { + if (user.id == items.first["id"]) { + user.userStatus = items.first["userStatus"]; + } + } + notifyListeners(); + } + + void filter(String value) async { + List? tmp = []; + if (value.isEmpty || value == "") { + tmp = pChatHistory; + } else { + for (var element in pChatHistory!) { + if (element.userName!.toLowerCase().contains(value.toLowerCase())) { + tmp.add(element); + } + } + } + searchedChats = tmp; + notifyListeners(); + } + + Future onMsgReceived(List? parameters) async { + List data = []; + for (dynamic msg in parameters!) { + data = getSingleUserChatintoModel(jsonEncode(msg)); + logger.d(msg); + } + userChatHistory.add(data.first); + notifyListeners(); + scrollDown(); + } + + void onUserTyping(List? parameters) { + print("==================== Typing Active =================="); + logger.d(parameters); + for (ChatUser user in searchedChats!) { + if (user.id == parameters![1] && parameters[0] == true) { + user.isTyping = parameters[0] as bool?; + } else { + Future.delayed( + const Duration(milliseconds: 500), + () { + user.isTyping = false; + }, + ); + } + } + notifyListeners(); + } + + void sendChatMessage(String chatMessage, int targetUserId, String targetUserName) async { + if (chatMessage == null || chatMessage.isEmpty) { + return; + } + String chatData = + '{"contant":"$chatMessage","contantNo":"8a129295-36d7-7185-5d34-cc4eec7bcba4","chatEventId":1,"fileTypeId":null,"currentUserId":42062,"chatSource":1,"userChatHistoryLineRequestList":[{"isSeen":false,"isDelivered":false,"targetUserId":$targetUserId,"targetUserStatus":1}],"conversationId":"715f8b13-96ee-cd36-cb07-5a982a219982"}'; + await hubConnection.invoke("AddChatUserAsync", args: [json.decode(chatData)]); + userChatHistory.add( + SingleUserChatModel( + chatEventId: 1, + chatSource: 1, + contant: chatMessage, + contantNo: "8a129295-36d7-7185-5d34-cc4eec7bcba4", + conversationId: "715f8b13-96ee-cd36-cb07-5a982a219982", + createdDate: DateTime.now(), + currentUserId: 42062, + currentUserName: "aamir.muhammad", + targetUserId: targetUserId, + targetUserName: targetUserName, + ), + ); + message.clear(); + notifyListeners(); + scrollDown(); + } + + void scrollDown() { + scrollController.animateTo( + scrollController.position.maxScrollExtent + 100, + curve: Curves.easeOut, + duration: const Duration(milliseconds: 300), + ); + + // scrollController.animateTo(double.parse(userChatHistory.length.toString()), duration: Duration(milliseconds: 500), curve: Curves.fastOutSlowIn); + + notifyListeners(); + } + +// void _scrollListener() { +// if (scrollController.position.extentAfter.toInt() <= 0 && canCallApi) { +// if (userChatHistory.length < _ayatTangheemTypeMapped.totalItemsCount) { +// currentPageNo++; +// if (widget.tangheemQuery == null) { +// getTangheemData(); +// } else { +// getTangheemDataByKeyword(); +// } +// } +// canCallApi = false; +// } +// } + +} diff --git a/lib/classes/colors.dart b/lib/classes/colors.dart index eb25c48..dba3db5 100644 --- a/lib/classes/colors.dart +++ b/lib/classes/colors.dart @@ -9,6 +9,7 @@ class MyColors { static const Color gradiantEndColor = Color(0xff259db7); static const Color textMixColor = Color(0xff2BB8A6); static const Color backgroundColor = Color(0xffF8F8F8); + static const Color grey41Color = Color(0xff414141); static const Color grey57Color = Color(0xff575757); static const Color grey67Color = Color(0xff676767); static const Color grey77Color = Color(0xff777777); @@ -54,4 +55,5 @@ class MyColors { static const Color green2DColor = Color(0xff32D892); static const Color greyC4Color = Color(0xffC4C4C4); static const Color grey35Color = Color(0xff535353); + static const Color grey9DColor = Color(0xff9D9D9D); } diff --git a/lib/classes/consts.dart b/lib/classes/consts.dart index 12f818e..e82c30a 100644 --- a/lib/classes/consts.dart +++ b/lib/classes/consts.dart @@ -9,6 +9,16 @@ class ApiConsts { static String swpRest = baseUrlServices + "SWP.svc/REST/"; static String user = baseUrlServices + "api/User/"; static String cocRest = baseUrlServices + "COCWS.svc/REST/"; + + static String chatServerBaseUrl = "https://apiderichat.hmg.com"; + static String chatServerBaseApiUrl = "https://apiderichat.hmg.com/api/"; + static String chatHubConnectionUrl = chatServerBaseUrl + "/ConnectionChatHub"; + static String chatSearchMember = "user/getUserWithStatusAndFavAsync/"; + static String chatRecentUrl = "UserChatHistory/getchathistorybyuserid"; //For a Mem + static String chatSingleUserHistoryUrl = "UserChatHistory/GetUserChatHistory"; +// 42062 is CurrentUserID and 36745 is targetUserID and 0 is For Pagination +// static String chatSearchMember = "https://apiderichat.hmg.com/api/user/getUserWithStatusAndFavAsync/aamir.muhammad/36239"; + } class SharedPrefsConsts { diff --git a/lib/classes/utils.dart b/lib/classes/utils.dart index 098f65c..dbe451c 100644 --- a/lib/classes/utils.dart +++ b/lib/classes/utils.dart @@ -137,6 +137,17 @@ class Utils { ).center; } + static Widget getNoChatWidget(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SvgPicture.asset('assets/images/not_found.svg', width: 110.0, height: 110.0), + LocaleKeys.noDataAvailable.tr().toText16().paddingOnly(top: 15), + ], + ).center; + } + static Uint8List getPostBytes(img) { try { var b64 = img.replaceFirst('data:image/png;base64,', ''); diff --git a/lib/config/routes.dart b/lib/config/routes.dart index 21058ad..85ba6ff 100644 --- a/lib/config/routes.dart +++ b/lib/config/routes.dart @@ -3,6 +3,8 @@ import 'package:mohem_flutter_app/ui/attendance/add_vacation_rule_screen.dart'; import 'package:mohem_flutter_app/ui/attendance/monthly_attendance_screen.dart'; import 'package:mohem_flutter_app/ui/attendance/vacation_rule_screen.dart'; import 'package:mohem_flutter_app/ui/bottom_sheets/attendence_details_bottom_sheet.dart'; +import 'package:mohem_flutter_app/ui/chat/chat_detailed_screen.dart'; +import 'package:mohem_flutter_app/ui/chat/chat_home.dart'; import 'package:mohem_flutter_app/ui/landing/dashboard_screen.dart'; import 'package:mohem_flutter_app/ui/landing/survey_screen.dart'; import 'package:mohem_flutter_app/ui/landing/today_attendance_screen.dart'; @@ -166,6 +168,10 @@ class AppRoutes { static const String changePassword = "/changePassword"; + //Chat + static const String chat = "/chat"; + static const String chatDetailed = "/chatDetailed"; + static final Map routes = { login: (context) => LoginScreen(), verifyLogin: (context) => VerifyLoginScreen(), @@ -262,5 +268,9 @@ class AppRoutes { subordinateLeave: (context) => SubordinateLeave(), changePassword: (context) => ChangePasswordScreen(), + + //Chat + chat: (context) => ChatHomeScreen(), + chatDetailed: (context) => ChatDetailScreen(), }; } diff --git a/lib/generated/locale_keys.g.dart b/lib/generated/locale_keys.g.dart index a236325..dbfed5e 100644 --- a/lib/generated/locale_keys.g.dart +++ b/lib/generated/locale_keys.g.dart @@ -438,4 +438,8 @@ abstract class LocaleKeys { static const gender_with_arg = 'gender.with_arg'; static const gender = 'gender'; static const reset_locale = 'reset_locale'; + static const chat = 'chat'; + static const mychats = 'mychats'; + static const createNewChat = 'createNewChat'; + } diff --git a/lib/main.dart b/lib/main.dart index d5869fb..0048020 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:logger/logger.dart'; +import 'package:mohem_flutter_app/api/chat/chat_provider_model.dart'; import 'package:mohem_flutter_app/app_state/app_state.dart'; import 'package:mohem_flutter_app/config/routes.dart'; import 'package:mohem_flutter_app/generated/codegen_loader.g.dart'; @@ -43,6 +44,7 @@ Future main() async { providers: [ ChangeNotifierProvider(create: (_) => DashboardProviderModel()), ChangeNotifierProvider(create: (_) => EITProviderModel()), + ChangeNotifierProvider(create: (_) => ChatProviderModel()) ], child: MyApp(), ), diff --git a/lib/models/chat/get_search_user_chat_model.dart b/lib/models/chat/get_search_user_chat_model.dart new file mode 100644 index 0000000..ceee0de --- /dev/null +++ b/lib/models/chat/get_search_user_chat_model.dart @@ -0,0 +1,82 @@ +class ChatUserModel { + ChatUserModel({ + this.response, + this.errorResponses, + }); + + List? response; + dynamic errorResponses; + + factory ChatUserModel.fromJson(Map json) => ChatUserModel( + response: json["response"] == null ? null : List.from(json["response"].map((x) => ChatUser.fromJson(x))), + errorResponses: json["errorResponses"], + ); + + Map toJson() => { + "response": response == null ? null : List.from(response!.map((x) => x.toJson())), + "errorResponses": errorResponses, + }; +} + +class ChatUser { + ChatUser({ + this.id, + this.userName, + this.email, + this.phone, + this.title, + this.userStatus, + this.image, + this.unreadMessageCount, + this.userAction, + this.isPin, + this.isFav, + this.isAdmin, + this.isTyping, + }); + + int? id; + String? userName; + String? email; + dynamic? phone; + dynamic? title; + int? userStatus; + dynamic? image; + int? unreadMessageCount; + dynamic? userAction; + bool? isPin; + bool? isFav; + bool? isAdmin; + bool? isTyping; + + factory ChatUser.fromJson(Map json) => ChatUser( + id: json["id"] == null ? null : json["id"], + userName: json["userName"] == null ? null : json["userName"], + email: json["email"] == null ? null : json["email"], + phone: json["phone"], + title: json["title"], + userStatus: json["userStatus"] == null ? null : json["userStatus"], + image: json["image"], + unreadMessageCount: json["unreadMessageCount"] == null ? null : json["unreadMessageCount"], + userAction: json["userAction"], + isPin: json["isPin"] == null ? null : json["isPin"], + isFav: json["isFav"] == null ? null : json["isFav"], + isAdmin: json["isAdmin"] == null ? null : json["isAdmin"], + isTyping: false, + ); + + Map toJson() => { + "id": id == null ? null : id, + "userName": userName == null ? null : userName, + "email": email == null ? null : email, + "phone": phone, + "title": title, + "userStatus": userStatus == null ? null : userStatus, + "image": image, + "unreadMessageCount": unreadMessageCount == null ? null : unreadMessageCount, + "userAction": userAction, + "isPin": isPin == null ? null : isPin, + "isFav": isFav == null ? null : isFav, + "isAdmin": isAdmin == null ? null : isAdmin, + }; +} diff --git a/lib/models/chat/get_single_user_chat_list_Model.dart b/lib/models/chat/get_single_user_chat_list_Model.dart new file mode 100644 index 0000000..0e3cb22 --- /dev/null +++ b/lib/models/chat/get_single_user_chat_list_Model.dart @@ -0,0 +1,119 @@ +class SingleUserChatModel { + SingleUserChatModel({ + this.userChatHistoryId, + this.userChatHistoryLineId, + this.contant, + this.contantNo, + this.currentUserId, + this.currentUserName, + this.targetUserId, + this.targetUserName, + this.encryptedTargetUserId, + this.encryptedTargetUserName, + this.chatEventId, + this.fileTypeId, + this.isSeen, + this.isDelivered, + this.createdDate, + this.chatSource, + this.conversationId, + this.fileTypeResponse, + this.userChatReplyResponse, + }); + + int? userChatHistoryId; + int? userChatHistoryLineId; + String? contant; + String? contantNo; + int? currentUserId; + String? currentUserName; + int? targetUserId; + String? targetUserName; + dynamic encryptedTargetUserId; + dynamic encryptedTargetUserName; + int? chatEventId; + dynamic fileTypeId; + bool? isSeen; + bool? isDelivered; + DateTime? createdDate; + int? chatSource; + String? conversationId; + FileTypeResponse? fileTypeResponse; + dynamic userChatReplyResponse; + + factory SingleUserChatModel.fromJson(Map json) => SingleUserChatModel( + userChatHistoryId: json["userChatHistoryId"] == null ? null : json["userChatHistoryId"], + userChatHistoryLineId: json["userChatHistoryLineId"] == null ? null : json["userChatHistoryLineId"], + contant: json["contant"] == null ? null : json["contant"], + contantNo: json["contantNo"] == null ? null : json["contantNo"], + currentUserId: json["currentUserId"] == null ? null : json["currentUserId"], + currentUserName: json["currentUserName"] == null ? null : json["currentUserName"], + targetUserId: json["targetUserId"] == null ? null : json["targetUserId"], + targetUserName: json["targetUserName"] == null ? null : json["targetUserName"], + encryptedTargetUserId: json["encryptedTargetUserId"], + encryptedTargetUserName: json["encryptedTargetUserName"], + chatEventId: json["chatEventId"] == null ? null : json["chatEventId"], + fileTypeId: json["fileTypeId"], + isSeen: json["isSeen"] == null ? null : json["isSeen"], + isDelivered: json["isDelivered"] == null ? null : json["isDelivered"], + createdDate: json["createdDate"] == null ? null : DateTime.parse(json["createdDate"]), + chatSource: json["chatSource"] == null ? null : json["chatSource"], + conversationId: json["conversationId"] == null ? null : json["conversationId"], + fileTypeResponse: json["fileTypeResponse"] == null ? null : FileTypeResponse.fromJson(json["fileTypeResponse"]), + userChatReplyResponse: json["userChatReplyResponse"], + ); + + Map toJson() => { + "userChatHistoryId": userChatHistoryId == null ? null : userChatHistoryId, + "userChatHistoryLineId": userChatHistoryLineId == null ? null : userChatHistoryLineId, + "contant": contant == null ? null : contant, + "contantNo": contantNo == null ? null : contantNo, + "currentUserId": currentUserId == null ? null : currentUserId, + "currentUserName": currentUserName == null ? null : currentUserName, + "targetUserId": targetUserId == null ? null : targetUserId, + "targetUserName": targetUserName == null ? null : targetUserName, + "encryptedTargetUserId": encryptedTargetUserId, + "encryptedTargetUserName": encryptedTargetUserName, + "chatEventId": chatEventId == null ? null : chatEventId, + "fileTypeId": fileTypeId, + "isSeen": isSeen == null ? null : isSeen, + "isDelivered": isDelivered == null ? null : isDelivered, + "createdDate": createdDate == null ? null : createdDate!.toIso8601String(), + "chatSource": chatSource == null ? null : chatSource, + "conversationId": conversationId == null ? null : conversationId, + "fileTypeResponse": fileTypeResponse == null ? null : fileTypeResponse!.toJson(), + "userChatReplyResponse": userChatReplyResponse, + }; +} + +class FileTypeResponse { + FileTypeResponse({ + this.fileTypeId, + this.fileTypeName, + this.fileTypeDescription, + this.fileKind, + this.fileName, + }); + + int? fileTypeId; + dynamic fileTypeName; + dynamic fileTypeDescription; + dynamic fileKind; + dynamic fileName; + + factory FileTypeResponse.fromJson(Map json) => FileTypeResponse( + fileTypeId: json["fileTypeId"] == null ? null : json["fileTypeId"], + fileTypeName: json["fileTypeName"], + fileTypeDescription: json["fileTypeDescription"], + fileKind: json["fileKind"], + fileName: json["fileName"], + ); + + Map toJson() => { + "fileTypeId": fileTypeId == null ? null : fileTypeId, + "fileTypeName": fileTypeName, + "fileTypeDescription": fileTypeDescription, + "fileKind": fileKind, + "fileName": fileName, + }; +} diff --git a/lib/ui/chat/chat_bubble.dart b/lib/ui/chat/chat_bubble.dart new file mode 100644 index 0000000..74d2e2b --- /dev/null +++ b/lib/ui/chat/chat_bubble.dart @@ -0,0 +1,88 @@ +import 'package:flutter/material.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'; + +class ChatBubble extends StatelessWidget { + const ChatBubble( + {Key? key, + required this.text, + required this.isCurrentUser, + required this.isSeen, + required this.isDelivered, + required this.dateTime}) + : super(key: key); + final String text; + final bool isCurrentUser; + final bool isSeen; + final bool isDelivered; + final String dateTime; + + @override + Widget build(BuildContext context) { + return Padding( + // asymmetric padding + padding: EdgeInsets.fromLTRB( + isCurrentUser ? 64.0 : 16.0, + 4, + isCurrentUser ? 16.0 : 64.0, + 4, + ), + child: Align( + // align the child within the container + alignment: isCurrentUser ? Alignment.centerRight : Alignment.centerLeft, + child: DecoratedBox( + // chat bubble decoration + decoration: BoxDecoration( + color: Colors.white, + gradient: isCurrentUser + ? null + : LinearGradient( + transform: GradientRotation(.46), + begin: Alignment.topRight, + end: Alignment.bottomLeft, + colors: [ + MyColors.gradiantEndColor, + MyColors.gradiantStartColor, + ]), + borderRadius: BorderRadius.circular(10), + ), + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + text.toText12( + color: + isCurrentUser ? MyColors.grey57Color : MyColors.white), + 8.height, + Row( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + dateTime.toText12( + color: isCurrentUser + ? MyColors.grey41Color.withOpacity(.5) + : Colors.white.withOpacity(0.7)), + if (isCurrentUser) 5.width, + if (isCurrentUser) + Icon( + isDelivered + ? Icons.done_all + : Icons.done_all, + color: isSeen + ? MyColors.textMixColor + : MyColors.grey9DColor, + size: 14, + ) + ], + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/ui/chat/chat_detailed_screen.dart b/lib/ui/chat/chat_detailed_screen.dart new file mode 100644 index 0000000..17ba325 --- /dev/null +++ b/lib/ui/chat/chat_detailed_screen.dart @@ -0,0 +1,86 @@ +import 'dart:async'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:mohem_flutter_app/api/chat/chat_provider_model.dart'; +import 'package:mohem_flutter_app/classes/colors.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/widgets/shimmer/dashboard_shimmer_widget.dart'; +import 'package:provider/provider.dart'; + +class ChatDetailScreen extends StatelessWidget { + dynamic userDetails; + late ChatProviderModel data; + + ChatDetailScreen({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + userDetails = ModalRoute.of(context)!.settings.arguments; + data = Provider.of(context, listen: false); + data.getSingleUserChatHistory(senderUID: "42062", receiverUID: userDetails["targetUser"].id, pagination: "0"); + Timer(const Duration(seconds: 1), () => data.scrollDown()); + return Scaffold( + backgroundColor: const Color(0xFFF8F8F8), + appBar: AppBarWidget(context, title: userDetails["targetUser"].userName, showHomeButton: false, image: userDetails["targetUser"].image), + body: Consumer( + builder: (BuildContext context, ChatProviderModel m, Widget? child) { + return (m.isLoading + ? ChatHomeShimmer() + : Column( + children: [ + Expanded( + child: ListView.builder( + controller: m.scrollController, + shrinkWrap: true, + itemCount: m.userChatHistory.length, + padding: const EdgeInsets.symmetric(vertical: 10), + itemBuilder: (BuildContext context, int i) { + return ChatBubble( + text: m.userChatHistory[i].contant.toString(), + isSeen: m.userChatHistory[i].isSeen == true ? true : false, + isCurrentUser: m.userChatHistory[i].currentUserId == 42062 ? true : false, + isDelivered: m.userChatHistory[i].currentUserId == 42062 && m.userChatHistory[i].isDelivered == true ? true : false, + dateTime: m.userChatHistory[i].createdDate.toString(), + ); + }, + ), + ), + Card( + margin: EdgeInsets.zero, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: TextField( + controller: m.message, + decoration: InputDecoration( + hintText: 'Type here to reply', + hintStyle: const TextStyle(color: MyColors.grey98Color), + border: InputBorder.none, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + disabledBorder: InputBorder.none, + contentPadding: const EdgeInsets.symmetric(vertical: 10, horizontal: 15), + suffixIcon: IconButton( + icon: SvgPicture.asset( + "assets/icons/chat/chat_send_icon.svg", + height: 26, + width: 35, + ), + onPressed: () { + m.sendChatMessage(m.message.text, userDetails["targetUser"].id, userDetails["targetUser"].userName); + }, + ), + ), + ), + ), + ), + ], + )); + }, + ), + ); + } +} diff --git a/lib/ui/chat/chat_home.dart b/lib/ui/chat/chat_home.dart new file mode 100644 index 0000000..236a73f --- /dev/null +++ b/lib/ui/chat/chat_home.dart @@ -0,0 +1,175 @@ +import 'dart:convert'; + +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:mohem_flutter_app/api/chat/chat_provider_model.dart'; +import 'package:mohem_flutter_app/classes/colors.dart'; +import 'package:mohem_flutter_app/config/routes.dart'; +import 'package:mohem_flutter_app/extensions/string_extensions.dart'; +import 'package:mohem_flutter_app/generated/locale_keys.g.dart'; +import 'package:mohem_flutter_app/widgets/app_bar_widget.dart'; +import 'package:mohem_flutter_app/widgets/bottom_sheet.dart'; +import 'package:mohem_flutter_app/widgets/bottom_sheets/search_employee_bottom_sheet.dart'; +import 'package:mohem_flutter_app/widgets/shimmer/dashboard_shimmer_widget.dart'; +import 'package:provider/provider.dart'; + +class ChatHomeScreen extends StatefulWidget { + const ChatHomeScreen({Key? key}) : super(key: key); + + @override + State createState() => _ChatHomeScreenState(); +} + +class _ChatHomeScreenState extends State { + TextEditingController search = new TextEditingController(); + late ChatProviderModel data; + + @override + void initState() { + super.initState(); + data = Provider.of(context, listen: false); + data.buildHubConnection(); + data.getUserRecentChats(); + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + appBar: AppBarWidget(context, title: "My Chats", showHomeButton: false), + body: Consumer(builder: (BuildContext context, ChatProviderModel m, Widget? child) { + return m.isLoading + ? ChatHomeShimmer() + : ListView( + shrinkWrap: true, + physics: const AlwaysScrollableScrollPhysics(), + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 20), + child: TextField( + onChanged: (String val) { + m.filter(val); + }, + decoration: InputDecoration( + border: InputBorder.none, + focusedBorder: InputBorder.none, + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(5), + borderSide: const BorderSide( + color: Color(0xFFE5E5E5), + ), + ), + errorBorder: InputBorder.none, + disabledBorder: InputBorder.none, + contentPadding: const EdgeInsets.symmetric(horizontal: 15, vertical: 10), + hintText: "Search from chat", + hintStyle: const TextStyle(color: MyColors.lightTextColor, fontStyle: FontStyle.italic), + filled: true, + fillColor: const Color(0xFFF7F7F7), + ), + ), + ), + if (m.searchedChats != null) + ListView.separated( + itemCount: m.searchedChats!.length, + padding: const EdgeInsets.only(top: 0), + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (BuildContext context, int index) { + return ListTile( + leading: Stack( + children: [ + SvgPicture.asset( + "assets/images/user.svg", + height: 48, + width: 48, + ), + Positioned( + right: 5, + bottom: 1, + child: Container( + width: 10, + height: 10, + decoration: BoxDecoration( + color: m.searchedChats![index].userStatus == 1 ? MyColors.green2DColor : Colors.red, + borderRadius: const BorderRadius.all( + Radius.circular(10), + ), + ), + ), + ) + ], + ), + title: (m.searchedChats![index].userName ?? "").toText14(color: MyColors.darkTextColor), + subtitle: (m.searchedChats![index].isTyping == true ? "Something is Typing" : "Last message text").toText11(color: MyColors.normalTextColor), + trailing: ("Today").toText10(color: MyColors.lightTextColor), + minVerticalPadding: 0, + onTap: () { + Navigator.pushNamed( + context, + AppRoutes.chatDetailed, + arguments: {"currentUser": "42062", "targetUser": m.searchedChats![index]}, + ); + }, + ); + }, + separatorBuilder: (BuildContext context, int index) => const Padding( + padding: EdgeInsets.only(right: 10, left: 70), + child: Divider( + color: Color(0xFFE5E5E5), + ), + ), + ), + // if (searchedUsersList == null) Utils.getNoChatWidget(context), + ], + ); + }), + floatingActionButton: FloatingActionButton( + child: Container( + width: 60, + height: 60, + decoration: const BoxDecoration( + shape: BoxShape.circle, + gradient: LinearGradient( + transform: GradientRotation(.46), + begin: Alignment.topRight, + end: Alignment.bottomLeft, + colors: [ + MyColors.gradiantEndColor, + MyColors.gradiantStartColor, + ], + ), + ), + child: const Icon( + Icons.add, + size: 30, + color: MyColors.white, + ), + ), + onPressed: () async { + // var userData = await ChatApiClient() + // .getChatMemberFromSearch("aamir.muhammad", 36239); + showMyBottomSheet( + context, + child: SearchEmployeeBottomSheet( + title: LocaleKeys.searchForEmployee.tr(), + apiMode: LocaleKeys.delegate.tr(), + onSelectEmployee: (_selectedEmployee) { + // Navigator.pop(context); + // selectedReplacementEmployee = _selectedEmployee; + setState(() {}); + }, + ), + ); + }, + ), + ); + } +} diff --git a/lib/ui/landing/dashboard_screen.dart b/lib/ui/landing/dashboard_screen.dart index b6f3599..026d17e 100644 --- a/lib/ui/landing/dashboard_screen.dart +++ b/lib/ui/landing/dashboard_screen.dart @@ -414,7 +414,16 @@ class _DashboardScreenState extends State { "assets/icons/item_for_sale.svg", color: currentIndex == 3 ? MyColors.grey3AColor : MyColors.grey98Color, ).paddingAll(4), - label: LocaleKeys.itemsForSale.tr(), + label: LocaleKeys.chat.tr(), + ), + BottomNavigationBarItem( + icon: SvgPicture.asset( + "assets/icons/chat/chat.svg", + color: currentIndex == 4 + ? MyColors.grey3AColor + : MyColors.grey98Color, + ).paddingAll(4), + label: LocaleKeys.chat.tr(), ), ], currentIndex: currentIndex, @@ -432,6 +441,8 @@ class _DashboardScreenState extends State { Navigator.pushNamed(context, AppRoutes.workList); } else if (index == 3) { Navigator.pushNamed(context, AppRoutes.itemsForSale); + }else if (index == 4) { + Navigator.pushNamed(context, AppRoutes.chat); } }, ), diff --git a/lib/widgets/app_bar_widget.dart b/lib/widgets/app_bar_widget.dart index 51bee9c..745dedf 100644 --- a/lib/widgets/app_bar_widget.dart +++ b/lib/widgets/app_bar_widget.dart @@ -1,11 +1,17 @@ import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:mohem_flutter_app/classes/colors.dart'; import 'package:mohem_flutter_app/config/routes.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'; -AppBar AppBarWidget(BuildContext context, {required String title, bool showHomeButton = true, bool showNotificationButton = false, bool showMemberButton = false}) { +AppBar AppBarWidget(BuildContext context, + {required String title, + bool showHomeButton = true, + bool showNotificationButton = false, + bool showMemberButton = false, + String? image}) { return AppBar( leadingWidth: 0, // leading: GestureDetector( @@ -18,10 +24,18 @@ AppBar AppBarWidget(BuildContext context, {required String title, bool showHomeB children: [ GestureDetector( behavior: HitTestBehavior.opaque, - onTap: Feedback.wrapForTap(() => Navigator.maybePop(context), context), - child: const Icon(Icons.arrow_back_ios, color: MyColors.darkIconColor), + onTap: + Feedback.wrapForTap(() => Navigator.maybePop(context), context), + child: + const Icon(Icons.arrow_back_ios, color: MyColors.darkIconColor), ), 4.width, + if (image != null) SvgPicture.asset( + image, + height: 40, + width: 40, + ), + if (image != null) 14.width, title.toText24(color: MyColors.darkTextColor, isBold: true).expanded, ], ), @@ -32,7 +46,8 @@ AppBar AppBarWidget(BuildContext context, {required String title, bool showHomeB if (showHomeButton) IconButton( onPressed: () { - Navigator.popUntil(context, ModalRoute.withName(AppRoutes.dashboard)); + Navigator.popUntil( + context, ModalRoute.withName(AppRoutes.dashboard)); }, icon: const Icon(Icons.home, color: MyColors.darkIconColor), ), diff --git a/lib/widgets/bottom_sheets/search_employee_bottom_sheet.dart b/lib/widgets/bottom_sheets/search_employee_bottom_sheet.dart index 9aabbb7..e807a7b 100644 --- a/lib/widgets/bottom_sheets/search_employee_bottom_sheet.dart +++ b/lib/widgets/bottom_sheets/search_employee_bottom_sheet.dart @@ -22,10 +22,16 @@ class SearchEmployeeBottomSheet extends StatefulWidget { List? actionHistoryList; Function(ReplacementList) onSelectEmployee; - SearchEmployeeBottomSheet({required this.title, required this.apiMode, this.notificationID, this.actionHistoryList, required this.onSelectEmployee}); + SearchEmployeeBottomSheet( + {required this.title, + required this.apiMode, + this.notificationID, + this.actionHistoryList, + required this.onSelectEmployee}); @override - State createState() => _SearchEmployeeBottomSheetState(); + State createState() => + _SearchEmployeeBottomSheetState(); } class _SearchEmployeeBottomSheetState extends State { @@ -53,8 +59,12 @@ class _SearchEmployeeBottomSheetState extends State { userId: _selectedSearchIndex == 1 ? searchText : "", email: _selectedSearchIndex == 2 ? searchText : "", ); - favouriteUserList = replacementList?.where((element) => element.isFavorite ?? false).toList(); - nonFavouriteUserList = replacementList?.where((element) => !(element.isFavorite ?? false)).toList(); + favouriteUserList = replacementList + ?.where((element) => element.isFavorite ?? false) + .toList(); + nonFavouriteUserList = replacementList + ?.where((element) => !(element.isFavorite ?? false)) + .toList(); Utils.hideLoading(context); setState(() {}); } catch (e) { @@ -104,7 +114,8 @@ class _SearchEmployeeBottomSheetState extends State { IconButton( constraints: const BoxConstraints(), onPressed: () async { - await SystemChannels.textInput.invokeMethod('TextInput.hide'); + await SystemChannels.textInput + .invokeMethod('TextInput.hide'); fetchUserByInput(); }, icon: Icon(Icons.search)) @@ -123,7 +134,8 @@ class _SearchEmployeeBottomSheetState extends State { ListView.separated( physics: const NeverScrollableScrollPhysics(), shrinkWrap: true, - itemBuilder: (cxt, index) => employeeItemView(favouriteUserList![index]), + itemBuilder: (cxt, index) => + employeeItemView(favouriteUserList![index]), separatorBuilder: (cxt, index) => Container( height: 1, color: MyColors.borderE3Color, @@ -137,7 +149,8 @@ class _SearchEmployeeBottomSheetState extends State { ListView.separated( physics: const NeverScrollableScrollPhysics(), shrinkWrap: true, - itemBuilder: (cxt, index) => employeeItemView(nonFavouriteUserList![index]), + itemBuilder: (cxt, index) => employeeItemView( + nonFavouriteUserList![index]), separatorBuilder: (cxt, index) => Container( height: 1, color: MyColors.borderE3Color, @@ -148,7 +161,10 @@ class _SearchEmployeeBottomSheetState extends State { ).expanded ], ).paddingOnly(left: 21, right: 21, bottom: 0, top: 21).expanded, - Container(width: double.infinity, height: 1, color: MyColors.lightGreyEFColor), + Container( + width: double.infinity, + height: 1, + color: MyColors.lightGreyEFColor), DefaultButton( LocaleKeys.cancel.tr(), () { @@ -185,7 +201,11 @@ class _SearchEmployeeBottomSheetState extends State { Expanded( child: (replacement.employeeDisplayName ?? "").toText12(), ), - Icon(Icons.star, size: 16, color: replacement.isFavorite! ? MyColors.yellowFavColor : MyColors.borderCEColor), + Icon(Icons.star, + size: 16, + color: replacement.isFavorite! + ? MyColors.yellowFavColor + : MyColors.borderCEColor), ], ), ), @@ -208,7 +228,9 @@ class _SearchEmployeeBottomSheetState extends State { width: double.infinity, height: double.infinity, decoration: BoxDecoration( - color: value == groupValue ? MyColors.grey3AColor : Colors.transparent, + color: value == groupValue + ? MyColors.grey3AColor + : Colors.transparent, borderRadius: BorderRadius.all(const Radius.circular(100)), ), ), diff --git a/lib/widgets/shimmer/dashboard_shimmer_widget.dart b/lib/widgets/shimmer/dashboard_shimmer_widget.dart index cd5d88f..369b808 100644 --- a/lib/widgets/shimmer/dashboard_shimmer_widget.dart +++ b/lib/widgets/shimmer/dashboard_shimmer_widget.dart @@ -187,3 +187,70 @@ class ServicesMenuShimmer extends StatelessWidget { ); } } + +class ChatHomeShimmer extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16.0), + child: Column( + mainAxisSize: MainAxisSize.max, + children: [ + Expanded( + child: Shimmer.fromColors( + baseColor: Colors.white, + highlightColor: Colors.grey.shade100, + child: ListView.builder( + itemBuilder: (_, __) => Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 48.0, + height: 48.0, + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.all(Radius.circular(40))), + ), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 8.0), + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: double.infinity, + height: 8.0, + color: Colors.white, + ), + const Padding( + padding: EdgeInsets.symmetric(vertical: 2.0), + ), + Container( + width: double.infinity, + height: 8.0, + color: Colors.white, + ), + const Padding( + padding: EdgeInsets.symmetric(vertical: 2.0), + ), + Container( + width: 40.0, + height: 8.0, + color: Colors.white, + ), + ], + ), + ) + ], + ), + ), + itemCount: 6, + ), + ), + ), + ], + )); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 8cc52df..8fd19ab 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -78,6 +78,12 @@ dependencies: auto_size_text: ^3.0.0 pull_to_refresh: ^2.0.0 + #Chat + signalr_netcore: ^1.3.3 + logging: ^1.0.1 + + + dev_dependencies: flutter_test: sdk: flutter @@ -110,6 +116,7 @@ flutter: - assets/icons/ - assets/images/ - assets/images/login/ + - assets/icons/chat/ - assets/images/logos/ - assets/images/drawer/ - assets/icons/nfc/ic_nfc.png