diff --git a/lib/constant.dart b/lib/constant.dart index 3fb704e3..ca0bfd1a 100644 --- a/lib/constant.dart +++ b/lib/constant.dart @@ -41,6 +41,8 @@ T max(Iterable list, int Function(T) key) { return tt; } + + extension ListExtension on Iterable { Map> lGroupBy(S Function(T) key) { diff --git a/lib/im/SocketClient.dart b/lib/im/SocketClient.dart index ee2bd1e9..80c012d8 100644 --- a/lib/im/SocketClient.dart +++ b/lib/im/SocketClient.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:convert'; import 'dart:core'; import 'dart:core'; @@ -37,16 +38,29 @@ class SocketClient { }, onError: (Object error, StackTrace stackTrace) { debugPrint("socket-listen-error: $error, stackTrace: $stackTrace"); + }, onDone: () { + debugPrint("socket-listen-down: down"); }); authRequest(shared.getString("token")); }).catchError((error) { debugPrint("socket-connect-error: $error"); + reconnectTime = 1500; + _socket = null; reconnect(); }); } + heartbeat() { + Timer.periodic(const Duration(milliseconds: 3000), (timer) { + Uint8List data = utf8.encode(jsonEncode({"heartbeat": DateTime.now().millisecondsSinceEpoch})); + MsgData msgData = MsgData(to: "0", from: userId, type: MsgType.TEXT, data: data); + final proto2 = Proto(5, 1, msgData.writeToBuffer()); + _socket.add(proto2.toBytes()); + }); + } + int reconnectTime = 1500; reconnect() { @@ -68,6 +82,7 @@ class SocketClient { dispose() { if (_socket != null) { + _socket = null; _socket.close(); } } @@ -109,6 +124,7 @@ class SocketClient { checkSocket() { if (_socket == null) { + reconnectTime = 1500; reconnect(); return false; } diff --git a/lib/im/chat_details_page.dart b/lib/im/chat_details_page.dart index 769a04a1..4f254ed2 100644 --- a/lib/im/chat_details_page.dart +++ b/lib/im/chat_details_page.dart @@ -67,11 +67,15 @@ class _ChatDetailsPage extends State List messages = []; loadMessageList() async { + ImUser imUser = await hxDatabase.queryImUserById(_toUser.mid); + if (imUser == null) { + await hxDatabase.insertOrUpdateImUser(_toUser.toJson()); + } selfUserId = (await SharedPreferences.getInstance()).getString("userId"); // unread msg 2 read state await hxDatabase.readMessage(selfUserId, _toUser.mid); - messages = await hxDatabase.queryUList(_toUser.mid); + messages = await hxDatabase.queryUList(_toUser.mid, pageSize: 100); socketClient.addCallback(_toUser.mid, (Message message) { messages.add(message); @@ -98,7 +102,6 @@ class _ChatDetailsPage extends State commentFocus.addListener(_focusNodeListener); loadMessageList(); - } void jumpToBottom() { @@ -259,21 +262,21 @@ class _ChatDetailsPage extends State leading: true, leadingColor: Colors.black, action: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { - setState(() { - Navigator.of(context).pushNamed('/router/chat_setting'); - copyIndex = 0; - }); - }, - child: Container( - alignment: Alignment.center, - child: Icon( - Icons.more_horiz, - color: Colors.black, - size: 30, - ), + behavior: HitTestBehavior.opaque, + onTap: () { + setState(() { + Navigator.of(context).pushNamed('/router/chat_setting'); + copyIndex = 0; + }); + }, + child: Container( + alignment: Alignment.center, + child: Icon( + Icons.more_horiz, + color: Colors.black, + size: 30, ), + ), ), ), body: Container( @@ -286,17 +289,17 @@ class _ChatDetailsPage extends State child: Column( children: [ GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: () { - setState(() { - emojiShowing = false; - isKeyBoardShow = emojiShowing; - moreShow = false; - isKeyBoardShow = moreShow; - FocusScope.of(context).requestFocus(FocusNode()); - }); - }, - child: chatDetailsList(), + behavior: HitTestBehavior.translucent, + onTap: () { + setState(() { + emojiShowing = false; + isKeyBoardShow = emojiShowing; + moreShow = false; + isKeyBoardShow = moreShow; + FocusScope.of(context).requestFocus(FocusNode()); + }); + }, + child: chatDetailsList(), ), ], ), @@ -344,7 +347,7 @@ class _ChatDetailsPage extends State child: Column( children: [ Text( - "3月08日 上午 12:10", + "${DateTime.fromMillisecondsSinceEpoch(int.parse(message.time))}", textAlign: TextAlign.center, style: TextStyle( color: Color(0xFFA29E9E), @@ -352,23 +355,23 @@ class _ChatDetailsPage extends State fontWeight: MyFontWeight.regular, ), ), - if (messages.indexOf(message) == (messages.length - 1)) - Padding( - padding: EdgeInsets.only(top: 10.h, bottom: 24.h), - child: Text( - "在对方未回复或关注你之前,你只能发送一条信息", - textAlign: TextAlign.center, - style: TextStyle( - color: Color(0xFFA29E9E), - fontSize: 10.sp, - fontWeight: MyFontWeight.regular, - ), - ), - ), - if (messages.indexOf(message) == (messages.length - 1)) - SizedBox( - height: 16.h, - ), + // if (messages.indexOf(message) == (messages.length - 1)) + // Padding( + // padding: EdgeInsets.only(top: 10.h, bottom: 24.h), + // child: Text( + // "在对方未回复或关注你之前,你只能发送一条信息", + // textAlign: TextAlign.center, + // style: TextStyle( + // color: Color(0xFFA29E9E), + // fontSize: 10.sp, + // fontWeight: MyFontWeight.regular, + // ), + // ), + // ), + // if (messages.indexOf(message) == (messages.length - 1)) + // SizedBox( + // height: 16.h, + // ), if (copyIndex == 1) Stack( alignment: Alignment.bottomCenter, @@ -382,7 +385,8 @@ class _ChatDetailsPage extends State borderRadius: BorderRadius.circular(6), ), padding: EdgeInsets.symmetric( - horizontal: 32.w, vertical: 7.5.h, + horizontal: 32.w, + vertical: 7.5.h, ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -454,13 +458,17 @@ class _ChatDetailsPage extends State ), /// not self - if (!isSelf && !isText) + if (!isSelf && isText) + SizedBox( + height: 10.h, + ), + if (!isSelf && isText) Padding( padding: EdgeInsets.only(left: 17.w, right: 39.w), child: Row( children: [ MImage( - _toUser.avatar, + _toUser.avatar, isCircle: true, height: 44.h, width: 44.h, @@ -472,45 +480,48 @@ class _ChatDetailsPage extends State width: 12.w, ), Expanded( + child: Container( + alignment: Alignment.centerLeft, child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(6), - color: Color(0xFFFFFFFF), - boxShadow: [ - BoxShadow( - color: Color(0xFFA8A3A3).withAlpha(12), - offset: Offset(0, 4), - blurRadius: 4, - spreadRadius: 0, - ) - ], + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(6), + color: Color(0xFFFFFFFF), + boxShadow: [ + BoxShadow( + color: Color(0xFFA8A3A3).withAlpha(12), + offset: Offset(0, 4), + blurRadius: 4, + spreadRadius: 0, ), - padding: EdgeInsets.symmetric( - vertical: 8.h, horizontal: 12.w), - child: GestureDetector( - onLongPress: () { - setState(() { - copyIndex = 1; - }); - }, - child: Text( - tex = message.content, - textAlign: TextAlign.left, - style: TextStyle( - height: 1.2.h, - color: Color(0XFF0D0D0D), - fontSize: 17.sp, - fontWeight: MyFontWeight.medium, - ), - ), - ))), + ], + ), + padding: EdgeInsets.symmetric( + vertical: 8.h, + horizontal: 12.w, + ), + child: GestureDetector( + onLongPress: () { + setState(() { + copyIndex = 1; + }); + }, + child: Text( + tex = message.content, + textAlign: TextAlign.left, + style: TextStyle( + height: 1.2.h, + color: Color(0XFF0D0D0D), + fontSize: 17.sp, + fontWeight: MyFontWeight.medium, + ), + ), + ), + ),), + ), ], ), ), - if (!isSelf && isText) - SizedBox( - height: 40.h, - ), + if (copyIndex == 1) Stack( alignment: Alignment.bottomCenter, @@ -595,6 +606,10 @@ class _ChatDetailsPage extends State ), /// self + if (isSelf && isText) + SizedBox( + height: 10.h, + ), if (isSelf && isText) Padding( padding: EdgeInsets.only(left: 36.w, right: 16.w), @@ -630,9 +645,19 @@ class _ChatDetailsPage extends State decoration: BoxDecoration( borderRadius: BorderRadius.circular(6), color: Color(0xFF32A060), + boxShadow: [ + BoxShadow( + color: Color(0xFFA8A3A3).withAlpha(12), + offset: Offset(0, 4), + blurRadius: 4, + spreadRadius: 0, + ), + ], + ), + padding: EdgeInsets.symmetric( + vertical: 8.h, + horizontal: 12.w, ), - padding: - EdgeInsets.symmetric(vertical: 8.h, horizontal: 12.w), child: GestureDetector( onLongPress: () { setState(() { @@ -732,19 +757,19 @@ class _ChatDetailsPage extends State ), /// no reply long time - if (messages.indexOf(message) == 0) - Padding( - padding: EdgeInsets.only(left: 17.w, right: 17.w, top: 24.h), - child: Text( - "由于对方没有回复你,你只能发送一条消息,需要对方关注或回复后才能恢复正常聊天", - textAlign: TextAlign.center, - style: TextStyle( - color: Color(0xFFA29E9E), - fontSize: 10.sp, - fontWeight: MyFontWeight.regular, - ), - ), - ), + // if (messages.indexOf(message) == 0) + // Padding( + // padding: EdgeInsets.only(left: 17.w, right: 17.w, top: 24.h), + // child: Text( + // "由于对方没有回复你,你只能发送一条消息,需要对方关注或回复后才能恢复正常聊天", + // textAlign: TextAlign.center, + // style: TextStyle( + // color: Color(0xFFA29E9E), + // fontSize: 10.sp, + // fontWeight: MyFontWeight.regular, + // ), + // ), + // ), ], ), ); @@ -784,14 +809,21 @@ class _ChatDetailsPage extends State if (commentText.trim() == "") { return; } - socketClient.sendMessage(_toUser.mid, commentText).then((value) { + socketClient + .sendMessage(_toUser.mid, commentText) + .then((value) { Message message = value; messages.insert(0, message); chatController.clear(); - if (mounted) - setState(() {}); + if (mounted) setState(() {}); + + if (scrollController.position != null) { + double offset = scrollController.position.maxScrollExtent; + debugPrint("offset: $offset"); + scrollController.animateTo(offset + 50, duration: const Duration(milliseconds: 500), curve: Curves.easeIn); + } + }); - // widget.queryMemberComment(commentText); }, maxLines: 8, minLines: 1, diff --git a/lib/im/database/hx_database.dart b/lib/im/database/hx_database.dart index e0338bb7..2af0d7a8 100644 --- a/lib/im/database/hx_database.dart +++ b/lib/im/database/hx_database.dart @@ -38,7 +38,7 @@ class HxDatabase { } Future lastMessage(String userId) async { - if (db == null) { + if (db == null || !db.isOpen) { return Future.value(); } String sql = @@ -56,7 +56,7 @@ class HxDatabase { } Future> queryList(userId) { - if (db == null) { + if (db == null || !db.isOpen) { return Future.value([]); } String sql = @@ -72,7 +72,7 @@ class HxDatabase { } Future> queryUList(userId, {int page = 1, int pageSize = 10}) { - if (db == null) { + if (db == null || !db.isOpen) { return Future.value([]); } int start = (page - 1) * pageSize; @@ -86,7 +86,7 @@ class HxDatabase { } Future> messageUnreadCount(List userIds, String selfUserId) async { - if (db == null) { + if (db == null || !db.isOpen) { return Future.value({}); } List messages = await db.query( @@ -102,7 +102,7 @@ class HxDatabase { } Future> queryListAll() { - if (db == null) { + if (db == null || !db.isOpen) { return Future.value(); } String sql = 'SELECT * FROM Message ORDER BY time DESC'; @@ -114,7 +114,7 @@ class HxDatabase { } update(Map message) { - if (db == null) { + if (db == null || !db.isOpen) { return Future.value(0); } debugPrint("Message_insert: $message"); @@ -123,21 +123,23 @@ class HxDatabase { } Future insert(Map message) async { - if (db == null) { + if (db == null || !db.isOpen) { return Future.value(0); } debugPrint("Message_insert: $message"); return db.insert("Message", message); } + /// update message read state readMessage(String selfUserId, String userId) { - if (db == null) { + if (db == null || !db.isOpen) { return Future.value([]); } db.update("Message", {"state": 1}, where: "fromId = ? AND toId = ? AND state = 0 AND isDelete = 0", whereArgs: [userId, selfUserId]); } + Future insertOrUpdateImUser(Map imUserMap) async { - if (db == null) { + if (db == null || !db.isOpen) { return Future.value(0); } debugPrint("imUser_insert: $imUserMap"); @@ -149,7 +151,7 @@ class HxDatabase { } Future> queryImUser(List userIds) async { - if (db == null) { + if (db == null || !db.isOpen) { return Future.value([]); } String query = 'SELECT * FROM ImUser WHERE mid IN (${userIds.map((mid) => "'$mid'").join(',')})'; @@ -161,7 +163,7 @@ class HxDatabase { } Future queryImUserById(String userId) async { - if (db == null) { + if (db == null || !db.isOpen) { return Future.value(); } List imUser = await db.query("ImUser", @@ -204,4 +206,5 @@ class HxDatabase { await migration.migrate(migrationDatabase); } } + } diff --git a/lib/im/im_view/im_page.dart b/lib/im/im_view/im_page.dart index 60c1e81d..fbf7c6a9 100644 --- a/lib/im/im_view/im_page.dart +++ b/lib/im/im_view/im_page.dart @@ -74,25 +74,17 @@ class _IMPage extends State implements OnChatMessage { super.initState(); OnChatMsgInstance.instance.onChatMessage = this; - loadMessageList(); + initSocketClient(); + SharedPreferences.getInstance().then((value) { - apiService = - ApiService(Dio(), token: value.getString("token"), context: context); - queryMsgStats(); + apiService = ApiService(Dio(), token: value.getString("token"), context: context); + // queryMsgStats(); }); } - _refresh() { - pageNum = 1; - loadMessageList(); - queryMsgStats(); - } - - loadMessageList() async { + initSocketClient() async { SharedPreferences shared = await SharedPreferences.getInstance(); String userId = shared.getString("userId"); - - debugPrint("messages: loadMessageList"); socketClient.addCallback(userId, (Message message) { if (userIds.contains(message.fromId)) { userIds.remove(message.fromId); @@ -101,28 +93,62 @@ class _IMPage extends State implements OnChatMessage { lastMessageMap[message.fromId] = message; + sortConversation(lastMessageMap); + debugPrint("messages_records : ${message.toJson()}"); if (contactMap[message.fromId] == null) { queryMemberInfo([message.fromId]); return; } - if (mounted) { - setState(() {}); - } + refreshState(); }); - debugPrint("messages: queryList"); + loadMessageList(); + } + + _refresh() { + pageNum = 1; + loadMessageList(); + // queryMsgStats(); + } + + loadMessageList() async { + SharedPreferences shared = await SharedPreferences.getInstance(); + String userId = shared.getString("userId"); messages = await hxDatabase.queryList(userId); - userIds = messages + lastMessageMap = messages.lGroupBy((p0) => p0.toId != userId ? p0.toId : p0.fromId).mGroupItem(key: (p1) => num.parse(p1.time)); + + await sortConversation(lastMessageMap); + + await queryUnreadCount(userIds, userId); + + await queryImUserInfo(userIds); + + refreshState(); + } + + /// update conversation by time sort + sortConversation(lastMessageMap) async { + SharedPreferences shared = await SharedPreferences.getInstance(); + String userId = shared.getString("userId"); + List sortMessages = lastMessageMap.values.toList(); + sortMessages.sort((a, b) => (num.parse(b.time)).compareTo(num.parse(a.time))); + userIds = sortMessages .map((e) => e.toId != userId ? e.toId : e.fromId) .toSet() .where((element) => element != userId) .toList(); - List contacts = (await hxDatabase.queryImUser(userIds)) ?? []; + } + + /// update conversation unreadcount + queryUnreadCount(userIds, userId) async { unreadCountMap = await hxDatabase.messageUnreadCount(userIds, userId); - lastMessageMap = messages.lGroupBy((p0) => p0.toId != userId ? p0.toId : p0.fromId).mGroupItem(key: (p1) => num.parse(p1.time)); + } + /// update imuser info by mids + queryImUserInfo(userIds) async { + List contacts = (await hxDatabase.queryImUser(userIds)) ?? []; if (contacts?.isEmpty ?? true) { await queryMemberInfo(userIds); return; @@ -134,17 +160,15 @@ class _IMPage extends State implements OnChatMessage { } } contactMap = contacts.lGroupBy((p0) => p0.mid).mGroupItem(); - - if (mounted) { - setState(() {}); - } } + /// update one conversation last message ,and update conversation sort void updateLastMessage(String userId) async { Message message = await hxDatabase.lastMessage(userId); debugPrint("lastmessage: $userId ${message.content} ${message.toJson()}"); if (message != null) { lastMessageMap[userId] = message; + await sortConversation(lastMessageMap); refreshState(); } } @@ -197,34 +221,34 @@ class _IMPage extends State implements OnChatMessage { // } // } - queryMsgStats() async { - if (apiService == null) { - SharedPreferences value = await SharedPreferences.getInstance(); - apiService = ApiService( - Dio(), - context: context, - token: value.getString("token"), - ); - } - BaseData> baseData = await apiService.stats().catchError((onError) { - debugPrint("stats.error: $onError"); - }); - if (baseData != null && baseData.isSuccess) { - setState(() { - msgNumber.forEach((key, value) { - msgNumber[key] = 0; - }); - baseData.data.forEach((element) { - if (msgNumber.containsKey(element.name)) { - msgNumber[element.name] = element.number; - } - }); - }); - _refreshController.loadComplete(); - _refreshController.refreshCompleted(); - } - EasyLoading.dismiss(); - } + // queryMsgStats() async { + // if (apiService == null) { + // SharedPreferences value = await SharedPreferences.getInstance(); + // apiService = ApiService( + // Dio(), + // context: context, + // token: value.getString("token"), + // ); + // } + // BaseData> baseData = await apiService.stats().catchError((onError) { + // debugPrint("stats.error: $onError"); + // }); + // if (baseData != null && baseData.isSuccess) { + // setState(() { + // msgNumber.forEach((key, value) { + // msgNumber[key] = 0; + // }); + // baseData.data.forEach((element) { + // if (msgNumber.containsKey(element.name)) { + // msgNumber[element.name] = element.number; + // } + // }); + // }); + // _refreshController.loadComplete(); + // _refreshController.refreshCompleted(); + // } + // EasyLoading.dismiss(); + // } ///批量查询用户信息 queryMemberInfo(List mids) async { @@ -242,8 +266,12 @@ class _IMPage extends State implements OnChatMessage { baseData.data.forEach((element) async { await hxDatabase.insertOrUpdateImUser(element.toJson()); }); - contactMap = baseData.data.lGroupBy((p0) => p0.mid).mGroupItem(); - setState(() {}); + baseData.data.forEach((element) { + if (contactMap[element.mid] == null) { + contactMap[element.mid] = element; + } + }); + refreshState(); } } else { SmartDialog.showToast(baseData.msg, alignment: Alignment.center); @@ -270,9 +298,7 @@ class _IMPage extends State implements OnChatMessage { controller: _refreshController, onRefresh: _refresh, onLoading: () { - setState(() { - _refresh(); - }); + _refresh(); }, child: Container( // color: Colors.white,