diff --git a/assets/local/localization_en.json b/assets/local/localization_en.json index 80715cb..09d45e8 100644 --- a/assets/local/localization_en.json +++ b/assets/local/localization_en.json @@ -148,6 +148,9 @@ "pm.save.btn":"Save Payment Method", "pm.delete.confirm":"Delete this Payment Method?", + "message.view.detail":"View Deatil", + "message.hint.input":"Type your message...", + "btn.save": "Save", "btn.approve":"Approve", "btn.delete":"Delete", diff --git a/assets/local/localization_mu.json b/assets/local/localization_mu.json index 4a10b67..b2b8251 100644 --- a/assets/local/localization_mu.json +++ b/assets/local/localization_mu.json @@ -150,6 +150,9 @@ "pm.save.btn":"သိမ်းဆည်းရန်", "pm.delete.confirm":"ငွေပေးချေစနစ်ကို ဖျက်မလား?", + "message.view.detail":"အသေးစိတ် ကြည့်ရန်", + "message.hint.input":"စာကို ဒီမှာ ရိုက်ထည့်ပါ...", + "btn.save":"သိမ်းဆည်းရန်", "btn.approve":"အတည်ပြုရန်", "btn.delete":"ဖျက်ရန်", diff --git a/lib/app.dart b/lib/app.dart index 4df76fa..a22e425 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -1,5 +1,3 @@ -import 'dart:io'; - import 'package:fcs/fcs/common/localization/app_translations_delegate.dart'; import 'package:fcs/fcs/common/localization/transalation.dart'; import 'package:fcs/fcs/common/pages/contact/model/contact_model.dart'; @@ -14,7 +12,6 @@ import 'package:fcs/fcs/common/pages/package/model/shipment_model.dart'; import 'package:fcs/fcs/common/pages/payment_methods/model/payment_method_model.dart'; import 'package:fcs/fcs/common/pages/staff/model/staff_model.dart'; import 'package:fcs/fcs/common/pages/term/model/term_model.dart'; -import 'package:fcs/fcs/common/services/services.dart'; import 'package:fcs/model/buyer_model.dart'; import 'package:fcs/model/delivery_model.dart'; import 'package:fcs/model/discount_model.dart'; @@ -25,7 +22,6 @@ import 'package:fcs/model/reg_model.dart'; import 'package:fcs/model/report_model.dart'; import 'package:fcs/model/storage_model.dart'; import 'package:fcs/model/test_model.dart'; -import 'package:fcs/model_fcs/message_model.dart'; import 'package:fcs/pages/email_page.dart'; import 'package:fcs/pages/login_page.dart'; import 'package:flutter/cupertino.dart'; @@ -34,6 +30,7 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:provider/provider.dart'; +import 'fcs/common/pages/chat/model/message_model.dart'; import 'fcs/common/pages/home_page.dart'; import 'fcs/common/pages/splash_page.dart'; import 'fcs/common/pages/welcome_page.dart'; @@ -41,7 +38,6 @@ import 'model/announcement_model.dart'; import 'model/chart_model.dart'; import 'model/device_model.dart'; import 'model/do_model.dart'; -import 'model/employee_model.dart'; import 'model/invoice_model.dart'; import 'model/log_model.dart'; import 'model/main_model.dart'; @@ -110,6 +106,7 @@ class _AppState extends State { ..addModel(staffModel) ..addModel(shipmentModel) ..addModel(packageModel) + ..addModel(messageModel) ..addModel(marketModel); mainModel2.init(); @@ -138,69 +135,10 @@ class _AppState extends State { ..addModel(pickUpModel) ..addModel(shipmentRateModel) ..addModel(boxModel) - ..addModel(messageModel) ..addModel(shipmentRateModel) ..addModel(invoiceModel) ..addModel(discountModel); this.mainModel.init(); - - _initLocalNotifications(); - Services.instance.messagingService.init((message) { - print("Message from FCM:$message"); - _showNotification(message); - }); - } - - _initLocalNotifications() { - var initializationSettingsAndroid = - new AndroidInitializationSettings('@mipmap/ic_launcher'); - var initializationSettingsIOS = new IOSInitializationSettings(); - var initializationSettings = new InitializationSettings( - initializationSettingsAndroid, initializationSettingsIOS); - _flutterLocalNotificationsPlugin.initialize(initializationSettings); - } - - static Future _showNotification(Map message) async { - var pushTitle; - var pushText; - var action; - - if (Platform.isAndroid) { - var nodeData = message['notification']; - pushTitle = nodeData['title']; - pushText = nodeData['body']; - action = nodeData['action']; - } else { - pushTitle = message['title']; - pushText = message['body']; - action = message['action']; - } - print("AppPushs params pushTitle : $pushTitle"); - print("AppPushs params pushText : $pushText"); - print("AppPushs params pushAction : $action"); - - // @formatter:off - var platformChannelSpecificsAndroid = new AndroidNotificationDetails( - 'your channel id', 'your channel name', 'your channel description', - playSound: true, - enableVibration: true, - importance: Importance.Max, - priority: Priority.High); - // @formatter:on - var platformChannelSpecificsIos = - new IOSNotificationDetails(presentSound: true); - var platformChannelSpecifics = new NotificationDetails( - platformChannelSpecificsAndroid, platformChannelSpecificsIos); - - new Future.delayed(Duration.zero, () { - _flutterLocalNotificationsPlugin.show( - 0, - pushTitle, - pushText, - platformChannelSpecifics, - payload: 'No_Sound', - ); - }); } void onLocaleChange(Locale locale) { diff --git a/lib/fcs/common/data/providers/auth_fb.dart b/lib/fcs/common/data/providers/auth_fb.dart index 48a8df2..f2635fc 100644 --- a/lib/fcs/common/data/providers/auth_fb.dart +++ b/lib/fcs/common/data/providers/auth_fb.dart @@ -125,24 +125,24 @@ class AuthFb { log.info("Claims:${idToken.claims}"); - User user = User(); - user.status = idToken.claims["st"]; - user.phoneNumber = firebaseUser.phoneNumber; + String cid = idToken.claims["cid"]; + User user; + if (cid != null && cid != "") { + user = await getUserFromFirestore(cid); + } + if (user == null) { + user = User(); + user.id = cid; + user.phoneNumber = firebaseUser.phoneNumber; + user.status = idToken.claims["st"]; + } // add privileges String privileges = idToken.claims["pr"]; if (privileges != null && privileges != "") { user.privileges = privileges.split(":").toList(); } - String cid = idToken.claims["cid"]; - if (cid != null && cid != "") { - User _user = await getUserFromFirestore(cid); - if (_user != null) { - user.id = cid; - user.fcsID = _user.fcsID; - user.name = _user.name; - } - } + return user; } diff --git a/lib/fcs/common/data/providers/common_data_provider.dart b/lib/fcs/common/data/providers/common_data_provider.dart index cb6c318..670ef50 100644 --- a/lib/fcs/common/data/providers/common_data_provider.dart +++ b/lib/fcs/common/data/providers/common_data_provider.dart @@ -1,4 +1,5 @@ import 'package:fcs/fcs/common/domain/entities/payment_method.dart'; +import 'package:fcs/fcs/common/domain/vo/message.dart'; import 'package:fcs/fcs/common/helpers/api_helper.dart'; import 'package:fcs/fcs/common/helpers/firebase_helper.dart'; import 'package:logging/logging.dart'; @@ -20,4 +21,15 @@ class CommonDataProvider { return await requestAPI("/payment_methods", "DELETE", payload: {"id": id}, token: await getToken()); } + + Future sendMessage(Message message) async { + return await requestAPI("/messages", "POST", + payload: message.toMap(), token: await getToken()); + } + + Future seenMessage(String ownerID, bool seenByOwner) async { + return await requestAPI("/messages/seen", "POST", + payload: {"owner_id": ownerID, "seen_by_owner": seenByOwner}, + token: await getToken()); + } } diff --git a/lib/fcs/common/data/providers/messaging_fcm.dart b/lib/fcs/common/data/providers/messaging_fcm.dart index 0bbe054..76b1e46 100644 --- a/lib/fcs/common/data/providers/messaging_fcm.dart +++ b/lib/fcs/common/data/providers/messaging_fcm.dart @@ -24,7 +24,8 @@ class MessagingFCM { FirebaseMessaging _firebaseMessaging; - MessagingFCM(OnNotify onMessage, {OnNotify onLaunch, OnNotify onResume}) { + MessagingFCM(OnNotify onMessage, + {OnNotify onLaunch, OnNotify onResume, OnSetupComplete onSetupComplete}) { _firebaseMessaging = FirebaseMessaging(); _firebaseMessaging.configure( onMessage: (Map message) async { @@ -48,6 +49,7 @@ class MessagingFCM { log.info("Settings registered: $settings"); }); _firebaseMessaging.getToken().then((String token) { + if (onSetupComplete != null) onSetupComplete(token); log.info("Messaging Token:$token"); }); } diff --git a/lib/fcs/common/data/providers/user_data_provider.dart b/lib/fcs/common/data/providers/user_data_provider.dart index 2e5d02d..78261bc 100644 --- a/lib/fcs/common/data/providers/user_data_provider.dart +++ b/lib/fcs/common/data/providers/user_data_provider.dart @@ -27,6 +27,16 @@ class UserDataProvider { payload: {"id": userID}, token: await getToken()); } + Future uploadMsgToken(String token) async { + return await requestAPI("/messages/token", "POST", + payload: {"token": token}, token: await getToken()); + } + + Future removeMsgToken(String token) async { + return await requestAPI("/messages/token", "DELETE", + payload: {"token": token}, token: await getToken()); + } + Future findUser(String phoneNumber) async { QuerySnapshot querySnap = await Firestore.instance .collection(user_collection) diff --git a/lib/fcs/common/domain/constants.dart b/lib/fcs/common/domain/constants.dart index defa7b7..2c66e1f 100644 --- a/lib/fcs/common/domain/constants.dart +++ b/lib/fcs/common/domain/constants.dart @@ -5,6 +5,7 @@ const setting_doc_id = "setting"; const privilege_collection = "privileges"; const markets_collection = "markets"; const packages_collection = "packages"; +const messages_collection = "messages"; const user_requested_status = "requested"; const user_invited_status = "invited"; @@ -14,6 +15,10 @@ const pkg_files_path = "/packages"; // Link page const page_payment_methods = "payment_methods"; const page_buying_instructions = "buying_instructions"; + +// Message type +const message_type_package = "t_p"; + ////////////////////////////// const ok_doc_id = "ok"; diff --git a/lib/fcs/common/domain/entities/user.dart b/lib/fcs/common/domain/entities/user.dart index 141f376..1fd8bac 100644 --- a/lib/fcs/common/domain/entities/user.dart +++ b/lib/fcs/common/domain/entities/user.dart @@ -1,4 +1,10 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:fcs/fcs/common/helpers/const.dart'; +import 'package:fcs/fcs/common/pages/package/package_info.dart'; +import 'package:intl/intl.dart'; + +DateFormat dayFormat = DateFormat("MMM dd yyyy"); +DateFormat timeFormat = DateFormat("HH:mm"); class User { String id; @@ -6,6 +12,37 @@ class User { String phoneNumber; String status; String fcsID; + DateTime lastMessageTime; + String lastMessage; + int userUnseenCount; + int fcsUnseenCount; + + String get initial => name != null && name != "" ? name.substring(0, 1) : "?"; + + String get getLastMessage { + var msg = lastMessage ?? "Say hi to $name"; + if (msg.length > 30) return msg.substring(0, 30) + " ... "; + return msg; + } + + String get getLastMessageTime { + if (lastMessageTime == null) return ""; + DateTime today = DateTime.now(); + if (lastMessageTime.year == today.year && + lastMessageTime.month == today.month && + lastMessageTime.day == today.day) { + return timeFormat.format(lastMessageTime); + } else { + return dateFormat.format(lastMessageTime); + } + } + + String get getUserUnseenCount => userUnseenCount != null + ? userUnseenCount > 100 ? "99+" : userUnseenCount.toString() + : "0"; + String get getFcsUnseenCount => fcsUnseenCount != null + ? fcsUnseenCount > 100 ? "99+" : fcsUnseenCount.toString() + : "0"; List privileges = []; @@ -16,14 +53,17 @@ class User { bool get invited => status != null && status == userStatusInvited; bool get requested => status != null && status == userStatusRequested; String get share => "Your phone number:$phoneNumber"; - User({ - this.id, - this.name, - this.phoneNumber, - this.fcsID, - this.status, - this.privileges, - }); + User( + {this.id, + this.name, + this.phoneNumber, + this.fcsID, + this.status, + this.privileges, + this.lastMessage, + this.lastMessageTime, + this.userUnseenCount, + this.fcsUnseenCount}); factory User.fromJson(Map json) { return User( @@ -32,6 +72,7 @@ class User { fcsID: json['fcs_id'], phoneNumber: json['phone_number'], status: json['status'], + lastMessage: json['last_message'], ); } @@ -49,6 +90,8 @@ class User { } factory User.fromMap(Map map, String docID) { + var _date = (map['message_time'] as Timestamp); + List _privileges = map['privileges'] == null ? [] : map['privileges'].cast(); @@ -58,7 +101,11 @@ class User { phoneNumber: map['phone_number'], status: map['status'], fcsID: map['fcs_id'], - privileges: _privileges); + privileges: _privileges, + lastMessage: map['last_message'], + userUnseenCount: map['user_unseen_count'], + fcsUnseenCount: map['fcs_unseen_count'], + lastMessageTime: _date == null ? null : _date.toDate()); } bool isCustomer() { diff --git a/lib/fcs/common/domain/vo/message.dart b/lib/fcs/common/domain/vo/message.dart new file mode 100644 index 0000000..fc86b58 --- /dev/null +++ b/lib/fcs/common/domain/vo/message.dart @@ -0,0 +1,58 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; + +class Message { + String id; + String message; + DateTime date; + String receiverID; + String receiverName; + String senderID; + String senderName; + String messageType; + String messageID; + + Message( + {this.id, + this.message, + this.date, + this.receiverID, + this.receiverName, + this.senderID, + this.senderName, + this.messageType, + this.messageID}); + bool fromToday() { + var now = DateTime.now(); + return date.day == now.day && + date.month == now.month && + date.year == now.year; + } + + Map toMap() { + return { + 'message': message, + "receiver_id": receiverID, + }; + } + + bool sameDay(Message another) { + return date.year == another.date.year && + date.month == another.date.month && + date.day == another.date.day; + } + + factory Message.fromMap(Map map, String id) { + var date = (map['date'] as Timestamp); + return Message( + id: id, + message: map['message'], + senderID: map['sender_id'], + senderName: map['sender_name'], + receiverID: map['receiver_id'], + receiverName: map['receiver_name'], + messageType: map['msg_type'], + messageID: map['msg_id'], + date: date != null ? date.toDate() : null, + ); + } +} diff --git a/lib/fcs/common/helpers/pagination_model.dart b/lib/fcs/common/helpers/pagination_model.dart new file mode 100644 index 0000000..ce0d192 --- /dev/null +++ b/lib/fcs/common/helpers/pagination_model.dart @@ -0,0 +1,152 @@ +import 'dart:async'; + +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:logging/logging.dart'; + +/* + * PaginationModel load data in page + * and listen to document change based on 'update_time' and 'delete_time' fields + * of the document + */ +class PaginationModel { + final log = Logger('PaginationModel'); + + List ids = []; + DocumentSnapshot prev; + int rowPerLoad = 10; + bool ended = false; + + StreamSubscription listener; + CollectionReference listeningCol; + Query pageQuery; + + PaginationModel(CollectionReference listeningCol, Query pageQuery, + {this.rowPerLoad = 10}) { + this.listeningCol = listeningCol; + this.pageQuery = pageQuery; + initData(); + } + + void initData() async { + _clearState(); + _initListener(); + load(); + } + + void _clearState() { + prev = null; + ids = []; + ended = false; + if (listener != null) listener.cancel(); + listener = null; + if (controller != null) controller.close(); + } + + StreamController controller; + Stream listen() { + if (controller != null) { + controller.close(); + } + controller = StreamController(onCancel: _clearState); + return controller.stream; + } + + void close() { + _clearState(); + } + + final String updateTimeField = 'update_time'; + final String deleteTimeField = 'delete_time'; + void _initListener() { + Query _query = + listeningCol.orderBy(updateTimeField, descending: true).limit(1); + _query.getDocuments(source: Source.server).then((QuerySnapshot snapshot) { + int count = snapshot.documents.length; + int updateTime = 0; + if (count == 1) { + updateTime = snapshot.documents[0].data[updateTimeField]; + } + + Query _queryListener = listeningCol + .where(updateTimeField, isGreaterThan: updateTime) + .orderBy(updateTimeField, descending: true); + + listener = + _queryListener.snapshots(includeMetadataChanges: true).listen((qs) { + qs.documentChanges.forEach((c) { + switch (c.type) { + case DocumentChangeType.added: + log.info("added!! $c"); + _update(c.document.documentID, c.document.data); + break; + case DocumentChangeType.modified: + log.info("modified!! $c"); + _update(c.document.documentID, c.document.data); + break; + default: + } + }); + }); + }); + } + + void _update(String id, Map data) { + if (ids.contains(id)) { + var deleted = data[deleteTimeField]; + if (deleted > 0) { + ids.remove(id); + controller.add(Result( + id: id, + data: data, + documentChangeType: DocumentChangeType.removed)); + } else { + controller.add(Result( + id: id, + data: data, + documentChangeType: DocumentChangeType.modified)); + } + } else { + ids.add(id); + controller.add(Result( + id: id, data: data, documentChangeType: DocumentChangeType.added)); + } + } + + Future load() async { + Query _query = + prev != null ? pageQuery.startAfterDocument(prev) : pageQuery; + try { + await _query + .where(deleteTimeField, isEqualTo: 0) + .limit(rowPerLoad) + .getDocuments(source: Source.server) + .then((QuerySnapshot snapshot) { + int count = snapshot.documents.length; + ended = count < rowPerLoad; + prev = count > 0 ? snapshot.documents[count - 1] : prev; + snapshot.documents.forEach((e) { + if (!ids.contains(e.documentID)) log.shout("load!! $e"); + ids.add(e.documentID); + controller.add(Result( + id: e.documentID, + data: e.data, + documentChangeType: DocumentChangeType.added)); + }); + if (ended) { + controller.add(Result(isEnded: true)); + } + }); + } catch (e) { + log.warning("Error!! $e"); + } + return ended; + } +} + +class Result { + String id; + Map data; + DocumentChangeType documentChangeType; + bool isEnded; + Result({this.id, this.data, this.documentChangeType, this.isEnded = false}); +} diff --git a/lib/fcs/common/pages/chat/bubble.dart b/lib/fcs/common/pages/chat/bubble.dart new file mode 100644 index 0000000..c6f8a05 --- /dev/null +++ b/lib/fcs/common/pages/chat/bubble.dart @@ -0,0 +1,135 @@ +import 'package:fcs/fcs/common/helpers/theme.dart'; +import 'package:fcs/fcs/common/pages/package/package_info.dart'; +import 'package:fcs/fcs/common/pages/util.dart'; +import 'package:fcs/fcs/common/pages/widgets/fcs_id_icon.dart'; +import 'package:fcs/fcs/common/pages/widgets/local_text.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; + +DateFormat dayFormat = DateFormat("MMM dd yyyy"); +DateFormat timeFormat = DateFormat("HH:mm"); + +typedef CallbackOnViewDetail(); + +class Bubble extends StatelessWidget { + Bubble( + {this.message, + this.date, + this.delivered, + this.isMine, + this.sender, + this.isSystem, + this.isCustomer, + this.showDate, + this.callbackOnViewDetail}); + + final CallbackOnViewDetail callbackOnViewDetail; + final DateTime date; + final String message, sender; + final bool delivered, isMine, isSystem, isCustomer, showDate; + + @override + Widget build(BuildContext context) { + final bg = isMine ? Colors.greenAccent.shade100 : Colors.white; + final align = isMine ? CrossAxisAlignment.end : CrossAxisAlignment.start; + final icon = delivered ? Icons.done_all : Icons.done; + final radius = isMine + ? BorderRadius.only( + topLeft: Radius.circular(25.0), + bottomLeft: Radius.circular(25.0), + bottomRight: Radius.circular(30.0), + ) + : BorderRadius.only( + topRight: Radius.circular(25.0), + bottomLeft: Radius.circular(30.0), + bottomRight: Radius.circular(25.0), + ); + return Column( + crossAxisAlignment: align, + children: [ + showDate ? Center(child: Text(dateFormat.format(date))) : Container(), + Container( + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context).size.width * 0.8, minWidth: 10), + margin: const EdgeInsets.all(3.0), + padding: const EdgeInsets.all(8.0), + decoration: BoxDecoration( + boxShadow: [ + BoxShadow( + blurRadius: .5, + spreadRadius: 1.0, + color: Colors.black.withOpacity(.32)) + ], + color: bg, + borderRadius: radius, + ), + child: Column( + crossAxisAlignment: align, + children: (isMine && isCustomer) || (!isMine && !isCustomer) + ? [getMsg(context, icon)] + : isSystem + ? [ + FcsIDIcon(), + getMsg(context, icon), + FlatButton( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10.0), + ), + color: Colors.blue[50], + onPressed: () => _viewDetail(), + child: Text( + getLocalString(context, "message.view.detail"), + style: TextStyle( + color: primaryColor, + fontWeight: FontWeight.bold))) + ] + : [ + Text(isCustomer ? "FCS Team" : sender, + style: TextStyle( + color: Colors.black38, + fontSize: 10.0, + )), + getMsg(context, icon), + ], + ), + ) + ], + ); + } + + getMsg(BuildContext context, IconData iconData) { + return Stack( + children: [ + Padding( + padding: EdgeInsets.only(right: 48.0), + child: Text(message, + style: hasUnicode(message) + ? newLabelStyleMM(color: primaryColor) + : newLabelStyle(color: primaryColor))), + Positioned( + bottom: 0.0, + right: 0.0, + child: Row( + children: [ + Text(timeFormat.format(date), + style: TextStyle( + color: Colors.black38, + fontSize: 10.0, + )), + SizedBox(width: 3.0), + Icon( + iconData, + size: 12.0, + color: Colors.black38, + ) + ], + ), + ), + ], + ); + } + + _viewDetail() { + if (callbackOnViewDetail != null) callbackOnViewDetail(); + } +} diff --git a/lib/fcs/common/pages/chat/chat_page.dart b/lib/fcs/common/pages/chat/chat_page.dart new file mode 100644 index 0000000..9393b42 --- /dev/null +++ b/lib/fcs/common/pages/chat/chat_page.dart @@ -0,0 +1,576 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:firebase_storage/firebase_storage.dart'; +import 'package:flutter/material.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:intl/intl.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import 'full_photo.dart'; +import 'loading.dart'; + +final themeColor = Color(0xfff5a623); +final primaryColor = Color(0xff203152); +final greyColor = Color(0xffaeaeae); +final greyColor2 = Color(0xffE8E8E8); + +class Chat extends StatelessWidget { + final String peerId; + final String peerAvatar; + + Chat({Key key, @required this.peerId, @required this.peerAvatar}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text( + 'CHAT', + style: TextStyle(color: primaryColor, fontWeight: FontWeight.bold), + ), + centerTitle: true, + ), + body: ChatScreen( + peerId: peerId, + peerAvatar: peerAvatar, + ), + ); + } +} + +class ChatScreen extends StatefulWidget { + final String peerId; + final String peerAvatar; + + ChatScreen({Key key, @required this.peerId, @required this.peerAvatar}) + : super(key: key); + + @override + State createState() => + ChatScreenState(peerId: peerId, peerAvatar: peerAvatar); +} + +class ChatScreenState extends State { + ChatScreenState({Key key, @required this.peerId, @required this.peerAvatar}); + GlobalKey key = GlobalKey(); + + String peerId; + String peerAvatar; + String id; + + List listMessage = new List.from([]); + int _limit = 20; + final int _limitIncrement = 20; + String groupChatId; + SharedPreferences prefs; + + File imageFile; + bool isLoading; + String imageUrl; + + final TextEditingController textEditingController = TextEditingController(); + final ScrollController listScrollController = ScrollController(); + + _scrollListener() { + if (listScrollController.offset >= + listScrollController.position.maxScrollExtent && + !listScrollController.position.outOfRange) { + print("reach the bottom"); + setState(() { + print("reach the bottom"); + _limit += _limitIncrement; + }); + } + if (listScrollController.offset <= + listScrollController.position.minScrollExtent && + !listScrollController.position.outOfRange) { + print("reach the top"); + setState(() { + print("reach the top"); + }); + } + } + + @override + void initState() { + super.initState(); + listScrollController.addListener(_scrollListener); + + groupChatId = ''; + + isLoading = false; + imageUrl = ''; + + readLocal(); + } + + readLocal() async { + prefs = await SharedPreferences.getInstance(); + id = prefs.getString('id') ?? ''; + if (id.hashCode <= peerId.hashCode) { + groupChatId = '$id-$peerId'; + } else { + groupChatId = '$peerId-$id'; + } + + Firestore.instance + .collection('users') + .document(id) + .updateData({'chattingWith': peerId}); + + setState(() {}); + } + + Future getImage() async { + ImagePicker imagePicker = ImagePicker(); + PickedFile pickedFile; + + pickedFile = await imagePicker.getImage(source: ImageSource.gallery); + imageFile = File(pickedFile.path); + + if (imageFile != null) { + setState(() { + isLoading = true; + }); + uploadFile(); + } + } + + Future uploadFile() async { + String fileName = DateTime.now().millisecondsSinceEpoch.toString(); + StorageReference reference = FirebaseStorage.instance.ref().child(fileName); + StorageUploadTask uploadTask = reference.putFile(imageFile); + StorageTaskSnapshot storageTaskSnapshot = await uploadTask.onComplete; + storageTaskSnapshot.ref.getDownloadURL().then((downloadUrl) { + imageUrl = downloadUrl; + setState(() { + isLoading = false; + onSendMessage(imageUrl, 1); + }); + }, onError: (err) { + setState(() { + isLoading = false; + }); + }); + } + + void onSendMessage(String content, int type) { + // type: 0 = text, 1 = image, 2 = sticker + if (content.trim() != '') { + textEditingController.clear(); + + var documentReference = Firestore.instance + .collection('messages') + .document(groupChatId) + .collection(groupChatId) + .document(DateTime.now().millisecondsSinceEpoch.toString()); + + Firestore.instance.runTransaction((transaction) async { + transaction.set( + documentReference, + { + 'idFrom': id, + 'idTo': peerId, + 'timestamp': DateTime.now().millisecondsSinceEpoch.toString(), + 'content': content, + 'type': type + }, + ); + }); + listScrollController.animateTo(0.0, + duration: Duration(milliseconds: 300), curve: Curves.easeOut); + } + } + + Widget buildItem(int index, DocumentSnapshot document) { + if (document.data['idFrom'] == id) { + // Right (my message) + return Row( + children: [ + document.data['type'] == 0 + // Text + ? Container( + child: Text( + document.data['content'], + style: TextStyle(color: primaryColor), + ), + padding: EdgeInsets.fromLTRB(15.0, 10.0, 15.0, 10.0), + width: 200.0, + decoration: BoxDecoration( + color: greyColor2, + borderRadius: BorderRadius.circular(8.0)), + margin: EdgeInsets.only( + bottom: isLastMessageRight(index) ? 20.0 : 10.0, + right: 10.0), + ) + : document.data['type'] == 1 + // Image + ? Container( + child: FlatButton( + child: Material( + child: CachedNetworkImage( + placeholder: (context, url) => Container( + child: CircularProgressIndicator( + valueColor: + AlwaysStoppedAnimation(themeColor), + ), + width: 200.0, + height: 200.0, + padding: EdgeInsets.all(70.0), + decoration: BoxDecoration( + color: greyColor2, + borderRadius: BorderRadius.all( + Radius.circular(8.0), + ), + ), + ), + errorWidget: (context, url, error) => Material( + child: Image.asset( + 'images/img_not_available.jpeg', + width: 200.0, + height: 200.0, + fit: BoxFit.cover, + ), + borderRadius: BorderRadius.all( + Radius.circular(8.0), + ), + clipBehavior: Clip.hardEdge, + ), + imageUrl: document.data['content'], + width: 200.0, + height: 200.0, + fit: BoxFit.cover, + ), + borderRadius: BorderRadius.all(Radius.circular(8.0)), + clipBehavior: Clip.hardEdge, + ), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => FullPhoto( + url: document.data['content']))); + }, + padding: EdgeInsets.all(0), + ), + margin: EdgeInsets.only( + bottom: isLastMessageRight(index) ? 20.0 : 10.0, + right: 10.0), + ) + // Sticker + : Container( + child: Image.asset( + 'images/${document.data['content']}.gif', + width: 100.0, + height: 100.0, + fit: BoxFit.cover, + ), + margin: EdgeInsets.only( + bottom: isLastMessageRight(index) ? 20.0 : 10.0, + right: 10.0), + ), + ], + mainAxisAlignment: MainAxisAlignment.end, + ); + } else { + // Left (peer message) + return Container( + child: Column( + children: [ + Row( + children: [ + isLastMessageLeft(index) + ? Material( + child: CachedNetworkImage( + placeholder: (context, url) => Container( + child: CircularProgressIndicator( + strokeWidth: 1.0, + valueColor: + AlwaysStoppedAnimation(themeColor), + ), + width: 35.0, + height: 35.0, + padding: EdgeInsets.all(10.0), + ), + imageUrl: peerAvatar, + width: 35.0, + height: 35.0, + fit: BoxFit.cover, + ), + borderRadius: BorderRadius.all( + Radius.circular(18.0), + ), + clipBehavior: Clip.hardEdge, + ) + : Container(width: 35.0), + document.data['type'] == 0 + ? Container( + child: Text( + document.data['content'], + style: TextStyle(color: Colors.white), + ), + padding: EdgeInsets.fromLTRB(15.0, 10.0, 15.0, 10.0), + width: 200.0, + decoration: BoxDecoration( + color: primaryColor, + borderRadius: BorderRadius.circular(8.0)), + margin: EdgeInsets.only(left: 10.0), + ) + : document.data['type'] == 1 + ? Container( + child: FlatButton( + child: Material( + child: CachedNetworkImage( + placeholder: (context, url) => Container( + child: CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation( + themeColor), + ), + width: 200.0, + height: 200.0, + padding: EdgeInsets.all(70.0), + decoration: BoxDecoration( + color: greyColor2, + borderRadius: BorderRadius.all( + Radius.circular(8.0), + ), + ), + ), + errorWidget: (context, url, error) => + Material( + child: Image.asset( + 'images/img_not_available.jpeg', + width: 200.0, + height: 200.0, + fit: BoxFit.cover, + ), + borderRadius: BorderRadius.all( + Radius.circular(8.0), + ), + clipBehavior: Clip.hardEdge, + ), + imageUrl: document.data['content'], + width: 200.0, + height: 200.0, + fit: BoxFit.cover, + ), + borderRadius: + BorderRadius.all(Radius.circular(8.0)), + clipBehavior: Clip.hardEdge, + ), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => FullPhoto( + url: document.data['content']))); + }, + padding: EdgeInsets.all(0), + ), + margin: EdgeInsets.only(left: 10.0), + ) + : Container( + child: Image.asset( + 'images/${document.data['content']}.gif', + width: 100.0, + height: 100.0, + fit: BoxFit.cover, + ), + margin: EdgeInsets.only( + bottom: isLastMessageRight(index) ? 20.0 : 10.0, + right: 10.0), + ), + ], + ), + + // Time + isLastMessageLeft(index) + ? Container( + child: Text( + DateFormat('dd MMM kk:mm').format( + DateTime.fromMillisecondsSinceEpoch( + int.parse(document.data['timestamp']))), + style: TextStyle( + color: greyColor, + fontSize: 12.0, + fontStyle: FontStyle.italic), + ), + margin: EdgeInsets.only(left: 50.0, top: 5.0, bottom: 5.0), + ) + : Container() + ], + crossAxisAlignment: CrossAxisAlignment.start, + ), + margin: EdgeInsets.only(bottom: 10.0), + ); + } + } + + bool isLastMessageLeft(int index) { + if ((index > 0 && + listMessage != null && + listMessage[index - 1].data['idFrom'] == id) || + index == 0) { + return true; + } else { + return false; + } + } + + bool isLastMessageRight(int index) { + if ((index > 0 && + listMessage != null && + listMessage[index - 1].data['idFrom'] != id) || + index == 0) { + return true; + } else { + return false; + } + } + + Future onBackPress() { + Firestore.instance + .collection('users') + .document(id) + .updateData({'chattingWith': null}); + Navigator.pop(context); + + return Future.value(false); + } + + @override + Widget build(BuildContext context) { + return WillPopScope( + child: Stack( + children: [ + Column( + children: [ + // List of messages + buildListMessage(), + + // Input content + buildInput(), + ], + ), + + // Loading + buildLoading() + ], + ), + onWillPop: onBackPress, + ); + } + + Widget buildLoading() { + return Positioned( + child: isLoading ? const Loading() : Container(), + ); + } + + Widget buildInput() { + return Container( + child: Row( + children: [ + // Button send image + Material( + child: Container( + margin: EdgeInsets.symmetric(horizontal: 1.0), + child: IconButton( + icon: Icon(Icons.image), + onPressed: getImage, + color: primaryColor, + ), + ), + color: Colors.white, + ), + Material( + child: Container( + margin: EdgeInsets.symmetric(horizontal: 1.0), + child: IconButton( + icon: Icon(Icons.face), + onPressed: () => {}, + color: primaryColor, + ), + ), + color: Colors.white, + ), + + // Edit text + Flexible( + child: Container( + child: TextField( + onSubmitted: (value) { + onSendMessage(textEditingController.text, 0); + }, + style: TextStyle(color: primaryColor, fontSize: 15.0), + controller: textEditingController, + decoration: InputDecoration.collapsed( + hintText: 'Type your message...', + hintStyle: TextStyle(color: greyColor), + ), + ), + ), + ), + + // Button send message + Material( + child: Container( + margin: EdgeInsets.symmetric(horizontal: 8.0), + child: IconButton( + icon: Icon(Icons.send), + onPressed: () => onSendMessage(textEditingController.text, 0), + color: primaryColor, + ), + ), + color: Colors.white, + ), + ], + ), + width: double.infinity, + height: 50.0, + decoration: BoxDecoration( + border: Border(top: BorderSide(color: greyColor2, width: 0.5)), + color: Colors.white), + ); + } + + Widget buildListMessage() { + return Flexible( + child: groupChatId == '' + ? Center( + child: CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(themeColor))) + : StreamBuilder( + stream: Firestore.instance + .collection('messages') + .document(groupChatId) + .collection(groupChatId) + .orderBy('timestamp', descending: true) + .limit(_limit) + .snapshots(), + builder: (context, snapshot) { + if (!snapshot.hasData) { + return Center( + child: CircularProgressIndicator( + valueColor: + AlwaysStoppedAnimation(themeColor))); + } else { + listMessage.addAll(snapshot.data.documents); + return ListView.builder( + padding: EdgeInsets.all(10.0), + itemBuilder: (context, index) => + buildItem(index, snapshot.data.documents[index]), + itemCount: snapshot.data.documents.length, + reverse: true, + controller: listScrollController, + ); + } + }, + ), + ); + } +} diff --git a/lib/fcs/common/pages/chat/full_photo.dart b/lib/fcs/common/pages/chat/full_photo.dart new file mode 100644 index 0000000..294b1cb --- /dev/null +++ b/lib/fcs/common/pages/chat/full_photo.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; +import 'package:photo_view/photo_view.dart'; +import 'package:cached_network_image/cached_network_image.dart'; + +import 'chat_page.dart'; + +class FullPhoto extends StatelessWidget { + final String url; + + FullPhoto({Key key, @required this.url}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text( + 'FULL PHOTO', + style: TextStyle(color: primaryColor, fontWeight: FontWeight.bold), + ), + centerTitle: true, + ), + body: FullPhotoScreen(url: url), + ); + } +} + +class FullPhotoScreen extends StatefulWidget { + final String url; + + FullPhotoScreen({Key key, @required this.url}) : super(key: key); + + @override + State createState() => FullPhotoScreenState(url: url); +} + +class FullPhotoScreenState extends State { + final String url; + + FullPhotoScreenState({Key key, @required this.url}); + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Container( + child: PhotoView(imageProvider: CachedNetworkImageProvider(url))); + } +} diff --git a/lib/fcs/common/pages/chat/loading.dart b/lib/fcs/common/pages/chat/loading.dart new file mode 100644 index 0000000..ffbf320 --- /dev/null +++ b/lib/fcs/common/pages/chat/loading.dart @@ -0,0 +1,18 @@ +import 'package:fcs/fcs/common/helpers/theme.dart'; +import 'package:flutter/material.dart'; + +class Loading extends StatelessWidget { + const Loading(); + + @override + Widget build(BuildContext context) { + return Container( + child: Center( + child: CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(primaryColor), + ), + ), + color: Colors.white.withOpacity(0.8), + ); + } +} diff --git a/lib/fcs/common/pages/chat/message_detail.dart b/lib/fcs/common/pages/chat/message_detail.dart new file mode 100644 index 0000000..50e31ff --- /dev/null +++ b/lib/fcs/common/pages/chat/message_detail.dart @@ -0,0 +1,172 @@ +import 'package:fcs/fcs/common/domain/constants.dart'; +import 'package:fcs/fcs/common/domain/entities/package.dart'; +import 'package:fcs/fcs/common/domain/vo/message.dart'; +import 'package:fcs/fcs/common/helpers/theme.dart'; +import 'package:fcs/fcs/common/pages/chat/model/message_model.dart'; +import 'package:fcs/fcs/common/pages/package/model/package_model.dart'; +import 'package:fcs/fcs/common/pages/package/package_info.dart'; +import 'package:fcs/fcs/common/pages/util.dart'; +import 'package:fcs/fcs/common/pages/widgets/bottom_up_page_route.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; + +import 'bubble.dart'; + +class MessageDetail extends StatelessWidget { + final String receiverName; + final String receiverID; + final MessageModel messageModel; + final TextEditingController textEditingController = TextEditingController(); + final ScrollController listScrollController = ScrollController(); + + MessageDetail( + {Key key, this.messageModel, this.receiverName, this.receiverID}) + : super(key: key) { + listScrollController.addListener(() { + if (listScrollController.offset >= + listScrollController.position.maxScrollExtent && + !listScrollController.position.outOfRange) { + if (!messageModel.isEnded) messageModel.load(); + } + if (listScrollController.offset <= + listScrollController.position.minScrollExtent && + !listScrollController.position.outOfRange) {} + }); + } + + @override + Widget build(BuildContext context) { + String userID = Provider.of(context).user.id; + + return Scaffold( + appBar: AppBar( + backgroundColor: primaryColor, + elevation: .9, + title: Text( + receiverName ?? "FCS Team", + ), + actions: [], + ), + body: Padding( + padding: EdgeInsets.all(16.0), + child: Column( + children: [ + Expanded( + child: ListView.builder( + itemBuilder: (context, index) { + var msg = messageModel.messages[index]; + Message next; + bool showDate = false; + if (messageModel.messages.length > index + 1) { + next = messageModel.messages[index + 1]; + if (!msg.sameDay(next)) { + showDate = true; + } + } + if (messageModel.messages.length - 1 == index && + messageModel.isEnded) { + showDate = true; + } + return buildBubble( + msg, userID, showDate, () => _viewDetail(context, msg)); + }, + itemCount: messageModel.messages.length, + reverse: true, + controller: listScrollController, + )), + buildInput(context), + ], + ), + ), + ); + } + + Widget buildBubble(Message msg, String userID, bool showDate, + CallbackOnViewDetail callback) { + return Bubble( + message: msg.message, + date: msg.date, + delivered: true, + sender: msg.senderName, + isMine: msg.senderID == userID || msg.receiverID == receiverID, + isCustomer: receiverID == null, + showDate: showDate, + isSystem: msg.messageType != null && msg.messageType != "", + callbackOnViewDetail: callback, + ); + } + + Widget buildInput(BuildContext context) { + return Container( + padding: EdgeInsets.only(top: 3), + child: Row( + children: [ + Flexible( + child: Container( + child: TextField( + onSubmitted: (value) { + Provider.of(context, listen: false) + .sendMessage(textEditingController.text, receiverID); + textEditingController.text = ""; + }, + style: TextStyle(color: primaryColor, fontSize: 15.0), + maxLines: 10, + minLines: 1, + keyboardType: TextInputType.multiline, + controller: textEditingController, + decoration: InputDecoration( + focusedBorder: const OutlineInputBorder( + borderSide: + const BorderSide(color: primaryColor, width: 1.0), + ), + border: new OutlineInputBorder( + borderRadius: const BorderRadius.all( + const Radius.circular(10.0), + ), + ), + hintText: getLocalString(context, "message.hint.input"), + hintStyle: TextStyle( + color: Colors.grey, + ), + ), + ), + ), + ), + + // Button send message + Material( + child: Container( + margin: EdgeInsets.symmetric(horizontal: 8.0), + child: IconButton( + icon: Icon(Icons.send), + onPressed: () { + Provider.of(context, listen: false) + .sendMessage(textEditingController.text, receiverID); + textEditingController.text = ""; + }, + color: primaryColor, + ), + ), + color: Colors.white, + ), + ], + ), + width: double.infinity, + decoration: BoxDecoration( + border: Border(top: BorderSide(color: Colors.grey[700], width: 0.5)), + color: Colors.white), + ); + } + + _viewDetail(BuildContext context, Message message) async { + if (message.messageType == message_type_package && + message.messageID != null && + message.messageID != "") { + PackageModel packageModel = + Provider.of(context, listen: false); + Package p = await packageModel.getPackage(message.messageID); + Navigator.push(context, BottomUpPageRoute(PackageInfo(package: p))); + } + } +} diff --git a/lib/fcs/common/pages/chat/model/message_model.dart b/lib/fcs/common/pages/chat/model/message_model.dart new file mode 100644 index 0000000..ba7c268 --- /dev/null +++ b/lib/fcs/common/pages/chat/model/message_model.dart @@ -0,0 +1,96 @@ +import 'dart:async'; + +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:fcs/fcs/common/domain/constants.dart'; +import 'package:fcs/fcs/common/domain/vo/message.dart'; +import 'package:fcs/fcs/common/pages/model/base_model.dart'; +import 'package:fcs/fcs/common/services/services.dart'; +import 'package:logging/logging.dart'; + +class MessageModel extends BaseModel { + final log = Logger('MessageModel'); + + List messages; + + void initUser(user) { + super.initUser(user); + } + + @override + logout() async { + if (listener != null) await listener.cancel(); + messages = []; + } + + Query query; + DocumentSnapshot prevSnap; + bool isEnded; + bool isLoading; + String userID; + StreamSubscription listener; + + static const int rowPerLoad = 20; + void initQuery(String userID) { + this.messages = []; + this.userID = userID; + this.prevSnap = null; + query = Firestore.instance + .collection("$user_collection/$userID/$messages_collection") + .orderBy('date', descending: true); + load(); + } + + Future load() async { + Query _query = + prevSnap != null ? query.startAfterDocument(prevSnap) : query; + QuerySnapshot snapshot = + await _query.limit(rowPerLoad).getDocuments(source: Source.server); + + int count = snapshot.documents.length; + isEnded = count < rowPerLoad; + prevSnap = count > 0 ? snapshot.documents[count - 1] : prevSnap; + + snapshot.documents.forEach((e) { + messages.add(Message.fromMap(e.data, e.documentID)); + if (messages.length == 1) { + _initListener(e); + } + }); + notifyListeners(); + } + + void _initListener(DocumentSnapshot snap) { + if (listener != null) listener.cancel(); + + listener = Firestore.instance + .collection("$user_collection/$userID/$messages_collection") + .endBeforeDocument(snap) + .orderBy('date', descending: true) + .snapshots(includeMetadataChanges: true) + .listen((qs) { + qs.documentChanges.forEach((c) { + switch (c.type) { + case DocumentChangeType.added: + log.info("added!! $c"); + messages.insert( + 0, Message.fromMap(c.document.data, c.document.documentID)); + notifyListeners(); + break; + case DocumentChangeType.modified: + log.info("modified!! $c"); + break; + default: + } + }); + }); + } + + Future sendMessage(String msg, String receiverID) { + Message message = Message(message: msg, receiverID: receiverID); + return Services.instance.commonService.sendMessage(message); + } + + Future seenMessages(String ownerID, bool seenByOwner) { + return Services.instance.commonService.seenMessage(ownerID, seenByOwner); + } +} diff --git a/lib/fcs/common/pages/chat/notification_list.dart b/lib/fcs/common/pages/chat/notification_list.dart new file mode 100644 index 0000000..ba2d7dc --- /dev/null +++ b/lib/fcs/common/pages/chat/notification_list.dart @@ -0,0 +1,129 @@ +import 'package:fcs/fcs/common/domain/vo/message.dart'; +import 'package:fcs/fcs/common/helpers/theme.dart'; +import 'package:fcs/fcs/common/pages/chat/message_detail.dart'; +import 'package:fcs/fcs/common/pages/chat/model/message_model.dart'; +import 'package:fcs/fcs/common/pages/widgets/bottom_up_page_route.dart'; +import 'package:fcs/fcs/common/pages/widgets/local_text.dart'; +import 'package:fcs/fcs/common/pages/widgets/progress.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; + +class NotificationList extends StatefulWidget { + @override + _NotificationListState createState() => _NotificationListState(); +} + +class _NotificationListState extends State { + var timeFormatter = new DateFormat('KK:mm a'); + var dateFormatter = new DateFormat('dd MMM yyyy'); + final double dotSize = 25.0; + int _selectedIndex = 0; + bool _isLoading = false; + bool _isClicked = false; + + @override + Widget build(BuildContext context) { + MessageModel messageModel = Provider.of(context); + + return LocalProgress( + inAsyncCall: _isLoading, + child: Scaffold( + appBar: AppBar( + centerTitle: true, + leading: new IconButton( + icon: new Icon( + Icons.close, + ), + onPressed: () => Navigator.of(context).pop(), + ), + backgroundColor: primaryColor, + title: LocalText( + context, + 'message.title', + fontSize: 20, + color: Colors.white, + ), + ), + body: new ListView.separated( + separatorBuilder: (context, index) => Divider( + color: Colors.black, + ), + scrollDirection: Axis.vertical, + padding: EdgeInsets.only(top: 5), + shrinkWrap: true, + itemCount: messageModel.messages.length, + itemBuilder: (BuildContext context, int index) { + Message msg = messageModel.messages[index]; + return Stack( + children: [ + InkWell( + onTap: () => _display(msg), + child: Row( + children: [ + Expanded( + child: new Padding( + padding: const EdgeInsets.symmetric(vertical: 10.0), + child: new Row( + children: [ + new Padding( + padding: new EdgeInsets.symmetric( + horizontal: 22.0 - dotSize / 2), + child: Icon( + Icons.account_circle, + color: primaryColor, + size: 60, + ), + ), + new Expanded( + child: new Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + new Text( + msg.receiverName, + style: new TextStyle(fontSize: 15.0), + ), + ], + ), + ), + ], + ), + ), + ), + Padding( + padding: const EdgeInsets.only(right: 18.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + timeFormatter.format(msg.date), + style: TextStyle(color: Colors.grey), + ), + ), + msg.fromToday() + ? Container() + : Text( + dateFormatter.format(msg.date), + style: TextStyle(color: Colors.grey), + ), + ], + ), + ) + ], + ), + ), + ], + ); + }), + ), + ); + } + + _display(Message msg) { + Navigator.push(context, + BottomUpPageRoute(MessageDetail(receiverName: msg.receiverName))); + } +} diff --git a/lib/fcs/common/pages/contact/contact_editor.dart b/lib/fcs/common/pages/contact/contact_editor.dart index 92248e0..f2965cd 100644 --- a/lib/fcs/common/pages/contact/contact_editor.dart +++ b/lib/fcs/common/pages/contact/contact_editor.dart @@ -56,10 +56,12 @@ class _ContactEditorState extends State { final usaAddreesBox = InputText( labelTextKey: 'contact.usa.address', iconData: CupertinoIcons.location, + maxLines: 3, controller: _usaAddress); final mmAddressBox = InputText( labelTextKey: 'contact.mm.address', iconData: CupertinoIcons.location, + maxLines: 3, controller: _mmAddress); final emailBox = InputText( labelTextKey: 'contact.email', diff --git a/lib/fcs/common/pages/customer/customer_list.dart b/lib/fcs/common/pages/customer/customer_list.dart index 7dc1f8a..b0164a5 100644 --- a/lib/fcs/common/pages/customer/customer_list.dart +++ b/lib/fcs/common/pages/customer/customer_list.dart @@ -1,13 +1,15 @@ import 'package:fcs/fcs/common/domain/constants.dart'; import 'package:fcs/fcs/common/domain/entities/user.dart'; import 'package:fcs/fcs/common/helpers/theme.dart'; +import 'package:fcs/fcs/common/pages/chat/message_detail.dart'; +import 'package:fcs/fcs/common/pages/chat/model/message_model.dart'; import 'package:fcs/fcs/common/pages/customer/customer_editor.dart'; import 'package:fcs/fcs/common/pages/customer/model/customer_model.dart'; import 'package:fcs/fcs/common/pages/model/main_model.dart'; import 'package:fcs/fcs/common/pages/user_search/user_serach.dart'; import 'package:fcs/fcs/common/pages/widgets/bottom_up_page_route.dart'; import 'package:fcs/fcs/common/pages/widgets/local_text.dart'; -import 'package:fcs/widget/progress.dart'; +import 'package:fcs/fcs/common/pages/widgets/progress.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_icons/flutter_icons.dart'; @@ -69,10 +71,9 @@ class _CustomerListState extends State { Expanded( child: ListView.separated( separatorBuilder: (context, index) => Divider( - color: Colors.black, + color: Colors.grey, ), scrollDirection: Axis.vertical, - padding: EdgeInsets.only(left: 15, right: 15), shrinkWrap: true, itemCount: customerModel.customers.length, itemBuilder: (BuildContext context, int index) { @@ -88,73 +89,113 @@ class _CustomerListState extends State { Widget _item(User customer) { return InkWell( - onTap: () => _select(customer), - child: Row( - children: [ - Expanded( - child: new Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: new Row( - children: [ - Icon( - Feather.user, - color: primaryColor, - size: 40, - ), - new Expanded( - child: new Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - new Text( - customer.name, - style: new TextStyle( - fontSize: 15.0, color: primaryColor), - ), - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: new Text( - customer.phoneNumber, - style: new TextStyle( - fontSize: 15.0, color: Colors.grey), + onTap: () => _gotoMsg(customer), + child: Padding( + padding: const EdgeInsets.only(left: 12.0, right: 12), + child: Row( + children: [ + Expanded( + child: new Padding( + padding: const EdgeInsets.symmetric(vertical: 2.0), + child: new Row( + children: [ + InkWell( + onTap: () => _select(customer), + child: Padding( + padding: const EdgeInsets.all(5.0), + child: Container( + padding: const EdgeInsets.only( + left: 10.0, right: 10, top: 6, bottom: 6), + decoration: BoxDecoration( + color: primaryColor, + borderRadius: + BorderRadius.all(Radius.circular(35.0))), + child: Text( + customer.initial, + style: TextStyle(fontSize: 30, color: Colors.white), ), ), - ], + ), ), - ), - ], + new Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 8.0), + child: new Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(top: 2.0), + child: new Text( + customer.name, + style: new TextStyle( + fontSize: 20.0, color: primaryColor), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 2.0), + child: new Text( + customer.getLastMessage, + style: new TextStyle( + fontSize: 15.0, color: Colors.grey), + ), + ), + ], + ), + ), + ), + ], + ), ), ), - ), - Column( - children: [ - Padding( - padding: const EdgeInsets.only(right: 5), - child: _status(customer.status), - ), - customer.status == user_invited_status - ? FlatButton( - onPressed: () => _share(customer), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(18.0), - side: BorderSide(color: primaryColor)), - child: Row( - children: [ - Text( - "Share", - style: TextStyle(fontSize: 12), - ), - Icon(Icons.share, color: primaryColor), - ], - ), - ) - : Container(), - ], - ), - ], + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Padding( + padding: const EdgeInsets.only(right: 5), + child: _status(customer.status), + ), + Padding( + padding: const EdgeInsets.only(right: 5), + child: Text(customer.getLastMessageTime), + ), + getCount(customer), + customer.status == user_invited_status + ? FlatButton( + onPressed: () => _share(customer), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(18.0), + side: BorderSide(color: primaryColor)), + child: Row( + children: [ + Text( + "Share", + style: TextStyle(fontSize: 12), + ), + Icon(Icons.share, color: primaryColor), + ], + ), + ) + : Container(), + ], + ), + ], + ), ), ); } + Widget getCount(User customer) { + return customer.fcsUnseenCount != null && customer.fcsUnseenCount > 0 + ? Container( + padding: const EdgeInsets.all(8.0), + decoration: + BoxDecoration(shape: BoxShape.circle, color: secondaryColor), + child: Text(customer.getFcsUnseenCount, + style: TextStyle(color: Colors.white)), + ) + : Container(); + } + Widget _status(String status) { return user_requested_status == status ? Text(status, style: TextStyle(color: primaryColor, fontSize: 14)) @@ -166,6 +207,26 @@ class _CustomerListState extends State { .push(BottomUpPageRoute(CustomerEditor(customer: customer))); } + _gotoMsg(User customer) { + MessageModel messageModel = + Provider.of(context, listen: false); + messageModel.initQuery(customer.id); + Navigator.of(context) + .push(BottomUpPageRoute(MessageDetail( + receiverID: customer.id, + receiverName: customer.name, + messageModel: messageModel, + ))) + .then((value) { + if (customer.fcsUnseenCount > 0) { + messageModel.seenMessages(customer.id, false); + } + }); + if (customer.fcsUnseenCount > 0) { + messageModel.seenMessages(customer.id, false); + } + } + _share(User user) async { MainModel mainModel = Provider.of(context, listen: false); String appUrl = mainModel.setting.appUrl; diff --git a/lib/fcs/common/pages/customer/model/customer_model.dart b/lib/fcs/common/pages/customer/model/customer_model.dart index 1cb943e..7e9d524 100644 --- a/lib/fcs/common/pages/customer/model/customer_model.dart +++ b/lib/fcs/common/pages/customer/model/customer_model.dart @@ -50,6 +50,7 @@ class CustomerModel extends BaseModel { customerListener = Firestore.instance .collection("/$user_collection") .where("is_sys_admin", isEqualTo: false) + .orderBy("message_time", descending: true) .snapshots() .listen((QuerySnapshot snapshot) { customers.clear(); @@ -87,4 +88,10 @@ class CustomerModel extends BaseModel { log.warning("error:$e"); } } + + Future getUser(String id) async { + String path = "/$user_collection"; + var snap = await Firestore.instance.collection(path).document(id).get(); + return User.fromMap(snap.data, snap.documentID); + } } diff --git a/lib/fcs/common/pages/home_page.dart b/lib/fcs/common/pages/home_page.dart index ebd9000..71696cc 100644 --- a/lib/fcs/common/pages/home_page.dart +++ b/lib/fcs/common/pages/home_page.dart @@ -1,7 +1,14 @@ +import 'dart:async'; +import 'dart:io'; + import 'package:fcs/fcs/common/domain/entities/user.dart'; import 'package:fcs/fcs/common/localization/transalation.dart'; import 'package:fcs/fcs/common/pages/buying_instruction/buying_online.dart'; +import 'package:fcs/fcs/common/pages/chat/message_detail.dart'; +import 'package:fcs/fcs/common/pages/chat/model/message_model.dart'; +import 'package:fcs/fcs/common/pages/chat/notification_list.dart'; import 'package:fcs/fcs/common/pages/customer/customer_list.dart'; +import 'package:fcs/fcs/common/pages/customer/model/customer_model.dart'; import 'package:fcs/fcs/common/pages/faq/faq_list_page.dart'; import 'package:fcs/fcs/common/pages/model/language_model.dart'; import 'package:fcs/fcs/common/pages/model/main_model.dart'; @@ -10,9 +17,10 @@ import 'package:fcs/fcs/common/pages/payment_methods/payment_method_page.dart'; import 'package:fcs/fcs/common/pages/staff/staff_list.dart'; import 'package:fcs/fcs/common/pages/util.dart'; import 'package:fcs/fcs/common/pages/widgets/action_button.dart'; +import 'package:fcs/fcs/common/pages/widgets/badge.dart'; import 'package:fcs/fcs/common/pages/widgets/bottom_widgets.dart'; +import 'package:fcs/fcs/common/services/services.dart'; import 'package:fcs/pages/discount_list.dart'; -import 'package:fcs/pages/notification_list.dart'; import 'package:fcs/pages/shipment_list.dart'; import 'package:fcs/pages/term.dart'; import 'package:fcs/pages_fcs/box_list.dart'; @@ -24,6 +32,7 @@ import 'package:fcs/widget/right_left_page_rout.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_icons/flutter_icons.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:intl/intl.dart'; import 'package:logging/logging.dart'; @@ -49,10 +58,129 @@ class _HomePageState extends State { bool login = false; bool customer = true; List isSelected = [true, false]; + static FlutterLocalNotificationsPlugin _flutterLocalNotificationsPlugin = + FlutterLocalNotificationsPlugin(); @override void initState() { super.initState(); + MainModel mainModel = Provider.of(context, listen: false); + + Services.instance.messagingService.init( + (message) { + print("Message from FCM:$message"); + _showNotification(message); + }, + onLaunch: (m) => _showNotiContent(m), + onResume: (m) => _showNotiContent(m), + onSetupComplete: (token) { + mainModel.setMessaginToken = token; + }); + _initLocalNotifications(); + } + + String notiUserID, notiUserName; + _showNotiContent(Map message) { + try { + Map map = Map.from(message["data"]); + notiUserID = map['user_id']; + notiUserName = map['user_name']; + _startNotiTimer(); + print("Notification:$map"); + } catch (e) { + print("Error:$e"); + } + } + + _startNotiTimer() async { + var _duration = new Duration(milliseconds: 500); + new Timer.periodic(_duration, (t) => displayNoti(t)); + } + + void displayNoti(Timer timer) async { + MainModel mainModel = Provider.of(context, listen: false); + if (mainModel.isLogin()) { + timer.cancel(); + bool isCustomer = mainModel.isCustomer(); + String receiverID = isCustomer ? mainModel.user.id : notiUserID; + String receiverName = isCustomer ? mainModel.user.name : notiUserName; + MessageModel messageModel = + Provider.of(context, listen: false); + messageModel.initQuery(receiverID); + User user = mainModel.user; + if (!isCustomer) { + CustomerModel customerModel = + Provider.of(context, listen: false); + user = await customerModel.getUser(receiverID); + } + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => MessageDetail( + messageModel: messageModel, + receiverID: receiverID, + receiverName: receiverName, + ))).then((value) { + if (user.userUnseenCount > 0) { + messageModel.seenMessages(user.id, true); + } + }); + if (user.userUnseenCount > 0) { + messageModel.seenMessages(user.id, true); + } + } + } + + _initLocalNotifications() { + var initializationSettingsAndroid = + new AndroidInitializationSettings('@mipmap/ic_launcher'); + var initializationSettingsIOS = new IOSInitializationSettings(); + var initializationSettings = new InitializationSettings( + initializationSettingsAndroid, initializationSettingsIOS); + _flutterLocalNotificationsPlugin.initialize(initializationSettings); + } + + static Future _showNotification(Map message) async { + var pushTitle; + var pushText; + var action; + + if (Platform.isAndroid) { + var nodeData = message['notification']; + pushTitle = nodeData['title']; + pushText = nodeData['body']; + action = nodeData['action']; + } else { + pushTitle = message['title']; + pushText = message['body']; + action = message['action']; + } + print("AppPushs params pushTitle : $pushTitle"); + print("AppPushs params pushText : $pushText"); + print("AppPushs params pushAction : $action"); + + // @formatter:off + var platformChannelSpecificsAndroid = new AndroidNotificationDetails( + 'your channel id', 'your channel name', 'your channel description', + playSound: true, + enableVibration: true, + importance: Importance.Max, + priority: Priority.High); + // @formatter:on + var platformChannelSpecificsIos = + new IOSNotificationDetails(presentSound: true); + var platformChannelSpecifics = new NotificationDetails( + platformChannelSpecificsAndroid, platformChannelSpecificsIos); + + new Future.delayed(Duration.zero, () { + _flutterLocalNotificationsPlugin.show( + 0, + pushTitle, + pushText, + platformChannelSpecifics, + payload: 'No_Sound', + ); + }); } void dispose() { @@ -73,6 +201,7 @@ class _HomePageState extends State { @override Widget build(BuildContext context) { User user = Provider.of(context).user; + customer = Provider.of(context).isCustomer(); if (user == null) { return Container(); } @@ -130,13 +259,26 @@ class _HomePageState extends State { ); }); - final notiBtn = + final notiBtnOrg = _buildBtn("message.btn", icon: Icons.message, btnCallback: () { + MessageModel messageModel = + Provider.of(context, listen: false); + messageModel.initQuery(user.id); Navigator.push( context, - BottomUpPageRoute(NotificationList()), - ); + BottomUpPageRoute(MessageDetail( + messageModel: messageModel, + )), + ).then((value) { + if (user.userUnseenCount > 0) { + messageModel.seenMessages(user.id, true); + } + }); + if (user.userUnseenCount > 0) { + messageModel.seenMessages(user.id, true); + } }); + final notiBtn = badgeCounter(notiBtnOrg, user.userUnseenCount); final staffBtn = _buildBtn( "staff.title", @@ -182,7 +324,7 @@ class _HomePageState extends State { // customer ? widgets.add(buyingBtn) : ""; // customer || owner ? widgets.add(pickUpBtn) : ""; // !customer ? widgets.add(shipmentBtn) : ""; - // customer || owner ? widgets.add(notiBtn) : ""; + customer ? widgets.add(notiBtn) : ""; user.hasStaffs() ? widgets.add(staffBtn) : ""; // owner ? widgets.add(fcsProfileBtn) : ""; // widgets.add(shipmentCostBtn); diff --git a/lib/fcs/common/pages/model/main_model.dart b/lib/fcs/common/pages/model/main_model.dart index 7c72f58..3d5ca67 100644 --- a/lib/fcs/common/pages/model/main_model.dart +++ b/lib/fcs/common/pages/model/main_model.dart @@ -17,9 +17,15 @@ class MainModel extends ChangeNotifier { final log = Logger('MainModel'); List models = []; + String messagingToken; User user; PackageInfo packageInfo; + set setMessaginToken(token) { + this.messagingToken = token; + uploadMsgToken(); + } + Setting setting; bool isLoaded = false; @@ -36,10 +42,12 @@ class MainModel extends ChangeNotifier { notifyListeners(); }); Services.instance.authService.onAuthStatus().listen((event) async { - this.user = event; this.user = await Services.instance.authService.getUser(refreshIdToken: true); _initUser(user); + if (user != null) { + uploadMsgToken(); + } notifyListeners(); }); } @@ -99,6 +107,7 @@ class MainModel extends ChangeNotifier { .getUserStream(user.id) .listen((event) { this.user = event; + models.forEach((m) => m.initUser(user)); notifyListeners(); }); @@ -166,11 +175,23 @@ class MainModel extends ChangeNotifier { return authResult; } - Future signout() { - this.user = null; + Future uploadMsgToken() { + if (messagingToken == null || user == null) return null; + return Services.instance.userService.uploadMsgToken(messagingToken); + } + + Future removeMsgToken() { + if (messagingToken == null || user == null) return null; + return Services.instance.userService.removeMsgToken(messagingToken); + } + + Future signout() async { // logout models models.forEach((m) => m.logout()); + await removeMsgToken(); + this.user = null; notifyListeners(); + return Services.instance.authService.signout(); } diff --git a/lib/fcs/common/pages/package/package_info.dart b/lib/fcs/common/pages/package/package_info.dart index 1dd1937..222614c 100644 --- a/lib/fcs/common/pages/package/package_info.dart +++ b/lib/fcs/common/pages/package/package_info.dart @@ -1,5 +1,6 @@ import 'package:fcs/fcs/common/domain/entities/package.dart'; import 'package:fcs/fcs/common/helpers/theme.dart'; +import 'package:fcs/fcs/common/pages/model/main_model.dart'; import 'package:fcs/fcs/common/pages/package/package_editor.dart'; import 'package:fcs/fcs/common/pages/util.dart'; import 'package:fcs/fcs/common/pages/widgets/bottom_up_page_route.dart'; @@ -53,6 +54,8 @@ class _PackageInfoState extends State { @override Widget build(BuildContext context) { + bool isCustomer = Provider.of(context).isCustomer(); + final trackingIdBox = DisplayText( text: _package.trackingID, labelText: getLocalString(context, "package.tracking.id"), @@ -107,10 +110,12 @@ class _PackageInfoState extends State { color: primaryColor, ), actions: [ - IconButton( - icon: Icon(Icons.edit, color: primaryColor), - onPressed: _gotoEditor, - ) + isCustomer + ? Container() + : IconButton( + icon: Icon(Icons.edit, color: primaryColor), + onPressed: _gotoEditor, + ) ], ), body: Card( diff --git a/lib/fcs/common/pages/util.dart b/lib/fcs/common/pages/util.dart index 4be719c..537e014 100644 --- a/lib/fcs/common/pages/util.dart +++ b/lib/fcs/common/pages/util.dart @@ -1,6 +1,10 @@ import 'package:fcs/fcs/common/localization/app_translations.dart'; +import 'package:fcs/fcs/common/pages/chat/message_detail.dart'; +import 'package:fcs/fcs/common/pages/chat/model/message_model.dart'; import 'package:fcs/fcs/common/pages/model/language_model.dart'; +import 'package:fcs/fcs/common/pages/model/main_model.dart'; import 'package:fcs/fcs/common/pages/widgets/local_text.dart'; +import 'package:fcs/fcs/common/services/services.dart'; import 'package:fcs/widget/label_widgets.dart'; import 'package:flutter/material.dart'; import 'package:flutter_colorpicker/flutter_colorpicker.dart'; @@ -613,3 +617,21 @@ Widget fcsButton(BuildContext context, String text, String getLocalString(BuildContext context, String key) { return AppTranslations.of(context).text(key); } + +void showToast(GlobalKey key, String text) { + final ScaffoldState scaffold = key.currentState; + scaffold.showSnackBar( + SnackBar( + content: Text(text), + backgroundColor: secondaryColor, + duration: Duration(seconds: 1), + ), + ); +} + +bool hasUnicode(String text) { + final int maxBits = 128; + List unicodeSymbols = + text.codeUnits.where((ch) => ch > maxBits).toList(); + return unicodeSymbols.length > 0; +} diff --git a/lib/fcs/common/pages/widgets/badge.dart b/lib/fcs/common/pages/widgets/badge.dart new file mode 100644 index 0000000..b737620 --- /dev/null +++ b/lib/fcs/common/pages/widgets/badge.dart @@ -0,0 +1,38 @@ +import 'package:fcs/fcs/common/helpers/theme.dart'; +import 'package:flutter/material.dart'; + +Widget badgeCounter(Widget child, int counter) { + return Container( + width: 120, + height: 130, + child: new Stack( + children: [ + child, + counter != null && counter != 0 + ? new Positioned( + right: 12, + top: 12, + child: new Container( + padding: EdgeInsets.all(8), + decoration: new BoxDecoration( + shape: BoxShape.circle, + color: secondaryColor, + ), + constraints: BoxConstraints( + minWidth: 14, + minHeight: 14, + ), + child: Text( + counter > 99 ? '+99' : '$counter', + style: TextStyle( + color: Colors.white, + ), + textAlign: TextAlign.center, + ), + ), + ) + : new Container() + ], + ), + ); +} diff --git a/lib/fcs/common/pages/widgets/fcs_id_icon.dart b/lib/fcs/common/pages/widgets/fcs_id_icon.dart index 38a016f..f571cf8 100644 --- a/lib/fcs/common/pages/widgets/fcs_id_icon.dart +++ b/lib/fcs/common/pages/widgets/fcs_id_icon.dart @@ -5,11 +5,13 @@ class FcsIDIcon extends StatelessWidget { Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.all(8.0), - child: SizedBox( + child: Container( width: 25, height: 25, child: FittedBox( - child: Image.asset("assets/logo.jpg"), + child: Image.asset( + "assets/logo.jpg", + ), fit: BoxFit.fill, ), ), diff --git a/lib/fcs/common/pages/widgets/multi_img_file.dart b/lib/fcs/common/pages/widgets/multi_img_file.dart index cb10c48..fec6bc3 100644 --- a/lib/fcs/common/pages/widgets/multi_img_file.dart +++ b/lib/fcs/common/pages/widgets/multi_img_file.dart @@ -113,11 +113,19 @@ class _MultiImageFileState extends State { ), child: fileContainers[index].file == null ? CachedNetworkImage( + width: 50, + height: 50, imageUrl: fileContainers[index].url, - placeholder: (context, url) => Container( - width: 50, - height: 50, - child: CircularProgressIndicator()), + placeholder: (context, url) => Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + width: 30, + height: 30, + child: CircularProgressIndicator()), + ], + ), errorWidget: (context, url, error) => Icon(Icons.error), ) diff --git a/lib/fcs/common/services/common_imp.dart b/lib/fcs/common/services/common_imp.dart index 19a2d47..c420f02 100644 --- a/lib/fcs/common/services/common_imp.dart +++ b/lib/fcs/common/services/common_imp.dart @@ -1,6 +1,7 @@ import 'package:fcs/fcs/common/data/providers/common_data_provider.dart'; import 'package:fcs/fcs/common/data/providers/user_data_provider.dart'; import 'package:fcs/fcs/common/domain/entities/payment_method.dart'; +import 'package:fcs/fcs/common/domain/vo/message.dart'; import 'package:flutter/material.dart'; import 'common_service.dart'; @@ -26,4 +27,14 @@ class CommonServiceImp implements CommonService { Future updatePaymentMethod(PaymentMethod paymentMethod) { return commonDataProvider.updatePaymentMethod(paymentMethod); } + + @override + Future sendMessage(Message message) { + return commonDataProvider.sendMessage(message); + } + + @override + Future seenMessage(String ownerID, bool seenByOwner) { + return commonDataProvider.seenMessage(ownerID, seenByOwner); + } } diff --git a/lib/fcs/common/services/common_service.dart b/lib/fcs/common/services/common_service.dart index 01c8566..b321ed7 100644 --- a/lib/fcs/common/services/common_service.dart +++ b/lib/fcs/common/services/common_service.dart @@ -1,8 +1,13 @@ import 'package:fcs/fcs/common/domain/entities/payment_method.dart'; +import 'package:fcs/fcs/common/domain/vo/message.dart'; abstract class CommonService { - // Payment Service + // Payment Future createPaymentMethod(PaymentMethod paymentMethod); Future updatePaymentMethod(PaymentMethod paymentMethod); Future deletePayment(String id); + + // Messaging + Future sendMessage(Message message); + Future seenMessage(String ownerID, bool seenByOwner); } diff --git a/lib/fcs/common/services/messaging_imp.dart b/lib/fcs/common/services/messaging_imp.dart index 2d9940b..9951e84 100644 --- a/lib/fcs/common/services/messaging_imp.dart +++ b/lib/fcs/common/services/messaging_imp.dart @@ -8,9 +8,12 @@ class MessagingServiceImp implements MessagingService { static MessagingFCM messagingFCM; @override - void init(onMessage, {OnNotify onLaunch, OnNotify onResume}) { - messagingFCM = - MessagingFCM(onMessage, onLaunch: onLaunch, onResume: onResume); + void init(onMessage, + {OnNotify onLaunch, OnNotify onResume, OnSetupComplete onSetupComplete}) { + messagingFCM = MessagingFCM(onMessage, + onLaunch: onLaunch, + onResume: onResume, + onSetupComplete: onSetupComplete); } @override diff --git a/lib/fcs/common/services/messaging_service.dart b/lib/fcs/common/services/messaging_service.dart index f22ecd4..f58ea52 100644 --- a/lib/fcs/common/services/messaging_service.dart +++ b/lib/fcs/common/services/messaging_service.dart @@ -1,7 +1,9 @@ typedef OnNotify(Map message); +typedef OnSetupComplete(String token); abstract class MessagingService { - void init(OnNotify onMessage, {OnNotify onLaunch, OnNotify onResume}); + void init(OnNotify onMessage, + {OnNotify onLaunch, OnNotify onResume, OnSetupComplete onSetupComplete}); Future subscribe(String topic); Future unsubscribe(String topic); } diff --git a/lib/fcs/common/services/user_imp.dart b/lib/fcs/common/services/user_imp.dart index 07e3493..51d5e8c 100644 --- a/lib/fcs/common/services/user_imp.dart +++ b/lib/fcs/common/services/user_imp.dart @@ -38,4 +38,14 @@ class UserServiceImp implements UserService { Future> searchUser(String term) { return userDataProvider.searchUser(term); } + + @override + Future removeMsgToken(String token) { + return userDataProvider.removeMsgToken(token); + } + + @override + Future uploadMsgToken(String token) { + return userDataProvider.uploadMsgToken(token); + } } diff --git a/lib/fcs/common/services/user_service.dart b/lib/fcs/common/services/user_service.dart index b6a5d29..93d8c32 100644 --- a/lib/fcs/common/services/user_service.dart +++ b/lib/fcs/common/services/user_service.dart @@ -6,4 +6,6 @@ abstract class UserService { Future acceptRequest(String userID); Future findUser(String phoneNumber); Future> searchUser(String term); + Future uploadMsgToken(String token); + Future removeMsgToken(String token); } diff --git a/lib/main-dev.dart b/lib/main-dev.dart index cd69cbe..ef4a875 100644 --- a/lib/main-dev.dart +++ b/lib/main-dev.dart @@ -10,6 +10,8 @@ void main() { flavor: Flavor.DEV, color: Colors.blue, apiURL: "https://asia-northeast1-fcs-dev1.cloudfunctions.net/API", + reportURL: "http://petrok.mokkon.com:8091", + reportProjectID: "fcs-dev", level: Level.ALL); runApp(App()); } diff --git a/lib/model_fcs/message_model.dart b/lib/model_fcs/message_model.dart index 4e2e020..99798db 100644 --- a/lib/model_fcs/message_model.dart +++ b/lib/model_fcs/message_model.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:fcs/fcs/common/domain/constants.dart'; import 'package:fcs/model/base_model.dart'; import 'package:fcs/vo/message.dart'; import 'package:fcs/vo/package.dart'; @@ -23,8 +24,7 @@ class MessageModel extends BaseModel { senderName: "FCS System", receiverName: "Ko Myo Min", date: DateTime(2020, 6, 1, 1, 1, 1), - message: - "'A202-3 #1'", + message: "'A202-3 #1'", ), Message( senderName: "FCS System", @@ -43,8 +43,7 @@ class MessageModel extends BaseModel { senderName: "FCS System", receiverName: "Ko Myo Min", date: DateTime(2020, 6, 1, 2, 1, 1), - message: - "'INV202005010387'", + message: "'INV202005010387'", ), Message( senderName: "FCS System", @@ -64,8 +63,7 @@ class MessageModel extends BaseModel { senderName: "FCS System", receiverName: "Shipper", date: DateTime(2020, 6, 1, 1, 1, 1), - message: - "'A202-3 #1'", + message: "'A202-3 #1'", ), Message( senderName: "FCS System", @@ -78,8 +76,7 @@ class MessageModel extends BaseModel { senderName: "FCS System", receiverName: "Shipper", date: DateTime(2020, 6, 1, 2, 1, 1), - message: - "'INV202005010387'", + message: "'INV202005010387'", ), Message( senderName: "FCS System", diff --git a/lib/util.dart b/lib/util.dart index 8bbfaa8..cfb338f 100644 --- a/lib/util.dart +++ b/lib/util.dart @@ -13,7 +13,7 @@ class DateUtil { } String updatePhoneNumber(String phoneNumber) { - if(phoneNumber==null) return null; + if (phoneNumber == null) return null; if (phoneNumber.startsWith("09")) { return "959" + phoneNumber.substring(2); } diff --git a/lib/vo/message.dart b/lib/vo/message.dart index 144fe81..1679d6d 100644 --- a/lib/vo/message.dart +++ b/lib/vo/message.dart @@ -1,10 +1,14 @@ class Message { + String id; String receiverName; String message; DateTime date; String senderName; bool isMe; - Message({this.receiverName, this.message, this.date, this.senderName,this.isMe}); + + Message( + {this.receiverName, this.message, this.date, this.senderName, this.isMe}); + bool fromToday() { var now = DateTime.now(); return date.day == now.day &&