Browse Source

feat(im): 添加视频通话邀请弹框功能

- 在IMManager中添加视频通话消息处理逻辑
- 解析CALL类型消息并显示视频通话邀请弹框
- 实现通话邀请的接听、拒绝和跳转功能
- 添加VideoCallInviteDialog组件用于显示通话邀请
- 优化消息通知弹框的边距样式
- 在pubspec.yaml中添加必要的资源文件路径配置
master
Jolie 3 months ago
parent
commit
03545aeb1c
4 changed files with 322 additions and 4 deletions
  1. 159
      lib/im/im_manager.dart
  2. 2
      lib/widget/message/message_notification_dialog.dart
  3. 157
      lib/widget/message/video_call_invite_dialog.dart
  4. 8
      pubspec.yaml

159
lib/im/im_manager.dart

@ -17,6 +17,9 @@ import '../pages/mine/login_page.dart';
import '../pages/message/chat_page.dart'; import '../pages/message/chat_page.dart';
import '../network/user_api.dart'; import '../network/user_api.dart';
import '../widget/message/message_notification_dialog.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 { class _NotificationMessage {
@ -1387,6 +1390,162 @@ class IMManager {
return; 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<String, dynamic>;
final callType = callInfo['callType'] as String?;
final callStatus = callInfo['callStatus'] as String?;
// missed calling
if (callType == 'video' && (callStatus == 'missed' || callStatus == 'calling')) {
//
Map<String, dynamic>? 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<ConversationController>()) {
final conversationController = Get.find<ConversationController>();
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<ChatController>(tag: tag)) {
chatController = Get.find<ChatController>(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<ChatController>(tag: tag)) {
chatController = Get.find<ChatController>(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 // _activeChatControllers ID
if (_activeChatControllers.containsKey(fromId)) { if (_activeChatControllers.containsKey(fromId)) {

2
lib/widget/message/message_notification_dialog.dart

@ -23,7 +23,7 @@ class MessageNotificationDialog extends StatelessWidget {
return GestureDetector( return GestureDetector(
onTap: onTap, onTap: onTap,
child: Container( 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), padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 12.h),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,

157
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,
),
),
),
],
),
],
),
),
);
}
}

8
pubspec.yaml

@ -65,7 +65,7 @@ dependencies:
audioplayers: ^6.5.1 audioplayers: ^6.5.1
video_thumbnail: ^0.5.3 # 视频缩略图生成 video_thumbnail: ^0.5.3 # 视频缩略图生成
fluwx: ^5.7.5 fluwx: ^5.7.5
# # tobias: ^5.3.1
# # tobias: ^5.3.1
agora_rtc_engine: ^6.5.3 agora_rtc_engine: ^6.5.3
agora_rtm: ^2.2.5 agora_rtm: ^2.2.5
agora_token_generator: ^1.0.0 agora_token_generator: ^1.0.0
@ -79,7 +79,7 @@ dependencies:
im_flutter_sdk: 4.15.2 im_flutter_sdk: 4.15.2
webview_flutter: ^4.13.0 webview_flutter: ^4.13.0
ota_update: ^7.1.0 ota_update: ^7.1.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
@ -108,7 +108,8 @@ flutter:
# To add assets to your application, add an assets section, like this: # To add assets to your application, add an assets section, like this:
assets: assets:
- build/app/outputs/flutter-apk/
- assets/images/
- assets/images/emoji/
# - images/a_dot_ham.jpeg # - images/a_dot_ham.jpeg
# An images asset can refer to one or more resolution-specific "variants", see # 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" image_path: "assets/images/app_logo.jpg"
min_sdk_android: 21 min_sdk_android: 21
remove_alpha_ios: true remove_alpha_ios: true
Loading…
Cancel
Save