|
|
@ -1,4 +1,5 @@ |
|
|
import 'package:dating_touchme_app/extension/ex_widget.dart'; |
|
|
import 'package:dating_touchme_app/extension/ex_widget.dart'; |
|
|
|
|
|
import 'package:dating_touchme_app/widget/live/svga_player_widget.dart'; |
|
|
import 'package:flutter/material.dart'; |
|
|
import 'package:flutter/material.dart'; |
|
|
import 'package:flutter_screenutil/flutter_screenutil.dart'; |
|
|
import 'package:flutter_screenutil/flutter_screenutil.dart'; |
|
|
import 'package:get/get.dart'; |
|
|
import 'package:get/get.dart'; |
|
|
@ -76,13 +77,7 @@ class _ChatPageState extends State<ChatPage> { |
|
|
if (Get.isRegistered<ChatController>(tag: tag)) { |
|
|
if (Get.isRegistered<ChatController>(tag: tag)) { |
|
|
Get.delete<ChatController>(tag: tag); |
|
|
Get.delete<ChatController>(tag: tag); |
|
|
} |
|
|
} |
|
|
_controller = Get.put( |
|
|
|
|
|
ChatController( |
|
|
|
|
|
userId: widget.userId, |
|
|
|
|
|
userData: widget.userData, |
|
|
|
|
|
), |
|
|
|
|
|
tag: tag, |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
_controller = Get.put(ChatController(userId: widget.userId, userData: widget.userData), tag: tag); |
|
|
|
|
|
|
|
|
// 监听滚动,当滚动到顶部时加载更多消息 |
|
|
// 监听滚动,当滚动到顶部时加载更多消息 |
|
|
_scrollController.addListener(() { |
|
|
_scrollController.addListener(() { |
|
|
@ -158,11 +153,7 @@ class _ChatPageState extends State<ChatPage> { |
|
|
maskColor: TDTheme.of(context).fontGyColor2, |
|
|
maskColor: TDTheme.of(context).fontGyColor2, |
|
|
onDismiss: () { |
|
|
onDismiss: () { |
|
|
_controller.setDialogDismiss(false); |
|
|
_controller.setDialogDismiss(false); |
|
|
}, |
|
|
|
|
|
maskWidget: GestureDetector( |
|
|
|
|
|
onTap: () => SmartDialog.dismiss(), |
|
|
|
|
|
child: Container(color: Colors.transparent), |
|
|
|
|
|
), |
|
|
|
|
|
|
|
|
} |
|
|
); |
|
|
); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@ -185,6 +176,9 @@ class _ChatPageState extends State<ChatPage> { |
|
|
// 显示通话类型选择弹框 |
|
|
// 显示通话类型选择弹框 |
|
|
void _showCallTypeSelectionDialog(ChatController controller, {List<ChatAudioProductModel>? products}) { |
|
|
void _showCallTypeSelectionDialog(ChatController controller, {List<ChatAudioProductModel>? products}) { |
|
|
// 隐藏键盘 |
|
|
// 隐藏键盘 |
|
|
|
|
|
if(_controller.isDialogShowing.value){ |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
FocusScope.of(context).unfocus(); |
|
|
FocusScope.of(context).unfocus(); |
|
|
_controller.setDialogDismiss(true); |
|
|
_controller.setDialogDismiss(true); |
|
|
SmartDialog.show( |
|
|
SmartDialog.show( |
|
|
@ -227,14 +221,10 @@ class _ChatPageState extends State<ChatPage> { |
|
|
}, |
|
|
}, |
|
|
alignment: Alignment.bottomCenter, |
|
|
alignment: Alignment.bottomCenter, |
|
|
animationType: SmartAnimationType.centerFade_otherSlide, |
|
|
animationType: SmartAnimationType.centerFade_otherSlide, |
|
|
maskColor: TDTheme.of(context).fontGyColor2, |
|
|
|
|
|
|
|
|
maskColor: TDTheme.of(context).fontGyColor3, |
|
|
onDismiss: () { |
|
|
onDismiss: () { |
|
|
_controller.setDialogDismiss(false); |
|
|
_controller.setDialogDismiss(false); |
|
|
}, |
|
|
|
|
|
maskWidget: GestureDetector( |
|
|
|
|
|
onTap: () => SmartDialog.dismiss(), |
|
|
|
|
|
child: Container(color: Colors.transparent), |
|
|
|
|
|
), |
|
|
|
|
|
|
|
|
} |
|
|
); |
|
|
); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@ -256,172 +246,178 @@ class _ChatPageState extends State<ChatPage> { |
|
|
Get.back(); |
|
|
Get.back(); |
|
|
} |
|
|
} |
|
|
}, |
|
|
}, |
|
|
child: Scaffold( |
|
|
|
|
|
backgroundColor: Color(0xffF5F5F5), |
|
|
|
|
|
appBar: AppBar( |
|
|
|
|
|
title: Row( |
|
|
|
|
|
mainAxisSize: MainAxisSize.min, |
|
|
|
|
|
children: [ |
|
|
|
|
|
Text( |
|
|
|
|
|
controller.userNickName ?? widget.userData?.nickName ?? '', |
|
|
|
|
|
style: TextStyle(fontSize: 18.sp), |
|
|
|
|
|
), |
|
|
|
|
|
// 使用 Obx 来响应在线状态的变化 |
|
|
|
|
|
Obx(() { |
|
|
|
|
|
final isOnline = controller.isUserOnline.value; |
|
|
|
|
|
if (isOnline) { |
|
|
|
|
|
return Row( |
|
|
|
|
|
mainAxisSize: MainAxisSize.min, |
|
|
|
|
|
children: [ |
|
|
|
|
|
SizedBox(width: 8.w), |
|
|
|
|
|
Container( |
|
|
|
|
|
width: 46.w, |
|
|
|
|
|
height: 26.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), |
|
|
|
|
|
|
|
|
child: Stack( |
|
|
|
|
|
children: [ |
|
|
|
|
|
Scaffold( |
|
|
|
|
|
backgroundColor: Color(0xffF5F5F5), |
|
|
|
|
|
appBar: AppBar( |
|
|
|
|
|
title: Row( |
|
|
|
|
|
mainAxisSize: MainAxisSize.min, |
|
|
|
|
|
children: [ |
|
|
|
|
|
Text( |
|
|
|
|
|
controller.userNickName ?? widget.userData?.nickName ?? '', |
|
|
|
|
|
style: TextStyle(fontSize: 18.sp), |
|
|
|
|
|
), |
|
|
|
|
|
// 使用 Obx 来响应在线状态的变化 |
|
|
|
|
|
Obx(() { |
|
|
|
|
|
final isOnline = controller.isUserOnline.value; |
|
|
|
|
|
if (isOnline) { |
|
|
|
|
|
return Row( |
|
|
|
|
|
mainAxisSize: MainAxisSize.min, |
|
|
|
|
|
children: [ |
|
|
|
|
|
SizedBox(width: 8.w), |
|
|
|
|
|
Container( |
|
|
|
|
|
width: 46.w, |
|
|
|
|
|
height: 26.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), |
|
|
|
|
|
), |
|
|
|
|
|
), |
|
|
), |
|
|
), |
|
|
), |
|
|
), |
|
|
|
|
|
], |
|
|
|
|
|
); |
|
|
|
|
|
} |
|
|
|
|
|
return SizedBox.shrink(); |
|
|
|
|
|
}), |
|
|
|
|
|
], |
|
|
|
|
|
), |
|
|
|
|
|
centerTitle: false, |
|
|
|
|
|
actions: [ |
|
|
|
|
|
Container( |
|
|
|
|
|
padding: EdgeInsets.only(right: 16.w), |
|
|
|
|
|
child: Image.asset(Assets.imagesMore, width: 16.w), |
|
|
|
|
|
).onTap(() { |
|
|
|
|
|
// 跳转到聊天设置页面,传递用户信息 |
|
|
|
|
|
Get.to(() => ChatSettingsPage( |
|
|
|
|
|
userId: widget.userId, |
|
|
|
|
|
userData: widget.userData ?? _controller.userData, |
|
|
|
|
|
)); |
|
|
|
|
|
}), |
|
|
|
|
|
], |
|
|
|
|
|
leading: IconButton( |
|
|
|
|
|
icon: Icon(Icons.arrow_back_ios), |
|
|
|
|
|
onPressed: () { |
|
|
|
|
|
Get.back(); |
|
|
|
|
|
}, |
|
|
|
|
|
), |
|
|
|
|
|
), |
|
|
|
|
|
body: GestureDetector( |
|
|
|
|
|
onTap: () { |
|
|
|
|
|
// 点击空白处收起键盘 |
|
|
|
|
|
FocusManager.instance.primaryFocus?.unfocus(); |
|
|
|
|
|
// 关闭底部面板(表情、更多选项、语音输入) |
|
|
|
|
|
ChatInputBar.closePanels(_chatInputBarKey); |
|
|
|
|
|
}, |
|
|
|
|
|
behavior: HitTestBehavior.opaque, |
|
|
|
|
|
child: Column( |
|
|
|
|
|
children: [ |
|
|
|
|
|
// 消息列表区域 |
|
|
|
|
|
Expanded( |
|
|
|
|
|
child: Container( |
|
|
|
|
|
color: Color(0xffF5F5F5), |
|
|
|
|
|
child: ListView.builder( |
|
|
|
|
|
controller: _scrollController, |
|
|
|
|
|
reverse: false, |
|
|
|
|
|
padding: EdgeInsets.only( |
|
|
|
|
|
top: 16.w, |
|
|
|
|
|
left: 16.w, |
|
|
|
|
|
right: 16.w, |
|
|
|
|
|
bottom: 16.w, |
|
|
), |
|
|
), |
|
|
), |
|
|
|
|
|
], |
|
|
|
|
|
); |
|
|
|
|
|
} |
|
|
|
|
|
return SizedBox.shrink(); |
|
|
|
|
|
}), |
|
|
|
|
|
], |
|
|
|
|
|
), |
|
|
|
|
|
centerTitle: false, |
|
|
|
|
|
actions: [ |
|
|
|
|
|
Container( |
|
|
|
|
|
padding: EdgeInsets.only(right: 16.w), |
|
|
|
|
|
child: Image.asset(Assets.imagesMore, width: 16.w), |
|
|
|
|
|
).onTap(() { |
|
|
|
|
|
// 跳转到聊天设置页面,传递用户信息 |
|
|
|
|
|
Get.to(() => ChatSettingsPage( |
|
|
|
|
|
userId: widget.userId, |
|
|
|
|
|
userData: widget.userData ?? _controller.userData, |
|
|
|
|
|
)); |
|
|
|
|
|
}), |
|
|
|
|
|
], |
|
|
|
|
|
leading: IconButton( |
|
|
|
|
|
icon: Icon(Icons.arrow_back_ios), |
|
|
|
|
|
onPressed: () { |
|
|
|
|
|
Get.back(); |
|
|
|
|
|
}, |
|
|
|
|
|
), |
|
|
|
|
|
), |
|
|
|
|
|
body: GestureDetector( |
|
|
|
|
|
onTap: () { |
|
|
|
|
|
// 点击空白处收起键盘 |
|
|
|
|
|
FocusManager.instance.primaryFocus?.unfocus(); |
|
|
|
|
|
// 关闭底部面板(表情、更多选项、语音输入) |
|
|
|
|
|
ChatInputBar.closePanels(_chatInputBarKey); |
|
|
|
|
|
}, |
|
|
|
|
|
behavior: HitTestBehavior.opaque, |
|
|
|
|
|
child: Column( |
|
|
|
|
|
children: [ |
|
|
|
|
|
// 消息列表区域 |
|
|
|
|
|
Expanded( |
|
|
|
|
|
child: Container( |
|
|
|
|
|
color: Color(0xffF5F5F5), |
|
|
|
|
|
child: ListView.builder( |
|
|
|
|
|
controller: _scrollController, |
|
|
|
|
|
reverse: false, |
|
|
|
|
|
padding: EdgeInsets.only( |
|
|
|
|
|
top: 16.w, |
|
|
|
|
|
left: 16.w, |
|
|
|
|
|
right: 16.w, |
|
|
|
|
|
bottom: 16.w, |
|
|
|
|
|
), |
|
|
|
|
|
itemCount: controller.messages.length + 1, // 添加用户信息卡片 |
|
|
|
|
|
// 🚀 性能优化:添加缓存范围,减少重建 |
|
|
|
|
|
cacheExtent: 500, // 缓存屏幕外500像素的内容 |
|
|
|
|
|
itemBuilder: (context, index) { |
|
|
|
|
|
// 第一个是用户信息卡片 |
|
|
|
|
|
if (index == 0) { |
|
|
|
|
|
return _buildUserInfoCard(controller, widget.userData); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
final message = controller.messages[index - 1]; |
|
|
|
|
|
Get.log('message: $message'); |
|
|
|
|
|
final isSentByMe = |
|
|
|
|
|
message.direction == MessageDirection.SEND; |
|
|
|
|
|
|
|
|
itemCount: controller.messages.length + 1, // 添加用户信息卡片 |
|
|
|
|
|
// 🚀 性能优化:添加缓存范围,减少重建 |
|
|
|
|
|
cacheExtent: 500, // 缓存屏幕外500像素的内容 |
|
|
|
|
|
itemBuilder: (context, index) { |
|
|
|
|
|
// 第一个是用户信息卡片 |
|
|
|
|
|
if (index == 0) { |
|
|
|
|
|
return _buildUserInfoCard(controller, widget.userData); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
final previousMessage = index > 1 |
|
|
|
|
|
? controller.messages[index - 2] |
|
|
|
|
|
: null; |
|
|
|
|
|
|
|
|
final message = controller.messages[index - 1]; |
|
|
|
|
|
Get.log('message347: $message'); |
|
|
|
|
|
final isSentByMe = |
|
|
|
|
|
message.direction == MessageDirection.SEND; |
|
|
|
|
|
|
|
|
// 检查是否需要显示508错误提示(使用临时状态,不持久化) |
|
|
|
|
|
final showRoseError = isSentByMe && |
|
|
|
|
|
message.status == MessageStatus.FAIL && |
|
|
|
|
|
controller.shouldShowRoseError(message.msgId); |
|
|
|
|
|
|
|
|
final previousMessage = index > 1 |
|
|
|
|
|
? controller.messages[index - 2] |
|
|
|
|
|
: null; |
|
|
|
|
|
|
|
|
// 检查是否需要显示敏感词错误提示(使用临时状态,不持久化) |
|
|
|
|
|
final showSensitiveWordError = isSentByMe && |
|
|
|
|
|
message.status == MessageStatus.FAIL && |
|
|
|
|
|
controller.shouldShowSensitiveWordError(message.msgId); |
|
|
|
|
|
|
|
|
// 检查是否需要显示508错误提示(使用临时状态,不持久化) |
|
|
|
|
|
final showRoseError = isSentByMe && |
|
|
|
|
|
message.status == MessageStatus.FAIL && |
|
|
|
|
|
controller.shouldShowRoseError(message.msgId); |
|
|
|
|
|
|
|
|
// 🚀 性能优化:为每个消息项设置唯一的 key |
|
|
|
|
|
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), |
|
|
|
|
|
// 显示敏感词错误提示(错误码E0001) |
|
|
|
|
|
if (showSensitiveWordError) _buildSensitiveWordErrorHint(context), |
|
|
|
|
|
], |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
// 检查是否需要显示敏感词错误提示(使用临时状态,不持久化) |
|
|
|
|
|
final showSensitiveWordError = isSentByMe && |
|
|
|
|
|
message.status == MessageStatus.FAIL && |
|
|
|
|
|
controller.shouldShowSensitiveWordError(message.msgId); |
|
|
|
|
|
|
|
|
|
|
|
// 🚀 性能优化:为每个消息项设置唯一的 key |
|
|
|
|
|
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), |
|
|
|
|
|
// 显示敏感词错误提示(错误码E0001) |
|
|
|
|
|
if (showSensitiveWordError) _buildSensitiveWordErrorHint(context), |
|
|
|
|
|
], |
|
|
|
|
|
); |
|
|
|
|
|
}, |
|
|
|
|
|
), |
|
|
|
|
|
), |
|
|
|
|
|
), |
|
|
|
|
|
// 使用抽离的聊天输入栏组件 |
|
|
|
|
|
ChatInputBar( |
|
|
|
|
|
key: _chatInputBarKey, |
|
|
|
|
|
onSendMessage: (message) async { |
|
|
|
|
|
await controller.sendMessage(message); |
|
|
|
|
|
}, |
|
|
|
|
|
onImageSelected: (imagePaths) async { |
|
|
|
|
|
// 为每个图片路径调用控制器的方法发送图片消息 |
|
|
|
|
|
for (var imagePath in imagePaths) { |
|
|
|
|
|
await controller.sendImageMessage(imagePath); |
|
|
|
|
|
} |
|
|
|
|
|
}, |
|
|
|
|
|
onVoiceRecorded: (filePath, seconds) async { |
|
|
|
|
|
// 处理语音录音完成,回传文件路径和秒数 |
|
|
|
|
|
await controller.sendVoiceMessage(filePath, seconds); |
|
|
|
|
|
}, |
|
|
|
|
|
onGiftTap: () { |
|
|
|
|
|
// 显示礼物弹窗 |
|
|
|
|
|
_showGiftPopup(); |
|
|
|
|
|
}, |
|
|
|
|
|
// 视频通话回调 |
|
|
|
|
|
onVideoCall: () async { |
|
|
|
|
|
// 先调用获取聊天音频产品列表接口 |
|
|
|
|
|
final products = await CallController.instance.listChatAudioProduct(widget.userId); |
|
|
|
|
|
// 显示通话类型选择弹框,传入产品数据 |
|
|
|
|
|
_showCallTypeSelectionDialog(controller, products: products); |
|
|
}, |
|
|
}, |
|
|
), |
|
|
), |
|
|
), |
|
|
|
|
|
|
|
|
], |
|
|
), |
|
|
), |
|
|
// 使用抽离的聊天输入栏组件 |
|
|
|
|
|
ChatInputBar( |
|
|
|
|
|
key: _chatInputBarKey, |
|
|
|
|
|
onSendMessage: (message) async { |
|
|
|
|
|
await controller.sendMessage(message); |
|
|
|
|
|
}, |
|
|
|
|
|
onImageSelected: (imagePaths) async { |
|
|
|
|
|
// 为每个图片路径调用控制器的方法发送图片消息 |
|
|
|
|
|
for (var imagePath in imagePaths) { |
|
|
|
|
|
await controller.sendImageMessage(imagePath); |
|
|
|
|
|
} |
|
|
|
|
|
}, |
|
|
|
|
|
onVoiceRecorded: (filePath, seconds) async { |
|
|
|
|
|
// 处理语音录音完成,回传文件路径和秒数 |
|
|
|
|
|
await controller.sendVoiceMessage(filePath, seconds); |
|
|
|
|
|
}, |
|
|
|
|
|
onGiftTap: () { |
|
|
|
|
|
// 显示礼物弹窗 |
|
|
|
|
|
_showGiftPopup(); |
|
|
|
|
|
}, |
|
|
|
|
|
// 视频通话回调 |
|
|
|
|
|
onVideoCall: () async { |
|
|
|
|
|
// 先调用获取聊天音频产品列表接口 |
|
|
|
|
|
final products = await CallController.instance.listChatAudioProduct(widget.userId); |
|
|
|
|
|
// 显示通话类型选择弹框,传入产品数据 |
|
|
|
|
|
_showCallTypeSelectionDialog(controller, products: products); |
|
|
|
|
|
}, |
|
|
|
|
|
), |
|
|
), |
|
|
], |
|
|
|
|
|
), |
|
|
), |
|
|
), |
|
|
|
|
|
|
|
|
const SvgaPlayerWidget(), |
|
|
|
|
|
], |
|
|
), |
|
|
), |
|
|
|
|
|
|
|
|
); |
|
|
); |
|
|
}, |
|
|
}, |
|
|
); |
|
|
); |
|
|
|