From 03545aeb1ca84e5f18e4c1f95c1b7c2ede0a1edd Mon Sep 17 00:00:00 2001 From: Jolie <412895109@qq.com> Date: Fri, 26 Dec 2025 23:56:24 +0800 Subject: [PATCH] =?UTF-8?q?feat(im):=20=E6=B7=BB=E5=8A=A0=E8=A7=86?= =?UTF-8?q?=E9=A2=91=E9=80=9A=E8=AF=9D=E9=82=80=E8=AF=B7=E5=BC=B9=E6=A1=86?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在IMManager中添加视频通话消息处理逻辑 - 解析CALL类型消息并显示视频通话邀请弹框 - 实现通话邀请的接听、拒绝和跳转功能 - 添加VideoCallInviteDialog组件用于显示通话邀请 - 优化消息通知弹框的边距样式 - 在pubspec.yaml中添加必要的资源文件路径配置 --- lib/im/im_manager.dart | 159 ++++++++++++++++++ .../message/message_notification_dialog.dart | 2 +- .../message/video_call_invite_dialog.dart | 157 +++++++++++++++++ pubspec.yaml | 8 +- 4 files changed, 322 insertions(+), 4 deletions(-) create mode 100644 lib/widget/message/video_call_invite_dialog.dart diff --git a/lib/im/im_manager.dart b/lib/im/im_manager.dart index 224c538..e15b80a 100644 --- a/lib/im/im_manager.dart +++ b/lib/im/im_manager.dart @@ -17,6 +17,9 @@ import '../pages/mine/login_page.dart'; import '../pages/message/chat_page.dart'; import '../network/user_api.dart'; import '../widget/message/message_notification_dialog.dart'; +import '../widget/message/video_call_invite_dialog.dart'; +import '../pages/message/video_call_page.dart'; +import '../controller/message/call_manager.dart'; // 消息通知数据结构 class _NotificationMessage { @@ -1387,6 +1390,162 @@ class IMManager { return; } + // 处理视频通话消息(CALL消息)- 显示特殊的视频通话邀请弹框 + if (message.body.type == MessageType.TXT) { + try { + final textBody = message.body as EMTextMessageBody; + final content = textBody.content; + 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?; + 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'); + } + } + + String? nickName; + String? avatarUrl; + + 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'); + } + } + } + + 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, + ); + }, + ); + }, + 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'); + } + } + } + } catch (e) { + // 解析失败,继续处理 + if (Get.isLogEnable) { + Get.log('⚠️ [IMManager] 解析消息内容失败: $e'); + } + } + } + // 检查发送者是否是当前正在聊天的用户 // 如果 _activeChatControllers 中包含该用户ID,说明当前正在和该用户聊天 if (_activeChatControllers.containsKey(fromId)) { diff --git a/lib/widget/message/message_notification_dialog.dart b/lib/widget/message/message_notification_dialog.dart index cd60e31..e3fd386 100644 --- a/lib/widget/message/message_notification_dialog.dart +++ b/lib/widget/message/message_notification_dialog.dart @@ -23,7 +23,7 @@ class MessageNotificationDialog extends StatelessWidget { return GestureDetector( onTap: onTap, child: Container( - margin: EdgeInsets.symmetric(horizontal: 16.w), + margin: EdgeInsets.symmetric(horizontal: 16.w, vertical: 30.h), padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 12.h), decoration: BoxDecoration( color: Colors.white, diff --git a/lib/widget/message/video_call_invite_dialog.dart b/lib/widget/message/video_call_invite_dialog.dart new file mode 100644 index 0000000..a83ecf1 --- /dev/null +++ b/lib/widget/message/video_call_invite_dialog.dart @@ -0,0 +1,157 @@ +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'; + +/// 视频通话邀请弹框 +class VideoCallInviteDialog extends StatelessWidget { + final String avatarUrl; + final String nickName; + final VoidCallback? onTap; // 点击弹框主体区域(只跳转,不接通) + final VoidCallback? onAccept; // 点击接通按钮(接通并跳转) + final VoidCallback? onReject; // 点击挂断按钮(拒绝) + + const VideoCallInviteDialog({ + super.key, + required this.avatarUrl, + required this.nickName, + this.onTap, + this.onAccept, + this.onReject, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Container( + margin: EdgeInsets.symmetric(horizontal: 16.w, vertical: 30.h), + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.9), + borderRadius: BorderRadius.circular(16.w), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.3), + blurRadius: 12, + offset: Offset(0, 4.h), + ), + ], + ), + child: Row( + children: [ + // 左侧:头像和昵称、文案 + Expanded( + child: Row( + children: [ + // 头像 + ClipOval( + child: avatarUrl.isNotEmpty + ? CachedNetworkImage( + imageUrl: avatarUrl, + width: 56.w, + height: 56.w, + fit: BoxFit.cover, + placeholder: (context, url) => Image.asset( + Assets.imagesUserAvatar, + width: 56.w, + height: 56.w, + fit: BoxFit.cover, + ), + errorWidget: (context, url, error) => Image.asset( + Assets.imagesUserAvatar, + width: 56.w, + height: 56.w, + fit: BoxFit.cover, + ), + ) + : Image.asset( + Assets.imagesUserAvatar, + width: 56.w, + height: 56.w, + fit: BoxFit.cover, + ), + ), + SizedBox(width: 12.w), + // 昵称和文案 + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + // 昵称 + Text( + nickName, + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: Colors.white, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + SizedBox(height: 4.h), + // 邀请文案 + Text( + '邀请你视频通话', + style: TextStyle( + fontSize: 13.sp, + color: Colors.white.withOpacity(0.8), + ), + ), + ], + ), + ), + ], + ), + ), + SizedBox(width: 12.w), + // 右侧:接通和挂断按钮 + Row( + children: [ + // 挂断按钮 + GestureDetector( + onTap: onReject, + behavior: HitTestBehavior.opaque, + child: Container( + width: 44.w, + height: 44.w, + decoration: BoxDecoration( + color: Colors.red, + shape: BoxShape.circle, + ), + child: Icon( + Icons.call_end, + color: Colors.white, + size: 24.w, + ), + ), + ), + SizedBox(width: 12.w), + // 接通按钮 + GestureDetector( + onTap: onAccept, + behavior: HitTestBehavior.opaque, + child: Container( + width: 44.w, + height: 44.w, + decoration: BoxDecoration( + color: Colors.green, + shape: BoxShape.circle, + ), + child: Icon( + Icons.call, + color: Colors.white, + size: 24.w, + ), + ), + ), + ], + ), + ], + ), + ), + ); + } +} + diff --git a/pubspec.yaml b/pubspec.yaml index 7be9d96..0d3fddd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -65,7 +65,7 @@ dependencies: audioplayers: ^6.5.1 video_thumbnail: ^0.5.3 # 视频缩略图生成 fluwx: ^5.7.5 -# # tobias: ^5.3.1 + # # tobias: ^5.3.1 agora_rtc_engine: ^6.5.3 agora_rtm: ^2.2.5 agora_token_generator: ^1.0.0 @@ -79,7 +79,7 @@ dependencies: im_flutter_sdk: 4.15.2 webview_flutter: ^4.13.0 ota_update: ^7.1.0 - + dev_dependencies: flutter_test: sdk: flutter @@ -108,7 +108,8 @@ flutter: # To add assets to your application, add an assets section, like this: assets: - - build/app/outputs/flutter-apk/ + - assets/images/ + - assets/images/emoji/ # - images/a_dot_ham.jpeg # An images asset can refer to one or more resolution-specific "variants", see @@ -144,3 +145,4 @@ flutter_launcher_icons: image_path: "assets/images/app_logo.jpg" min_sdk_android: 21 remove_alpha_ios: true +