Browse Source

优化逻辑

ios
Jolie 4 months ago
parent
commit
ffe0efa6f3
15 changed files with 992 additions and 16 deletions
  1. BIN
      assets/images/accept_call.png
  2. BIN
      assets/images/plat_voice_message.png
  3. BIN
      assets/images/plat_voice_message_self.png
  4. BIN
      assets/images/reject_call.png
  5. BIN
      assets/images/send_call.png
  6. BIN
      assets/images/send_video_call.png
  7. BIN
      assets/images/voice_bg_message.png
  8. BIN
      assets/images/voice_bg_message_self.png
  9. 395
      lib/controller/message/call_manager.dart
  10. 122
      lib/controller/message/chat_controller.dart
  11. 8
      lib/generated/assets.dart
  12. 74
      lib/pages/message/chat_page.dart
  13. 314
      lib/widget/message/call_item.dart
  14. 58
      lib/widget/message/chat_input_bar.dart
  15. 37
      lib/widget/message/message_item.dart

BIN
assets/images/accept_call.png

Before After
Width: 80  |  Height: 80  |  Size: 1004 B

BIN
assets/images/plat_voice_message.png

Before After
Width: 76  |  Height: 76  |  Size: 1.0 KiB

BIN
assets/images/plat_voice_message_self.png

Before After
Width: 76  |  Height: 76  |  Size: 937 B

BIN
assets/images/reject_call.png

Before After
Width: 80  |  Height: 80  |  Size: 1018 B

BIN
assets/images/send_call.png

Before After
Width: 80  |  Height: 80  |  Size: 822 B

BIN
assets/images/send_video_call.png

Before After
Width: 94  |  Height: 60  |  Size: 707 B

BIN
assets/images/voice_bg_message.png

Before After
Width: 208  |  Height: 100  |  Size: 1.4 KiB

BIN
assets/images/voice_bg_message_self.png

Before After
Width: 208  |  Height: 100  |  Size: 1.4 KiB

395
lib/controller/message/call_manager.dart

@ -0,0 +1,395 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:im_flutter_sdk/im_flutter_sdk.dart';
import 'chat_controller.dart';
///
enum CallType {
voice, //
video, //
}
///
enum CallStatus {
calling, //
missed, //
cancelled, //
rejected, //
connected, //
}
///
class CallManager extends GetxController {
static CallManager? _instance;
static CallManager get instance {
_instance ??= Get.put(CallManager());
return _instance!;
}
//
final Rx<CallSession?> currentCall = Rx<CallSession?>(null);
//
Timer? _callTimer;
int _callDurationSeconds = 0;
final RxInt callDurationSeconds = RxInt(0);
CallManager() {
print('📞 [CallManager] 通话管理器已初始化');
}
///
/// [targetUserId] ID
/// [callType]
/// [chatController]
Future<bool> initiateCall({
required String targetUserId,
required CallType callType,
ChatController? chatController,
}) async {
try {
if (currentCall.value != null) {
SmartDialog.showToast('已有通话正在进行中');
return false;
}
print('📞 [CallManager] 发起${callType == CallType.video ? "视频" : "语音"}通话,目标用户: $targetUserId');
//
final session = CallSession(
targetUserId: targetUserId,
callType: callType,
status: CallStatus.calling,
isInitiator: true,
startTime: DateTime.now(),
);
currentCall.value = session;
//
final callTypeStr = callType == CallType.video ? 'video' : 'voice';
await _sendCallMessage(
targetUserId: targetUserId,
callType: callTypeStr,
callStatus: 'missed', //
chatController: chatController,
);
// TODO: SDK
// await RTCManager.instance.startCall(targetUserId, callType);
return true;
} catch (e) {
print('❌ [CallManager] 发起通话失败: $e');
SmartDialog.showToast('发起通话失败: $e');
currentCall.value = null;
return false;
}
}
///
/// [message]
/// [chatController]
Future<bool> acceptCall({
required EMMessage message,
ChatController? chatController,
}) async {
try {
final callInfo = _parseCallInfo(message);
if (callInfo == null) {
return false;
}
final targetUserId = message.from ?? '';
final callTypeStr = callInfo['callType'] as String?;
final callType = callTypeStr == 'video' ? CallType.video : CallType.voice;
print('📞 [CallManager] 接听${callType == CallType.video ? "视频" : "语音"}通话');
//
final session = CallSession(
targetUserId: targetUserId,
callType: callType,
status: CallStatus.calling,
isInitiator: false,
startTime: DateTime.now(),
);
currentCall.value = session;
//
_startCallTimer();
//
await _updateCallMessageStatus(
message: message,
callStatus: 'calling',
callDuration: 0,
chatController: chatController,
);
// TODO: SDK
// await RTCManager.instance.acceptCall(targetUserId, callType);
return true;
} catch (e) {
print('❌ [CallManager] 接听通话失败: $e');
SmartDialog.showToast('接听通话失败: $e');
return false;
}
}
///
/// [message]
/// [chatController]
Future<bool> rejectCall({
required EMMessage message,
ChatController? chatController,
}) async {
try {
print('📞 [CallManager] 拒绝通话');
//
await _updateCallMessageStatus(
message: message,
callStatus: 'rejected',
chatController: chatController,
);
//
currentCall.value = null;
// TODO: SDK
// await RTCManager.instance.rejectCall(message.from ?? '');
return true;
} catch (e) {
print('❌ [CallManager] 拒绝通话失败: $e');
SmartDialog.showToast('拒绝通话失败: $e');
return false;
}
}
///
/// [message]
/// [chatController]
Future<bool> cancelCall({
EMMessage? message,
ChatController? chatController,
}) async {
try {
print('📞 [CallManager] 取消通话');
//
if (message != null) {
await _updateCallMessageStatus(
message: message,
callStatus: 'cancelled',
chatController: chatController,
);
}
//
_stopCallTimer();
//
currentCall.value = null;
// TODO: SDK
// await RTCManager.instance.cancelCall();
return true;
} catch (e) {
print('❌ [CallManager] 取消通话失败: $e');
SmartDialog.showToast('取消通话失败: $e');
return false;
}
}
///
/// [callDuration]
/// [chatController]
Future<bool> endCall({
required int callDuration,
EMMessage? message,
ChatController? chatController,
}) async {
try {
print('📞 [CallManager] 结束通话,时长: ${callDuration}');
//
_stopCallTimer();
//
if (message != null) {
await _updateCallMessageStatus(
message: message,
callStatus: 'calling',
callDuration: callDuration,
chatController: chatController,
);
}
//
currentCall.value = null;
// TODO: SDK
// await RTCManager.instance.endCall();
return true;
} catch (e) {
print('❌ [CallManager] 结束通话失败: $e');
SmartDialog.showToast('结束通话失败: $e');
return false;
}
}
///
void _startCallTimer() {
_callDurationSeconds = 0;
callDurationSeconds.value = 0;
_callTimer?.cancel();
_callTimer = Timer.periodic(Duration(seconds: 1), (timer) {
_callDurationSeconds++;
callDurationSeconds.value = _callDurationSeconds;
});
}
///
void _stopCallTimer() {
_callTimer?.cancel();
_callTimer = null;
_callDurationSeconds = 0;
callDurationSeconds.value = 0;
}
///
Future<bool> _sendCallMessage({
required String targetUserId,
required String callType,
required String callStatus,
int? callDuration,
ChatController? chatController,
}) async {
try {
// chatController使
if (chatController != null) {
return await chatController.sendCallMessage(
callType: callType,
callStatus: callStatus,
callDuration: callDuration,
);
}
// IMManager
final callInfoMap = <String, dynamic>{
'callType': callType,
'callStatus': callStatus,
};
if (callDuration != null) {
callInfoMap['callDuration'] = callDuration;
}
final callInfoJson = jsonEncode(callInfoMap);
final content = '[CALL:]$callInfoJson';
final message = EMMessage.createTxtSendMessage(
targetId: targetUserId,
content: content,
);
await EMClient.getInstance.chatManager.sendMessage(message);
return true;
} catch (e) {
print('❌ [CallManager] 发送通话消息失败: $e');
return false;
}
}
///
Future<bool> _updateCallMessageStatus({
required EMMessage message,
required String callStatus,
int? callDuration,
ChatController? chatController,
}) async {
try {
//
final callInfo = _parseCallInfo(message);
if (callInfo == null) {
return false;
}
final callType = callInfo['callType'] as String? ?? 'voice';
final targetUserId = message.from ?? message.to ?? '';
//
return await _sendCallMessage(
targetUserId: targetUserId,
callType: callType,
callStatus: callStatus,
callDuration: callDuration,
chatController: chatController,
);
} catch (e) {
print('❌ [CallManager] 更新通话消息状态失败: $e');
return false;
}
}
///
Map<String, dynamic>? _parseCallInfo(EMMessage message) {
try {
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<String, dynamic>;
}
}
} catch (e) {
print('解析通话信息失败: $e');
}
return null;
}
///
bool get isInCall => currentCall.value != null;
///
int get currentCallDuration => callDurationSeconds.value;
@override
void onClose() {
_stopCallTimer();
currentCall.value = null;
super.onClose();
}
}
///
class CallSession {
final String targetUserId;
final CallType callType;
final CallStatus status;
final bool isInitiator; //
final DateTime startTime;
DateTime? endTime;
CallSession({
required this.targetUserId,
required this.callType,
required this.status,
required this.isInitiator,
required this.startTime,
this.endTime,
});
///
int get duration {
final end = endTime ?? DateTime.now();
return end.difference(startTime).inSeconds;
}
}

122
lib/controller/message/chat_controller.dart

@ -1,5 +1,6 @@
import 'package:get/get.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'dart:convert';
import '../../im/im_manager.dart';
import 'package:im_flutter_sdk/im_flutter_sdk.dart';
@ -64,9 +65,10 @@ class ChatController extends GetxController {
///
Future<bool> sendMessage(String content) async {
EMMessage? tempMessage;
try {
// 使
final tempMessage = EMMessage.createTxtSendMessage(
tempMessage = EMMessage.createTxtSendMessage(
targetId: userId,
content: content,
);
@ -79,16 +81,31 @@ class ChatController extends GetxController {
final message = await IMManager.instance.sendTextMessage(content, userId);
if (message != null) {
//
final index = messages.indexWhere((msg) => msg.msgId == tempMessage.msgId);
final index = messages.indexWhere((msg) => msg.msgId == tempMessage!.msgId);
if (index != -1) {
messages[index] = message;
}
update();
//
_refreshConversationList();
// PROGRESS SDK
//
if (message.status == MessageStatus.PROGRESS) {
// PROGRESS 3
_checkMessageStatusUntilComplete(message.msgId, maxAttempts: 6);
}
return true;
} else {
// FAIL
//
final index = messages.indexWhere((msg) => msg.msgId == tempMessage!.msgId);
if (index != -1) {
// SDK FAIL
Future.delayed(Duration(milliseconds: 300), () {
update();
});
}
update();
SmartDialog.showToast('消息发送失败,请点击重发');
return false;
@ -97,6 +114,13 @@ class ChatController extends GetxController {
if (Get.isLogEnable) {
Get.log('发送消息失败: $e');
}
//
if (tempMessage != null) {
final index = messages.indexWhere((msg) => msg.msgId == tempMessage!.msgId);
if (index != -1) {
update();
}
}
SmartDialog.showToast('消息发送失败: $e');
return false;
}
@ -486,6 +510,75 @@ class ChatController extends GetxController {
return message.status == MessageStatus.FAIL;
}
///
/// [callType] 'voice' 'video'
/// [callStatus] 'calling' 'missed' 'cancelled' 'rejected'
/// [callDuration] callStatus 'calling'
Future<bool> sendCallMessage({
required String callType, // 'voice' 'video'
required String callStatus, // 'calling', 'missed', 'cancelled', 'rejected'
int? callDuration, //
}) async {
try {
//
final callInfoMap = <String, dynamic>{
'callType': callType,
'callStatus': callStatus,
};
if (callDuration != null) {
callInfoMap['callDuration'] = callDuration;
}
final callInfoJson = jsonEncode(callInfoMap);
// [CALL:] + JSON
final content = '[CALL:]$callInfoJson';
// 使
final tempMessage = EMMessage.createTxtSendMessage(
targetId: userId,
content: content,
);
//
messages.add(tempMessage);
update();
//
final messageToSend = EMMessage.createTxtSendMessage(
targetId: userId,
content: content,
);
//
try {
final sentMessage = await EMClient.getInstance.chatManager.sendMessage(messageToSend);
//
final index = messages.indexWhere((msg) => msg.msgId == tempMessage.msgId);
if (index != -1) {
messages[index] = sentMessage;
}
update();
//
_refreshConversationList();
return true;
} catch (e) {
// FAIL
update();
if (Get.isLogEnable) {
Get.log('发送通话消息失败: $e');
}
SmartDialog.showToast('通话消息发送失败: $e');
return false;
}
} catch (e) {
if (Get.isLogEnable) {
Get.log('发送通话消息失败: $e');
}
SmartDialog.showToast('通话消息发送失败: $e');
return false;
}
}
///
Future<bool> resendMessage(EMMessage failedMessage) async {
try {
@ -541,4 +634,27 @@ class ChatController extends GetxController {
}
}
}
/// PROGRESS
void _checkMessageStatusUntilComplete(String messageId, {int maxAttempts = 6, int attempt = 0}) {
if (attempt >= maxAttempts) {
//
return;
}
Future.delayed(Duration(milliseconds: 500), () {
final index = messages.indexWhere((msg) => msg.msgId == messageId);
if (index != -1) {
final message = messages[index];
if (message.status != MessageStatus.PROGRESS) {
// UI
update();
return;
} else {
// PROGRESS
_checkMessageStatusUntilComplete(messageId, maxAttempts: maxAttempts, attempt: attempt + 1);
}
}
});
}
}

8
lib/generated/assets.dart

@ -67,6 +67,7 @@ class Assets {
static const String emojiEmoji63 = 'assets/images/emoji/emoji_63.png';
static const String emojiEmoji64 = 'assets/images/emoji/emoji_64.png';
static const String imagesAd = 'assets/images/ad.png';
static const String imagesAcceptCall = 'assets/images/accept_call.png';
static const String imagesAdd = 'assets/images/add.png';
static const String imagesAliPay = 'assets/images/ali_pay.png';
static const String imagesArrow = 'assets/images/arrow.png';
@ -138,17 +139,22 @@ class Assets {
static const String imagesPhotoUncheck = 'assets/images/photo_uncheck.png';
static const String imagesPlayIcon = 'assets/images/play_icon.png';
static const String imagesPlayer = 'assets/images/player.png';
static const String imagesPlatVoiceMessage = 'assets/images/plat_voice_message.png';
static const String imagesPlatVoiceMessageSelf = 'assets/images/plat_voice_message_self.png';
static const String imagesRealChecked = 'assets/images/real_checked.png';
static const String imagesRealName = 'assets/images/real_name.png';
static const String imagesRealUncheck = 'assets/images/real_uncheck.png';
static const String imagesRealnameHelp = 'assets/images/realname_help.png';
static const String imagesRejectCall = 'assets/images/reject_call.png';
static const String imagesRose = 'assets/images/rose.png';
static const String imagesRoseBanner = 'assets/images/rose_banner.png';
static const String imagesRoseGift = 'assets/images/rose_gift.png';
static const String imagesRoseWhite = 'assets/images/rose_white.png';
static const String imagesSearch = 'assets/images/search.png';
static const String imagesSeat = 'assets/images/seat.png';
static const String imagesSendCall = 'assets/images/send_call.png';
static const String imagesSendMessageIcon = 'assets/images/send_message_icon.png';
static const String imagesSendVideoCall = 'assets/images/send_video_call.png';
static const String imagesSetting = 'assets/images/setting.png';
static const String imagesSettingIcon = 'assets/images/setting_icon.png';
static const String imagesShop = 'assets/images/shop.png';
@ -174,6 +180,8 @@ class Assets {
static const String imagesVipVisitor = 'assets/images/vip_visitor.png';
static const String imagesVipVoice = 'assets/images/vip_voice.png';
static const String imagesVoice = 'assets/images/voice.png';
static const String imagesVoiceBgMessage = 'assets/images/voice_bg_message.png';
static const String imagesVoiceBgMessageSelf = 'assets/images/voice_bg_message_self.png';
static const String imagesVoiceIcon = 'assets/images/voice_icon.png';
static const String imagesWallet = 'assets/images/wallet.png';
static const String imagesWechatPay = 'assets/images/wechat_pay.png';

74
lib/pages/message/chat_page.dart

@ -6,6 +6,7 @@ import 'package:im_flutter_sdk/im_flutter_sdk.dart';
import '../../controller/message/chat_controller.dart';
import '../../controller/message/voice_player_manager.dart';
// import '../../controller/message/call_manager.dart'; //
import '../../generated/assets.dart';
import '../../../widget/message/chat_input_bar.dart';
import '../../../widget/message/message_item.dart';
@ -24,6 +25,8 @@ class _ChatPageState extends State<ChatPage> {
final ScrollController _scrollController = ScrollController();
bool _isLoadingMore = false;
late ChatController _controller;
bool _isInitialLoad = true; //
int _previousMessageCount = 0; //
@override
void initState() {
@ -43,18 +46,49 @@ class _ChatPageState extends State<ChatPage> {
}
});
//
//
_controller.messages.listen((_) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_scrollController.hasClients && _controller.messages.isNotEmpty) {
//
_scrollController.animateTo(
_scrollController.position.maxScrollExtent,
duration: Duration(milliseconds: 300),
curve: Curves.easeOut,
);
}
});
final currentCount = _controller.messages.length;
//
//
final shouldScroll = _isInitialLoad || (currentCount > _previousMessageCount && !_controller.messages.last.direction.isSend);
if (shouldScroll) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_scrollController.hasClients && _controller.messages.isNotEmpty) {
//
_scrollController.animateTo(
_scrollController.position.maxScrollExtent,
duration: Duration(milliseconds: 300),
curve: Curves.easeOut,
);
}
//
if (_isInitialLoad) {
_isInitialLoad = false;
}
_previousMessageCount = currentCount;
});
} else {
_previousMessageCount = currentCount;
}
});
//
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_controller.messages.isNotEmpty) {
Future.delayed(Duration(milliseconds: 100), () {
if (_scrollController.hasClients && mounted) {
_scrollController.animateTo(
_scrollController.position.maxScrollExtent,
duration: Duration(milliseconds: 300),
curve: Curves.easeOut,
);
_isInitialLoad = false;
_previousMessageCount = _controller.messages.length;
}
});
}
});
}
@ -175,6 +209,24 @@ class _ChatPageState extends State<ChatPage> {
// /
await controller.sendVideoMessage(filePath, duration);
},
//
// onVoiceCall: () async {
// //
// await CallManager.instance.initiateCall(
// targetUserId: widget.userId,
// callType: CallType.voice,
// chatController: controller,
// );
// },
//
// onVideoCall: () async {
// //
// await CallManager.instance.initiateCall(
// targetUserId: widget.userId,
// callType: CallType.video,
// chatController: controller,
// );
// },
),
],
),

314
lib/widget/message/call_item.dart

@ -0,0 +1,314 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:im_flutter_sdk/im_flutter_sdk.dart';
import '../../generated/assets.dart';
///
enum CallType {
voice, //
video, //
}
///
enum CallStatus {
calling, //
missed, //
cancelled, //
rejected, //
}
class CallItem extends StatelessWidget {
final EMMessage message;
final bool isSentByMe;
final bool showTime;
final String formattedTime;
final VoidCallback? onResend;
const CallItem({
required this.message,
required this.isSentByMe,
required this.showTime,
required this.formattedTime,
this.onResend,
super.key,
});
/// 使JSON格式
Map<String, dynamic>? _parseCallInfo() {
try {
if (message.body.type == MessageType.TXT) {
final textBody = message.body as EMTextMessageBody;
final content = textBody.content;
// [CALL:]
if (content != null && content.startsWith('[CALL:]')) {
final jsonStr = content.substring(7); // '[CALL:]'
return jsonDecode(jsonStr) as Map<String, dynamic>;
}
}
} catch (e) {
print('解析通话信息失败: $e');
}
return null;
}
///
CallType? _getCallType() {
final callInfo = _parseCallInfo();
if (callInfo != null) {
final callTypeStr = callInfo['callType'] as String?;
if (callTypeStr == 'voice') {
return CallType.voice;
} else if (callTypeStr == 'video') {
return CallType.video;
}
}
return null;
}
///
CallStatus? _getCallStatus() {
final callInfo = _parseCallInfo();
if (callInfo != null) {
final statusStr = callInfo['callStatus'] as String?;
if (statusStr == 'calling') {
return CallStatus.calling;
} else if (statusStr == 'missed') {
return CallStatus.missed;
} else if (statusStr == 'cancelled') {
return CallStatus.cancelled;
} else if (statusStr == 'rejected') {
return CallStatus.rejected;
}
}
return CallStatus.missed; //
}
///
int? _getCallDuration() {
final callInfo = _parseCallInfo();
if (callInfo != null) {
return callInfo['callDuration'] as int?;
}
return null;
}
///
String _formatDuration(int seconds) {
final minutes = seconds ~/ 60;
final secs = seconds % 60;
if (minutes > 0) {
return '${minutes}:${secs.toString().padLeft(2, '0')}';
} else {
return '${secs}';
}
}
///
String _getIconAsset() {
final callType = _getCallType();
final callStatus = _getCallStatus();
if (isSentByMe) {
//
if (callType == CallType.video) {
return Assets.imagesSendVideoCall;
} else {
return Assets.imagesSendCall;
}
} else {
//
if (callStatus == CallStatus.rejected) {
return Assets.imagesRejectCall;
} else {
return Assets.imagesAcceptCall;
}
}
}
///
String _getStatusText() {
final callType = _getCallType();
final callStatus = _getCallStatus();
final duration = _getCallDuration();
if (callStatus == CallStatus.calling && duration != null) {
//
return _formatDuration(duration);
} else if (callStatus == CallStatus.missed) {
return callType == CallType.video ? '未接听视频通话' : '未接听语音通话';
} else if (callStatus == CallStatus.cancelled) {
return callType == CallType.video ? '已取消视频通话' : '已取消语音通话';
} else if (callStatus == CallStatus.rejected) {
return callType == CallType.video ? '已拒绝视频通话' : '已拒绝语音通话';
} else {
return callType == CallType.video ? '视频通话' : '语音通话';
}
}
@override
Widget build(BuildContext context) {
final callType = _getCallType();
if (callType == null) {
//
return SizedBox.shrink();
}
final iconAsset = _getIconAsset();
final statusText = _getStatusText();
final callStatus = _getCallStatus();
final isCalling = callStatus == CallStatus.calling;
return Column(
children: [
//
if (showTime) _buildTimeLabel(),
Container(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h),
child: Row(
mainAxisAlignment:
isSentByMe ? MainAxisAlignment.end : MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (!isSentByMe) _buildAvatar(),
if (!isSentByMe) SizedBox(width: 8.w),
//
if (isSentByMe)
Align(
alignment: Alignment.center,
child: Container(
margin: EdgeInsets.only(top: 10.h),
child: _buildMessageStatus(),
),
),
if (isSentByMe) SizedBox(width: 10.w),
//
Container(
constraints: BoxConstraints(maxWidth: 200.w),
margin: EdgeInsets.only(top: 10.h),
padding: EdgeInsets.symmetric(
horizontal: 16.w,
vertical: 12.h,
),
decoration: BoxDecoration(
color: isSentByMe ? Color(0xff8E7BF6) : Colors.white,
borderRadius: BorderRadius.only(
topLeft:
isSentByMe ? Radius.circular(12.w) : Radius.circular(0),
topRight:
isSentByMe ? Radius.circular(0) : Radius.circular(12.w),
bottomLeft: Radius.circular(12.w),
bottomRight: Radius.circular(12.w),
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
//
Image.asset(
iconAsset,
width: 24.w,
height: 24.w,
fit: BoxFit.contain,
),
SizedBox(width: 8.w),
//
Flexible(
child: Text(
statusText,
style: TextStyle(
fontSize: 14.sp,
color: isSentByMe ? Colors.white : Colors.black87,
fontWeight: isCalling ? FontWeight.w500 : FontWeight.normal,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
if (isSentByMe) SizedBox(width: 8.w),
if (isSentByMe) _buildAvatar(),
],
),
),
],
);
}
//
Widget _buildTimeLabel() {
return Container(
alignment: Alignment.center,
padding: EdgeInsets.symmetric(horizontal: 16.w),
child: Container(
padding: EdgeInsets.symmetric(horizontal: 12.w),
child: Text(
formattedTime,
style: TextStyle(fontSize: 12.sp, color: Colors.grey),
),
),
);
}
//
Widget _buildAvatar() {
return Container(
width: 40.w,
height: 40.w,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20.w),
image: DecorationImage(
image: AssetImage(Assets.imagesAvatarsExample),
fit: BoxFit.cover,
),
),
);
}
//
Widget _buildMessageStatus() {
if (!isSentByMe) {
return SizedBox.shrink();
}
final status = message.status;
if (status == MessageStatus.FAIL) {
//
return GestureDetector(
onTap: onResend,
child: Container(
width: 20.w,
height: 20.w,
decoration: BoxDecoration(
color: Colors.red.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(
Icons.refresh,
size: 14.w,
color: Colors.red,
),
),
);
} else if (status == MessageStatus.PROGRESS) {
//
return Container(
width: 16.w,
height: 16.w,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.grey),
),
);
} else {
//
return SizedBox.shrink();
}
}
}

58
lib/widget/message/chat_input_bar.dart

@ -14,12 +14,16 @@ class ChatInputBar extends StatefulWidget {
final ValueChanged<List<String>>? onImageSelected;
final Function(String filePath, int seconds)? onVoiceRecorded;
final Function(String filePath, int duration)? onVideoRecorded;
final VoidCallback? onVoiceCall; //
final VoidCallback? onVideoCall; //
const ChatInputBar({
required this.onSendMessage,
this.onImageSelected,
this.onVoiceRecorded,
this.onVideoRecorded,
this.onVoiceCall,
this.onVideoCall,
super.key,
});
@ -29,6 +33,7 @@ class ChatInputBar extends StatefulWidget {
class _ChatInputBarState extends State<ChatInputBar> {
final TextEditingController _textController = TextEditingController();
final FocusNode _focusNode = FocusNode();
bool _isMoreOptionsVisible = false;
bool _isVoiceVisible = false;
bool _isVideoVisible = false;
@ -108,6 +113,38 @@ class _ChatInputBarState extends State<ChatInputBar> {
});
}
//
void _closeAllPanels() {
if (!mounted) return;
if (_isMoreOptionsVisible || _isVoiceVisible || _isVideoVisible || _isEmojiVisible) {
setState(() {
_isMoreOptionsVisible = false;
_isVoiceVisible = false;
_isVideoVisible = false;
_isEmojiVisible = false;
});
}
}
@override
void initState() {
super.initState();
//
_focusNode.addListener(() {
if (_focusNode.hasFocus && mounted) {
//
_closeAllPanels();
}
});
}
@override
void dispose() {
_focusNode.dispose();
_textController.dispose();
super.dispose();
}
void _handleEmojiSelected(EmojiItem emoji) {
//
final currentText = _textController.text;
@ -203,6 +240,7 @@ class _ChatInputBarState extends State<ChatInputBar> {
//
TextField(
controller: _textController,
focusNode: _focusNode,
decoration: InputDecoration(
border: InputBorder.none,
hintText: "请输入聊天内容~",
@ -267,18 +305,34 @@ class _ChatInputBarState extends State<ChatInputBar> {
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
//
//
Image.asset(
Assets.imagesAudio,
width: 24.w,
height: 24.w,
).onTap(_toggleVoiceOptions),
//
//
Image.asset(
Assets.imagesVideo,
width: 24.w,
height: 24.w,
).onTap(_toggleVideoOptions),
//
// Image.asset(
// Assets.imagesSendCall,
// width: 24.w,
// height: 24.w,
// ).onTap(() {
// widget.onVoiceCall?.call();
// }),
//
// Image.asset(
// Assets.imagesSendVideoCall,
// width: 24.w,
// height: 24.w,
// ).onTap(() {
// widget.onVideoCall?.call();
// }),
//
Image.asset(Assets.imagesGift, width: 24.w, height: 24.w),
//

37
lib/widget/message/message_item.dart

@ -7,6 +7,7 @@ import 'text_item.dart';
import 'image_item.dart';
import 'voice_item.dart';
import 'video_item.dart';
import 'call_item.dart';
import '../../controller/message/chat_controller.dart';
class MessageItem extends StatelessWidget {
@ -21,10 +22,46 @@ class MessageItem extends StatelessWidget {
super.key,
});
//
bool _isCallMessage() {
try {
if (message.body.type == MessageType.TXT) {
final textBody = message.body as EMTextMessageBody;
final content = textBody.content;
// [CALL:]
if (content != null && content.startsWith('[CALL:]')) {
return true;
}
}
} catch (e) {
//
}
return false;
}
@override
Widget build(BuildContext context) {
print('📨 [MessageItem] 渲染消息,类型: ${message.body.type}');
//
if (message.body.type == MessageType.TXT && _isCallMessage()) {
return CallItem(
message: message,
isSentByMe: isSentByMe,
showTime: shouldShowTime(),
formattedTime: formatMessageTime(message.serverTime),
onResend: () {
// Get找到ChatController并调用重发方法
try {
final controller = Get.find<ChatController>();
controller.resendMessage(message);
} catch (e) {
print('重发消息失败: $e');
}
},
);
}
//
if (message.body.type == MessageType.TXT) {
final textBody = message.body as EMTextMessageBody;

Loading…
Cancel
Save