diff --git a/assets/audio/call.mp3 b/assets/audio/call.mp3 new file mode 100644 index 0000000..ebc4b7e Binary files /dev/null and b/assets/audio/call.mp3 differ 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