diff --git a/lib/models/websocket_model.dart b/lib/models/websocket_model.dart index a58325e..3d17564 100644 --- a/lib/models/websocket_model.dart +++ b/lib/models/websocket_model.dart @@ -17,8 +17,9 @@ import 'package:together_mobile/notification_api.dart'; enum SocketStatus { connected, - closed, + reconnecting, error, + closed, } class WebSocketManager extends ChangeNotifier { @@ -29,21 +30,17 @@ class WebSocketManager extends ChangeNotifier { Timer? heartBeatTimer; Timer? serverTimer; Timer? reconnectTimer; - int reconnectCount = 30; + int reconnectCount = 10; int reconnectTimes = 0; - Duration timeout = const Duration(seconds: 4); + Duration heartBeatTimeout = const Duration(seconds: 4); + Duration reconnectTimeout = const Duration(seconds: 3); void connect(String userId, bool isReconnect) { id = userId; wsUrl = Uri.parse('ws://10.0.2.2:8000/ws/$id?is_reconnect=$isReconnect'); + // This doesn't blcok the programe whethe it connect the server or not + // So heartBeat will be executre straightly channel = WebSocketChannel.connect(wsUrl); - socketStatus = SocketStatus.connected; - print('websocket connected <$channel>'); - if (reconnectTimer != null) { - reconnectTimer!.cancel(); - reconnectTimer = null; - reconnectTimes = 0; - } heartBeatInspect(); channel.stream.listen(onData, onError: onError, onDone: onDone); @@ -54,6 +51,7 @@ class WebSocketManager extends ChangeNotifier { wsUrl = Uri(); id = ''; socketStatus = SocketStatus.closed; + notifyListeners(); heartBeatTimer?.cancel(); serverTimer?.cancel(); reconnectTimer?.cancel(); @@ -64,11 +62,22 @@ class WebSocketManager extends ChangeNotifier { } void onData(jsonData) { + // If socket can receive msg, that means connection is estabilished + socketStatus = SocketStatus.connected; + notifyListeners(); + print('websocket connected <$channel>'); + if (reconnectTimer != null) { + reconnectTimer!.cancel(); + reconnectTimer = null; + reconnectTimes = 0; + } + heartBeatInspect(); + Map data = json.decode(jsonData); switch (data['event']) { case 'friend-chat-msg': - receiveFriendMsg(data); + receiveFriendMsg(data, true); case 'apply-friend': receiveApplyFriend(data); case 'friend-added': @@ -80,21 +89,53 @@ class WebSocketManager extends ChangeNotifier { case 'group-chat-creation': receiveGroupChatCreation(data); case 'group-chat-msg': - receiveGroupChatMsg(data); + receiveGroupChatMsg(data, true); } } + // This will be trigger while server or client close the connection + // for example server is restarting void onDone() { print('websocket disconnected <$channel>'); + if (socketStatus == SocketStatus.closed) { + // Client close the connection return; } - reconnect(); + if (socketStatus == SocketStatus.connected) { + // Server close the connection + socketStatus = SocketStatus.reconnecting; + notifyListeners(); + print(111111111111111); + reconnectTimes++; + reconnect(); + } + if (reconnectTimes >= reconnectCount) { + socketStatus = SocketStatus.error; + } } - void onError(Object error) {} + // This will be trigger while server exactly shutdown + void onError(Object error, StackTrace st) { + print('Websocket connect occurs error: <$error>'); + // print(st); + if (reconnectTimes >= reconnectCount) { + socketStatus = SocketStatus.error; + if (heartBeatTimer != null) { + heartBeatTimer!.cancel(); + heartBeatTimer = null; + } + channel.sink.close(); + notifyListeners(); + } else { + print('${reconnectTimes}th reconnection'); + reconnect(); + } + } void heartBeatInspect() { + print('start heartbeat inspect......'); + if (heartBeatTimer != null) { heartBeatTimer!.cancel(); heartBeatTimer = null; @@ -104,32 +145,19 @@ class WebSocketManager extends ChangeNotifier { serverTimer!.cancel(); serverTimer = null; } - print('start heartbeat inspect......'); - heartBeatTimer = Timer(timeout, () { + + heartBeatTimer = Timer(heartBeatTimeout, () { channel.sink.add(json.encode({'event': 'ping'})); - serverTimer = Timer(timeout, () { + serverTimer = Timer(heartBeatTimeout, () { + // This will trigger the onDone callback channel.sink.close(status.internalServerError); - socketStatus = SocketStatus.closed; + socketStatus = SocketStatus.reconnecting; + notifyListeners(); }); }); } void reconnect() { - if (socketStatus == SocketStatus.error) { - if (heartBeatTimer != null) { - heartBeatTimer!.cancel(); - heartBeatTimer = null; - } - - if (serverTimer != null) { - serverTimer!.cancel(); - serverTimer = null; - } - return; - } - - print('websocket reconnecting......'); - if (heartBeatTimer != null) { heartBeatTimer!.cancel(); heartBeatTimer = null; @@ -140,23 +168,29 @@ class WebSocketManager extends ChangeNotifier { serverTimer = null; } - reconnectTimer = Timer.periodic(timeout, (timer) { + if (reconnectTimer != null) { + reconnectTimer!.cancel(); + reconnectTimer = null; + } + + reconnectTimer = Timer(reconnectTimeout, () { if (reconnectTimes < reconnectCount) { - connect(id, true); + print('websocket reconnecting......'); reconnectTimes++; + connect(id, true); } else { print('reconnection times exceed the max times......'); // If it is still disconnection after reconnect 30 times, set the socket // status to error, means the network is bad, and stop reconnecting. socketStatus = SocketStatus.error; + notifyListeners(); channel.sink.close(); - timer.cancel(); } }); } } -void receiveFriendMsg(Map msg) async { +void receiveFriendMsg(Map msg, bool isShowNotification) async { print('=================收到了好友信息事件=================='); print(msg); print('======================================='); @@ -209,6 +243,10 @@ void receiveFriendMsg(Map msg) async { ), ); + if (!isShowNotification) { + return; + } + String name = getIt.get().friends[senderId]!.friendRemark.isEmpty ? getIt.get().friends[senderId]!.nickname : getIt.get().friends[senderId]!.friendRemark; @@ -280,10 +318,6 @@ void receiveChatImages(Map msg) async { await file.create(recursive: true); await file.writeAsBytes(List.from(msg['bytes'])); } - // File file = await File('$chatImageDir/${msg['filename']}').create( - // recursive: true, - // // ); - // await file.writeAsBytes(msg['bytes']); } void receiveGroupChatCreation(Map msg) { @@ -295,7 +329,8 @@ void receiveGroupChatCreation(Map msg) { getIt.get().addGroupChatProfile(groupChatId, msg); } -void receiveGroupChatMsg(Map msg) async { +void receiveGroupChatMsg( + Map msg, bool isShowNotification) async { print('=================收到了群聊信息事件=================='); print(msg); print('======================================='); @@ -349,6 +384,10 @@ void receiveGroupChatMsg(Map msg) async { ), ); + if (!isShowNotification) { + return; + } + String avatar = getIt.get().groupChats[groupChatId]!.avatar; late String name; diff --git a/lib/request/message.dart b/lib/request/message.dart new file mode 100644 index 0000000..a749df1 --- /dev/null +++ b/lib/request/message.dart @@ -0,0 +1,14 @@ +import 'package:dio/dio.dart'; + +import 'server.dart'; + +Future> getUnreceivedMsg(String userId) async { + Response response = await request.get( + '/message/unreceived', + queryParameters: { + 'receiver_id': userId, + }, + ); + + return response.data; +} diff --git a/lib/router/chat_router.dart b/lib/router/chat_router.dart index 76e76ed..afce700 100644 --- a/lib/router/chat_router.dart +++ b/lib/router/chat_router.dart @@ -12,7 +12,7 @@ final chatRouter = GoRoute( name: 'Chat', builder: (context, state) { getIt.get().changeRoute('Chat'); - return const ChatScreen(); + return ChatScreen(); }, routes: [ GoRoute( diff --git a/lib/screens/chat/chat_screen copy.dart b/lib/screens/chat/chat_screen copy.dart new file mode 100755 index 0000000..456dfab --- /dev/null +++ b/lib/screens/chat/chat_screen copy.dart @@ -0,0 +1,227 @@ +import 'package:flutter/material.dart'; + +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:hive_flutter/hive_flutter.dart'; +import 'package:together_mobile/database/hive_database.dart'; +import 'package:together_mobile/request/message.dart'; + +import 'package:together_mobile/screens/chat/components/group_chat_chat_tile.dart'; +import 'components/friend_chat_tile.dart'; +import 'components/add_menu.dart'; +import 'package:together_mobile/database/box_type.dart'; +import 'package:together_mobile/models/websocket_model.dart'; +import 'package:together_mobile/utils/format_datetime.dart'; +import 'package:together_mobile/models/contact_model.dart'; +import 'package:together_mobile/models/apply_list_model.dart'; +import 'package:together_mobile/request/apply.dart'; +import 'package:together_mobile/request/server.dart'; +import 'package:together_mobile/request/contact.dart'; +import 'package:together_mobile/models/user_model.dart'; +import 'package:together_mobile/models/init_get_it.dart'; +import 'package:together_mobile/request/user_profile.dart'; + +class ChatScreen extends StatefulWidget { + const ChatScreen({super.key}); + + @override + State createState() => _ChatScreenState(); +} + +class _ChatScreenState extends State { + Future _initData() async { + if (!getIt.get().isInitialised) { + await HiveDatabase.init(); + + getIt.get().connect(getIt.get().id, false); + + String userId = getIt.get().id; + + List> res = await Future.wait([ + getMyProfile(userId), + getApplyList(userId), + getContact(userId), + ]); + + await getIt.get().init(res[0]['data']); + + if (res[1]['code'] == 10600) { + getIt.get().init(res[1]['data']); + } + + if (res[2]['code'] == 10700) { + getIt.get().init(res[2]['data']); + } + + Map contactAcctProfRes = await getContactAccountProfiles( + getIt.get().friends.keys.toList(), + getIt.get().groupChats.keys.toList(), + ); + + if (contactAcctProfRes['code'] == 10700) { + getIt.get().init(contactAcctProfRes['data']); + } + + await _getUnreceivedMsg(userId); + } + + return Future(() => true); + } + + Future _getUnreceivedMsg(String userId) async { + print('触发了获取信息事件..................'); + final res = await getUnreceivedMsg(userId); + + if (res['code'] == 10900) { + for (var msg in res['data'] as List>) { + if (msg['event'] == 'friend-chat-msg') { + receiveFriendMsg(msg, false); + } else if (msg['event'] == 'group-chat-msg') { + receiveGroupChatMsg(msg, false); + } + } + } + } + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: _initData(), + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.hasData) { + return Scaffold( + appBar: AppBar( + leading: getIt.get().avatar.isEmpty + ? const CircleAvatar( + backgroundImage: AssetImage('assets/images/user_2.png'), + ) + : CircleAvatar( + backgroundImage: CachedNetworkImageProvider( + '$userAvatarsUrl/${getIt.get().avatar}', + ), + ), + title: Text(getIt.get().nickname), + centerTitle: true, + actions: [ + IconButton( + onPressed: () {}, + splashRadius: 20, + icon: const Icon(Icons.search), + ), + const AddMenu(), + ], + ), + // Use ListView.builder because it renders list element on demand + body: RefreshIndicator( + onRefresh: () async { + String userId = getIt.get().id; + if (getIt.get().socketStatus == + SocketStatus.closed) { + getIt.get().connect(userId, false); + } + await _getUnreceivedMsg(userId); + }, + child: ValueListenableBuilder( + valueListenable: + Hive.box('chat_setting').listenable(), + builder: (context, Box box, _) { + final List openedChat = + box.values.where((element) => element.isOpen).toList(); + + // latestMsg on the top + openedChat.sort( + (a, b) => b.latestDateTime.compareTo(a.latestDateTime), + ); + + if (openedChat.isEmpty) { + return const Center( + child: Text( + '没有最新消息', + style: TextStyle( + fontSize: 18, + letterSpacing: 5.0, + ), + ), + ); + } else { + return ListView.builder( + physics: const BouncingScrollPhysics( + parent: AlwaysScrollableScrollPhysics(), + ), + itemCount: openedChat.length, + itemBuilder: (BuildContext context, int index) { + String contactId = openedChat[index].contactId; + String showedTime = formatTileDateTime( + openedChat[index].latestDateTime, + ); + int unreadCount = openedChat[index].unreadCount; + + return ValueListenableBuilder( + valueListenable: + Hive.box('message_$contactId') + .listenable(), + builder: (context, messageTBox, _) { + int length = messageTBox.length; + if (length > 0) { + MessageT messageT = + messageTBox.getAt(length - 1)!; + if (openedChat[index].type == 0) { + return FriendChatTile( + key: ValueKey(contactId), + index: index, + contactId: contactId, + senderId: messageT.senderId, + messageType: messageT.type, + text: messageT.text, + attachments: messageT.attachments, + dateTime: showedTime, + isShowTime: messageT.isShowTime, + unreadCount: unreadCount, + ); + } else { + return GroupChatChatTile( + key: ValueKey(contactId), + index: index, + contactId: contactId, + senderId: messageT.senderId, + messageType: messageT.type, + text: messageT.text, + attachments: messageT.attachments, + dateTime: showedTime, + isShowTime: messageT.isShowTime, + unreadCount: unreadCount, + ); + } + } else { + return const SizedBox(); + } + }, + ); + }, + ); + } + }, + ), + ), + ); + } else { + return const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: 60, + height: 60, + child: CircularProgressIndicator(), + ), + Padding( + padding: EdgeInsets.only(top: 20), + child: Text('Loading data....'), + ) + ], + ), + ); + } + }, + ); + } +} diff --git a/lib/screens/chat/chat_screen.dart b/lib/screens/chat/chat_screen.dart index 21e211b..c540671 100755 --- a/lib/screens/chat/chat_screen.dart +++ b/lib/screens/chat/chat_screen.dart @@ -1,8 +1,11 @@ import 'package:flutter/material.dart'; import 'package:cached_network_image/cached_network_image.dart'; +import 'package:get_it_mixin/get_it_mixin.dart'; import 'package:hive_flutter/hive_flutter.dart'; +import 'package:together_mobile/common/constants.dart'; import 'package:together_mobile/database/hive_database.dart'; +import 'package:together_mobile/request/message.dart'; import 'package:together_mobile/screens/chat/components/group_chat_chat_tile.dart'; import 'components/friend_chat_tile.dart'; @@ -19,24 +22,26 @@ import 'package:together_mobile/models/user_model.dart'; import 'package:together_mobile/models/init_get_it.dart'; import 'package:together_mobile/request/user_profile.dart'; -class ChatScreen extends StatefulWidget { - const ChatScreen({super.key}); +class ChatScreen extends StatefulWidget with GetItStatefulWidgetMixin { + ChatScreen({super.key}); @override State createState() => _ChatScreenState(); } -class _ChatScreenState extends State { +class _ChatScreenState extends State with GetItStateMixin { Future _initData() async { if (!getIt.get().isInitialised) { await HiveDatabase.init(); getIt.get().connect(getIt.get().id, false); + String userId = getIt.get().id; + List> res = await Future.wait([ - getMyProfile(getIt.get().id), - getApplyList(getIt.get().id), - getContact(getIt.get().id), + getMyProfile(userId), + getApplyList(userId), + getContact(userId), ]); await getIt.get().init(res[0]['data']); @@ -57,13 +62,34 @@ class _ChatScreenState extends State { if (contactAcctProfRes['code'] == 10700) { getIt.get().init(contactAcctProfRes['data']); } + + await _getUnreceivedMsg(userId); } return Future(() => true); } + Future _getUnreceivedMsg(String userId) async { + print('触发了获取信息事件..................'); + final res = await getUnreceivedMsg(userId); + + if (res['code'] == 10900) { + for (var msg in res['data'] as List>) { + if (msg['event'] == 'friend-chat-msg') { + receiveFriendMsg(msg, false); + } else if (msg['event'] == 'group-chat-msg') { + receiveGroupChatMsg(msg, false); + } + } + } + } + @override Widget build(BuildContext context) { + SocketStatus socketStatus = watchOnly( + (WebSocketManager wm) => wm.socketStatus, + ); + return FutureBuilder( future: _initData(), builder: (BuildContext context, AsyncSnapshot snapshot) { @@ -91,95 +117,131 @@ class _ChatScreenState extends State { ], ), // Use ListView.builder because it renders list element on demand - body: RefreshIndicator( - onRefresh: () async { - return Future.delayed( - const Duration( - seconds: 2, + body: Column( + children: [ + if (socketStatus == SocketStatus.reconnecting) + Container( + height: 35, + width: double.maxFinite, + alignment: Alignment.center, + color: kErrorColor.withOpacity(0.35), + child: const Text( + '网络中断,正在重新连接......', + style: TextStyle( + fontSize: 16, + color: kErrorColor, + ), + ), ), - ); - }, - child: ValueListenableBuilder( - valueListenable: - Hive.box('chat_setting').listenable(), - builder: (context, Box box, _) { - final List openedChat = - box.values.where((element) => element.isOpen).toList(); - - // latestMsg on the top - openedChat.sort( - (a, b) => b.latestDateTime.compareTo(a.latestDateTime), - ); - - if (openedChat.isEmpty) { - return const Center( - child: Text( - '没有最新消息', - style: TextStyle( - fontSize: 18, - letterSpacing: 5.0, - ), + if (socketStatus == SocketStatus.error) + Container( + height: 35, + width: double.maxFinite, + alignment: Alignment.center, + color: kErrorColor.withOpacity(0.35), + child: const Text( + '网络异常,请下划尝试重新连接!', + style: TextStyle( + fontSize: 16, + color: kErrorColor, ), - ); - } else { - return ListView.builder( - physics: const BouncingScrollPhysics( - parent: AlwaysScrollableScrollPhysics(), - ), - itemCount: openedChat.length, - itemBuilder: (BuildContext context, int index) { - String contactId = openedChat[index].contactId; - String showedTime = formatTileDateTime( - openedChat[index].latestDateTime, - ); - int unreadCount = openedChat[index].unreadCount; + ), + ), + Expanded( + child: RefreshIndicator( + onRefresh: () async { + String userId = getIt.get().id; + if (socketStatus == SocketStatus.error) { + getIt.get().connect(userId, false); + } + await _getUnreceivedMsg(userId); + }, + child: ValueListenableBuilder( + valueListenable: + Hive.box('chat_setting').listenable(), + builder: (context, Box box, _) { + final List openedChat = box.values + .where((element) => element.isOpen) + .toList(); - return ValueListenableBuilder( - valueListenable: - Hive.box('message_$contactId') - .listenable(), - builder: (context, messageTBox, _) { - int length = messageTBox.length; - if (length > 0) { - MessageT messageT = - messageTBox.getAt(length - 1)!; - if (openedChat[index].type == 0) { - return FriendChatTile( - key: ValueKey(contactId), - index: index, - contactId: contactId, - senderId: messageT.senderId, - messageType: messageT.type, - text: messageT.text, - attachments: messageT.attachments, - dateTime: showedTime, - isShowTime: messageT.isShowTime, - unreadCount: unreadCount, - ); - } else { - return GroupChatChatTile( - key: ValueKey(contactId), - index: index, - contactId: contactId, - senderId: messageT.senderId, - messageType: messageT.type, - text: messageT.text, - attachments: messageT.attachments, - dateTime: showedTime, - isShowTime: messageT.isShowTime, - unreadCount: unreadCount, - ); - } - } else { - return const SizedBox(); - } - }, + // latestMsg on the top + openedChat.sort( + (a, b) => + b.latestDateTime.compareTo(a.latestDateTime), ); + + if (openedChat.isEmpty) { + return const Center( + child: Text( + '没有最新消息', + style: TextStyle( + fontSize: 18, + letterSpacing: 5.0, + ), + ), + ); + } else { + return ListView.builder( + physics: const BouncingScrollPhysics( + parent: AlwaysScrollableScrollPhysics(), + ), + itemCount: openedChat.length, + itemBuilder: (BuildContext context, int index) { + String contactId = openedChat[index].contactId; + String showedTime = formatTileDateTime( + openedChat[index].latestDateTime, + ); + int unreadCount = openedChat[index].unreadCount; + + return ValueListenableBuilder( + valueListenable: + Hive.box('message_$contactId') + .listenable(), + builder: (context, messageTBox, _) { + int length = messageTBox.length; + if (length > 0) { + MessageT messageT = + messageTBox.getAt(length - 1)!; + if (openedChat[index].type == 0) { + return FriendChatTile( + key: ValueKey(contactId), + index: index, + contactId: contactId, + senderId: messageT.senderId, + messageType: messageT.type, + text: messageT.text, + attachments: messageT.attachments, + dateTime: showedTime, + isShowTime: messageT.isShowTime, + unreadCount: unreadCount, + ); + } else { + return GroupChatChatTile( + key: ValueKey(contactId), + index: index, + contactId: contactId, + senderId: messageT.senderId, + messageType: messageT.type, + text: messageT.text, + attachments: messageT.attachments, + dateTime: showedTime, + isShowTime: messageT.isShowTime, + unreadCount: unreadCount, + ); + } + } else { + return const SizedBox(); + } + }, + ); + }, + ); + } }, - ); - } - }, - ), + ), + ), + ), + ], ), ); } else {