add messaging

This commit is contained in:
Sai Naw Wun
2020-09-20 05:34:49 +06:30
parent cc56a18613
commit cb622b004b
38 changed files with 1931 additions and 180 deletions

View File

@@ -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",

View File

@@ -150,6 +150,9 @@
"pm.save.btn":"သိမ်းဆည်းရန်",
"pm.delete.confirm":"ငွေပေးချေစနစ်ကို ဖျက်မလား?",
"message.view.detail":"အသေးစိတ် ကြည့်ရန်",
"message.hint.input":"စာကို ဒီမှာ ရိုက်ထည့်ပါ...",
"btn.save":"သိမ်းဆည်းရန်",
"btn.approve":"အတည်ပြုရန်",
"btn.delete":"ဖျက်ရန်",

View File

@@ -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<App> {
..addModel(staffModel)
..addModel(shipmentModel)
..addModel(packageModel)
..addModel(messageModel)
..addModel(marketModel);
mainModel2.init();
@@ -138,69 +135,10 @@ class _AppState extends State<App> {
..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<String, dynamic> 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) {

View File

@@ -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;
}

View File

@@ -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<void> sendMessage(Message message) async {
return await requestAPI("/messages", "POST",
payload: message.toMap(), token: await getToken());
}
Future<void> seenMessage(String ownerID, bool seenByOwner) async {
return await requestAPI("/messages/seen", "POST",
payload: {"owner_id": ownerID, "seen_by_owner": seenByOwner},
token: await getToken());
}
}

View File

@@ -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<String, dynamic> 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");
});
}

View File

@@ -27,6 +27,16 @@ class UserDataProvider {
payload: {"id": userID}, token: await getToken());
}
Future<void> uploadMsgToken(String token) async {
return await requestAPI("/messages/token", "POST",
payload: {"token": token}, token: await getToken());
}
Future<void> removeMsgToken(String token) async {
return await requestAPI("/messages/token", "DELETE",
payload: {"token": token}, token: await getToken());
}
Future<User> findUser(String phoneNumber) async {
QuerySnapshot querySnap = await Firestore.instance
.collection(user_collection)

View File

@@ -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";

View File

@@ -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<String> 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<String, dynamic> 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<String, dynamic> map, String docID) {
var _date = (map['message_time'] as Timestamp);
List<String> _privileges =
map['privileges'] == null ? [] : map['privileges'].cast<String>();
@@ -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() {

View File

@@ -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<String, dynamic> 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<String, dynamic> 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,
);
}
}

View File

@@ -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<T> {
final log = Logger('PaginationModel');
List<String> ids = [];
DocumentSnapshot prev;
int rowPerLoad = 10;
bool ended = false;
StreamSubscription<QuerySnapshot> 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<Result> controller;
Stream<Result> listen() {
if (controller != null) {
controller.close();
}
controller = StreamController<Result>(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<String, dynamic> 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<bool> 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<String, dynamic> data;
DocumentChangeType documentChangeType;
bool isEnded;
Result({this.id, this.data, this.documentChangeType, this.isEnded = false});
}

View File

@@ -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: <Widget>[
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: <Widget>[
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: <Widget>[
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();
}
}

View File

@@ -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<ChatScreen> {
ChatScreenState({Key key, @required this.peerId, @required this.peerAvatar});
GlobalKey key = GlobalKey();
String peerId;
String peerAvatar;
String id;
List<DocumentSnapshot> 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: <Widget>[
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<Color>(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: <Widget>[
Row(
children: <Widget>[
isLastMessageLeft(index)
? Material(
child: CachedNetworkImage(
placeholder: (context, url) => Container(
child: CircularProgressIndicator(
strokeWidth: 1.0,
valueColor:
AlwaysStoppedAnimation<Color>(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<Color>(
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<bool> 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: <Widget>[
Column(
children: <Widget>[
// 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: <Widget>[
// 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<Color>(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<Color>(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,
);
}
},
),
);
}
}

View File

@@ -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<FullPhotoScreen> {
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)));
}
}

View File

@@ -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<Color>(primaryColor),
),
),
color: Colors.white.withOpacity(0.8),
);
}
}

View File

@@ -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<MessageModel>(context).user.id;
return Scaffold(
appBar: AppBar(
backgroundColor: primaryColor,
elevation: .9,
title: Text(
receiverName ?? "FCS Team",
),
actions: <Widget>[],
),
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: <Widget>[
Flexible(
child: Container(
child: TextField(
onSubmitted: (value) {
Provider.of<MessageModel>(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<MessageModel>(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<PackageModel>(context, listen: false);
Package p = await packageModel.getPackage(message.messageID);
Navigator.push<bool>(context, BottomUpPageRoute(PackageInfo(package: p)));
}
}
}

View File

@@ -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<Message> 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<QuerySnapshot> 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<void> 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<void> sendMessage(String msg, String receiverID) {
Message message = Message(message: msg, receiverID: receiverID);
return Services.instance.commonService.sendMessage(message);
}
Future<void> seenMessages(String ownerID, bool seenByOwner) {
return Services.instance.commonService.seenMessage(ownerID, seenByOwner);
}
}

View File

@@ -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<NotificationList> {
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<MessageModel>(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: <Widget>[
InkWell(
onTap: () => _display(msg),
child: Row(
children: <Widget>[
Expanded(
child: new Padding(
padding: const EdgeInsets.symmetric(vertical: 10.0),
child: new Row(
children: <Widget>[
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: <Widget>[
new Text(
msg.receiverName,
style: new TextStyle(fontSize: 15.0),
),
],
),
),
],
),
),
),
Padding(
padding: const EdgeInsets.only(right: 18.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
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)));
}
}

View File

@@ -56,10 +56,12 @@ class _ContactEditorState extends State<ContactEditor> {
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',

View File

@@ -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<CustomerList> {
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<CustomerList> {
Widget _item(User customer) {
return InkWell(
onTap: () => _select(customer),
child: Row(
children: <Widget>[
Expanded(
child: new Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: new Row(
children: <Widget>[
Icon(
Feather.user,
color: primaryColor,
size: 40,
),
new Expanded(
child: new Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
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: <Widget>[
Expanded(
child: new Padding(
padding: const EdgeInsets.symmetric(vertical: 2.0),
child: new Row(
children: <Widget>[
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: <Widget>[
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<CustomerList> {
.push(BottomUpPageRoute(CustomerEditor(customer: customer)));
}
_gotoMsg(User customer) {
MessageModel messageModel =
Provider.of<MessageModel>(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<MainModel>(context, listen: false);
String appUrl = mainModel.setting.appUrl;

View File

@@ -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<User> 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);
}
}

View File

@@ -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<HomePage> {
bool login = false;
bool customer = true;
List<bool> isSelected = [true, false];
static FlutterLocalNotificationsPlugin _flutterLocalNotificationsPlugin =
FlutterLocalNotificationsPlugin();
@override
void initState() {
super.initState();
MainModel mainModel = Provider.of<MainModel>(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<String, dynamic> message) {
try {
Map<String, dynamic> map = Map<String, dynamic>.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<MainModel>(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<MessageModel>(context, listen: false);
messageModel.initQuery(receiverID);
User user = mainModel.user;
if (!isCustomer) {
CustomerModel customerModel =
Provider.of<CustomerModel>(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<String, dynamic> 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<HomePage> {
@override
Widget build(BuildContext context) {
User user = Provider.of<MainModel>(context).user;
customer = Provider.of<MainModel>(context).isCustomer();
if (user == null) {
return Container();
}
@@ -130,13 +259,26 @@ class _HomePageState extends State<HomePage> {
);
});
final notiBtn =
final notiBtnOrg =
_buildBtn("message.btn", icon: Icons.message, btnCallback: () {
MessageModel messageModel =
Provider.of<MessageModel>(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<HomePage> {
// 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);

View File

@@ -17,9 +17,15 @@ class MainModel extends ChangeNotifier {
final log = Logger('MainModel');
List<BaseModel> 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<void> signout() {
this.user = null;
Future<void> uploadMsgToken() {
if (messagingToken == null || user == null) return null;
return Services.instance.userService.uploadMsgToken(messagingToken);
}
Future<void> removeMsgToken() {
if (messagingToken == null || user == null) return null;
return Services.instance.userService.removeMsgToken(messagingToken);
}
Future<void> signout() async {
// logout models
models.forEach((m) => m.logout());
await removeMsgToken();
this.user = null;
notifyListeners();
return Services.instance.authService.signout();
}

View File

@@ -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<PackageInfo> {
@override
Widget build(BuildContext context) {
bool isCustomer = Provider.of<MainModel>(context).isCustomer();
final trackingIdBox = DisplayText(
text: _package.trackingID,
labelText: getLocalString(context, "package.tracking.id"),
@@ -107,10 +110,12 @@ class _PackageInfoState extends State<PackageInfo> {
color: primaryColor,
),
actions: <Widget>[
IconButton(
icon: Icon(Icons.edit, color: primaryColor),
onPressed: _gotoEditor,
)
isCustomer
? Container()
: IconButton(
icon: Icon(Icons.edit, color: primaryColor),
onPressed: _gotoEditor,
)
],
),
body: Card(

View File

@@ -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<int> unicodeSymbols =
text.codeUnits.where((ch) => ch > maxBits).toList();
return unicodeSymbols.length > 0;
}

View File

@@ -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: <Widget>[
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()
],
),
);
}

View File

@@ -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,
),
),

View File

@@ -113,11 +113,19 @@ class _MultiImageFileState extends State<MultiImageFile> {
),
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),
)

View File

@@ -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<void> updatePaymentMethod(PaymentMethod paymentMethod) {
return commonDataProvider.updatePaymentMethod(paymentMethod);
}
@override
Future<void> sendMessage(Message message) {
return commonDataProvider.sendMessage(message);
}
@override
Future<void> seenMessage(String ownerID, bool seenByOwner) {
return commonDataProvider.seenMessage(ownerID, seenByOwner);
}
}

View File

@@ -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<void> createPaymentMethod(PaymentMethod paymentMethod);
Future<void> updatePaymentMethod(PaymentMethod paymentMethod);
Future<void> deletePayment(String id);
// Messaging
Future<void> sendMessage(Message message);
Future<void> seenMessage(String ownerID, bool seenByOwner);
}

View File

@@ -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

View File

@@ -1,7 +1,9 @@
typedef OnNotify(Map<String, dynamic> 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<void> subscribe(String topic);
Future<void> unsubscribe(String topic);
}

View File

@@ -38,4 +38,14 @@ class UserServiceImp implements UserService {
Future<List<User>> searchUser(String term) {
return userDataProvider.searchUser(term);
}
@override
Future<void> removeMsgToken(String token) {
return userDataProvider.removeMsgToken(token);
}
@override
Future<void> uploadMsgToken(String token) {
return userDataProvider.uploadMsgToken(token);
}
}

View File

@@ -6,4 +6,6 @@ abstract class UserService {
Future<void> acceptRequest(String userID);
Future<User> findUser(String phoneNumber);
Future<List<User>> searchUser(String term);
Future<void> uploadMsgToken(String token);
Future<void> removeMsgToken(String token);
}

View File

@@ -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());
}

View File

@@ -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",

View File

@@ -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);
}

View File

@@ -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 &&