From 9feded7369ad60464877a3d6af472fe342409e12 Mon Sep 17 00:00:00 2001 From: Jolie <> Date: Tue, 9 Dec 2025 23:18:57 +0800 Subject: [PATCH] =?UTF-8?q?=E8=81=8A=E5=A4=A9=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/images/filter_icon.png | Bin 0 -> 402 bytes assets/images/last_msg_icon.png | Bin 0 -> 857 bytes assets/images/online_msg_icon.png | Bin 0 -> 831 bytes assets/images/unread_icon.png | Bin 0 -> 892 bytes lib/controller/message/chat_controller.dart | 88 +++++++- .../message/conversation_controller.dart | 135 +++++++++-- lib/generated/assets.dart | 5 +- lib/im/im_manager.dart | 154 ++++++++++++- lib/pages/message/chat_page.dart | 211 +++++++++++++++++- lib/pages/message/conversation_tab.dart | 34 ++- lib/pages/message/message_page.dart | 130 ++++++++++- lib/widget/message/message_item.dart | 10 + lib/widget/message/voice_item.dart | 4 + 13 files changed, 710 insertions(+), 61 deletions(-) create mode 100644 assets/images/filter_icon.png create mode 100644 assets/images/last_msg_icon.png create mode 100644 assets/images/online_msg_icon.png create mode 100644 assets/images/unread_icon.png diff --git a/assets/images/filter_icon.png b/assets/images/filter_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..0df962092cfcd52faa248b53cbdc715d2b7550bf GIT binary patch literal 402 zcmV;D0d4+?P)Px#1am@3R0s$N2z&@+hyVZqJV``BR9J=WnL%=dAP_}=PjU;7VedmuCLU!@GGyQP z@ED!NIH}1LRy8O>CA_7D@JfMiBLM(2J5`;C=nDWu#G@j_%wAQeUl)l!MI=;m!sS(H z#hoqk%%$ewTXZQaUCK(AveKojG%59-b5krYkAR3~5%I}VrGx-LRVQXne|@Edp8L<4 zQ{iZlSCXY3AA+SIqB(g^iHK)r0D!7~94#awA#3%O{#aIz=Zrkh%JRviaBK zKGmhHG%1xm(V5v<+om2>W@n$Ox`g&WHJzpXY1y~dbf)`iXJ3^6oqdn;$Px#1am@3R0s$N2z&@+hyVZs14%?dRA_LDu1Fb*4&*F=FK)B@kSbd;6fm|UL z8Wt|Oz*v>?H(0X;{J`Zvl^;l#>)*A7{ zGUZ>tf4xihE1MDKwGbEpsAe(ZX7=Ez!*Q9jDx1a=C+m;zR`dV@kmTl2n6YGol0JM3 z$b=za_e>bA!*QAGEJBNcjZVF^8bBuTloNoD_NT?xgreK5;eZNA3bmMEv|2V1Cv`80 zlLGLQB*rM0mnxuF@Q&90wAh+SulAAxGu*=dSY0Yw_D)Y%K8d<7TZJ-#k0$-W7GSiz1Nf=|+eA2f$ zvSa|Tl>|;P<@HHzKKTsi*bvFmA{i9&#O*Qk>YAG_8(TztVfNcx3i6H-VRP$`BWdca z2s`J&)|2J3tpaL!3GyEIv(>d3+89HUmtunQ*$iy} z`R*S-Ls?oR(!RTSpO^+q=c&&;fsoFKZ2Ru^1FuM+Y$u6dr-f*{HQSaJTPoFg$}#o1 zx^5+y)>%%6j`h7>gyCSF4N#6lV!;-F`LxCm@ugH{?$a6r zba!$6wbq!rI>AhZG%$IFn5p3QGCkXkp7RBQXbo-v1ZvF{0~!FwXn+Dn0;;RNPE{qc j(V_|53o@Uv)>nT3w;vDwd}V1y00000NkvXXu0mjfuSSb4 literal 0 HcmV?d00001 diff --git a/assets/images/online_msg_icon.png b/assets/images/online_msg_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..bc28da084ea29ac6573ea32ea9160da290445e41 GIT binary patch literal 831 zcmV-F1Hk-=P)Px#1am@3R0s$N2z&@+hyVZr=}AOERA_2dx+#mL*QSQy>qc6QN%oW2mup;Mo1|Y08G>LHD6NRZ4CfIhzkI1jM1&} zMD2r8N;Q*IMFE948)Htjaf=Dp5?uwZB>t;p@l5s`z%j5y1Gowy{>(B!WUU&8K{)4h z35V72X9o_Ee&opX8^27$GdiZY#* z;c*-%aw(bB7`OxoA>MQLOA_Wbx7Joymj&ZEeuI=iYwdgLxh{4ykco2@LjqNe2$~J%`v$s@*N5z{I$dMoP=U3^PAZaFZ>O%5t_OycfZyC_NKF z!qYUpwbR)iC5KKwf&ime6 zL=viv3Xjri6l^b@bJxD_6}$$#rs0lZemQ;k*HR5oG{fAxo;f7dsA;UCgy$NmV?k=c zb!It@RZ_#XZD0&7ZEJEwa?V|u(MI)suZCgxn;MS@QFS;ReuLqfE5x__8 zL5xh?+FJXW!viSvB_fkE@H6LKfFCV-lkywFB{6F(@=;@?#7O4{uAE+~CJ686twnKW z7nDk36PxN5ydullgqgA2hn@*syuWX2Q#vm3II4UJLNYtpycbJ8H``Id5sNCzrK#9A zNjgY4^fubcmk7l`;y|*5_aN)V;wny%^onqcQP%>-P+=3aB23+$J;-{olP=I*w?@0L z7E84TIbPeX`JqyHlxAaKI>RGvu5Oap!}!Yum4?ry19&Oy#Ek`w?~i5d#Id&&nD`pr zXy!|`qZgi?-35e)TC&`gT19&yWD<5DjmVy5ECYJ^vKTHMab7fD2*kEh06BDza@iZ8 z>4lXfUL8;Ohf5%(RR0xWV~p5-s|G%Zn=Re@M#21=2#~J_t002ov JPDHLkV1h;9a$Eoa literal 0 HcmV?d00001 diff --git a/assets/images/unread_icon.png b/assets/images/unread_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..8786cf5f8e7897dbfdf853c4d6085cf020116e60 GIT binary patch literal 892 zcmV-?1B3jDP)Px#1am@3R0s$N2z&@+hyVZsCP_p=RA_IpQ+%%Rf8O96~AcL;UY+NuUAL6+wG@oF#t zKcg;7M8BMKpX+GmF${xk+mYT_5%-qOc!fxyM`bIi2?=4v^E=|~8R=vfOhFLV~uk&s$oG{7t#nf>W3 zh!>YRlia+|=Tj3O^;?tE?m1(O=Qa_&U*eY<^T`EXYhjE55xpw}v54ILY!5795Ar2r z$7v~mz7aXBRSdA(CKu#%-^4bhvHznXk}F$mLTAH&A%NwFJ>26C=(#~ekAGhS(k&fD zq!yEdOt@4PxTIK1pfqrlVb29$dw@HOh!|t;-ut3&5@Wn+Q`X=@$7ko9ZDf>JxRFc# zn?&K^HO|Ke>V+S8C4-8ra`Ib^*$JrT0@9(zc=F09?@SpPFb^U*YKYya<;I=?TPdqR&K0@38%lD)q>7V=<}s-rtWf zr&N75b?D8i)59?QZrk>OX=VClg?MX}6qmCJOzKuhw?*RbmH zLS@E5LRCVEHL+#d8Qm>cT`vL3OFm8B9J}0{nTfk)AQMZKl=Uyme_4i%N{unxwBff0 zfH;y&q2@kfwE(G$zzDhi7?y}0wFTvB0bbl4YcQVrYrR^P02vp1bIx5w_`z+<9b3EM z;!PZKfTFwHJyEQI1o=aAhgD^jqsasHoA}!=zH_M2z-h<>N&(pA9d&E}3V#4zyZrDs Sx>RBS0000 giftProducts = [].obs; + // 需要显示508错误提示的消息ID集合(临时状态,不持久化) + final Set _roseErrorMessageIds = {}; + // 网络服务 final NetworkService _networkService = NetworkService(); + + /// 检查消息是否需要显示508错误提示 + bool shouldShowRoseError(String messageId) { + return _roseErrorMessageIds.contains(messageId); + } + + /// 添加需要显示508错误提示的消息ID + void addRoseErrorMessageId(String messageId) { + _roseErrorMessageIds.add(messageId); + update(); + } ChatController({ required this.userId, @@ -356,12 +370,17 @@ class ChatController extends GetxController { // 发送失败,更新消息状态 final index = messages.indexWhere((msg) => msg.msgId == tempMessage!.msgId); if (index != -1) { - // 等待一下让 SDK 更新状态为 FAIL - Future.delayed(Duration(milliseconds: 300), () { - update(); - }); + // 注意:EMMessage对象的状态是只读的,需要通过SDK的onError回调来更新 + // 这里先更新UI,等待onError回调更新实际的消息对象 + update(); + + // 刷新会话列表,确保聊天列表能显示失败状态 + _refreshConversationList(); + + if (Get.isLogEnable) { + Get.log('❌ [ChatController] 消息发送失败,等待onError回调更新状态: msgId=${tempMessage.msgId}'); + } } - update(); SmartDialog.showToast('消息发送失败,请点击重发'); return false; } @@ -684,16 +703,50 @@ class ChatController extends GetxController { } } } else { - // 刷新时替换整个列表 - messages.assignAll(validMessages); + // 刷新时替换整个列表,但需要去重(处理重发消息的情况) + // 对于相同内容的消息,只保留最新的(msgId更大的) + // 重新进入页面时,清空临时错误提示状态(不持久化) + _roseErrorMessageIds.clear(); + + final Map contentToMessage = {}; + + for (var msg in validMessages) { + // 生成消息的唯一标识(基于内容和时间戳) + String contentKey; + if (msg.body.type == MessageType.TXT) { + final textBody = msg.body as EMTextMessageBody; + contentKey = '${msg.direction}_${msg.serverTime}_${textBody.content}'; + } else { + // 对于其他类型的消息,使用时间戳和类型作为key(处理重发时msgId改变的情况) + contentKey = '${msg.direction}_${msg.serverTime}_${msg.body.type}'; + } + + // 如果已存在相同内容的消息,比较msgId,保留更大的(更新的) + if (contentToMessage.containsKey(contentKey)) { + final existingMsg = contentToMessage[contentKey]!; + // 比较msgId,保留更大的(通常更新的消息ID更大) + if (msg.msgId.compareTo(existingMsg.msgId) > 0) { + contentToMessage[contentKey] = msg; + } + } else { + contentToMessage[contentKey] = msg; + } + } + + // 将去重后的消息列表转换为列表并排序 + final deduplicatedMessages = contentToMessage.values.toList(); + // 按时间戳排序(从旧到新) + deduplicatedMessages.sort((a, b) => a.serverTime.compareTo(b.serverTime)); + + messages.assignAll(deduplicatedMessages); // 更新游标为最旧的消息ID(列表开头) - if (validMessages.isNotEmpty) { - _cursor = validMessages.first.msgId; + if (deduplicatedMessages.isNotEmpty) { + _cursor = deduplicatedMessages.first.msgId; } else { _cursor = null; } if (Get.isLogEnable) { - Get.log('刷新消息成功,数量: ${validMessages.length}'); + Get.log('刷新消息成功,去重前: ${validMessages.length} 条,去重后: ${deduplicatedMessages.length} 条'); } } @@ -969,6 +1022,20 @@ class ChatController extends GetxController { return false; } + // 先删除SDK中的旧消息(避免重新加载时出现重复) + try { + final conversationId = failedMessage.conversationId ?? userId; + await IMManager.instance.deleteMessage(conversationId, failedMessage.msgId); + if (Get.isLogEnable) { + Get.log('✅ [ChatController] 已删除SDK中的旧消息: ${failedMessage.msgId}'); + } + } catch (e) { + // 如果删除失败,记录日志但继续重发流程 + if (Get.isLogEnable) { + Get.log('⚠️ [ChatController] 删除SDK中的旧消息失败: $e,继续重发'); + } + } + // 调用IMManager的重发方法 final newMessage = await IMManager.instance.resendMessage(failedMessage); if (newMessage != null) { @@ -1086,6 +1153,7 @@ class ChatController extends GetxController { } else { // 状态仍然是 PROGRESS,但消息对象可能已更新(SDK内部状态可能已变化) // 更新消息对象以确保使用最新的消息数据 + messages[currentIndex] = latestMessage; update(); } diff --git a/lib/controller/message/conversation_controller.dart b/lib/controller/message/conversation_controller.dart index 25c2890..605d2d2 100644 --- a/lib/controller/message/conversation_controller.dart +++ b/lib/controller/message/conversation_controller.dart @@ -2,6 +2,7 @@ import 'package:get/get.dart'; import 'package:im_flutter_sdk/im_flutter_sdk.dart'; import '../../im/im_manager.dart'; import '../../model/mine/user_base_data.dart'; +import 'chat_controller.dart'; // 扩展类用于存储用户信息(包括业务系统的信息) class ExtendedUserInfo { @@ -157,26 +158,42 @@ class ConversationController extends GetxController { // 如果缓存中已有该用户信息,跳过 if (_userInfoCache.containsKey(targetUserId)) continue; - // 获取会话的最新消息(最多获取最近20条) + // 获取会话的最新消息(增加数量以提高找到用户信息的概率) final messages = await conversation.loadMessages( - loadCount: 20, + loadCount: 50, // 从20增加到50,提高找到用户信息的概率 ); + if (Get.isLogEnable) { + Get.log('🔍 [ConversationController] 开始提取用户信息: userId=$targetUserId, 消息数量=${messages.length}'); + } + // 从消息中提取用户信息 // 接收消息:提取发送者信息(sender_ 前缀) // 发送消息:提取接收者信息(receiver_ 前缀) + // 遍历所有消息,找到最新的包含用户信息的消息 + ExtendedUserInfo? foundUserInfo; for (var message in messages) { Map? attributes; try { attributes = message.attributes; } catch (e) { + if (Get.isLogEnable) { + Get.log(' ⚠️ 无法访问消息 attributes: msgId=${message.msgId}, error=$e'); + } continue; } if (attributes == null || attributes.isEmpty) { + if (Get.isLogEnable) { + Get.log(' ⚠️ 消息 attributes 为空: msgId=${message.msgId}, direction=${message.direction}'); + } continue; } + if (Get.isLogEnable) { + Get.log(' 📨 检查消息: msgId=${message.msgId}, direction=${message.direction}, from=${message.from}, to=${message.to}, attributes keys=${attributes.keys.toList()}'); + } + if (message.direction == MessageDirection.RECEIVE) { // 接收到的消息:从扩展字段中提取发送者信息(sender_ 前缀) final fromUserId = message.from; @@ -186,17 +203,16 @@ class ConversationController extends GetxController { final avatarUrl = attributes['sender_avatarUrl'] as String? ?? attributes['avatarUrl'] as String?; if (nickName != null || avatarUrl != null) { - final extendedInfo = ExtendedUserInfo( + // 如果找到用户信息,保存(但继续遍历以找到最新的) + foundUserInfo = ExtendedUserInfo( userId: targetUserId, nickName: nickName, avatarUrl: avatarUrl, ); - _userInfoCache[targetUserId] = extendedInfo; - if (Get.isLogEnable) { - Get.log('✅ [ConversationController] 从接收消息恢复对方用户信息: userId=$targetUserId, nickName=$nickName'); + Get.log('✅ [ConversationController] 从接收消息找到用户信息: userId=$targetUserId, nickName=$nickName, msgId=${message.msgId}'); } - // 找到一个就足够了,跳出循环 + // 继续遍历,找到最新的消息(因为消息是按时间倒序的,第一个就是最新的) break; } } @@ -209,22 +225,46 @@ class ConversationController extends GetxController { final avatarUrl = attributes['receiver_avatarUrl'] as String?; if (nickName != null || avatarUrl != null) { - final extendedInfo = ExtendedUserInfo( + // 如果找到用户信息,保存(但继续遍历以找到最新的) + foundUserInfo = ExtendedUserInfo( userId: targetUserId, nickName: nickName, avatarUrl: avatarUrl, ); - _userInfoCache[targetUserId] = extendedInfo; - if (Get.isLogEnable) { - Get.log('✅ [ConversationController] 从发送消息恢复对方用户信息: userId=$targetUserId, nickName=$nickName'); + Get.log('✅ [ConversationController] 从发送消息找到用户信息: userId=$targetUserId, nickName=$nickName, msgId=${message.msgId}'); } - // 找到一个就足够了,跳出循环 + // 继续遍历,找到最新的消息(因为消息是按时间倒序的,第一个就是最新的) break; } } } } + + // 如果找到了用户信息,保存到缓存 + if (foundUserInfo != null) { + _userInfoCache[targetUserId] = foundUserInfo; + } + + // 如果从消息中提取不到用户信息,尝试从环信获取(作为备选) + if (!_userInfoCache.containsKey(targetUserId)) { + try { + var data = await IMManager.instance.getContacts(targetUserId); + var emUserInfo = data[targetUserId]; + + if (emUserInfo != null && (emUserInfo.nickName?.isNotEmpty ?? false)) { + final extendedInfo = ExtendedUserInfo.fromEMUserInfo(emUserInfo); + _userInfoCache[targetUserId] = extendedInfo; + if (Get.isLogEnable) { + Get.log('✅ [ConversationController] 从环信获取到用户信息: userId=$targetUserId, nickName=${extendedInfo.nickName}'); + } + } + } catch (e) { + if (Get.isLogEnable) { + Get.log('⚠️ [ConversationController] 从环信获取用户信息失败: $e'); + } + } + } } catch (e) { if (Get.isLogEnable) { Get.log('⚠️ [ConversationController] 从会话提取用户信息失败: ${conversation.id}, 错误: $e'); @@ -256,21 +296,30 @@ class ConversationController extends GetxController { /// 获取会话的最新消息 String getLastMessageContent(EMMessage? message) { - if(message?.body.type == MessageType.TXT){ - final body = message?.body as EMTextMessageBody; + if (message == null) { + return '暂无消息'; + } + + // 检查消息是否发送失败(发送的消息且状态为FAIL) + if (message.direction == MessageDirection.SEND && message.status == MessageStatus.FAIL) { + return '[发送失败]'; + } + + if(message.body.type == MessageType.TXT){ + final body = message.body as EMTextMessageBody; return body.content; - }else if(message?.body.type == MessageType.IMAGE){ + }else if(message.body.type == MessageType.IMAGE){ return '[图片]'; - }else if(message?.body.type == MessageType.VOICE){ + }else if(message.body.type == MessageType.VOICE){ return '[语音]'; - }else if(message?.body.type == MessageType.VIDEO){ + }else if(message.body.type == MessageType.VIDEO){ return '[视频]'; - }else if(message?.body.type == MessageType.FILE){ + }else if(message.body.type == MessageType.FILE){ return '[文件]'; - }else if(message?.body.type == MessageType.LOCATION){ + }else if(message.body.type == MessageType.LOCATION){ return '[位置]'; - }else if(message?.body.type == MessageType.CUSTOM){ - final body = message?.body as EMCustomMessageBody; + }else if(message.body.type == MessageType.CUSTOM){ + final body = message.body as EMCustomMessageBody; // 检查是否是分享房间类型 if(body.event == 'live_room_invite'){ return '[分享房间]'; @@ -339,9 +388,9 @@ class ConversationController extends GetxController { createIfNeed: false, ); if (conversation != null) { - // 获取最近的消息(最多20条),查找包含用户信息的消息 + // 获取最近的消息(增加数量以提高找到用户信息的概率) final messages = await conversation.loadMessages( - loadCount: 20, + loadCount: 50, // 从20增加到50,提高找到用户信息的概率 ); // 从消息中查找用户信息(只查找接收到的消息,因为那是对方的用户信息) @@ -484,7 +533,45 @@ class ConversationController extends GetxController { } Future lastMessage(EMConversation conversation) async{ - return await conversation.latestMessage(); + try { + // 优先尝试从ChatController获取最后一条消息(如果ChatController还在内存中) + // 这样可以获取到失败状态的消息(SDK可能不会保存失败消息) + try { + final tag = 'chat_${conversation.id}'; + if (Get.isRegistered(tag: tag)) { + final chatController = Get.find(tag: tag); + if (chatController.messages.isNotEmpty) { + // 获取最后一条消息(列表末尾,按时间戳排序后最后一条是最新的) + // 注意:messages列表是按时间戳从旧到新排序的,所以last是最新的 + final lastMsg = chatController.messages.last; + if (Get.isLogEnable) { + Get.log('✅ [ConversationController] 从ChatController获取最后一条消息: msgId=${lastMsg.msgId}, status=${lastMsg.status}, direction=${lastMsg.direction}, content=${lastMsg.body.type == MessageType.TXT ? (lastMsg.body as EMTextMessageBody).content : lastMsg.body.type}'); + } + return lastMsg; + } + } + } catch (e) { + if (Get.isLogEnable) { + Get.log('⚠️ [ConversationController] 从ChatController获取最后一条消息失败: $e'); + } + } + + // 如果ChatController不存在或没有消息,从SDK获取 + final sdkMessage = await conversation.latestMessage(); + if (Get.isLogEnable) { + if (sdkMessage != null) { + Get.log('✅ [ConversationController] 从SDK获取最后一条消息: msgId=${sdkMessage.msgId}, status=${sdkMessage.status}, direction=${sdkMessage.direction}'); + } else { + Get.log('⚠️ [ConversationController] SDK返回的最后一条消息为null'); + } + } + return sdkMessage; + } catch (e) { + if (Get.isLogEnable) { + Get.log('⚠️ [ConversationController] 获取最后一条消息失败: $e'); + } + return null; + } } /// 删除会话 diff --git a/lib/generated/assets.dart b/lib/generated/assets.dart index 0b8d28a..f52f6cd 100644 --- a/lib/generated/assets.dart +++ b/lib/generated/assets.dart @@ -211,5 +211,8 @@ class Assets { static const String imagesWallet = 'assets/images/wallet.png'; static const String imagesWechatPay = 'assets/images/wechat_pay.png'; static const String imagesWomenIcon = 'assets/images/women_icon.png'; - static const String imagesAppLogo = 'assets/images/app_logo.png'; + static const String imagesFilterIcon = 'assets/images/filter_icon.png'; + static const String imagesLastMsgIcon = 'assets/images/last_msg_icon.png'; + static const String imagesOnlineMsgIcon = 'assets/images/online_msg_icon.png'; + static const String imagesUnreadIcon = 'assets/images/unread_icon.png'; } diff --git a/lib/im/im_manager.dart b/lib/im/im_manager.dart index bfa8f4f..dc28ec9 100644 --- a/lib/im/im_manager.dart +++ b/lib/im/im_manager.dart @@ -139,6 +139,94 @@ class IMManager { onError: (str, message, err){ //code: 508为拦截 Get.log('❌ [IMManager] 发送消息失败: $err----$str'); + + // 检查错误码是否为508(玫瑰不足) + try { + final errorCode = err.code; + + // 通知 ChatController 更新消息状态 + final targetId = message.to; + if (targetId != null) { + final controller = _activeChatControllers[targetId]; + if (controller != null) { + // 更新消息状态 + final index = controller.messages.indexWhere((msg) => msg.msgId == message.msgId); + if (index != -1) { + // 更新消息对象 + controller.messages[index] = message; + + // 如果是508错误,添加到临时错误提示集合中(不存储到消息attributes) + if (errorCode == 508) { + controller.addRoseErrorMessageId(message.msgId); + if (Get.isLogEnable) { + Get.log('⚠️ [IMManager] 检测到错误码508(玫瑰不足),已添加到临时错误提示集合: msgId=${message.msgId}'); + } + } + + controller.update(); + + // 刷新会话列表,确保聊天列表能显示失败状态 + try { + if (Get.isRegistered()) { + final conversationController = Get.find(); + conversationController.refreshConversations(); + } + } catch (e) { + // ConversationController 可能未注册,忽略错误 + } + + if (Get.isLogEnable) { + Get.log('✅ [IMManager] 已通知ChatController更新消息状态: $targetId, status=FAIL'); + } + } else { + // 如果找不到消息,尝试通过内容匹配(处理消息ID可能改变的情况) + if (message.body.type == MessageType.TXT) { + final textBody = message.body as EMTextMessageBody; + final content = textBody.content; + final contentIndex = controller.messages.indexWhere((msg) => + msg.body.type == MessageType.TXT && + (msg.body as EMTextMessageBody).content == content && + msg.direction == MessageDirection.SEND && + msg.status == MessageStatus.PROGRESS + ); + if (contentIndex != -1) { + // 更新消息对象 + final matchedMessage = controller.messages[contentIndex]; + controller.messages[contentIndex] = message; + + // 如果是508错误,添加到临时错误提示集合中(使用匹配到的消息ID) + if (errorCode == 508) { + controller.addRoseErrorMessageId(matchedMessage.msgId); + if (Get.isLogEnable) { + Get.log('⚠️ [IMManager] 检测到错误码508(玫瑰不足),已通过内容匹配添加到临时错误提示集合: msgId=${matchedMessage.msgId}'); + } + } + + controller.update(); + + // 刷新会话列表,确保聊天列表能显示失败状态 + try { + if (Get.isRegistered()) { + final conversationController = Get.find(); + conversationController.refreshConversations(); + } + } catch (e) { + // ConversationController 可能未注册,忽略错误 + } + + if (Get.isLogEnable) { + Get.log('✅ [IMManager] 已通过内容匹配更新消息状态: $targetId, status=FAIL'); + } + } + } + } + } + } + } catch (e) { + if (Get.isLogEnable) { + Get.log('⚠️ [IMManager] 处理错误码失败: $e'); + } + } }, )); _listenersRegistered = true; @@ -447,6 +535,9 @@ class IMManager { params: data, ); + // 在消息扩展字段中添加当前用户信息 + _addUserInfoToMessageExt(customMsg); + return await EMClient.getInstance.chatManager.sendMessage(customMsg); } @@ -637,6 +728,25 @@ class IMManager { // 注意:这里获取的是接收者自己的信息(用于发送方在聊天列表显示) receiverNickName = receiverChatController.userData!.nickName; receiverAvatarUrl = receiverChatController.userData!.profilePhoto; + } else { + // 如果 ChatController 没有 userData,尝试从 ConversationController 的缓存中获取 + try { + if (Get.isRegistered()) { + final conversationController = Get.find(); + final cachedUserInfo = conversationController.getCachedUserInfo(receiverId); + if (cachedUserInfo != null) { + receiverNickName = cachedUserInfo.nickName; + receiverAvatarUrl = cachedUserInfo.avatarUrl; + if (Get.isLogEnable) { + Get.log('✅ [IMManager] 从ConversationController缓存获取接收者信息: userId=$receiverId, nickName=$receiverNickName'); + } + } + } + } catch (e) { + if (Get.isLogEnable) { + Get.log('⚠️ [IMManager] 从ConversationController获取接收者信息失败: $e'); + } + } } } @@ -657,6 +767,9 @@ class IMManager { final cleanSenderAvatarUrl = senderAvatarUrl.trim().replaceAll('`', ''); message.attributes!['sender_avatarUrl'] = cleanSenderAvatarUrl; } + // 添加发送者在线状态(发送消息时肯定在线) + message.attributes!['sender_isOnline'] = 'true'; + message.attributes!['sender_lastActiveTime'] = DateTime.now().millisecondsSinceEpoch.toString(); // 接收者信息(receiver_ 前缀) if (receiverId != null && receiverId.isNotEmpty) { @@ -961,9 +1074,41 @@ class IMManager { if (targetId != null) { final controller = _activeChatControllers[targetId]; if (controller != null) { - // TODO: 在 ChatController 中添加更新消息状态的方法 - if (Get.isLogEnable) { - Get.log('通知消息状态变更: $targetId, status=$status'); + // 更新消息状态和错误码 + final index = controller.messages.indexWhere((msg) => msg.msgId == message.msgId); + if (index != -1) { + // 更新消息对象 + final updatedMessage = message; + controller.messages[index] = updatedMessage; + controller.update(); + + if (Get.isLogEnable) { + final errorCode = error?.code; + Get.log('✅ [IMManager] 已通知ChatController更新消息状态: $targetId, status=$status, errorCode=$errorCode'); + } + } else { + // 如果找不到消息,尝试通过内容匹配(处理消息ID可能改变的情况) + if (message.body.type == MessageType.TXT) { + final textBody = message.body as EMTextMessageBody; + final content = textBody.content; + final contentIndex = controller.messages.indexWhere((msg) => + msg.body.type == MessageType.TXT && + (msg.body as EMTextMessageBody).content == content && + msg.direction == MessageDirection.SEND && + msg.status == MessageStatus.PROGRESS + ); + if (contentIndex != -1) { + // 更新消息对象,包括错误码 + final updatedMessage = message; + controller.messages[contentIndex] = updatedMessage; + controller.update(); + + if (Get.isLogEnable) { + final errorCode = error?.code; + Get.log('✅ [IMManager] 已通过内容匹配更新消息状态: $targetId, status=$status, errorCode=$errorCode'); + } + } + } } } } @@ -1157,6 +1302,9 @@ class IMManager { return null; } + // 在消息扩展字段中添加当前用户信息(重要:确保重发的消息也包含用户信息) + _addUserInfoToMessageExt(newMessage); + // 重新发送消息 final result = await EMClient.getInstance.chatManager.sendMessage(newMessage); if (Get.isLogEnable) { diff --git a/lib/pages/message/chat_page.dart b/lib/pages/message/chat_page.dart index 39395b1..cb5df06 100644 --- a/lib/pages/message/chat_page.dart +++ b/lib/pages/message/chat_page.dart @@ -15,6 +15,7 @@ import '../../../widget/message/chat_gift_popup.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'chat_settings_page.dart'; import '../home/user_information_page.dart'; +import '../../../widget/live/live_recharge_popup.dart'; class ChatPage extends StatefulWidget { final String userId; // 对应 MarriageData.userId @@ -91,6 +92,113 @@ class _ChatPageState extends State { super.dispose(); } + // 检查用户是否在线 + bool _isUserOnline(ChatController controller) { + try { + // 如果是从用户详情页进入的(有userData),根据用户数据判断 + // 用户详情页显示在线,所以默认认为在线 + if (widget.userData != null) { + // 可以在这里添加从userData中读取在线状态的逻辑 + // 目前用户详情页显示在线,所以返回true + return true; + } + + // 如果是从消息列表进入的(没有userData),从消息中获取在线状态 + // 从消息列表中查找最后一条来自对方的消息 + final receiveMessages = controller.messages + .where((msg) => msg.direction == MessageDirection.RECEIVE) + .toList(); + + if (receiveMessages.isNotEmpty) { + // 获取最后一条接收到的消息 + final lastMessage = receiveMessages.last; + + // 从消息扩展字段中获取在线状态 + Map? attributes; + try { + attributes = lastMessage.attributes; + } catch (e) { + return false; + } + + if (attributes != null && attributes.isNotEmpty) { + // 检查 sender_isOnline 字段 + final isOnlineStr = attributes['sender_isOnline'] as String?; + if (isOnlineStr == 'true') { + // 进一步检查最后活跃时间(5分钟内认为在线) + final lastActiveTimeStr = attributes['sender_lastActiveTime'] as String?; + if (lastActiveTimeStr != null) { + try { + final lastActiveTime = int.parse(lastActiveTimeStr); + final now = DateTime.now().millisecondsSinceEpoch; + final diff = now - lastActiveTime; + // 5分钟内认为在线(5 * 60 * 1000 毫秒) + return diff < 5 * 60 * 1000; + } catch (e) { + // 解析失败,使用 isOnline 字段 + return true; + } + } else { + return true; + } + } + } + } + + // 如果没有接收到的消息,检查所有消息(包括发送的) + // 查找最近的消息,看看是否有在线状态信息 + if (controller.messages.isNotEmpty) { + // 按时间排序,获取最新的消息 + final sortedMessages = List.from(controller.messages) + ..sort((a, b) { + final timeA = a.serverTime; + final timeB = b.serverTime; + return timeB.compareTo(timeA); + }); + + for (final message in sortedMessages) { + Map? attributes; + try { + attributes = message.attributes; + } catch (e) { + continue; + } + + if (attributes != null && attributes.isNotEmpty) { + // 检查是否有在线状态信息(无论是发送者还是接收者) + final senderIsOnline = attributes['sender_isOnline'] as String?; + final receiverIsOnline = attributes['receiver_isOnline'] as String?; + final isOnlineStr = senderIsOnline ?? receiverIsOnline; + if (isOnlineStr == 'true') { + final senderLastActiveTime = attributes['sender_lastActiveTime'] as String?; + final receiverLastActiveTime = attributes['receiver_lastActiveTime'] as String?; + final lastActiveTimeStr = senderLastActiveTime ?? receiverLastActiveTime; + if (lastActiveTimeStr != null) { + try { + final lastActiveTime = int.parse(lastActiveTimeStr); + final now = DateTime.now().millisecondsSinceEpoch; + final diff = now - lastActiveTime; + // 5分钟内认为在线 + if (diff < 5 * 60 * 1000) { + return true; + } + } catch (e) { + return true; + } + } else { + return true; + } + } + } + } + } + + return false; + } catch (e) { + return false; + } + } + // 显示礼物弹窗 void _showGiftPopup() { final giftProducts = _controller.giftProducts.toList(); @@ -137,8 +245,38 @@ class _ChatPageState extends State { child: Scaffold( backgroundColor: Color(0xffF5F5F5), appBar: AppBar( - title: Text(controller.userNickName ?? widget.userData?.nickName ?? ''), - centerTitle: true, + title: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + controller.userNickName ?? widget.userData?.nickName ?? '', + style: TextStyle(fontSize: 18.sp), + ), + if (_isUserOnline(controller)) + SizedBox(width: 8.w), + // 在线标签(根据消息中的在线状态动态显示) + if (_isUserOnline(controller)) + Container( + width: 46.w, + height: 26.h, + // padding: EdgeInsets.symmetric(horizontal: 15.w, vertical: 1.h), + decoration: BoxDecoration( + color: Color.fromRGBO(234, 255, 219, 0.5), + borderRadius: BorderRadius.circular(178), + ), + child: Center( + child: Text( + '在线', + style: TextStyle( + fontSize: 12.sp, + color: Color.fromRGBO(38, 199, 124, 1), + ), + ), + ), + ), + ], + ), + centerTitle: false, actions: [ Container( padding: EdgeInsets.only(right: 16.w), @@ -196,13 +334,25 @@ class _ChatPageState extends State { ? controller.messages[index - 2] : null; + // 检查是否需要显示508错误提示(使用临时状态,不持久化) + final showRoseError = isSentByMe && + message.status == MessageStatus.FAIL && + controller.shouldShowRoseError(message.msgId); + // 🚀 性能优化:为每个消息项设置唯一的 key - return MessageItem( - key: ValueKey(message.msgId), - message: message, - isSentByMe: isSentByMe, - previousMessage: previousMessage, - chatController: controller, // 传递 controller 避免使用 Get.find + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + MessageItem( + key: ValueKey(message.msgId), + message: message, + isSentByMe: isSentByMe, + previousMessage: previousMessage, + chatController: controller, // 传递 controller 避免使用 Get.find + ), + // 显示错误提示(错误码508) + if (showRoseError) _buildRoseErrorHint(context), + ], ); }, ), @@ -535,4 +685,49 @@ class _ChatPageState extends State { } } } + + // 构建玫瑰不足错误提示 + Widget _buildRoseErrorHint(BuildContext context) { + return Container( + margin: EdgeInsets.only(top: 8.h, bottom: 8.h), + padding: EdgeInsets.symmetric(horizontal: 16.w), + width: double.infinity, + child: Center( + child: RichText( + textAlign: TextAlign.center, + text: TextSpan( + children: [ + TextSpan( + text: '发送信息所需的玫瑰不足,充值获取玫瑰继续嗨聊 ', + style: TextStyle( + fontSize: 11.sp, + color: Color.fromRGBO(199, 199, 199, 1), + ), + ), + WidgetSpan( + child: GestureDetector( + onTap: () { + // 隐藏键盘 + FocusScope.of(context).unfocus(); + SmartDialog.show( + alignment: Alignment.bottomCenter, + maskColor: Colors.black.withOpacity(0.5), + builder: (_) => const LiveRechargePopup(), + ); + }, + child: Text( + '立即充值', + style: TextStyle( + fontSize: 11.sp, + color: Color.fromRGBO(117, 98, 249, 1), + ), + ), + ), + ), + ], + ), + ), + ), + ); + } } diff --git a/lib/pages/message/conversation_tab.dart b/lib/pages/message/conversation_tab.dart index 57682de..e6405b4 100644 --- a/lib/pages/message/conversation_tab.dart +++ b/lib/pages/message/conversation_tab.dart @@ -108,11 +108,27 @@ class _ConversationTabState extends State height: 56, decoration: BoxDecoration( borderRadius: BorderRadius.circular(28), - image: DecorationImage( - image: NetworkImage(userInfo!.avatarUrl!), - fit: BoxFit.cover, - ), + image: (userInfo?.avatarUrl != null && userInfo!.avatarUrl!.isNotEmpty) + ? DecorationImage( + image: NetworkImage(userInfo.avatarUrl!), + fit: BoxFit.cover, + ) + : null, + color: (userInfo?.avatarUrl == null || (userInfo?.avatarUrl?.isEmpty ?? true)) + ? Colors.grey[300] + : null, ), + child: (userInfo?.avatarUrl == null || (userInfo?.avatarUrl?.isEmpty ?? true)) + ? ClipRRect( + borderRadius: BorderRadius.circular(28), + child: Image.asset( + Assets.imagesAvatarsExample, + width: 56, + height: 56, + fit: BoxFit.cover, + ), + ) + : null, ), const SizedBox(width: 12), Expanded( @@ -126,7 +142,7 @@ class _ConversationTabState extends State children: [ Expanded( child: Text( - userInfo.nickName ?? '', // 如果没有昵称,显示用户ID + userInfo?.nickName ?? conversation.id, // 如果没有昵称,显示用户ID style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w500, @@ -152,9 +168,13 @@ class _ConversationTabState extends State Expanded( child: Text( controller.getLastMessageContent(message), - style: const TextStyle( + style: TextStyle( fontSize: 14, - color: Colors.grey, + color: (message != null && + message.direction == MessageDirection.SEND && + message.status == MessageStatus.FAIL) + ? Color.fromRGBO(248, 85, 66, 1) // 发送失败时显示红色 + : Colors.grey, ), overflow: TextOverflow.ellipsis, ), diff --git a/lib/pages/message/message_page.dart b/lib/pages/message/message_page.dart index ddb94d1..f9937a9 100644 --- a/lib/pages/message/message_page.dart +++ b/lib/pages/message/message_page.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:dating_touchme_app/generated/assets.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; import 'conversation_tab.dart'; import 'friend_tab.dart'; @@ -15,6 +14,7 @@ class MessagePage extends StatefulWidget { class _MessagePageState extends State with AutomaticKeepAliveClientMixin, TickerProviderStateMixin { late TabController _tabController; + final GlobalKey _filterButtonKey = GlobalKey(); @override void initState() { @@ -67,17 +67,16 @@ class _MessagePageState extends State with AutomaticKeepAliveClient ], ), actions: [ - // 搜索按钮 + // 过滤器按钮 GestureDetector( - onTap: () { - // 搜索功能 - }, + key: _filterButtonKey, + onTap: () => _showFilterMenu(context), child: Container( - width: 36, - height: 36, + width: 20, + height: 20, margin: const EdgeInsets.only(right: 16), alignment: Alignment.center, - child: Image.asset(Assets.imagesSearch, width: 16.w,), + child: Image.asset(Assets.imagesFilterIcon, width: 20,), ), ), ], @@ -123,6 +122,121 @@ class _MessagePageState extends State with AutomaticKeepAliveClient ); } + // 显示过滤器菜单 + void _showFilterMenu(BuildContext context) { + final RenderBox? button = _filterButtonKey.currentContext?.findRenderObject() as RenderBox?; + final RenderBox? overlay = Overlay.of(context).context.findRenderObject() as RenderBox?; + + if (button == null || overlay == null) return; + + final Offset buttonPosition = button.localToGlobal(Offset.zero); + final Size buttonSize = button.size; + + // 计算菜单位置:在按钮下方,右对齐 + final double menuWidth = 160.0; // 菜单宽度 + final double screenWidth = overlay.size.width; + final double rightPadding = screenWidth - buttonPosition.dx - buttonSize.width; + + final RelativeRect position = RelativeRect.fromLTRB( + screenWidth - rightPadding - menuWidth, // left + buttonPosition.dy + buttonSize.height + 4, // top (按钮下方4px) + rightPadding, // right + overlay.size.height - (buttonPosition.dy + buttonSize.height + 4), // bottom + ); + + showMenu( + context: context, + position: position, + color: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + items: [ + PopupMenuItem( + padding: EdgeInsets.zero, + child: _buildFilterMenuItem( + icon: Assets.imagesLastMsgIcon, + text: '最后聊天时间', + showDivider: true, + onTap: () { + Navigator.pop(context); + // TODO: 实现按最后聊天时间排序 + }, + ), + ), + PopupMenuItem( + padding: EdgeInsets.zero, + child: _buildFilterMenuItem( + icon: Assets.imagesUnreadIcon, + text: '未读消息', + showDivider: true, + onTap: () { + Navigator.pop(context); + // TODO: 实现按未读消息筛选 + }, + ), + ), + PopupMenuItem( + padding: EdgeInsets.zero, + child: _buildFilterMenuItem( + icon: Assets.imagesOnlineMsgIcon, + text: '当前在线', + showDivider: false, + onTap: () { + Navigator.pop(context); + // TODO: 实现按在线状态筛选 + }, + ), + ), + ], + ); + } + + // 构建过滤器菜单项 + Widget _buildFilterMenuItem({ + required String icon, + required String text, + required bool showDivider, + required VoidCallback onTap, + }) { + return InkWell( + onTap: onTap, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 12), + child: Row( + children: [ + Image.asset( + icon, + width: 20, + height: 20, + ), + const SizedBox(width: 12), + Text( + text, + style: const TextStyle( + fontSize: 14, + color: Colors.black, + ), + ), + ], + ), + ), + if (showDivider) + Divider( + height: 0.5, + thickness: 0.5, + color: Colors.grey[300], + indent: 10, + endIndent: 10, + ), + ], + ), + ); + } + // 构建Tab内容区域 diff --git a/lib/widget/message/message_item.dart b/lib/widget/message/message_item.dart index 008ae03..add9483 100644 --- a/lib/widget/message/message_item.dart +++ b/lib/widget/message/message_item.dart @@ -176,9 +176,19 @@ class MessageItem extends StatelessWidget { return VoiceItem( voiceBody: voiceBody, messageId: message.msgId, // 传递消息ID作为音频唯一标识 + message: message, // 传递消息对象以获取状态和错误码 isSentByMe: isSentByMe, showTime: shouldShowTime(), formattedTime: formatMessageTime(message.serverTime), + onResend: () { + // 通过传入的 controller 或 Get 找到 ChatController 并调用重发方法 + try { + final controller = chatController ?? Get.find(); + controller.resendMessage(message); + } catch (e) { + print('重发消息失败: $e'); + } + }, ); } // 处理视频消息 diff --git a/lib/widget/message/voice_item.dart b/lib/widget/message/voice_item.dart index 6fc1656..6440767 100644 --- a/lib/widget/message/voice_item.dart +++ b/lib/widget/message/voice_item.dart @@ -14,16 +14,20 @@ import '../../../controller/message/voice_player_manager.dart'; class VoiceItem extends StatefulWidget { final EMVoiceMessageBody voiceBody; final String messageId; // 消息ID,用作音频的唯一标识 + final EMMessage? message; // 添加消息对象以获取状态和错误码 final bool isSentByMe; final bool showTime; final String formattedTime; + final VoidCallback? onResend; // 添加重发回调 const VoiceItem({ required this.voiceBody, required this.messageId, + this.message, required this.isSentByMe, required this.showTime, required this.formattedTime, + this.onResend, super.key, });