diff --git a/lib/controller/message/chat_controller.dart b/lib/controller/message/chat_controller.dart index 52f12c7..3dc20e2 100644 --- a/lib/controller/message/chat_controller.dart +++ b/lib/controller/message/chat_controller.dart @@ -3,6 +3,7 @@ import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'dart:convert'; import '../../im/im_manager.dart'; +import '../../im/chat_presence_manager.dart'; import '../../model/home/marriage_data.dart'; import '../../model/live/gift_product_model.dart'; import '../../network/network_service.dart'; @@ -21,6 +22,9 @@ class ChatController extends GetxController { // 外部传入的用户信息(用于在获取不到环信用户信息时使用) String? _externalNickName; String? _externalAvatarUrl; + + // 用户在线状态(从接口获取) + final RxBool isUserOnline = RxBool(false); // 消息列表 final RxList messages = RxList([]); @@ -31,6 +35,9 @@ class ChatController extends GetxController { // 视频发送状态 final RxBool isSendingVideo = RxBool(false); final RxString sendingStatus = RxString(''); + + // 语音发送状态(防止重复发送) + final RxBool isSendingVoice = RxBool(false); // 礼物产品列表 final RxList giftProducts = [].obs; @@ -83,15 +90,55 @@ class ChatController extends GetxController { // 初始化时获取用户信息和消息列表 fetchUserInfo(); - fetchMessages(); + fetchMessages().then((_) { + // 加载消息后,标记所有消息为已读 + markAllMessagesAsRead(); + }); // 加载礼物产品列表 loadGiftProducts(); + // 订阅对方用户在线状态(实时接收状态变化) + subscribeUserPresence(); // 如果有外部传入的用户信息,触发 UI 更新 if (_externalNickName != null || _externalAvatarUrl != null) { update(); } } + /// 订阅用户在线状态(实时接收状态变化) + Future subscribeUserPresence() async { + try { + await ChatPresenceManager().subscribeUserPresence( + userId: userId, + onStatusChanged: (isOnline) { + // 状态变化回调 + isUserOnline.value = isOnline; + if (Get.isLogEnable) { + Get.log('✅ [ChatController] 用户在线状态更新: userId=$userId, isOnline=$isOnline'); + } + update(); + }, + ); + } catch (e) { + if (Get.isLogEnable) { + Get.log('❌ [ChatController] 订阅用户在线状态异常: $e'); + } + // 获取异常时,默认为离线状态 + isUserOnline.value = false; + update(); + } + } + + /// 取消订阅用户在线状态 + Future unsubscribeUserPresence() async { + try { + await ChatPresenceManager().unsubscribeUserPresence(userId); + } catch (e) { + if (Get.isLogEnable) { + Get.log('❌ [ChatController] 取消订阅用户在线状态异常: $e'); + } + } + } + /// 从 ConversationController 的缓存中加载用户信息 void _loadUserInfoFromConversationCache() { try { @@ -155,6 +202,8 @@ class ChatController extends GetxController { @override void onClose() { + // 取消订阅用户在线状态 + unsubscribeUserPresence(); // 注销 ChatController IMManager.instance.unregisterChatController(userId); super.onClose(); @@ -487,8 +536,20 @@ class ChatController extends GetxController { /// 发送语音消息 Future sendVoiceMessage(String filePath, int seconds) async { + // 防止重复发送 + if (isSendingVoice.value) { + if (Get.isLogEnable) { + Get.log('⚠️ [ChatController] 语音消息正在发送中,忽略重复发送请求'); + } else { + print('⚠️ [ChatController] 语音消息正在发送中,忽略重复发送请求'); + } + return false; + } + EMMessage? tempMessage; try { + isSendingVoice.value = true; + if (Get.isLogEnable) { Get.log('📤 [ChatController] 准备发送语音消息: path=$filePath, duration=$seconds, targetUserId=$userId'); } else { @@ -552,6 +613,7 @@ class ChatController extends GetxController { }); } + isSendingVoice.value = false; return true; } else { // 发送失败,更新消息状态 @@ -559,10 +621,12 @@ class ChatController extends GetxController { if (index != -1) { update(); } + isSendingVoice.value = false; SmartDialog.showToast('语音发送失败,请点击重发'); return false; } } catch (e) { + isSendingVoice.value = false; if (Get.isLogEnable) { Get.log('发送语音消息失败: $e'); } diff --git a/lib/im/chat_presence_manager.dart b/lib/im/chat_presence_manager.dart new file mode 100644 index 0000000..4599922 --- /dev/null +++ b/lib/im/chat_presence_manager.dart @@ -0,0 +1,330 @@ +import 'package:im_flutter_sdk/im_flutter_sdk.dart'; + +/// 环信在线状态管理器 +class ChatPresenceManager { + // 单例模式 + static final ChatPresenceManager _instance = ChatPresenceManager._internal(); + factory ChatPresenceManager() => _instance; + ChatPresenceManager._internal(); + + // 用户ID到在线状态的映射 + final Map _onlineStatusMap = {}; + + // 用户ID到EMPresence的映射 + final Map _presenceMap = {}; + + // 监听器ID集合(避免重复添加监听器) + final Set _listenerIds = {}; + + // 在线状态变化回调 + final Map _statusCallbacks = {}; + + // 详细状态变化回调 + final Map _presenceCallbacks = {}; + + /// 订阅用户在线状态 + /// [userId] 目标用户ID + /// [onStatusChanged] 状态变化回调(可选) + /// [onPresenceChanged] 详细状态变化回调(可选) + Future subscribeUserPresence({ + required String userId, + Function(bool)? onStatusChanged, + Function(EMPresence)? onPresenceChanged, + }) async { + try { + // 存储回调 + if (onStatusChanged != null) { + _statusCallbacks[userId] = onStatusChanged; + } + if (onPresenceChanged != null) { + _presenceCallbacks[userId] = onPresenceChanged; + } + + // 订阅用户状态(7天有效期) + await EMClient.getInstance.presenceManager.subscribe( + members: [userId], + expiry: 604800, + ); + + // 立即获取一次当前状态(会触发回调,更新UI) + await _fetchUserPresence(userId); + + // 设置监听器(如果尚未设置) + _setupPresenceListener(userId); + + print('✅ [ChatPresenceManager] 订阅成功: userId=$userId'); + } on EMError catch (e) { + print('订阅用户[$userId]在线状态失败: ${e.code}, ${e.description}'); + rethrow; + } + } + + /// 批量订阅多个用户的在线状态 + Future subscribeUsersPresence({ + required List userIds, + Function(String, bool)? onStatusChanged, + Function(String, EMPresence)? onPresenceChanged, + }) async { + if (userIds.isEmpty) return; + + try { + // 批量订阅(环信最多支持100个) + await EMClient.getInstance.presenceManager.subscribe( + members: userIds, + expiry: 604800, + ); + + // 为每个用户设置回调 + for (final userId in userIds) { + if (onStatusChanged != null) { + _statusCallbacks[userId] = (isOnline) => onStatusChanged(userId, isOnline); + } + if (onPresenceChanged != null) { + _presenceCallbacks[userId] = (presence) => onPresenceChanged(userId, presence); + } + // 立即获取一次当前状态 + await _fetchUserPresence(userId); + } + + // 设置监听器 + _setupBatchPresenceListener(); + } on EMError catch (e) { + print('批量订阅用户在线状态失败: ${e.code}, ${e.description}'); + rethrow; + } + } + + /// 获取用户的在线状态 + bool isUserOnline(String userId) { + return _onlineStatusMap[userId] ?? false; + } + + /// 获取用户的详细在线状态信息 + EMPresence? getUserPresence(String userId) { + return _presenceMap[userId]; + } + + /// 获取在线状态文本描述 + String getStatusDescription(String userId) { + final presence = _presenceMap[userId]; + if (presence == null) return '未知'; + + final statusDesc = presence.statusDescription?.toLowerCase() ?? ''; + + // 根据标准状态返回 + if (statusDesc.contains('online')) return '在线'; + if (statusDesc.contains('away')) return '离开'; + if (statusDesc.contains('busy')) return '忙碌'; + if (statusDesc.contains('dnd')) return '勿扰'; + if (statusDesc.contains('offline')) return '离线'; + + return '未知'; + } + + /// 获取在线状态颜色 + int getStatusColor(String userId) { + final isOnline = _onlineStatusMap[userId] ?? false; + final presence = _presenceMap[userId]; + + if (!isOnline) return 0xFF999999; // 灰色 + + final statusDesc = presence?.statusDescription?.toLowerCase() ?? ''; + + if (statusDesc.contains('busy')) return 0xFFFF3B30; // 红色 + if (statusDesc.contains('away')) return 0xFFFF9500; // 橙色 + if (statusDesc.contains('dnd')) return 0xFFAF52DE; // 紫色 + if (statusDesc.contains('online')) return 0xFF34C759; // 绿色 + + return 0xFF34C759; // 默认绿色 + } + + /// 手动刷新用户在线状态 + Future refreshUserPresence(String userId) async { + try { + await _fetchUserPresence(userId); + } on EMError catch (e) { + print('刷新用户[$userId]在线状态失败: ${e.code}, ${e.description}'); + rethrow; + } + } + + /// 取消订阅用户在线状态 + Future unsubscribeUserPresence(String userId) async { + try { + // 取消订阅 + await EMClient.getInstance.presenceManager.unsubscribe(members: [userId]); + + // 清理回调 + _statusCallbacks.remove(userId); + _presenceCallbacks.remove(userId); + _onlineStatusMap.remove(userId); + _presenceMap.remove(userId); + } on EMError catch (e) { + print('取消订阅用户[$userId]在线状态失败: ${e.code}, ${e.description}'); + rethrow; + } + } + + /// 批量取消订阅 + Future unsubscribeUsersPresence(List userIds) async { + if (userIds.isEmpty) return; + + try { + await EMClient.getInstance.presenceManager.unsubscribe(members: userIds); + + for (final userId in userIds) { + _statusCallbacks.remove(userId); + _presenceCallbacks.remove(userId); + _onlineStatusMap.remove(userId); + _presenceMap.remove(userId); + } + } on EMError catch (e) { + print('批量取消订阅在线状态失败: ${e.code}, ${e.description}'); + rethrow; + } + } + + /// 清理所有订阅和监听器 + Future dispose() async { + // 清理监听器 + for (final listenerId in _listenerIds) { + try { + EMClient.getInstance.presenceManager.removeEventHandler(listenerId); + } catch (e) { + // 忽略清理错误 + } + } + _listenerIds.clear(); + + // 清理回调 + _statusCallbacks.clear(); + _presenceCallbacks.clear(); + + // 清理数据 + _onlineStatusMap.clear(); + _presenceMap.clear(); + } + + /// 私有方法:获取用户在线状态 + Future _fetchUserPresence(String userId) async { + try { + List presences = await EMClient.getInstance.presenceManager + .fetchPresenceStatus(members: [userId]); + + if (presences.isNotEmpty) { + final presence = presences.first; + print('📥 [ChatPresenceManager] 获取到Presence状态: userId=$userId, statusDescription=${presence.statusDescription}'); + _presenceMap[userId] = presence; + _updateOnlineStatus(userId, presence); + } else { + print('⚠️ [ChatPresenceManager] 未获取到Presence状态: userId=$userId'); + } + } on EMError catch (e) { + print('获取用户[$userId]在线状态失败: ${e.code}, ${e.description}'); + rethrow; + } + } + + /// 私有方法:更新在线状态 + void _updateOnlineStatus(String userId, EMPresence presence) { + final statusDesc = presence.statusDescription?.toLowerCase() ?? ''; + final oldStatus = _onlineStatusMap[userId] ?? false; + bool newStatus; + + // 判断在线状态逻辑 + if (statusDesc.contains('online') || statusDesc.contains('available')) { + newStatus = true; + } else if (statusDesc.contains('offline')) { + newStatus = false; + } else { + // away、busy、dnd 等状态,默认算"在线"(可根据业务需求调整) + newStatus = true; + } + + // 检查是否是首次设置状态 + final isFirstTime = !_onlineStatusMap.containsKey(userId); + + // 更新状态 + _onlineStatusMap[userId] = newStatus; + + // 打印调试信息 + print('🔄 [ChatPresenceManager] 更新在线状态: userId=$userId, statusDescription=$statusDesc, oldStatus=$oldStatus, newStatus=$newStatus, isFirstTime=$isFirstTime'); + + // 触发回调(如果状态发生变化,或者首次设置) + if (isFirstTime || oldStatus != newStatus) { + print('✅ [ChatPresenceManager] 触发状态回调: userId=$userId, isOnline=$newStatus'); + _statusCallbacks[userId]?.call(newStatus); + } + // 始终触发详细回调 + _presenceCallbacks[userId]?.call(presence); + } + + /// 私有方法:设置在线状态监听器 + void _setupPresenceListener(String userId) { + final listenerId = 'presence_$userId'; + if (_listenerIds.contains(listenerId)) return; + + EMClient.getInstance.presenceManager.addEventHandler( + listenerId, + EMPresenceEventHandler( + onPresenceStatusChanged: (list) { + print('📡 [ChatPresenceManager] 收到Presence状态变化通知: userId=$userId, 变化数量=${list.length}'); + for (var presence in list) { + if (presence.publisher == userId) { + print('✅ [ChatPresenceManager] 匹配到目标用户: userId=$userId'); + _presenceMap[userId] = presence; + _updateOnlineStatus(userId, presence); + break; + } + } + }, + ), + ); + + _listenerIds.add(listenerId); + } + + /// 私有方法:设置批量监听器 + void _setupBatchPresenceListener() { + const listenerId = 'presence_batch_listener'; + if (_listenerIds.contains(listenerId)) return; + + EMClient.getInstance.presenceManager.addEventHandler( + listenerId, + EMPresenceEventHandler( + onPresenceStatusChanged: (list) { + for (var presence in list) { + final userId = presence.publisher; + if (userId != null && _statusCallbacks.containsKey(userId)) { + _presenceMap[userId] = presence; + _updateOnlineStatus(userId, presence); + } + } + }, + ), + ); + + _listenerIds.add(listenerId); + } + + /// 私有方法:刷新所有已订阅用户的状态 + Future _refreshAllSubscribedUsers() async { + final userIds = _statusCallbacks.keys.toList(); + if (userIds.isEmpty) return; + + try { + List presences = await EMClient.getInstance.presenceManager + .fetchPresenceStatus(members: userIds); + + for (var presence in presences) { + final publisherId = presence.publisher; + if (publisherId != null && publisherId.isNotEmpty) { + _presenceMap[publisherId] = presence; + _updateOnlineStatus(publisherId, presence); + } + } + } on EMError catch (e) { + print('刷新所有订阅用户状态失败: ${e.code}, ${e.description}'); + } + } +} \ No newline at end of file diff --git a/lib/im/im_manager.dart b/lib/im/im_manager.dart index dc28ec9..806425d 100644 --- a/lib/im/im_manager.dart +++ b/lib/im/im_manager.dart @@ -4,9 +4,11 @@ import 'package:get_storage/get_storage.dart'; import 'package:im_flutter_sdk/im_flutter_sdk.dart'; import 'package:video_thumbnail/video_thumbnail.dart'; import 'package:path_provider/path_provider.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import '../controller/message/conversation_controller.dart'; import '../controller/message/chat_controller.dart'; import '../controller/global.dart'; +import '../pages/mine/login_page.dart'; // 完整的IM管理器实现,使用实际的SDK类型和方法 class IMManager { @@ -24,9 +26,13 @@ class IMManager { // 监听器标识符 static const String _connectionHandlerKey = 'im_manager_connection_handler'; static const String _chatHandlerKey = 'im_manager_chat_handler'; + static const String _presenceHandlerKey = 'im_manager_presence_handler'; // 存储活跃的 ChatController 实例,key 为 userId final Map _activeChatControllers = {}; + + // 存储 Presence 状态变化回调,key 为 userId + final Map _presenceCallbacks = {}; IMManager._internal() { print('IMManager instance created'); @@ -96,8 +102,11 @@ class IMManager { onUserKickedByOtherDevice: () { if (Get.isLogEnable) { Get.log('⚠️ [IMManager] 用户被其他设备踢出'); + } else { + print('⚠️ [IMManager] 用户被其他设备踢出'); } - // TODO: 可以在这里添加处理逻辑,如跳转到登录页 + // 处理踢下线逻辑 + _handleUserKickedOffline(); }, ), ); @@ -229,6 +238,52 @@ class IMManager { } }, )); + + // Presence 状态监听器 + EMClient.getInstance.presenceManager.addEventHandler( + _presenceHandlerKey, + EMPresenceEventHandler( + onPresenceStatusChanged: (List presences) { + if (Get.isLogEnable) { + Get.log('📡 [IMManager] 收到 Presence 状态变化: ${presences.length} 个用户'); + } + // 处理状态变化 + for (var presence in presences) { + final userId = presence.publisher; + if (userId != null && userId.isNotEmpty) { + // 使用 statusDescription 字段来判断在线状态 + final statusDesc = presence.statusDescription?.toLowerCase() ?? ''; + // 判断在线状态:online、available 等表示在线 + final isOnline = statusDesc == 'online' || statusDesc == 'available'; + + if (Get.isLogEnable) { + Get.log('📡 [IMManager] Presence状态变化: userId=$userId, statusDescription=$statusDesc, isOnline=$isOnline'); + } else { + print('📡 [IMManager] Presence状态变化: userId=$userId, statusDescription=$statusDesc, isOnline=$isOnline'); + } + + // 通知对应的回调 + final callback = _presenceCallbacks[userId]; + if (callback != null) { + callback(isOnline); + } + + // 通知对应的 ChatController + final controller = _activeChatControllers[userId]; + if (controller != null) { + controller.isUserOnline.value = isOnline; + controller.update(); + } + + if (Get.isLogEnable) { + Get.log('✅ [IMManager] 用户在线状态更新: userId=$userId, isOnline=$isOnline'); + } + } + } + }, + ), + ); + _listenersRegistered = true; if (Get.isLogEnable) { Get.log('✅ [IMManager] 监听器注册成功'); @@ -291,6 +346,45 @@ class IMManager { } } + /// 处理用户被踢下线 + void _handleUserKickedOffline() async { + try { + // 显示提示 + SmartDialog.showToast('您的账号在其他设备登录,已被强制下线'); + + // 先退出 IM 登录 + await logout(); + + // 清除会话列表和用户信息缓存 + if (Get.isRegistered()) { + final conversationController = Get.find(); + conversationController.clearConversations(); + } + + // 清除本地存储 + GetStorage().erase(); + + // 清除全局数据 + GlobalData().logout(); + + // 延迟一小段时间再跳转,确保用户看到提示 + await Future.delayed(Duration(milliseconds: 500)); + + // 跳转到登录页 + Get.offAll(() => LoginPage()); + + if (Get.isLogEnable) { + Get.log('✅ [IMManager] 用户被踢下线处理完成,已跳转到登录页'); + } + } catch (e) { + if (Get.isLogEnable) { + Get.log('❌ [IMManager] 处理踢下线失败: $e'); + } else { + print('❌ [IMManager] 处理踢下线失败: $e'); + } + } + } + /// 检查是否已登录 bool get isLoggedIn => _isLoggedIn; @@ -580,6 +674,137 @@ class IMManager { ]); } + /// 订阅用户在线状态(用于实时接收状态变化) + /// [userId] 用户ID + /// [callback] 状态变化回调函数 + Future subscribeUserPresence(String userId, Function(bool isOnline) callback) async { + try { + if (!_isLoggedIn) { + if (Get.isLogEnable) { + Get.log('⚠️ [IMManager] IM未登录,无法订阅在线状态'); + } + return false; + } + + // 保存回调 + _presenceCallbacks[userId] = callback; + + final presenceManager = EMClient.getInstance.presenceManager; + + // 订阅用户状态(有效期7天) + await presenceManager.subscribe( + members: [userId], + expiry: 7 * 24 * 60 * 60, // 7天 + ); + + if (Get.isLogEnable) { + Get.log('✅ [IMManager] 已订阅用户在线状态: userId=$userId'); + } + + // 立即获取一次状态 + final onlineStatus = await getUserPresenceStatus(userId); + if (onlineStatus != null) { + if (Get.isLogEnable) { + Get.log('✅ [IMManager] 订阅后立即获取状态: userId=$userId, isOnline=$onlineStatus'); + } else { + print('✅ [IMManager] 订阅后立即获取状态: userId=$userId, isOnline=$onlineStatus'); + } + callback(onlineStatus); + } else { + if (Get.isLogEnable) { + Get.log('⚠️ [IMManager] 订阅后获取状态失败: userId=$userId'); + } else { + print('⚠️ [IMManager] 订阅后获取状态失败: userId=$userId'); + } + } + + return true; + } catch (e) { + if (Get.isLogEnable) { + Get.log('❌ [IMManager] 订阅用户在线状态失败: $userId, 错误: $e'); + } else { + print('❌ [IMManager] 订阅用户在线状态失败: $userId, 错误: $e'); + } + return false; + } + } + + /// 取消订阅用户在线状态 + /// [userId] 用户ID + Future unsubscribeUserPresence(String userId) async { + try { + if (!_isLoggedIn) { + return false; + } + + // 移除回调 + _presenceCallbacks.remove(userId); + + final presenceManager = EMClient.getInstance.presenceManager; + await presenceManager.unsubscribe(members: [userId]); + + if (Get.isLogEnable) { + Get.log('✅ [IMManager] 已取消订阅用户在线状态: userId=$userId'); + } + + return true; + } catch (e) { + if (Get.isLogEnable) { + Get.log('❌ [IMManager] 取消订阅用户在线状态失败: $userId, 错误: $e'); + } + return false; + } + } + + /// 获取用户在线状态 + /// [userId] 用户ID + /// 返回 true 表示在线,false 表示离线,null 表示获取失败 + Future getUserPresenceStatus(String userId) async { + try { + if (!_isLoggedIn) { + if (Get.isLogEnable) { + Get.log('⚠️ [IMManager] IM未登录,无法获取在线状态'); + } + return null; + } + + final presenceManager = EMClient.getInstance.presenceManager; + + // 获取用户在线状态 + final presences = await presenceManager.fetchPresenceStatus( + members: [userId], + ); + + if (presences.isEmpty) { + if (Get.isLogEnable) { + Get.log('⚠️ [IMManager] 未获取到用户在线状态: $userId'); + } + return null; + } + + final presence = presences.first; + // 使用 statusDescription 字段来判断在线状态 + final statusDesc = presence.statusDescription?.toLowerCase() ?? ''; + // 判断在线状态:online、available 等表示在线 + final isOnline = statusDesc == 'online' || statusDesc == 'available'; + + if (Get.isLogEnable) { + Get.log('✅ [IMManager] 获取用户在线状态: userId=$userId, statusDescription=$statusDesc, isOnline=$isOnline'); + } else { + print('✅ [IMManager] 获取用户在线状态: userId=$userId, statusDescription=$statusDesc, isOnline=$isOnline'); + } + + return isOnline; + } catch (e) { + if (Get.isLogEnable) { + Get.log('❌ [IMManager] 获取用户在线状态失败: $userId, 错误: $e'); + } else { + print('❌ [IMManager] 获取用户在线状态失败: $userId, 错误: $e'); + } + return null; + } + } + /// 获取指定会话的消息记录 Future> getMessages( String conversationId, { diff --git a/lib/model/home/marriage_data.dart b/lib/model/home/marriage_data.dart index 6a8acf9..7f3e683 100644 --- a/lib/model/home/marriage_data.dart +++ b/lib/model/home/marriage_data.dart @@ -49,8 +49,8 @@ class MarriageData { factory MarriageData.fromJson(Map json) { return MarriageData( - miId: json['miId'] ?? '', - userId: json['userId'] ?? '', + miId: '1180578682436194304',//json['miId'] ?? '', + userId: '1114267797208305664',//json['userId'] ?? '', profilePhoto: json['profilePhoto'] ?? '', nickName: json['nickName'] ?? '', isRealNameCertified: json['isRealNameCertified'] ?? false, diff --git a/lib/model/mine/user_base_data.dart b/lib/model/mine/user_base_data.dart index d0baf36..a75673c 100644 --- a/lib/model/mine/user_base_data.dart +++ b/lib/model/mine/user_base_data.dart @@ -7,6 +7,7 @@ class UserBaseData { final String realName; final String userId; final int matchmakerType; + final bool? isOnline; // 在线状态 UserBaseData({ required this.matchmakerFlag, @@ -16,6 +17,7 @@ class UserBaseData { required this.realName, required this.userId, required this.matchmakerType, + this.isOnline, }); // 从JSON映射创建实例 @@ -28,6 +30,7 @@ class UserBaseData { phone: json['phone'] ?? '', realName: json['realName'] ?? '', userId: json['userId'] ?? '', + isOnline: json['isOnline'] as bool?, ); } @@ -41,6 +44,7 @@ class UserBaseData { 'phone': phone, 'realName': realName, 'userId': userId, + 'isOnline': isOnline, }; } diff --git a/lib/pages/message/chat_page.dart b/lib/pages/message/chat_page.dart index cb5df06..b536ef2 100644 --- a/lib/pages/message/chat_page.dart +++ b/lib/pages/message/chat_page.dart @@ -82,6 +82,13 @@ class _ChatPageState extends State { } }); }); + + // 页面可见后,延迟一小段时间标记所有消息为已读(确保页面已完全渲染) + WidgetsBinding.instance.addPostFrameCallback((_) { + Future.delayed(Duration(milliseconds: 300), () { + _controller.markAllMessagesAsRead(); + }); + }); } @override @@ -92,113 +99,6 @@ class _ChatPageState extends State { super.dispose(); } - // 检查用户是否在线 - bool _isUserOnline(ChatController controller) { - try { - // 如果是从用户详情页进入的(有userData),根据用户数据判断 - // 用户详情页显示在线,所以默认认为在线 - if (widget.userData != null) { - // 可以在这里添加从userData中读取在线状态的逻辑 - // 目前用户详情页显示在线,所以返回true - return true; - } - - // 如果是从消息列表进入的(没有userData),从消息中获取在线状态 - // 从消息列表中查找最后一条来自对方的消息 - final receiveMessages = controller.messages - .where((msg) => msg.direction == MessageDirection.RECEIVE) - .toList(); - - if (receiveMessages.isNotEmpty) { - // 获取最后一条接收到的消息 - final lastMessage = receiveMessages.last; - - // 从消息扩展字段中获取在线状态 - Map? attributes; - try { - attributes = lastMessage.attributes; - } catch (e) { - return false; - } - - if (attributes != null && attributes.isNotEmpty) { - // 检查 sender_isOnline 字段 - final isOnlineStr = attributes['sender_isOnline'] as String?; - if (isOnlineStr == 'true') { - // 进一步检查最后活跃时间(5分钟内认为在线) - final lastActiveTimeStr = attributes['sender_lastActiveTime'] as String?; - if (lastActiveTimeStr != null) { - try { - final lastActiveTime = int.parse(lastActiveTimeStr); - final now = DateTime.now().millisecondsSinceEpoch; - final diff = now - lastActiveTime; - // 5分钟内认为在线(5 * 60 * 1000 毫秒) - return diff < 5 * 60 * 1000; - } catch (e) { - // 解析失败,使用 isOnline 字段 - return true; - } - } else { - return true; - } - } - } - } - - // 如果没有接收到的消息,检查所有消息(包括发送的) - // 查找最近的消息,看看是否有在线状态信息 - if (controller.messages.isNotEmpty) { - // 按时间排序,获取最新的消息 - final sortedMessages = List.from(controller.messages) - ..sort((a, b) { - final timeA = a.serverTime; - final timeB = b.serverTime; - return timeB.compareTo(timeA); - }); - - for (final message in sortedMessages) { - Map? attributes; - try { - attributes = message.attributes; - } catch (e) { - continue; - } - - if (attributes != null && attributes.isNotEmpty) { - // 检查是否有在线状态信息(无论是发送者还是接收者) - final senderIsOnline = attributes['sender_isOnline'] as String?; - final receiverIsOnline = attributes['receiver_isOnline'] as String?; - final isOnlineStr = senderIsOnline ?? receiverIsOnline; - if (isOnlineStr == 'true') { - final senderLastActiveTime = attributes['sender_lastActiveTime'] as String?; - final receiverLastActiveTime = attributes['receiver_lastActiveTime'] as String?; - final lastActiveTimeStr = senderLastActiveTime ?? receiverLastActiveTime; - if (lastActiveTimeStr != null) { - try { - final lastActiveTime = int.parse(lastActiveTimeStr); - final now = DateTime.now().millisecondsSinceEpoch; - final diff = now - lastActiveTime; - // 5分钟内认为在线 - if (diff < 5 * 60 * 1000) { - return true; - } - } catch (e) { - return true; - } - } else { - return true; - } - } - } - } - } - - return false; - } catch (e) { - return false; - } - } - // 显示礼物弹窗 void _showGiftPopup() { final giftProducts = _controller.giftProducts.toList(); @@ -252,28 +152,36 @@ class _ChatPageState extends State { controller.userNickName ?? widget.userData?.nickName ?? '', style: TextStyle(fontSize: 18.sp), ), - if (_isUserOnline(controller)) - SizedBox(width: 8.w), - // 在线标签(根据消息中的在线状态动态显示) - if (_isUserOnline(controller)) - Container( - width: 46.w, - height: 26.h, - // padding: EdgeInsets.symmetric(horizontal: 15.w, vertical: 1.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), + // 使用 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, @@ -374,20 +282,6 @@ class _ChatPageState extends State { // 处理语音录音完成,回传文件路径和秒数 await controller.sendVoiceMessage(filePath, seconds); }, - onVideoRecorded: (filePath, duration) async { - print('🎬 [ChatPage] 收到视频录制/选择回调'); - print('文件路径: $filePath'); - print('时长: $duration 秒'); - - // 检查是否正在发送 - if (controller.isSendingVideo.value) { - print('⚠️ [ChatPage] 视频正在发送中,忽略新的发送请求'); - return; - } - - // 处理视频录制/选择完成,回传文件路径和时长 - await controller.sendVideoMessage(filePath, duration); - }, onGiftTap: () { // 显示礼物弹窗 _showGiftPopup(); diff --git a/lib/widget/message/chat_input_bar.dart b/lib/widget/message/chat_input_bar.dart index 00c73ae..70367fb 100644 --- a/lib/widget/message/chat_input_bar.dart +++ b/lib/widget/message/chat_input_bar.dart @@ -7,13 +7,11 @@ import '../../config/emoji_config.dart'; import '../emoji_panel.dart'; import 'more_options_view.dart'; import 'voice_input_view.dart'; -import 'video_input_view.dart'; class ChatInputBar extends StatefulWidget { final ValueChanged onSendMessage; final ValueChanged>? onImageSelected; final Function(String filePath, int seconds)? onVoiceRecorded; - final Function(String filePath, int duration)? onVideoRecorded; final VoidCallback? onVoiceCall; // 语音通话回调 final VoidCallback? onVideoCall; // 视频通话回调 final VoidCallback? onGiftTap; // 礼物按钮回调 @@ -22,7 +20,6 @@ class ChatInputBar extends StatefulWidget { required this.onSendMessage, this.onImageSelected, this.onVoiceRecorded, - this.onVideoRecorded, this.onVoiceCall, this.onVideoCall, this.onGiftTap, @@ -38,7 +35,6 @@ class _ChatInputBarState extends State { final FocusNode _focusNode = FocusNode(); bool _isMoreOptionsVisible = false; bool _isVoiceVisible = false; - bool _isVideoVisible = false; bool _isEmojiVisible = false; void _handleSendMessage() { @@ -55,7 +51,6 @@ class _ChatInputBarState extends State { _isMoreOptionsVisible = !_isMoreOptionsVisible; if (_isMoreOptionsVisible) { _isVoiceVisible = false; - _isVideoVisible = false; _isEmojiVisible = false; } // 收起键盘 @@ -82,34 +77,18 @@ class _ChatInputBarState extends State { _isVoiceVisible = !_isVoiceVisible; if (_isVoiceVisible) { _isMoreOptionsVisible = false; - _isVideoVisible = false; _isEmojiVisible = false; } FocusManager.instance.primaryFocus?.unfocus(); }); } - void _toggleVideoOptions() { - print('🎬 [ChatInputBar] 视频按钮被点击,当前状态: $_isVideoVisible'); - setState(() { - _isVideoVisible = !_isVideoVisible; - if (_isVideoVisible) { - _isMoreOptionsVisible = false; - _isVoiceVisible = false; - _isEmojiVisible = false; - } - FocusManager.instance.primaryFocus?.unfocus(); - }); - print('🎬 [ChatInputBar] 视频面板状态改变为: $_isVideoVisible'); - } - void _toggleEmojiPanel() { setState(() { _isEmojiVisible = !_isEmojiVisible; if (_isEmojiVisible) { _isMoreOptionsVisible = false; _isVoiceVisible = false; - _isVideoVisible = false; } FocusManager.instance.primaryFocus?.unfocus(); }); @@ -118,11 +97,10 @@ class _ChatInputBarState extends State { // 关闭所有控制面板 void _closeAllPanels() { if (!mounted) return; - if (_isMoreOptionsVisible || _isVoiceVisible || _isVideoVisible || _isEmojiVisible) { + if (_isMoreOptionsVisible || _isVoiceVisible || _isEmojiVisible) { setState(() { _isMoreOptionsVisible = false; _isVoiceVisible = false; - _isVideoVisible = false; _isEmojiVisible = false; }); } @@ -313,12 +291,6 @@ class _ChatInputBarState extends State { width: 24.w, height: 24.w, ).onTap(_toggleVoiceOptions), - // 视频消息按钮 - Image.asset( - Assets.imagesVideo, - width: 24.w, - height: 24.w, - ).onTap(_toggleVideoOptions), // 语音通话按钮(暂时注释) // Image.asset( // Assets.imagesSendCall, @@ -352,23 +324,17 @@ class _ChatInputBarState extends State { ], ), ), - // 更多选项展开视图(支持图片和视频) + // 更多选项展开视图(支持图片) MoreOptionsView( isVisible: _isMoreOptionsVisible, onImageSelected: _handleImageTap, onCameraSelected: _handleCameraTap, - onVideoSelected: widget.onVideoRecorded, ), // 语音输入展开视图(与 MoreOptionsView 相同的展开方式) VoiceInputView( isVisible: _isVoiceVisible, onVoiceRecorded: widget.onVoiceRecorded, ), - // 视频输入展开视图 - VideoInputView( - isVisible: _isVideoVisible, - onVideoRecorded: widget.onVideoRecorded, - ), // 表情面板 EmojiPanel( isVisible: _isEmojiVisible, diff --git a/lib/widget/message/image_item.dart b/lib/widget/message/image_item.dart index 1f8c789..c696ad4 100644 --- a/lib/widget/message/image_item.dart +++ b/lib/widget/message/image_item.dart @@ -1,18 +1,19 @@ import 'dart:io'; -import 'package:dating_touchme_app/generated/assets.dart'; -import 'package:dating_touchme_app/pages/message/image_viewer_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; import 'package:im_flutter_sdk/im_flutter_sdk.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:dating_touchme_app/generated/assets.dart'; +import 'package:dating_touchme_app/pages/message/image_viewer_page.dart'; -class ImageItem extends StatelessWidget { +class ImageItem extends StatefulWidget { final EMImageMessageBody imageBody; final bool isSentByMe; final bool showTime; final String formattedTime; - final EMMessage message; // 添加消息对象以获取状态 - final VoidCallback? onResend; // 添加重发回调 + final EMMessage message; + final VoidCallback? onResend; const ImageItem({ required this.imageBody, @@ -24,56 +25,263 @@ class ImageItem extends StatelessWidget { super.key, }); + @override + State createState() => _ImageItemState(); +} + +class _ImageItemState extends State { + bool _isDownloading = false; + bool _hasError = false; + String? _currentLocalPath; + String? _currentRemotePath; + bool _shouldHideProgressStatus = false; // 用于判断是否应该隐藏PROGRESS状态 + + @override + void initState() { + super.initState(); + _initImagePaths(); + _checkAndDownloadImage(); + + // 对于发送方,添加智能检测:检查远程路径是否可用 + if (widget.isSentByMe) { + // 立即检查是否有远程路径(说明已经上传成功) + _checkRemotePathAndUpdateStatus(); + + // 延迟检查,等待消息状态更新 + Future.delayed(Duration(milliseconds: 500), () { + if (mounted) { + _initImagePaths(); + _checkRemotePathAndUpdateStatus(); + setState(() {}); + } + }); + + // 如果1秒后状态还是PROGRESS,但有远程路径,认为发送成功,隐藏loading状态 + Future.delayed(Duration(seconds: 1), () { + if (mounted) { + _checkRemotePathAndUpdateStatus(); + } + }); + + // 如果2秒后状态还是PROGRESS,但有远程路径,认为发送成功,隐藏loading状态 + Future.delayed(Duration(seconds: 2), () { + if (mounted) { + _checkRemotePathAndUpdateStatus(); + } + }); + } + } + + // 检查远程路径并更新状态 + void _checkRemotePathAndUpdateStatus() { + // 重新获取路径(可能SDK已经更新) + final body = widget.message.body; + if (body is EMImageMessageBody) { + final remotePath = body.remotePath; + // 如果有远程路径,说明图片已经上传成功,即使状态还是PROGRESS也隐藏loading + if (remotePath != null && remotePath.isNotEmpty) { + if (!_shouldHideProgressStatus) { + print('✅ 发送方:检测到远程路径,隐藏loading状态: $remotePath'); + setState(() { + _shouldHideProgressStatus = true; + _currentRemotePath = remotePath; // 更新远程路径 + }); + } + } + } + } + + void _initImagePaths() { + // 对于发送方,优先使用构造函数传入的imageBody(发送时的原始路径) + // 对于接收方,从消息对象中获取 + if (widget.isSentByMe) { + _currentLocalPath = widget.imageBody.localPath; + _currentRemotePath = widget.imageBody.remotePath; + + // 如果构造函数传入的路径为空,再尝试从消息对象获取 + if ((_currentLocalPath == null || _currentLocalPath!.isEmpty) && + widget.message.body is EMImageMessageBody) { + final body = widget.message.body as EMImageMessageBody; + _currentLocalPath = body.localPath; + _currentRemotePath = body.remotePath; + } + } else { + // 接收方:优先使用消息体中的路径 + final body = widget.message.body; + if (body is EMImageMessageBody) { + _currentLocalPath = body.localPath; + _currentRemotePath = body.remotePath; + } else { + // 备用:使用构造函数传入的imageBody + _currentLocalPath = widget.imageBody.localPath; + _currentRemotePath = widget.imageBody.remotePath; + } + } + + print('📸 图片路径初始化:'); + print(' 本地路径: $_currentLocalPath'); + print(' 远程路径: $_currentRemotePath'); + print(' 是否发送方: ${widget.isSentByMe}'); + } + + Future _checkAndDownloadImage() async { + // 发送方不需要下载 + if (widget.isSentByMe) return; + + // 如果已经有本地文件,检查是否有效 + if (_currentLocalPath != null && _currentLocalPath!.isNotEmpty) { + final file = File(_currentLocalPath!); + if (await file.exists()) { + final size = await file.length(); + if (size > 1024) { // 文件至少1KB才认为有效 + print('✅ 本地图片已存在且有效: $_currentLocalPath, 大小: ${size/1024}KB'); + return; + } else { + print('⚠️ 本地图片文件太小或无效: $_currentLocalPath, 大小: $size bytes'); + } + } else { + print('⚠️ 本地图片文件不存在: $_currentLocalPath'); + } + } + + // 需要下载图片 + if (_currentRemotePath != null && _currentRemotePath!.isNotEmpty) { + if (!_isDownloading) { + await _downloadImage(); + } + } else { + print('❌ 没有可用的远程图片路径'); + setState(() { + _hasError = true; + }); + } + } + + Future _downloadImage() async { + if (_isDownloading) return; + + setState(() { + _isDownloading = true; + _hasError = false; + }); + + try { + print('📥 开始下载图片消息: ${widget.message.msgId}'); + + // 使用环信SDK下载图片(downloadAttachment 返回 void) + await EMClient.getInstance.chatManager + .downloadAttachment(widget.message); + + // 等待一小段时间,确保文件写入完成 + await Future.delayed(Duration(milliseconds: 300)); + + // 下载后,从消息对象获取新的本地路径(下载后会自动更新 body 中的路径) + final body = widget.message.body; + if (body is EMImageMessageBody) { + setState(() { + _currentLocalPath = body.localPath; + _currentRemotePath = body.remotePath; + }); + + print('✅ 图片下载完成: $_currentLocalPath'); + + // 验证文件 + if (_currentLocalPath != null && _currentLocalPath!.isNotEmpty) { + final file = File(_currentLocalPath!); + if (await file.exists()) { + final size = await file.length(); + print('✅ 文件验证成功,大小: ${size/1024}KB'); + } else { + print('⚠️ 下载后文件不存在: $_currentLocalPath'); + setState(() { + _hasError = true; + }); + } + } else { + print('⚠️ 下载后本地路径为空'); + setState(() { + _hasError = true; + }); + } + } else { + print('❌ 消息体类型错误: ${body.runtimeType}'); + setState(() { + _hasError = true; + }); + } + } catch (e) { + print('❌ 下载图片失败: $e'); + SmartDialog.showToast('图片下载失败'); + setState(() { + _hasError = true; + }); + } finally { + if (mounted) { + setState(() { + _isDownloading = false; + }); + } + } + } + + void _retryDownload() { + if (!_isDownloading) { + _downloadImage(); + } + } + @override Widget build(BuildContext context) { return Column( children: [ - // 显示时间 - if (showTime) _buildTimeLabel(), + if (widget.showTime) _buildTimeLabel(), Container( - padding: EdgeInsets.symmetric( - horizontal: 16.w, - vertical: 8.h, - ), + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h), child: Builder( builder: (context) { // 计算图片尺寸 double maxWidth = 180.w; double width = maxWidth; - double height = width * (304 / 289); // 按289:304比例计算高度 - final imageHeight = height + 10.h; // 加上 margin top (10.h) + double height = width * (304 / 289); + final imageHeight = height + 10.h; return Row( - mainAxisAlignment: isSentByMe ? MainAxisAlignment.end : MainAxisAlignment.start, + mainAxisAlignment: widget.isSentByMe + ? MainAxisAlignment.end + : MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (!isSentByMe) _buildAvatar(), - if (!isSentByMe) SizedBox(width: 8.w), - // 发送消息时,状态在左侧,与图片垂直居中对齐 - if (isSentByMe) + if (!widget.isSentByMe) _buildAvatar(), + if (!widget.isSentByMe) SizedBox(width: 8.w), + + // 发送消息的状态 + if (widget.isSentByMe) SizedBox( height: imageHeight, child: Center( child: _buildMessageStatus(), ), ), - if (isSentByMe) SizedBox(width: 10.w), + if (widget.isSentByMe) SizedBox(width: 10.w), + + // 图片容器 GestureDetector( onTap: _onImageTap, child: Container( margin: EdgeInsets.only(top: 10.h), decoration: BoxDecoration( - color: isSentByMe ? Color(0xff8E7BF6) : Colors.white, + color: widget.isSentByMe ? Color(0xff8E7BF6) : Colors.white, borderRadius: BorderRadius.circular(18.w), ), child: ClipRRect( borderRadius: BorderRadius.circular(18.w), - child: _buildImage(), + child: _buildImageContent(width, height), ), ), ), - if (isSentByMe) SizedBox(width: 8.w), - if (isSentByMe) _buildAvatar(), + + if (widget.isSentByMe) SizedBox(width: 8.w), + if (widget.isSentByMe) _buildAvatar(), ], ); }, @@ -83,88 +291,181 @@ class ImageItem extends StatelessWidget { ); } - // 构建时间标签 - 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 _buildImageContent(double width, double height) { + // 发送方和接收方的逻辑不同 + if (widget.isSentByMe) { + // 发送方:优先显示本地图片(发送时的原始路径) + return _buildSenderImage(width, height); + } + + // 接收方逻辑 + // 如果有错误且正在下载 + if (_hasError && !_isDownloading) { + return _buildErrorState(width, height); + } + + // 如果正在下载 + if (_isDownloading) { + return _buildDownloadingState(width, height); + } + + // 优先显示本地图片 + if (_currentLocalPath != null && _currentLocalPath!.isNotEmpty) { + return _buildLocalImage(width, height); + } + + // 显示远程图片 + if (_currentRemotePath != null && _currentRemotePath!.isNotEmpty) { + return _buildNetworkImage(width, height); + } + + // 默认显示错误状态 + return _buildErrorState(width, height); } - // 构建头像 - Widget _buildAvatar() { - return Container( - width: 40.w, - height: 40.w, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20.w), - image: DecorationImage( - image: AssetImage(Assets.imagesAvatarsExample), + // 发送方构建图片 + Widget _buildSenderImage(double width, double height) { + // 1. 优先尝试构造函数传入的本地路径(发送时的原始路径) + if (widget.imageBody.localPath.isNotEmpty) { + final file = File(widget.imageBody.localPath); + if (file.existsSync()) { + print('✅ 发送方使用原始本地路径: ${widget.imageBody.localPath}'); + return Image.file( + file, + width: width, + height: height, fit: BoxFit.cover, - ), - ), - ); - } - - // 构建图片 - Widget _buildImage() { - // 按289x304比例计算图片尺寸 - // 限制最大宽度为289,高度按比例计算 - double maxWidth = 180.w; - double width = maxWidth; - double height = width * (304 / 289); // 按289:304比例计算高度 - // 尝试显示本地图片 - final localPath = imageBody.localPath; - if (localPath.isNotEmpty) { - return Image.file( - File(localPath), - width: width, - height: height, - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) { - return _buildErrorContainer(width, height); - }, - ); + errorBuilder: (context, error, stackTrace) { + print('❌ 发送方加载本地图片失败: $error'); + // 如果构造函数路径失败,返回占位符 + return _buildSenderPlaceholder(width, height); + }, + ); + } else { + print('⚠️ 发送方原始本地路径文件不存在: ${widget.imageBody.localPath}'); + } } - - // 尝试显示网络原图 - final remotePath = imageBody.remotePath; - if (remotePath != null && remotePath.isNotEmpty) { + + // 2. 尝试当前本地路径(可能是从消息对象获取的) + if (_currentLocalPath != null && _currentLocalPath!.isNotEmpty) { + final file = File(_currentLocalPath!); + if (file.existsSync()) { + print('✅ 发送方使用当前本地路径: $_currentLocalPath'); + return Image.file( + file, + width: width, + height: height, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + print('❌ 发送方加载当前本地图片失败: $error'); + return _buildSenderPlaceholder(width, height); + }, + ); + } else { + print('⚠️ 发送方当前本地路径文件不存在: $_currentLocalPath'); + } + } + + // 3. 如果有远程路径,显示远程图片(但不要一直loading) + if (_currentRemotePath != null && _currentRemotePath!.isNotEmpty) { + print('🌐 发送方使用远程路径: $_currentRemotePath'); + // 对于发送方,如果已经超时隐藏了loading状态,网络图片加载失败时也显示占位符而不是一直loading return Image.network( - remotePath, + _currentRemotePath!, width: width, height: height, fit: BoxFit.cover, loadingBuilder: (context, child, loadingProgress) { - if (loadingProgress == null) return child; - return _buildLoadingContainer(width, height); + if (loadingProgress == null) { + print('✅ 发送方网络图片加载完成'); + return child; + } + // 如果已经超时隐藏了loading状态,不显示网络加载的loading + if (_shouldHideProgressStatus) { + print('⏭️ 发送方:已超时,跳过网络图片loading状态'); + return _buildSenderPlaceholder(width, height); + } + // 只在真正加载时显示loading,但设置超时 + return _buildLoadingState(width, height); }, errorBuilder: (context, error, stackTrace) { - return _buildErrorContainer(width, height); + print('❌ 发送方加载网络图片失败: $error'); + // 如果所有方法都失败,显示占位符而不是错误状态 + return _buildSenderPlaceholder(width, height); }, ); } + + // 4. 所有方法都失败,显示占位符 + print('⚠️ 发送方:所有路径都不可用,显示占位符'); + return _buildSenderPlaceholder(width, height); + } + + // 发送方占位符(避免显示错误状态,因为消息已发送成功) + Widget _buildSenderPlaceholder(double width, double height) { + return Container( + width: width, + height: height, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(18.w), + color: Color(0xff8E7BF6).withOpacity(0.1), + ), + child: Center( + child: Icon( + Icons.image, + size: 32.w, + color: Color(0xff8E7BF6).withOpacity(0.5), + ), + ), + ); + } + + // 构建本地图片 + Widget _buildLocalImage(double width, double height) { + try { + final file = File(_currentLocalPath!); + if (file.existsSync()) { + return Image.file( + file, + width: width, + height: height, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + print('❌ 加载本地图片失败: $error'); + return _buildErrorState(width, height); + }, + ); + } else { + print('❌ 本地图片文件不存在: $_currentLocalPath'); + return _buildErrorState(width, height); + } + } catch (e) { + print('❌ 加载本地图片异常: $e'); + return _buildErrorState(width, height); + } + } - // 默认显示错误容器 - return _buildErrorContainer(width, height); + // 构建网络图片 + Widget _buildNetworkImage(double width, double height) { + return Image.network( + _currentRemotePath!, + width: width, + height: height, + fit: BoxFit.cover, + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) return child; + return _buildLoadingState(width, height); + }, + errorBuilder: (context, error, stackTrace) { + print('❌ 加载网络图片失败: $error'); + return _buildErrorState(width, height); + }, + ); } - // 构建加载中的容器(网络图片加载时) - Widget _buildLoadingContainer(double width, double height) { + // 构建加载状态 + Widget _buildLoadingState(double width, double height) { return Container( width: width, height: height, @@ -175,28 +476,131 @@ class ImageItem extends StatelessWidget { child: Center( child: CircularProgressIndicator( strokeWidth: 2.w, - valueColor: AlwaysStoppedAnimation( - Colors.grey[400]!, + valueColor: AlwaysStoppedAnimation(Colors.grey[400]!), + ), + ), + ); + } + + // 构建下载中状态 + Widget _buildDownloadingState(double width, double height) { + return Container( + width: width, + height: height, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(18.w), + color: Colors.grey[200], + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator( + strokeWidth: 2.w, + valueColor: AlwaysStoppedAnimation(Color(0xff8E7BF6)), + ), + SizedBox(height: 8.h), + Text( + '下载中...', + style: TextStyle( + fontSize: 12.sp, + color: Colors.grey[600], + ), ), + ], + ), + ); + } + + // 构建错误状态 + Widget _buildErrorState(double width, double height) { + return Container( + width: width, + height: height, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(18.w), + color: Colors.grey[200], + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.broken_image, + size: 32.w, + color: Colors.grey[400], + ), + SizedBox(height: 8.h), + Text( + '图片加载失败', + style: TextStyle( + fontSize: 12.sp, + color: Colors.grey[600], + ), + ), + if (!widget.isSentByMe && _hasError) + Padding( + padding: EdgeInsets.only(top: 8.h), + child: ElevatedButton( + onPressed: _retryDownload, + style: ElevatedButton.styleFrom( + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 4.h), + backgroundColor: Color(0xff8E7BF6), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.w), + ), + ), + child: Text( + '重新加载', + style: TextStyle( + fontSize: 10.sp, + color: Colors.white, + ), + ), + ), + ), + ], + ), + ); + } + + // 构建时间标签 + Widget _buildTimeLabel() { + return Container( + alignment: Alignment.center, + padding: EdgeInsets.symmetric(horizontal: 16.w), + child: Container( + padding: EdgeInsets.symmetric(horizontal: 12.w), + child: Text( + widget.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(); - } + if (!widget.isSentByMe) return SizedBox.shrink(); - // 检查消息状态 - final status = message.status; + final status = widget.message.status; if (status == MessageStatus.FAIL) { - // 发送失败,显示重发按钮 return GestureDetector( - onTap: onResend, + onTap: widget.onResend, child: Container( width: 44.w, height: 26.h, @@ -216,56 +620,56 @@ class ImageItem extends StatelessWidget { ), ); } else if (status == MessageStatus.PROGRESS) { - // 发送中,显示加载动画 + // 如果应该隐藏PROGRESS状态(已超时且有远程路径),不显示loading + if (_shouldHideProgressStatus) { + return SizedBox.shrink(); + } return Container( width: 16.w, height: 16.w, child: CircularProgressIndicator( strokeWidth: 2, - valueColor: AlwaysStoppedAnimation( - Colors.grey, - ), + valueColor: AlwaysStoppedAnimation(Colors.grey), ), ); } else { - // 发送成功,不显示任何状态 return SizedBox.shrink(); } } - // 构建错误容器 - Widget _buildErrorContainer(double width, double height) { - return Container( - width: width, - height: height, - padding: EdgeInsets.all(8.w), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(18.w), - color: Colors.grey[200], - ), - alignment: Alignment.center, - child: Icon( - Icons.image_not_supported, - size: 32.w, - color: Colors.grey[400], - ), - ); - } - // 点击图片事件 void _onImageTap() { - // 优先使用本地路径,如果没有则使用网络路径 - String? imagePath = imageBody.localPath; - String? imageUrl = imageBody.remotePath; - - // 打开图片查看器 - Get.to( - () => ImageViewerPage( - imagePath: imagePath, - imageUrl: imageUrl, - ), - transition: Transition.fade, - duration: const Duration(milliseconds: 300), - ); + // 检查是否有可显示的图片 + bool hasImage = false; + String? imagePath; + String? imageUrl; + + // 优先使用本地图片 + if (_currentLocalPath != null && _currentLocalPath!.isNotEmpty) { + final file = File(_currentLocalPath!); + if (file.existsSync()) { + imagePath = _currentLocalPath; + hasImage = true; + } + } + + // 如果没有本地图片,使用远程图片 + if (!hasImage && _currentRemotePath != null && _currentRemotePath!.isNotEmpty) { + imageUrl = _currentRemotePath; + hasImage = true; + } + + if (hasImage) { + Get.to( + () => ImageViewerPage( + imagePath: imagePath, + imageUrl: imageUrl, + ), + transition: Transition.fade, + duration: const Duration(milliseconds: 300), + ); + } else { + SmartDialog.showToast('图片暂时无法查看'); + } } } \ No newline at end of file diff --git a/lib/widget/message/more_options_view.dart b/lib/widget/message/more_options_view.dart index 19de9ef..c10f2b0 100644 --- a/lib/widget/message/more_options_view.dart +++ b/lib/widget/message/more_options_view.dart @@ -1,10 +1,8 @@ -import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; import 'package:wechat_assets_picker/wechat_assets_picker.dart'; import 'package:wechat_camera_picker/wechat_camera_picker.dart'; -import 'package:video_player/video_player.dart'; import '../../generated/assets.dart'; @@ -12,13 +10,11 @@ class MoreOptionsView extends StatelessWidget { final bool isVisible; final ValueChanged> onImageSelected; final ValueChanged onCameraSelected; - final Function(String filePath, int duration)? onVideoSelected; const MoreOptionsView({ required this.isVisible, required this.onImageSelected, required this.onCameraSelected, - this.onVideoSelected, super.key, }); @@ -42,38 +38,26 @@ class MoreOptionsView extends StatelessWidget { GestureDetector( onTap: () async{ try { - print('📷 [MoreOptionsView] 打开相册选择图片/视频'); - // 支持同时选择图片和视频 + print('📷 [MoreOptionsView] 打开相册选择图片'); + // 只支持选择图片 List? result = await AssetPicker.pickAssets( context, pickerConfig: const AssetPickerConfig( - requestType: RequestType.common, // 支持图片和视频 + requestType: RequestType.image, // 只支持图片 maxAssets: 9, ), ); if (result != null && result.isNotEmpty) { - print('选择了 ${result.length} 个文件'); + print('选择了 ${result.length} 张图片'); - // 分别处理图片和视频 List imagePaths = []; for (var asset in result) { final file = await asset.file; if (file != null) { - print('文件类型: ${asset.type}, 路径: ${file.path}'); - - if (asset.type == AssetType.video) { - // 视频文件 - print('检测到视频文件'); - final duration = asset.duration; - if (onVideoSelected != null) { - onVideoSelected!(file.path, duration); - } - } else { - // 图片文件 - imagePaths.add(file.path); - } + print('图片路径: ${file.path}'); + imagePaths.add(file.path); } } @@ -84,7 +68,7 @@ class MoreOptionsView extends StatelessWidget { } } } catch (e) { - print('❌ 选择文件失败: $e'); + print('❌ 选择图片失败: $e'); if (Get.isLogEnable) { Get.log("选择图片失败: $e"); } @@ -114,36 +98,24 @@ class MoreOptionsView extends StatelessWidget { ), ), SizedBox(width: 40.w), - // 相机选项(支持拍照和拍视频) + // 相机选项(只支持拍照) GestureDetector( onTap: () async{ try { print('📷 [MoreOptionsView] 打开相机'); - // 支持拍照和录像 + // 只支持拍照 AssetEntity? entity = await CameraPicker.pickFromCamera( context, pickerConfig: const CameraPickerConfig( - enableRecording: true, // 启用录像功能 + enableRecording: false, // 禁用录像功能 ), ); if (entity != null) { final file = await entity.file; if (file != null) { - print('文件类型: ${entity.type}, 路径: ${file.path}'); - - if (entity.type == AssetType.video) { - // 拍摄的视频 - print('检测到视频文件'); - final duration = await _getVideoDuration(file.path); - if (onVideoSelected != null) { - onVideoSelected!(file.path, duration); - } - } else { - // 拍摄的照片 - print('检测到图片文件'); - onCameraSelected(file.path); - } + print('拍摄的图片路径: ${file.path}'); + onCameraSelected(file.path); } } } catch (e) { @@ -184,18 +156,4 @@ class MoreOptionsView extends StatelessWidget { : null, ); } - - // 获取视频时长 - Future _getVideoDuration(String filePath) async { - try { - final controller = VideoPlayerController.file(File(filePath)); - await controller.initialize(); - final duration = controller.value.duration.inSeconds; - await controller.dispose(); - return duration; - } catch (e) { - print('获取视频时长失败: $e'); - return 0; - } - } } \ No newline at end of file diff --git a/lib/widget/message/voice_item.dart b/lib/widget/message/voice_item.dart index 6440767..3c0a001 100644 --- a/lib/widget/message/voice_item.dart +++ b/lib/widget/message/voice_item.dart @@ -101,41 +101,128 @@ class _VoiceItemState extends State with TickerProviderStateMixin { try { // 获取音频文件路径 String? filePath; - final localPath = widget.voiceBody.localPath; + var localPath = widget.voiceBody.localPath; final remotePath = widget.voiceBody.remotePath; - // 优先使用本地路径 - if (remotePath != null && remotePath.isNotEmpty) { - // 只有远程路径,尝试使用远程路径(audioplayers 可能支持网络URL) - // 注意:如果远程路径是文件系统路径而不是URL,需要先下载 - if (remotePath.startsWith('http://') || - remotePath.startsWith('https://')) { - filePath = remotePath; + print('🔍 语音消息路径检查: localPath=$localPath, remotePath=$remotePath'); + + // 如果是接收到的消息且本地文件不存在或大小为0,先下载 + if (!widget.isSentByMe && widget.message != null) { + final localFile = localPath.isNotEmpty ? File(localPath) : null; + bool needDownload = false; + + if (localPath.isEmpty || localFile == null || !await localFile.exists()) { + needDownload = true; + print('📥 本地文件不存在,需要下载'); } else { - SmartDialog.showToast('音频文件不存在,请等待下载完成'); - print('远程音频文件路径: $remotePath'); - return; + final fileSize = await localFile.length(); + if (fileSize == 0) { + needDownload = true; + print('📥 本地文件大小为0,需要下载'); + } + } + + // 如果需要下载,先下载附件 + if (needDownload) { + try { + print('📥 开始下载语音文件...'); + await EMClient.getInstance.chatManager + .downloadAttachment(widget.message!); + + // 下载后,等待一小段时间确保文件写入完成 + await Future.delayed(Duration(milliseconds: 300)); + + // 下载后,从消息对象获取新的本地路径(下载后会自动更新 body 中的路径) + // 重新从消息对象获取 voiceBody,因为下载后路径会更新 + if (widget.message!.body is EMVoiceMessageBody) { + final updatedVoiceBody = widget.message!.body as EMVoiceMessageBody; + localPath = updatedVoiceBody.localPath; + print('✅ 语音文件下载完成,新路径: $localPath'); + } else { + print('⚠️ 消息 body 类型不是 EMVoiceMessageBody'); + } + + // 刷新状态 + setState(() {}); + } catch (e) { + print('❌ 下载语音文件失败: $e'); + SmartDialog.showToast('下载语音文件失败: $e'); + return; + } } - } else if (localPath.isNotEmpty) { + } + + // 优先使用本地路径 + if (localPath.isNotEmpty) { final localFile = File(localPath); if (await localFile.exists()) { - filePath = localPath; + // 检查文件大小,确保文件有效 + final fileSize = await localFile.length(); + if (fileSize > 0) { + filePath = localPath; + print('✅ 使用本地音频文件: $localPath, 文件大小: $fileSize bytes'); + } else { + print('⚠️ 本地音频文件大小为0: $localPath'); + // 文件大小为0,尝试使用远程路径 + if (remotePath != null && remotePath.isNotEmpty) { + if (remotePath.startsWith('http://') || + remotePath.startsWith('https://')) { + filePath = remotePath; + print('✅ 使用远程音频文件: $remotePath'); + } else { + SmartDialog.showToast('音频文件无效'); + print('⚠️ 本地文件大小为0,远程路径不是URL: $remotePath'); + return; + } + } else { + SmartDialog.showToast('音频文件无效'); + print('⚠️ 本地文件大小为0,且没有远程路径'); + return; + } + } + } else { + print('⚠️ 本地音频文件不存在: $localPath'); + // 本地文件不存在,尝试使用远程路径 + if (remotePath != null && remotePath.isNotEmpty) { + if (remotePath.startsWith('http://') || + remotePath.startsWith('https://')) { + filePath = remotePath; + print('✅ 使用远程音频文件: $remotePath'); + } else { + SmartDialog.showToast('音频文件不存在'); + print('⚠️ 本地文件不存在,远程路径不是URL: $remotePath'); + return; + } + } else { + SmartDialog.showToast('音频文件不存在'); + print('⚠️ 本地和远程路径都无效'); + return; + } + } + } else if (remotePath != null && remotePath.isNotEmpty) { + // 没有本地路径,直接使用远程路径 + if (remotePath.startsWith('http://') || + remotePath.startsWith('https://')) { + // 如果是HTTP URL,可以直接播放 + filePath = remotePath; + print('✅ 使用远程音频文件(无本地路径): $remotePath'); } else { SmartDialog.showToast('音频文件不存在'); - print('本地音频文件不存在: $localPath'); + print('⚠️ 远程路径不是URL: $remotePath'); return; } } if (filePath != null && filePath.isNotEmpty) { + print('🎵 开始播放音频: $filePath'); await _playerManager.play(widget.messageId, filePath); } else { SmartDialog.showToast('无法获取音频文件'); - print('音频文件路径为空'); + print('❌ 音频文件路径为空'); } } catch (e) { SmartDialog.showToast('播放失败: $e'); - print('播放音频失败: $e'); + print('❌ 播放音频失败: $e'); } }