From b6915f264d54553e74f34e7c94840bf2c64ac3fe Mon Sep 17 00:00:00 2001 From: Jolie <412895109@qq.com> Date: Sat, 27 Dec 2025 16:05:34 +0800 Subject: [PATCH] =?UTF-8?q?feat(call):=20=E5=AE=9E=E7=8E=B0=E9=80=9A?= =?UTF-8?q?=E8=AF=9D=E5=8A=9F=E8=83=BD=E5=B9=B6=E4=BC=98=E5=8C=96=E6=B6=88?= =?UTF-8?q?=E6=81=AF=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加音频播放器支持来电铃声循环播放 - 实现通话消息状态更新和消息修改功能 - 优化会话列表加载和刷新防抖机制 - 支持自定义通话消息格式替换旧文本格式 - 添加通话音频播放和停止控制 - 优化直播间小窗口显示和隐藏逻辑 - 实现通话邀请弹框和消息解析功能 - 添加资产文件支持音频资源 - 优化消息通知对话框支持emoji显示 - 移除旧格式消息处理逻辑简化代码结构 --- assets/audio/call.mp3 | Bin 0 -> 33437 bytes lib/controller/message/call_manager.dart | 158 ++++++-- .../message/conversation_controller.dart | 93 +++-- lib/generated/assets.dart | 1 + lib/im/im_manager.dart | 342 +++++++++--------- lib/pages/discover/live_room_page.dart | 21 +- lib/pages/message/conversation_tab.dart | 52 ++- lib/widget/live/draggable_overlay_widget.dart | 27 +- lib/widget/message/call_item.dart | 15 +- lib/widget/message/gift_item.dart | 13 +- lib/widget/message/message_item.dart | 18 +- .../message/message_notification_dialog.dart | 12 +- lib/widget/message/text_item.dart | 15 - pubspec.yaml | 1 + 14 files changed, 439 insertions(+), 329 deletions(-) create mode 100644 assets/audio/call.mp3 diff --git a/assets/audio/call.mp3 b/assets/audio/call.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..ebc4b7e1534a070ea670e3f540f8507e06080ea8 GIT binary patch literal 33437 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 literal 0 HcmV?d00001 diff --git a/lib/controller/message/call_manager.dart b/lib/controller/message/call_manager.dart index 8eedc7e..270fdd0 100644 --- a/lib/controller/message/call_manager.dart +++ b/lib/controller/message/call_manager.dart @@ -1,9 +1,10 @@ import 'dart:async'; -import 'dart:convert'; +import 'package:audioplayers/audioplayers.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:im_flutter_sdk/im_flutter_sdk.dart'; +import '../../generated/assets.dart'; import '../../im/im_manager.dart'; import 'chat_controller.dart'; @@ -37,9 +38,25 @@ class CallManager extends GetxController { Timer? _callTimer; int _callDurationSeconds = 0; final RxInt callDurationSeconds = RxInt(0); + + // 音频播放器(用于播放来电铃声) + final AudioPlayer _callAudioPlayer = AudioPlayer(); + bool _isPlayingCallAudio = false; CallManager() { print('📞 [CallManager] 通话管理器已初始化'); + // 监听音频播放完成事件,实现循环播放 + _callAudioPlayer.onPlayerComplete.listen((_) async { + if (_isPlayingCallAudio) { + // 如果还在播放状态,重新播放(循环播放) + try { + await _callAudioPlayer.play(AssetSource(Assets.audioCall.replaceFirst('assets/', ''))); + } catch (e) { + print('❌ [CallManager] 循环播放来电铃声失败: $e'); + _isPlayingCallAudio = false; + } + } + }); } /// 发起通话 @@ -78,6 +95,11 @@ class CallManager extends GetxController { chatController: chatController, ); + // 如果是视频通话,开始循环播放来电铃声 + if (callType == CallType.video) { + startCallAudio(); + } + // TODO: 这里可以集成实际的通话SDK,发起真正的通话 // 例如:await RTCManager.instance.startCall(targetUserId, callType); @@ -119,6 +141,9 @@ class CallManager extends GetxController { ); currentCall.value = session; + // 停止播放来电铃声(已接通) + stopCallAudio(); + // 开始计时 _startCallTimer(); @@ -151,6 +176,9 @@ class CallManager extends GetxController { try { print('📞 [CallManager] 拒绝通话'); + // 停止播放来电铃声(已拒绝) + stopCallAudio(); + // 更新通话消息状态为已拒绝 await _updateCallMessageStatus( message: message, @@ -191,6 +219,9 @@ class CallManager extends GetxController { ); } + // 停止播放来电铃声(已取消) + stopCallAudio(); + // 停止计时 _stopCallTimer(); @@ -219,6 +250,9 @@ class CallManager extends GetxController { try { print('📞 [CallManager] 结束通话,时长: ${callDuration}秒'); + // 停止播放来电铃声(通话结束) + stopCallAudio(); + // 停止计时 _stopCallTimer(); @@ -305,7 +339,7 @@ class CallManager extends GetxController { } } - /// 更新通话消息状态 + /// 更新通话消息状态(使用modifyMessage修改现有消息) Future _updateCallMessageStatus({ required EMMessage message, required String callStatus, @@ -320,26 +354,80 @@ class CallManager extends GetxController { } final callType = callInfo['callType'] as String? ?? 'voice'; - final targetUserId = message.from ?? message.to ?? ''; + final messageId = message.msgId; - // 发送更新的通话消息 - return await _sendCallMessage( - targetUserId: targetUserId, - callType: callType, - callStatus: callStatus, - callDuration: callDuration, - chatController: chatController, - ); + if (messageId.isEmpty) { + print('❌ [CallManager] 消息ID为空,无法修改消息'); + return false; + } + + // 如果是自定义消息,使用modifyMessage修改 + if (message.body.type == MessageType.CUSTOM) { + // 构建新的参数 + final callParams = { + 'callType': callType, + 'callStatus': callStatus, + }; + if (callDuration != null) { + callParams['callDuration'] = callDuration.toString(); + } + + // 创建新的消息体 + final customBody = EMCustomMessageBody( + event: 'call', + params: callParams, + ); + + // 使用modifyMessage修改消息 + final success = await IMManager.instance.modifyMessage( + messageId: messageId, + msgBody: customBody, + attributes: null, // 不修改扩展属性 + ); + + if (success) { + print('✅ [CallManager] 消息修改成功: messageId=$messageId, callStatus=$callStatus'); + + // 如果提供了chatController,更新本地消息列表 + if (chatController != null) { + // 更新消息体中的参数 + try { + final index = chatController.messages.indexWhere((msg) => msg.msgId == messageId); + if (index != -1) { + final updatedMessage = chatController.messages[index]; + if (updatedMessage.body.type == MessageType.CUSTOM) { + final customBody = updatedMessage.body as EMCustomMessageBody; + // 创建新的参数Map并更新 + final updatedParams = Map.from(customBody.params ?? {}); + updatedParams['callType'] = callType; + updatedParams['callStatus'] = callStatus; + if (callDuration != null) { + updatedParams['callDuration'] = callDuration.toString(); + } + // 注意:EMCustomMessageBody的params可能是只读的,这里可能需要重新创建消息 + // 暂时先通知UI更新,实际的消息体更新会在收到onMessageContentChanged回调时处理 + chatController.update(); + } + } + } catch (e) { + print('⚠️ [CallManager] 更新本地消息列表失败: $e'); + } + } + } + + return success; + } + // 如果不是自定义消息,返回失败 + return false; } catch (e) { print('❌ [CallManager] 更新通话消息状态失败: $e'); return false; } } - /// 从消息中解析通话信息(支持新格式的自定义消息和旧格式的文本消息) + /// 从自定义消息中解析通话信息 Map? _parseCallInfo(EMMessage message) { try { - // 新格式:自定义消息 if (message.body.type == MessageType.CUSTOM) { final customBody = message.body as EMCustomMessageBody; if (customBody.event == 'call' && customBody.params != null) { @@ -353,15 +441,6 @@ class CallManager extends GetxController { }; } } - // 旧格式:文本消息 - if (message.body.type == MessageType.TXT) { - final textBody = message.body as EMTextMessageBody; - final content = textBody.content; - if (content != null && content.startsWith('[CALL:]')) { - final jsonStr = content.substring(7); // 移除 '[CALL:]' 前缀 - return jsonDecode(jsonStr) as Map; - } - } } catch (e) { print('解析通话信息失败: $e'); } @@ -374,10 +453,45 @@ class CallManager extends GetxController { /// 获取当前通话时长(秒) int get currentCallDuration => callDurationSeconds.value; + /// 开始播放来电铃声(循环播放) + /// 可以是发起方或接收方调用 + Future startCallAudio() async { + if (_isPlayingCallAudio) { + return; // 已经在播放中 + } + + try { + _isPlayingCallAudio = true; + print('🔊 [CallManager] 开始播放来电铃声'); + await _callAudioPlayer.play(AssetSource(Assets.audioCall.replaceFirst('assets/', ''))); + } catch (e) { + print('❌ [CallManager] 播放来电铃声失败: $e'); + _isPlayingCallAudio = false; + } + } + + /// 停止播放来电铃声 + /// 可以是发起方或接收方调用 + Future stopCallAudio() async { + if (!_isPlayingCallAudio) { + return; // 没有在播放 + } + + try { + _isPlayingCallAudio = false; + print('🔇 [CallManager] 停止播放来电铃声'); + await _callAudioPlayer.stop(); + } catch (e) { + print('❌ [CallManager] 停止播放来电铃声失败: $e'); + } + } + @override void onClose() { + stopCallAudio(); _stopCallTimer(); currentCall.value = null; + _callAudioPlayer.dispose(); super.onClose(); } } diff --git a/lib/controller/message/conversation_controller.dart b/lib/controller/message/conversation_controller.dart index 73b414c..f76b192 100644 --- a/lib/controller/message/conversation_controller.dart +++ b/lib/controller/message/conversation_controller.dart @@ -1,4 +1,4 @@ -import 'dart:convert'; +import 'dart:async'; import 'package:get/get.dart'; import 'package:im_flutter_sdk/im_flutter_sdk.dart'; import '../../im/im_manager.dart'; @@ -62,6 +62,9 @@ class ConversationController extends GetxController { // 总未读数 final totalUnreadCount = 0.obs; + // 防抖定时器,用于避免频繁刷新会话列表 + Timer? _refreshDebounceTimer; + /// 缓存用户信息(公开方法,供 ChatController 调用) void cacheUserInfo(String userId, ExtendedUserInfo userInfo) { _userInfoCache[userId] = userInfo; @@ -118,8 +121,8 @@ class ConversationController extends GetxController { } /// 加载会话列表 - Future loadConversations() async { - if (isLoading.value) return; + Future loadConversations({bool showLoading = true}) async { + if (isLoading.value && showLoading) return; // 检查 IM 登录状态 if (!IMManager.instance.isLoggedIn) { @@ -132,7 +135,10 @@ class ConversationController extends GetxController { } try { - isLoading.value = true; + // 只有在需要显示加载状态时才设置 + if (showLoading) { + isLoading.value = true; + } errorMessage.value = ''; // 从IMManager获取会话列表 @@ -160,7 +166,9 @@ class ConversationController extends GetxController { } errorMessage.value = '加载会话列表失败,请稍后重试'; } finally { - isLoading.value = false; + if (showLoading) { + isLoading.value = false; + } } } @@ -297,8 +305,28 @@ class ConversationController extends GetxController { } } - /// 刷新会话列表 - Future refreshConversations() async { + @override + void onClose() { + _refreshDebounceTimer?.cancel(); + super.onClose(); + } + + /// 刷新会话列表(带防抖,避免频繁刷新导致闪烁) + Future refreshConversations({bool force = false}) async { + // 如果正在加载且不是强制刷新,使用防抖机制 + if (!force && isLoading.value) { + // 取消之前的定时器 + _refreshDebounceTimer?.cancel(); + // 设置新的定时器,延迟300ms后刷新 + _refreshDebounceTimer = Timer(const Duration(milliseconds: 300), () { + refreshConversations(force: true); + }); + return; + } + + // 取消之前的定时器 + _refreshDebounceTimer?.cancel(); + // 如果IM未登录,先尝试登录 if (!IMManager.instance.isLoggedIn) { if (Get.isLogEnable) { @@ -361,8 +389,8 @@ class ConversationController extends GetxController { return; } } else { - // 如果已登录,直接加载会话列表 - await loadConversations(); + // 如果已登录,直接加载会话列表(不显示加载状态,避免闪烁) + await loadConversations(showLoading: false); } } @@ -390,33 +418,7 @@ class ConversationController extends GetxController { if(message.body.type == MessageType.TXT){ final body = message.body as EMTextMessageBody; - final content = body.content; - - // 检查是否是CALL消息 - if (content != null && content.startsWith('[CALL:]')) { - try { - final jsonStr = content.substring(7); // 移除 '[CALL:]' 前缀 - final callInfo = jsonDecode(jsonStr) as Map; - final callType = callInfo['callType'] as String?; - if (callType == 'video') { - return '[视频通话]'; - } else if (callType == 'voice') { - return '[语音通话]'; - } - } catch (e) { - // 解析失败,返回原始内容 - if (Get.isLogEnable) { - Get.log('⚠️ [ConversationController] 解析CALL消息失败: $e'); - } - } - } - - // 检查是否是GIFT消息 - if (content != null && content.startsWith('[GIFT:]')) { - return '[礼物]'; - } - - return content ?? ''; + return body.content; }else if(message.body.type == MessageType.IMAGE){ return '[图片]'; }else if(message.body.type == MessageType.VOICE){ @@ -432,6 +434,25 @@ class ConversationController extends GetxController { // 检查是否是分享房间类型 if(body.event == 'live_room_invite'){ return '[分享房间]'; + } else if (body.event == 'gift') { + return '[礼物]'; + } else if (body.event == 'call') { + // 解析通话类型 + try { + if (body.params != null) { + final callType = body.params!['callType'] ?? 'voice'; + if (callType == 'video') { + return '[视频通话]'; + } else if (callType == 'voice') { + return '[语音通话]'; + } + } + } catch (e) { + if (Get.isLogEnable) { + Get.log('⚠️ [ConversationController] 解析通话消息类型失败: $e'); + } + } + return '[通话消息]'; } return '[自定义消息]'; } diff --git a/lib/generated/assets.dart b/lib/generated/assets.dart index 50c6531..5fe7891 100644 --- a/lib/generated/assets.dart +++ b/lib/generated/assets.dart @@ -2,6 +2,7 @@ class Assets { Assets._(); + static const String audioCall = 'assets/audio/call.mp3'; static const String emojiEmoji01 = 'assets/images/emoji/emoji_01.png'; static const String emojiEmoji02 = 'assets/images/emoji/emoji_02.png'; static const String emojiEmoji03 = 'assets/images/emoji/emoji_03.png'; diff --git a/lib/im/im_manager.dart b/lib/im/im_manager.dart index 18a7587..581f43b 100644 --- a/lib/im/im_manager.dart +++ b/lib/im/im_manager.dart @@ -1,6 +1,5 @@ import 'dart:io'; import 'dart:async'; -import 'dart:convert'; import 'package:flutter/widgets.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -1415,158 +1414,174 @@ class IMManager { } // 处理视频通话消息(CALL消息)- 显示特殊的视频通话邀请弹框 - if (message.body.type == MessageType.TXT) { - try { - final textBody = message.body as EMTextMessageBody; - final content = textBody.content; - if (content != null && content.startsWith('[CALL:]')) { - // 解析通话信息 + // 支持新格式的自定义消息和旧格式的文本消息 + Map? callInfo; + String? callType; + String? callStatus; + + try { + // 自定义消息 + if (message.body.type == MessageType.CUSTOM) { + final customBody = message.body as EMCustomMessageBody; + if (customBody.event == 'call' && customBody.params != null) { + final params = customBody.params!; + callType = params['callType'] ?? 'voice'; + callStatus = params['callStatus'] ?? 'missed'; + callInfo = { + 'callType': callType, + 'callStatus': callStatus, + }; + } + } + + // 如果解析到通话信息,检查是否需要显示视频通话邀请弹框 + if (callInfo != null && callType != null && callStatus != null) { + // 只处理视频通话且状态为 missed 或 calling 的消息(新邀请) + if (callType == 'video' && (callStatus == 'missed' || callStatus == 'calling')) { + // 获取用户信息 + Map? attributes; try { - final jsonStr = content.substring(7); // 移除 '[CALL:]' 前缀 - final callInfo = jsonDecode(jsonStr) as Map; - final callType = callInfo['callType'] as String?; - final callStatus = callInfo['callStatus'] as String?; - - // 只处理视频通话且状态为 missed 或 calling 的消息(新邀请) - if (callType == 'video' && (callStatus == 'missed' || callStatus == 'calling')) { - // 获取用户信息 - Map? attributes; - try { - attributes = message.attributes; - } catch (e) { - if (Get.isLogEnable) { - Get.log('⚠️ [IMManager] 无法访问消息扩展字段: $e'); - } - } + attributes = message.attributes; + } catch (e) { + if (Get.isLogEnable) { + Get.log('⚠️ [IMManager] 无法访问消息扩展字段: $e'); + } + } + + String? nickName; + String? avatarUrl; - String? nickName; - String? avatarUrl; + if (attributes != null) { + nickName = attributes['sender_nickName'] as String?; + avatarUrl = attributes['sender_avatarUrl'] as String?; + } - if (attributes != null) { - nickName = attributes['sender_nickName'] as String?; - avatarUrl = attributes['sender_avatarUrl'] as String?; + // 如果从消息扩展字段中获取不到,尝试从 ConversationController 的缓存中获取 + if ((nickName == null || nickName.isEmpty) || (avatarUrl == null || avatarUrl.isEmpty)) { + try { + if (Get.isRegistered()) { + final conversationController = Get.find(); + final cachedUserInfo = conversationController.getCachedUserInfo(fromId); + if (cachedUserInfo != null) { + nickName = nickName ?? cachedUserInfo.nickName; + avatarUrl = avatarUrl ?? cachedUserInfo.avatarUrl; + } + } + } catch (e) { + if (Get.isLogEnable) { + Get.log('⚠️ [IMManager] 从ConversationController获取用户信息失败: $e'); } + } + } - // 如果从消息扩展字段中获取不到,尝试从 ConversationController 的缓存中获取 - if ((nickName == null || nickName.isEmpty) || (avatarUrl == null || avatarUrl.isEmpty)) { - try { - if (Get.isRegistered()) { - final conversationController = Get.find(); - final cachedUserInfo = conversationController.getCachedUserInfo(fromId); - if (cachedUserInfo != null) { - nickName = nickName ?? cachedUserInfo.nickName; - avatarUrl = avatarUrl ?? cachedUserInfo.avatarUrl; + final finalNickName = nickName ?? fromId; + final finalAvatarUrl = avatarUrl ?? ''; + + // 接收方收到视频通话时,开始播放来电铃声 + final callManager = CallManager.instance; + callManager.startCallAudio(); + + // 显示视频通话邀请弹框 + SmartDialog.show( + builder: (context) { + return VideoCallInviteDialog( + avatarUrl: finalAvatarUrl, + nickName: finalNickName, + onTap: () async { + // 关闭弹框 + SmartDialog.dismiss(); + + // 停止播放来电铃声 + callManager.stopCallAudio(); + + // 只跳转到视频通话页面,不自动接通 + Get.to(() => VideoCallPage( + targetUserId: fromId, + isInitiator: false, + )); + }, + onAccept: () async { + // 关闭弹框 + SmartDialog.dismiss(); + + // 停止播放来电铃声(acceptCall 中也会停止,但这里提前停止以更快响应) + callManager.stopCallAudio(); + + // 接听通话 + ChatController? chatController; + try { + final tag = 'chat_$fromId'; + if (Get.isRegistered(tag: tag)) { + chatController = Get.find(tag: tag); + } + } catch (e) { + if (Get.isLogEnable) { + Get.log('⚠️ [IMManager] 获取ChatController失败: $e'); } } - } catch (e) { - if (Get.isLogEnable) { - Get.log('⚠️ [IMManager] 从ConversationController获取用户信息失败: $e'); + + final accepted = await callManager.acceptCall( + message: message, + chatController: chatController, + ); + + if (accepted) { + // 跳转到视频通话页面 + Get.to(() => VideoCallPage( + targetUserId: fromId, + isInitiator: false, + )); } - } - } - - final finalNickName = nickName ?? fromId; - final finalAvatarUrl = avatarUrl ?? ''; - - // 显示视频通话邀请弹框 - SmartDialog.show( - builder: (context) { - return VideoCallInviteDialog( - avatarUrl: finalAvatarUrl, - nickName: finalNickName, - onTap: () async { - // 关闭弹框 - SmartDialog.dismiss(); - - // 只跳转到视频通话页面,不自动接通 - Get.to(() => VideoCallPage( - targetUserId: fromId, - isInitiator: false, - )); - }, - onAccept: () async { - // 关闭弹框 - SmartDialog.dismiss(); - - // 接听通话 - final callManager = CallManager.instance; - ChatController? chatController; - try { - final tag = 'chat_$fromId'; - if (Get.isRegistered(tag: tag)) { - chatController = Get.find(tag: tag); - } - } catch (e) { - if (Get.isLogEnable) { - Get.log('⚠️ [IMManager] 获取ChatController失败: $e'); - } - } - - final accepted = await callManager.acceptCall( - message: message, - chatController: chatController, - ); - - if (accepted) { - // 跳转到视频通话页面 - Get.to(() => VideoCallPage( - targetUserId: fromId, - isInitiator: false, - )); - } - }, - onReject: () async { - // 关闭弹框 - SmartDialog.dismiss(); - - // 拒绝通话 - final callManager = CallManager.instance; - ChatController? chatController; - try { - final tag = 'chat_$fromId'; - if (Get.isRegistered(tag: tag)) { - chatController = Get.find(tag: tag); - } - } catch (e) { - if (Get.isLogEnable) { - Get.log('⚠️ [IMManager] 获取ChatController失败: $e'); - } - } - - await callManager.rejectCall( - message: message, - chatController: chatController, - ); - }, + }, + onReject: () async { + // 先关闭弹框 + SmartDialog.dismiss(); + + // 停止播放来电铃声(rejectCall 中也会停止,但这里提前停止以更快响应) + callManager.stopCallAudio(); + + // 拒绝通话(会修改消息状态为 rejected) + ChatController? chatController; + try { + final tag = 'chat_$fromId'; + if (Get.isRegistered(tag: tag)) { + chatController = Get.find(tag: tag); + } + } catch (e) { + if (Get.isLogEnable) { + Get.log('⚠️ [IMManager] 获取ChatController失败: $e'); + } + } + + // 调用拒绝通话,会使用 modifyMessage 修改消息状态 + await callManager.rejectCall( + message: message, + chatController: chatController, ); }, - alignment: Alignment.topCenter, - animationType: SmartAnimationType.centerFade_otherSlide, - animationTime: Duration(milliseconds: 300), - maskColor: Colors.transparent, - maskWidget: null, - clickMaskDismiss: false, ); - - if (Get.isLogEnable) { - Get.log('📞 [IMManager] 显示视频通话邀请弹框: $fromId'); - } - } - - // 对于所有 CALL 消息(包括视频和语音),都不显示普通消息通知弹框 - return; - } catch (e) { - // 解析失败,继续处理普通消息 - if (Get.isLogEnable) { - Get.log('⚠️ [IMManager] 解析CALL消息失败: $e'); - } + }, + alignment: Alignment.topCenter, + animationType: SmartAnimationType.centerFade_otherSlide, + animationTime: Duration(milliseconds: 300), + maskColor: Colors.transparent, + maskWidget: null, + clickMaskDismiss: false, + keepSingle: true, // 确保只有一个弹框显示 + ); + + if (Get.isLogEnable) { + Get.log('📞 [IMManager] 显示视频通话邀请弹框: $fromId'); } } - } catch (e) { - // 解析失败,继续处理 - if (Get.isLogEnable) { - Get.log('⚠️ [IMManager] 解析消息内容失败: $e'); - } + + // 对于所有 CALL 消息(包括视频和语音),都不显示普通消息通知弹框 + return; + } + } catch (e) { + // 解析失败,继续处理普通消息 + if (Get.isLogEnable) { + Get.log('⚠️ [IMManager] 解析CALL消息失败: $e'); } } @@ -1721,33 +1736,7 @@ class IMManager { try { if (message.body.type == MessageType.TXT) { final body = message.body as EMTextMessageBody; - final content = body.content; - - // 检查是否是CALL消息 - if (content != null && content.startsWith('[CALL:]')) { - try { - final jsonStr = content.substring(7); // 移除 '[CALL:]' 前缀 - final callInfo = jsonDecode(jsonStr) as Map; - final callType = callInfo['callType'] as String?; - if (callType == 'video') { - return '[视频通话]'; - } else if (callType == 'voice') { - return '[语音通话]'; - } - } catch (e) { - // 解析失败,返回原始内容 - if (Get.isLogEnable) { - Get.log('⚠️ [IMManager] 解析CALL消息失败: $e'); - } - } - } - - // 检查是否是GIFT消息 - if (content != null && content.startsWith('[GIFT:]')) { - return '[礼物]'; - } - - return content ?? ''; + return body.content; } else if (message.body.type == MessageType.IMAGE) { return '[图片]'; } else if (message.body.type == MessageType.VOICE) { @@ -1762,6 +1751,25 @@ class IMManager { final body = message.body as EMCustomMessageBody; if (body.event == 'live_room_invite') { return '[分享房间]'; + } else if (body.event == 'gift') { + return '[礼物]'; + } else if (body.event == 'call') { + // 解析通话类型 + try { + if (body.params != null) { + final callType = body.params!['callType'] ?? 'voice'; + if (callType == 'video') { + return '[视频通话]'; + } else if (callType == 'voice') { + return '[语音通话]'; + } + } + } catch (e) { + if (Get.isLogEnable) { + Get.log('⚠️ [IMManager] 解析通话消息类型失败: $e'); + } + } + return '[通话消息]'; } return '[自定义消息]'; } diff --git a/lib/pages/discover/live_room_page.dart b/lib/pages/discover/live_room_page.dart index edb665a..162d275 100644 --- a/lib/pages/discover/live_room_page.dart +++ b/lib/pages/discover/live_room_page.dart @@ -48,7 +48,9 @@ class _LiveRoomPageState extends State { _overlayController = Get.find(); // 进入直播间时,确保隐藏小窗口(延迟到 build 完成后执行,避免在 build 过程中触发 setState) WidgetsBinding.instance.addPostFrameCallback((_) { - _overlayController.hide(); + if (_overlayController.showOverlay.value) { + _overlayController.hide(); + } }); // 启用屏幕常亮 WakelockPlus.enable(); @@ -133,6 +135,13 @@ class _LiveRoomPageState extends State { @override Widget build(BuildContext context) { + // 在 build 方法开始时立即隐藏小窗口(使用 addPostFrameCallback 避免在 build 过程中触发 setState) + WidgetsBinding.instance.addPostFrameCallback((_) { + if (_overlayController.showOverlay.value) { + _overlayController.hide(); + } + }); + return PopScope( onPopInvokedWithResult: (bool didPop, Object? result) async { SmartDialog.dismiss(); @@ -194,10 +203,14 @@ class _LiveRoomPageState extends State { popularityText: popularityText, avatarAsset: avatarAsset, onCloseTap: () { + SmartDialog.dismiss(); + // 退出房间时清空RTM消息 + if (Get.isRegistered()) { + final roomController = Get.find(); + roomController.chatMessages.clear(); + } + _overlayController.show(); Get.back(); - Future.delayed(Duration(seconds: 1), (){ - _overlayController.show(); - }); }, ); }), diff --git a/lib/pages/message/conversation_tab.dart b/lib/pages/message/conversation_tab.dart index 21d0657..743b8d0 100644 --- a/lib/pages/message/conversation_tab.dart +++ b/lib/pages/message/conversation_tab.dart @@ -48,40 +48,30 @@ class _ConversationTabState extends State ); } - // 监听筛选类型变化,获取筛选后的会话列表 - // 使用 Obx 监听筛选类型变化,触发 FutureBuilder 重建 + // 直接使用 Obx 监听 conversations 和 filterType,避免 FutureBuilder 重建导致的闪烁 return Obx(() { - return FutureBuilder>( - future: controller.getFilteredConversations(), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const Center(child: CircularProgressIndicator()); - } + final filteredConversations = controller.conversations; - final filteredConversations = snapshot.data ?? []; - - if (filteredConversations.isEmpty) { - return Center( - child: Text( - controller.filterType.value == FilterType.none - ? '暂无会话' - : '暂无符合条件的会话', - style: const TextStyle( - fontSize: 14, - color: Colors.grey, - ), - ), - ); - } + if (filteredConversations.isEmpty) { + return Center( + child: Text( + controller.filterType.value == FilterType.none + ? '暂无会话' + : '暂无符合条件的会话', + style: const TextStyle( + fontSize: 14, + color: Colors.grey, + ), + ), + ); + } - return ListView.builder( - padding: const EdgeInsets.only(top: 8), - itemCount: filteredConversations.length, - itemBuilder: (context, index) { - final conversation = filteredConversations[index]; - return _buildConversationItem(conversation); - }, - ); + return ListView.builder( + padding: const EdgeInsets.only(top: 8), + itemCount: filteredConversations.length, + itemBuilder: (context, index) { + final conversation = filteredConversations[index]; + return _buildConversationItem(conversation); }, ); }); diff --git a/lib/widget/live/draggable_overlay_widget.dart b/lib/widget/live/draggable_overlay_widget.dart index dc9e728..062a41f 100644 --- a/lib/widget/live/draggable_overlay_widget.dart +++ b/lib/widget/live/draggable_overlay_widget.dart @@ -1,5 +1,6 @@ import 'package:agora_rtc_engine/agora_rtc_engine.dart'; import 'package:dating_touchme_app/controller/discover/room_controller.dart'; +import 'package:dating_touchme_app/controller/overlay_controller.dart'; import 'package:dating_touchme_app/extension/ex_widget.dart'; import 'package:dating_touchme_app/pages/discover/live_room_page.dart'; import 'package:flutter/material.dart'; @@ -31,6 +32,7 @@ class _DraggableOverlayWidgetState extends State { bool _isDragging = false; final RTCManager _rtcManager = RTCManager.instance; final RoomController _roomController = Get.find(); + final OverlayController _overlayController = Get.find(); @override void initState() { @@ -187,13 +189,26 @@ class _DraggableOverlayWidgetState extends State { ], ), ), - ).onTap(() { - // 先隐藏小窗口,再跳转到直播间 - widget.onClose?.call(); - // 使用 Future.microtask 确保小窗口先隐藏,然后再导航 - Future.microtask(() { + ).onTap(() async { + // 先隐藏小窗口(直接调用 OverlayController) + _overlayController.hide(); + // 等待响应式更新完成,确保小窗口完全隐藏后再跳转 + // 使用循环检查,最多等待300ms,确保小窗口已隐藏 + int waitCount = 0; + const maxWait = 6; // 最多等待6次,每次50ms,总共300ms + while (waitCount < maxWait && _overlayController.showOverlay.value) { + await Future.delayed(const Duration(milliseconds: 50)); + waitCount++; + } + // 确保小窗口已隐藏后再跳转到直播间 + if (!_overlayController.showOverlay.value) { Get.to(() => const LiveRoomPage(id: 0)); - }); + } else { + // 如果还是显示状态,强制隐藏后再跳转 + _overlayController.hide(); + await Future.delayed(const Duration(milliseconds: 100)); + Get.to(() => const LiveRoomPage(id: 0)); + } }); }), ); diff --git a/lib/widget/message/call_item.dart b/lib/widget/message/call_item.dart index 5fa3b12..ca55b31 100644 --- a/lib/widget/message/call_item.dart +++ b/lib/widget/message/call_item.dart @@ -1,4 +1,3 @@ -import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:im_flutter_sdk/im_flutter_sdk.dart'; @@ -39,10 +38,9 @@ class CallItem extends StatelessWidget { super.key, }); - /// 从消息内容中解析通话信息(支持新格式的自定义消息和旧格式的文本消息) + /// 从自定义消息中解析通话信息 Map? _parseCallInfo() { try { - // 新格式:自定义消息 if (message.body.type == MessageType.CUSTOM) { final customBody = message.body as EMCustomMessageBody; if (customBody.event == 'call' && customBody.params != null) { @@ -57,17 +55,6 @@ class CallItem extends StatelessWidget { }; } } - // 旧格式:文本消息,内容以 [CALL:] 开头 - if (message.body.type == MessageType.TXT) { - final textBody = message.body as EMTextMessageBody; - final content = textBody.content; - - // 检查是否是通话消息(以 [CALL:] 开头) - if (content.startsWith('[CALL:]')) { - final jsonStr = content.substring(7); // 移除 '[CALL:]' 前缀 - return jsonDecode(jsonStr) as Map; - } - } } catch (e) { print('解析通话信息失败: $e'); } diff --git a/lib/widget/message/gift_item.dart b/lib/widget/message/gift_item.dart index 7a17382..663cdc2 100644 --- a/lib/widget/message/gift_item.dart +++ b/lib/widget/message/gift_item.dart @@ -1,4 +1,3 @@ -import 'dart:convert'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; @@ -25,10 +24,9 @@ class GiftItem extends StatelessWidget { super.key, }); - /// 从自定义消息的 params 或旧格式的文本消息中解析礼物信息 + /// 从自定义消息的 params 中解析礼物信息 Map? _parseGiftInfo() { try { - // 新格式:自定义消息 if (message.body.type == MessageType.CUSTOM) { final customBody = message.body as EMCustomMessageBody; if (customBody.event == 'gift' && customBody.params != null) { @@ -43,15 +41,6 @@ class GiftItem extends StatelessWidget { }; } } - // 旧格式:文本消息,内容以 [GIFT:] 开头 - if (message.body.type == MessageType.TXT) { - final textBody = message.body as EMTextMessageBody; - final content = textBody.content; - if (content.startsWith('[GIFT:]')) { - final jsonStr = content.substring(7); // 移除 '[GIFT:]' 前缀 - return jsonDecode(jsonStr) as Map; - } - } } catch (e) { print('解析礼物信息失败: $e'); } diff --git a/lib/widget/message/message_item.dart b/lib/widget/message/message_item.dart index 96406ae..31e86a5 100644 --- a/lib/widget/message/message_item.dart +++ b/lib/widget/message/message_item.dart @@ -26,40 +26,26 @@ class MessageItem extends StatelessWidget { super.key, }); - // 检查是否是通话消息(支持新格式的自定义消息和旧格式的文本消息) + // 检查是否是通话消息(自定义消息) bool _isCallMessage() { try { - // 新格式:自定义消息 if (message.body.type == MessageType.CUSTOM) { final customBody = message.body as EMCustomMessageBody; return customBody.event == 'call'; } - // 旧格式:文本消息,内容以 [CALL:] 开头 - if (message.body.type == MessageType.TXT) { - final textBody = message.body as EMTextMessageBody; - final content = textBody.content; - // 检查是否是通话消息(以 [CALL:] 开头) - return content.startsWith('[CALL:]'); - } } catch (e) { // 解析失败,不是通话消息 } return false; } - // 检查是否是礼物消息(支持新格式的自定义消息和旧格式的文本消息) + // 检查是否是礼物消息(自定义消息) bool _isGiftMessage() { try { - // 新格式:自定义消息 if (message.body.type == MessageType.CUSTOM) { final customBody = message.body as EMCustomMessageBody; return customBody.event == 'gift'; } - // 旧格式:文本消息,内容以 [GIFT:] 开头 - if (message.body.type == MessageType.TXT) { - final textBody = message.body as EMTextMessageBody; - return textBody.content.startsWith('[GIFT:]'); - } } catch (e) { // 解析失败,不是礼物消息 } diff --git a/lib/widget/message/message_notification_dialog.dart b/lib/widget/message/message_notification_dialog.dart index e3fd386..c5a5cbd 100644 --- a/lib/widget/message/message_notification_dialog.dart +++ b/lib/widget/message/message_notification_dialog.dart @@ -2,6 +2,7 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:dating_touchme_app/generated/assets.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'emoji_text_widget.dart'; /// 消息通知弹框 class MessageNotificationDialog extends StatelessWidget { @@ -85,15 +86,14 @@ class MessageNotificationDialog extends StatelessWidget { overflow: TextOverflow.ellipsis, ), SizedBox(height: 4.h), - // 消息内容 - Text( - messageContent, - style: TextStyle( + // 消息内容(使用EmojiTextWidget支持emoji显示) + EmojiTextWidget( + text: messageContent, + textStyle: TextStyle( fontSize: 13.sp, color: Color(0xFF666666), ), - maxLines: 2, - overflow: TextOverflow.ellipsis, + emojiSize: 20.w, ), ], ), diff --git a/lib/widget/message/text_item.dart b/lib/widget/message/text_item.dart index de1b402..78f0ad7 100644 --- a/lib/widget/message/text_item.dart +++ b/lib/widget/message/text_item.dart @@ -27,23 +27,8 @@ class TextItem extends StatelessWidget { super.key, }); - /// 检查是否是旧格式的特殊消息(礼物、直播间邀请等) - bool _isLegacySpecialMessage() { - final content = textBody.content; - // 检查是否是旧格式的礼物消息或直播间邀请消息 - return content.startsWith('[GIFT:]') || - content.startsWith('[ROOM:]') || - content.startsWith('[CALL:]'); - } - @override Widget build(BuildContext context) { - // 如果是旧格式的特殊消息,不显示 JSON 内容 - if (_isLegacySpecialMessage()) { - // 返回空组件,不显示这些旧格式的消息 - return SizedBox.shrink(); - } - // 检查是否有金币信息(只对接收的消息显示) final revenueInfo = _getRevenueInfo(); diff --git a/pubspec.yaml b/pubspec.yaml index 0d3fddd..4e65d18 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -109,6 +109,7 @@ flutter: # To add assets to your application, add an assets section, like this: assets: - assets/images/ + - assets/audio/ - assets/images/emoji/ # - images/a_dot_ham.jpeg