import 'dart:async'; import 'dart:collection'; import 'dart:convert'; import 'dart:io'; import 'package:extended_image/extended_image.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_debouncer/flutter_debouncer.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:yumi/ui_kit/components/sc_tts.dart'; import 'package:yumi/app/constants/sc_global_config.dart'; import 'package:yumi/shared/tools/sc_message_utils.dart'; import 'package:yumi/shared/tools/sc_path_utils.dart'; import 'package:yumi/shared/tools/sc_room_utils.dart'; import 'package:yumi/shared/data_sources/sources/local/floating_screen_manager.dart'; import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart'; import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; import 'package:yumi/services/audio/rtc_manager.dart'; import 'package:provider/provider.dart'; import 'package:tencent_cloud_chat_sdk/enum/V2TimAdvancedMsgListener.dart'; import 'package:tencent_cloud_chat_sdk/enum/V2TimConversationListener.dart'; import 'package:tencent_cloud_chat_sdk/enum/V2TimGroupListener.dart'; import 'package:tencent_cloud_chat_sdk/enum/V2TimSDKListener.dart'; import 'package:tencent_cloud_chat_sdk/enum/conversation_type.dart'; import 'package:tencent_cloud_chat_sdk/enum/group_type.dart'; import 'package:tencent_cloud_chat_sdk/enum/log_level_enum.dart'; import 'package:tencent_cloud_chat_sdk/enum/message_status.dart'; import 'package:tencent_cloud_chat_sdk/manager/v2_tim_group_manager.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_callback.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation_result.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_member_info.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_image_elem.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_message_receipt.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_msg_create_info_result.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_user_full_info.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_user_status.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_value_callback.dart'; import 'package:tencent_cloud_chat_sdk/tencent_im_sdk_plugin.dart'; import 'package:yumi/app_localizations.dart'; import 'package:yumi/ui_kit/components/dialog/dialog_base.dart'; import 'package:yumi/app/constants/sc_room_msg_type.dart'; import 'package:yumi/shared/tools/sc_lk_event_bus.dart'; import 'package:yumi/shared/tools/sc_gift_vap_svga_manager.dart'; import 'package:yumi/shared/data_sources/sources/local/file_cache_manager.dart'; import 'package:yumi/shared/data_sources/sources/repositories/sc_config_repository_imp.dart'; import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository_imp.dart'; import 'package:yumi/shared/data_sources/models/message/big_broadcast_group_message.dart'; import 'package:yumi/shared/data_sources/models/message/sc_floating_message.dart'; import 'package:yumi/shared/business_logic/models/res/sc_broad_cast_luck_gift_push.dart'; import 'package:yumi/shared/business_logic/models/res/broad_cast_mic_change_push.dart' hide Data; import 'package:yumi/shared/business_logic/models/res/gift_res.dart'; import 'package:yumi/shared/business_logic/models/res/sc_public_message_page_res.dart'; import 'package:yumi/shared/business_logic/models/res/sc_room_theme_list_res.dart'; import 'package:yumi/services/general/sc_app_general_manager.dart'; import 'package:yumi/ui_kit/widgets/room/invite/invite_room_dialog.dart'; import 'package:yumi/ui_kit/widgets/room/red_packet/room_red_packet_models.dart'; import 'package:yumi/ui_kit/widgets/room/red_packet/room_red_packet_open_dialog.dart'; import 'package:yumi/ui_kit/widgets/room/red_packet/room_red_packet_pending_cache.dart'; import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart'; import 'package:yumi/shared/business_logic/models/res/login_res.dart'; import 'package:yumi/services/gift/gift_system_manager.dart'; import '../../shared/data_sources/models/enum/sc_gift_type.dart'; import '../../shared/data_sources/models/enum/sc_room_roles_type.dart'; typedef RoomNewMsgListener = Function(Msg msg); typedef OnNewMessageListener = Function(V2TimMessage? message, {String? msgId}); typedef OnNewANMessageListener = Function(Records? message); typedef OnRevokeMessageListener = Function(String msgId); typedef OnNewGroupMessageListener = Function(String groupID, V2TimMessage message); typedef OnMessageRecvC2CReadListener = Function(List messageIDList); typedef RtmProvider = RealTimeMessagingManager; class _LuckGiftPushQueueEntry { _LuckGiftPushQueueEntry({ required this.reward, required this.eventKey, required this.mergeKey, required this.updatedAt, }) { if (eventKey.isNotEmpty) { eventKeys.add(eventKey); } } SCBroadCastLuckGiftPush reward; final String eventKey; final String mergeKey; int updatedAt; final Set eventKeys = {}; void merge(SCBroadCastLuckGiftPush incoming, String incomingEventKey) { reward = reward.mergeReward(incoming); updatedAt = DateTime.now().millisecondsSinceEpoch; if (incomingEventKey.isNotEmpty) { eventKeys.add(incomingEventKey); } } } class RealTimeMessagingManager extends ChangeNotifier { static const int _giftComboMergeWindowMs = 3000; static const int _maxLuckGiftPushQueueLength = 12; static const int _luckyGiftFloatMinMultiple = 5; static const int _luckyGiftBurstMinMultiple = 10; static const int _luckyGiftBurstMinAwardAmount = 5000; static const int _luckyGiftBurstMergeDelayMs = 600; static const int _luckyGiftBurstMergeWindowMs = 1800; static const int _luckyGiftBurstRecentEventTtlMs = 5000; static const int _luckyGiftBurstPlaybackWatchdogMs = 8000; BuildContext? context; void _giftFxLog(String message) { debugPrint('[GiftFX][RTM] $message'); } void _enqueueGiftFloatingMessage( SCFloatingMessage message, { String? dedupKey, }) { if (SCGlobalConfig.isLowPerformanceDevice) { OverlayManager().addLowPerformanceCompensationMessage( message, dedupKey: dedupKey, ); return; } OverlayManager().addMessage(message); } String _giftFloatingDedupKey({ required String? roomId, required String? senderId, required String? receiverId, required String? giftId, required num? quantity, }) { return [ "gift", roomId ?? "", senderId ?? "", receiverId ?? "", giftId ?? "", quantity ?? 0, ].join("|"); } String _entryFloatingDedupKey({ required String? roomId, required String? userId, required String? resourceId, }) { return ["entry", roomId ?? "", userId ?? "", resourceId ?? ""].join("|"); } String _resolveEntryCompensationCover(PropsResources? resource) { return _firstNonBlank([ resource?.roomSendCoverUrl, resource?.roomOpenedUrl, resource?.cover, resource?.roomNotOpenedUrl, ]); } String _firstNonBlank(Iterable values) { for (final value in values) { final text = value?.trim(); if (text != null && text.isNotEmpty) { return text; } } return ""; } ///消息列表 List roomAllMsgList = []; List roomChatMsgList = []; List roomGiftMsgList = []; RoomNewMsgListener? msgAllListener; RoomNewMsgListener? msgChatListener; RoomNewMsgListener? msgGiftListener; RoomNewMsgListener? msgFloatingGiftListener; RoomNewMsgListener? msgLuckyGiftRewardTickerListener; RoomNewMsgListener? msgUserJoinListener; /// 当前会话 V2TimConversation? currentConversation; /// 会话列表缓存 Map conversationMap = {}; ///消息已读监听 OnMessageRecvC2CReadListener? onMessageRecvC2CReadListener; ///消息被撤回监听 OnRevokeMessageListener? onRevokeMessageListener; ///新消息监听 单聊 OnNewMessageListener? onNewMessageCurrentConversationListener; OnNewANMessageListener? onNewActivityMessageCurrentConversationListener; OnNewANMessageListener? onNewNotifcationMessageCurrentConversationListener; ///新消息监听 群聊 Map onNewMessageListenerGroupMap = {}; int allUnReadCount = 0; int messageUnReadCount = 0; int systemUnReadCount = 0; int customerUnReadCount = 0; int activityUnReadCount = 0; int notifcationUnReadCount = 0; SCBroadCastLuckGiftPush? currentPlayingLuckGift; final Queue<_LuckGiftPushQueueEntry> _luckGiftPushQueue = Queue(); String? _currentLuckGiftPushKey; Set _currentLuckGiftPushEventKeys = {}; Timer? _luckGiftPushPlayTimer; int _luckGiftPushPlaybackToken = 0; final Map _recentLuckGiftPushEventTimes = {}; int get currentLuckGiftBurstPlaybackToken => _luckGiftPushPlaybackToken; String? roomRedPacketBroadcastGroupId; String? roomRedPacketBroadcastRegionCode; Debouncer debouncer = Debouncer(); List conversationList = []; ///客服 SocialChatUserProfile? customerInfo; bool _isSystemConversation(V2TimConversation? conversation) { if (conversation == null) { return false; } return SCGlobalConfig.isSystemConversationId(conversation.conversationID) || SCGlobalConfig.isSystemUserId(conversation.userID); } int _systemUnreadCount() { int count = 0; for (final conversation in conversationMap.values) { if (_isSystemConversation(conversation)) { count += conversation.unreadCount ?? 0; } } return count; } V2TimConversation getPreferredSystemConversation() { final systemConversations = conversationMap.values.where(_isSystemConversation).toList() ..sort((e1, e2) { final time1 = e1.lastMessage?.timestamp ?? 0; final time2 = e2.lastMessage?.timestamp ?? 0; return time2.compareTo(time1); }); if (systemConversations.isNotEmpty) { return systemConversations.first; } return V2TimConversation( type: ConversationType.V2TIM_C2C, userID: SCGlobalConfig.primarySystemUserId, conversationID: SCGlobalConfig.primarySystemConversationId, ); } void getConversationList() { List list = conversationMap.values.toList(); list.removeWhere((element) { if (_isSystemConversation(element)) { return true; } if (element.conversationID == "c2c_${customerInfo?.id}") { return true; } if (element.conversationID == "c2c_atyou-newsletter") { ///删除这个会话,后台乱发的联系人。。防止无效未读数 clearC2CHistoryMessage(element.conversationID, false); return true; } if (element.lastMessage == null) { return true; } return false; }); list.sort((e1, e2) { int time1 = e1.lastMessage?.timestamp ?? 0; int time2 = e2.lastMessage?.timestamp ?? 0; return time2.compareTo(time1); }); conversationList = list; systemUnReadCount = _systemUnreadCount(); customerUnReadCount = conversationMap["c2c_${customerInfo?.id}"]?.unreadCount ?? 0; notifyListeners(); } init(BuildContext context) async { this.context = context; V2TimSDKListener sdkListener = V2TimSDKListener( onConnectFailed: (int code, String error) { // 连接失败的回调函数 // code 错误码 // error 错误信息 }, onConnectSuccess: () { // SDK 已经成功连接到腾讯云服务器 }, onConnecting: () { // SDK 正在连接到腾讯云服务器 }, onKickedOffline: () { // 当前用户被踢下线,此时可以 UI 提示用户,并再次调用 V2TIMManager 的 login() 函数重新登录。 }, onSelfInfoUpdated: (V2TimUserFullInfo info) { // 登录用户的资料发生了更新 // info登录用户的资料 }, onUserSigExpired: () { // 在线时票据过期:此时您需要生成新的 userSig 并再次调用 V2TIMManager 的 login() 函数重新登录。 }, onUserStatusChanged: (List userStatusList) { //用户状态变更通知 //userStatusList 用户状态变化的用户列表 //收到通知的情况:订阅过的用户发生了状态变更(包括在线状态和自定义状态),会触发该回调 //在 IM 控制台打开了好友状态通知开关,即使未主动订阅,当好友状态发生变更时,也会触发该回调 //同一个账号多设备登录,当其中一台设备修改了自定义状态,所有设备都会收到该回调 }, ); V2TimValueCallback initSDKRes = await TencentImSDKPlugin.v2TIMManager .initSDK( sdkAppID: int.parse(SCGlobalConfig.tencentImAppid), // SDKAppID loglevel: LogLevelEnum.V2TIM_LOG_ALL, // 日志登记等级 listener: sdkListener, // 事件监听器 ); if (initSDKRes.code == 0) {} try { customerInfo = await SCConfigRepositoryImp().customerService(); } catch (e) {} /// 登录 await loginTencetRtm(context); /// 初始化会话列表 // _onRefreshConversationSub = FTIM.getContactManager().addRefreshConversationListener(_onRefreshConversation); TencentImSDKPlugin.v2TIMManager .getConversationManager() .addConversationListener( listener: V2TimConversationListener( onNewConversation: (conversationList) { // _onRefreshConversation(conversationList); initConversation(); }, onTotalUnreadMessageCountChanged: (int totalUnreadCount) { messageUnReadCount = totalUnreadCount; systemUnReadCount = _systemUnreadCount(); customerUnReadCount = conversationMap["c2c_${customerInfo?.id}"]?.unreadCount ?? 0; allUnReadCount = messageUnReadCount + notifcationUnReadCount + activityUnReadCount; notifyListeners(); }, ), ); TencentImSDKPlugin.v2TIMManager.addGroupListener( listener: V2TimGroupListener( onMemberEnter: (String groupID, List memberList) { final rtcProvider = Provider.of( context, listen: false, ); if (groupID == rtcProvider.currenRoom?.roomProfile?.roomProfile?.roomAccount) { rtcProvider.fetchOnlineUsersList(notifyIfUnchanged: false); } }, onMemberLeave: (String groupID, V2TimGroupMemberInfo member) { final rtcProvider = Provider.of( context, listen: false, ); rtcProvider.removOnlineUser(groupID, member.userID!); if (groupID == rtcProvider.currenRoom?.roomProfile?.roomProfile?.roomAccount) { rtcProvider.fetchOnlineUsersList(notifyIfUnchanged: false); } }, onMemberKicked: ( String groupID, V2TimGroupMemberInfo opUser, List memberList, ) { ///踢出房间 if (memberList.isNotEmpty) { if (memberList.first.userID == AccountStorage().getCurrentUser()?.userProfile?.id) { final rtcProvider = Provider.of( context, listen: false, ); unawaited( rtcProvider.handleSelfMicRemovedByRemote(refreshMicList: false), ); ///退出房间 rtcProvider.exitCurrentVoiceRoomSession(false).whenComplete(() { SCRoomUtils.closeAllDialogs(); SmartDialog.show( tag: "showConfirmDialog", alignment: Alignment.center, debounce: true, animationType: SmartAnimationType.fade, builder: (_) { return MsgDialog( title: SCAppLocalizations.of(context)!.tips, msg: SCAppLocalizations.of(context)!.kickRoomTips, btnText: SCAppLocalizations.of(context)!.confirm, onEnsure: () {}, ); }, ); }); } } }, ), ); /// 新消息监听 // FTIM.getMessageManager().addNewMessagesListener(_onNewMessage); TencentImSDKPlugin.v2TIMManager.getMessageManager().addAdvancedMsgListener( listener: V2TimAdvancedMsgListener( onRecvC2CReadReceipt: (List receiptList) { //会话已读回调 }, onRecvMessageModified: (V2TimMessage message) { // msg 为被修改之后的消息对象 }, onRecvMessageReadReceipts: (List receiptList) { //群聊/单聊已读回调 List messageIDList = []; for (var element in receiptList) { messageIDList.add(element.msgID!); } onMessageRecvC2CReadListener?.call(messageIDList); }, onRecvMessageRevoked: (String messageId) { // 在本地维护的消息中处理被对方撤回的消息 TencentImSDKPlugin.v2TIMManager .getMessageManager() .deleteMessages(msgIDs: [messageId]) .then((result) { onRevokeMessageListener?.call(messageId); for (var ms in conversationList) { if (ms.lastMessage?.msgID == messageId) { ms.lastMessage?.status = MessageStatus.V2TIM_MSG_STATUS_LOCAL_REVOKED; break; } } notifyListeners(); }); }, onRecvNewMessage: (V2TimMessage message) async { _onNewMessage(message); }, onSendMessageProgress: (V2TimMessage message, int progress) { //文件上传进度回调 }, ), ); getAllUnReadCount(); joinBigBroadcastGroup(); unawaited(syncRoomRedPacketBroadcastGroup()); } /// 初始化会话 /// 打开聊天界面的时候调用 int sTime = 0; Future startConversation(V2TimConversation conversation) async { assert(conversation != null); int eTime = DateTime.now().millisecondsSinceEpoch; if (eTime - sTime > 5000) { sTime = eTime; currentConversation = conversation; await TencentImSDKPlugin.v2TIMManager .getMessageManager() .markC2CMessageAsRead(userID: conversation.userID!); notifyListeners(); return true; } else { return false; } } ///初始话会话 Future initConversation() async { V2TimValueCallback convList = await TencentImSDKPlugin.v2TIMManager .getConversationManager() .getConversationList(nextSeq: '0', count: 100); List? conversationList = convList.data?.conversationList; print('conversationList:${conversationList?.length}'); conversationMap.clear(); if (conversationList != null) { for (V2TimConversation? conversation in conversationList) { conversationMap[conversation!.conversationID] = conversation; } } getConversationList(); } // /// 所有消息未读数 // int getAllUnReadCount() { // allUnReadCount = 0; // for (var value in conversationList) { // allUnReadCount += value.unreadCount ?? 0; // } // // for (var value in systemConversationList) { // // i += value.unreadMessageNum; // // } // notifyListeners(); // return allUnReadCount; // } /// 所有消息未读数 void getAllUnReadCount() async { V2TimValueCallback res = await TencentImSDKPlugin.v2TIMManager .getConversationManager() .getTotalUnreadMessageCount(); if (res.code == 0) { print('初始未读总数: ${res.data}'); messageUnReadCount = res.data ?? 0; allUnReadCount = messageUnReadCount + activityUnReadCount + notifcationUnReadCount; notifyListeners(); // 这里可以先用初始值更新UI } } ///登录IM Future loginTencetRtm(BuildContext context) async { SocialChatLoginRes? userModel = AccountStorage().getCurrentUser(); bool logined = false; while (!logined && userModel != null) { await Future.delayed(Duration(milliseconds: 550)); try { if (userModel.userSig != null) { V2TimCallback res = await TencentImSDKPlugin.v2TIMManager.login( userID: userModel.userProfile?.id ?? "", userSig: userModel.userSig ?? "", ); print( 'tim voLogin:${res.code},${userModel.userProfile?.id},${userModel.userSig}', ); if (res.code == 0) { isLogout = false; // 登录成功逻辑 logined = true; print('tim 登录成功'); await initConversation(); unawaited(syncRoomRedPacketBroadcastGroup()); } else { // 登录失败逻辑 //print('timm 需要重新登录2'); print('tim 登录失败${res.code}'); SCTts.show('tim login fail'); } } else { //print('timm 需要重新登录sign'); SCTts.show('tim login fail'); } } catch (e) { //print('timm 登录异常:${e.toString()}'); SCTts.show('timm login fail:${e.toString()}'); } userModel = AccountStorage().getCurrentUser(); } } _onNewMessage(V2TimMessage message) { if (message.groupID != null) { final groupId = message.groupID!; ///全服通知 / 语区广播群 if (groupId == SCGlobalConfig.bigBroadcastGroup || groupId == roomRedPacketBroadcastGroupId) { _newBroadCastMsgRecv(groupId, message); return; } if (_shouldRouteUntrackedRoomRedPacketBroadcast(groupId, message)) { debugPrint( '[RoomRedPacket][IM] route untracked group as region broadcast ' 'group=$groupId cachedRegionGroup=$roomRedPacketBroadcastGroupId', ); _newBroadCastMsgRecv(groupId, message, forceRegionBroadcast: true); return; } ///群消息 for (var element in onNewMessageListenerGroupMap.values) { element?.call(groupId, message); } } else { ///单聊消息 _onNew1v1Message(message); } } _onNew1v1Message(V2TimMessage? message, {String? msgId}) async { for (var element in conversationList) { if (message?.userID == element.userID) { element.lastMessage = message; if (onNewMessageCurrentConversationListener == null) { element.unreadCount = element.unreadCount! + 1; } } } if (message?.userID == customerInfo?.id) { if (onNewMessageCurrentConversationListener == null) { conversationMap["c2c_${customerInfo?.id}"]?.unreadCount = (conversationMap["c2c_${customerInfo?.id}"]?.unreadCount ?? 0) + 1; } } systemUnReadCount = _systemUnreadCount(); notifyListeners(); onNewMessageCurrentConversationListener?.call(message, msgId: msgId); } void _onRefreshConversation(List conversations) { for (V2TimConversation conversation in conversations) { conversationMap[conversation.conversationID] = conversation; } getConversationList(); } ///创建房间im群聊 Future> createRoomGroup( String groupID, String groupName, ) { return V2TIMGroupManager().createGroup( groupID: groupID, groupType: GroupType.AVChatRoom, groupName: groupName, ); } ///加入房间的im群聊 Future joinRoomGroup(String groupID, String message) async { _resetLuckGiftBurstQueue(); var joinResult = await TencentImSDKPlugin.v2TIMManager.joinGroup( groupID: groupID, message: message, ); if (joinResult.code == 0) { onNewMessageListenerGroupMap[groupID] = _newGroupMsg; } return joinResult; } Future syncRoomRedPacketBroadcastGroup() async { for (var attempt = 0; attempt < 3 && !isLogout; attempt++) { try { debugPrint( '[RoomRedPacket][IM] sync region group attempt=${attempt + 1}', ); final group = await SCChatRoomRepository().roomRedPacketImGroup(); if (!group.canJoin) { debugPrint( '[RoomRedPacket][IM] skip join configured=${group.configured} ' 'groupId=${group.groupId} region=${group.regionCode} status=${group.status}', ); return; } final newGroupId = group.groupId.trim(); if (newGroupId == roomRedPacketBroadcastGroupId) { roomRedPacketBroadcastRegionCode = group.regionCode.trim(); debugPrint( '[RoomRedPacket][IM] region group unchanged group=$newGroupId ' 'region=${group.regionCode}', ); return; } final joinResult = await TencentImSDKPlugin.v2TIMManager.joinGroup( groupID: newGroupId, message: "", ); if (joinResult.code == 0 || joinResult.code == 10013) { final oldGroupId = roomRedPacketBroadcastGroupId; roomRedPacketBroadcastGroupId = newGroupId; roomRedPacketBroadcastRegionCode = group.regionCode.trim(); if (oldGroupId != null && oldGroupId.isNotEmpty && oldGroupId != newGroupId) { unawaited(quitGroup(oldGroupId)); } debugPrint( '[RoomRedPacket][IM] joined region group=$newGroupId region=${group.regionCode}', ); return; } debugPrint( '[RoomRedPacket][IM] join group failed code=${joinResult.code} desc=${joinResult.desc}', ); } catch (e) { debugPrint('[RoomRedPacket][IM] sync group failed: $e'); } await Future.delayed(Duration(milliseconds: 550)); } } ///发送文本消息 Future dispatchMessage( Msg msg, { bool showEmoticons = true, bool addLocal = true, }) async { if (addLocal) { addMsg(msg); } // 发送消息到腾讯云IM if (msg.groupId != null) { await _sendTencentMessage(msg); } notifyListeners(); } // 发送腾讯云消息 Future _sendTencentMessage(Msg msg) async { try { switch (msg.type) { case SCRoomMsgType.text: case SCRoomMsgType.shangMai: case SCRoomMsgType.emoticons: case SCRoomMsgType.xiaMai: case SCRoomMsgType.killXiaMai: case SCRoomMsgType.roomMusic: case SCRoomMsgType.roomRoleChange: case SCRoomMsgType.roomSettingUpdate: case SCRoomMsgType.roomBGUpdate: case SCRoomMsgType.qcfj: case SCRoomMsgType.fengMai: case SCRoomMsgType.jieFeng: case SCRoomMsgType.joinRoom: case SCRoomMsgType.gift: case SCRoomMsgType.bsm: case SCRoomMsgType.roomDice: case SCRoomMsgType.roomRPS: case SCRoomMsgType.roomLuckNumber: case SCRoomMsgType.image: case SCRoomMsgType.roomGameClose: case SCRoomMsgType.roomGameCreate: case SCRoomMsgType.luckGiftAnimOther: case SCRoomMsgType.roomRedPacketClaim: _sendRoomMessage(msg); break; default: break; } } catch (e) { print('发送消息失败: $e'); // 处理发送失败的情况 } } ///发送单聊文本消息 Future sendC2CTextMsg( String msg, V2TimConversation toConversation, ) async { // 创建文本消息 V2TimValueCallback createTextMessageRes = await TencentImSDKPlugin.v2TIMManager .getMessageManager() .createTextMessage( text: msg, // 文本信息 ); if (createTextMessageRes.code == 0) { // 文本信息创建成功 String id = createTextMessageRes.data!.id!; // 发送文本消息 // 在sendMessage时,若只填写receiver则发个人用户单聊消息 // 若只填写groupID则发群组消息 // 若填写了receiver与groupID则发群内的个人用户,消息在群聊中显示,只有指定receiver能看见 String receiveId = toConversation.userID!; V2TimValueCallback sendMessageRes = await TencentImSDKPlugin .v2TIMManager .getMessageManager() .sendMessage( id: id, // 创建的messageid receiver: toConversation.userID!, // 接收人id needReadReceipt: true, groupID: '', // 是否需要已读回执 ); if (sendMessageRes.code == 0) { // 发送成功 _onNew1v1Message(sendMessageRes.data); } else { SCTts.show( 'create fail,code:${sendMessageRes.code},${sendMessageRes.desc}', ); } } } ///发送单聊自定义消息 Future sendC2CCustomMsg( String msg, V2TimConversation toConversation, String extension, ) async { // 创建文本消息 V2TimValueCallback createCustomMessageRes = await TencentImSDKPlugin.v2TIMManager .getMessageManager() .createCustomMessage( data: msg, // 文本信息 extension: extension, ); if (createCustomMessageRes.code == 0) { // 文本信息创建成功 String id = createCustomMessageRes.data!.id!; // 发送文本消息 // 在sendMessage时,若只填写receiver则发个人用户单聊消息 // 若只填写groupID则发群组消息 // 若填写了receiver与groupID则发群内的个人用户,消息在群聊中显示,只有指定receiver能看见 String receiveId = toConversation.userID!; V2TimValueCallback sendMessageRes = await TencentImSDKPlugin .v2TIMManager .getMessageManager() .sendMessage( id: id, // 创建的messageid receiver: toConversation.userID!, // 接收人id needReadReceipt: true, isSupportMessageExtension: true, groupID: '', // 是否需要已读回执 ); if (sendMessageRes.code == 0) { // 发送成功 _onNew1v1Message(sendMessageRes.data); } else { SCTts.show( 'create fail,code:${sendMessageRes.code},${sendMessageRes.desc}', ); } } } ///发送单聊图片消息 Future sendImageMsg({ List? selectedList, File? file, required V2TimConversation conversation, }) async { if (file != null) { File newFile = await SCMessageUtils.createImageElem(file); V2TimValueCallback createImageMessageRes = await TencentImSDKPlugin.v2TIMManager .getMessageManager() .createImageMessage(imagePath: newFile.path); if (createImageMessageRes.code == 0) { String id = createImageMessageRes.data!.id!; V2TimValueCallback sendMessageRes = await TencentImSDKPlugin.v2TIMManager .getMessageManager() .sendMessage( id: id, receiver: conversation.userID!, needReadReceipt: true, groupID: '', ); if (sendMessageRes.code == 0) { // 发送成功 _onNew1v1Message(sendMessageRes.data); } else { SCTts.show( 'create fail,code:${sendMessageRes.code},${sendMessageRes.desc}', ); } } } else { if (selectedList != null) { for (File entity in selectedList) { String id = ""; V2TimMessage? message; //判断是视频或者图片消息 if (SCPathUtils.getFileType(entity.path) == "image") { V2TimValueCallback createImageMessageRes = await TencentImSDKPlugin.v2TIMManager .getMessageManager() .createImageMessage(imagePath: entity.path); if (createImageMessageRes.code == 0) { message = createImageMessageRes.data?.messageInfo; id = createImageMessageRes.data!.id!; // 创建图片 File newFile = await SCMessageUtils.createImageElem(entity); //发送 V2TimImageElem elem = V2TimImageElem(path: newFile.path); message?.imageElem = elem; } } else if (SCPathUtils.getFileType(entity.path) == "video_pic") { if (entity.lengthSync() > 50000000) { SCTts.show( SCAppLocalizations.of(context!)!.theVideoSizeCannotExceed, ); return; } // 复制一份视频 String md5Str1 = keyToMd5(entity.path); File newFile = File( "${FileCacheManager.videoCachePath}/$md5Str1.mp4", ); if (!newFile.existsSync()) { await entity.copy(newFile.path); } // 创建缩略图 String? thumbImagePath = await SCMessageUtils.generateFileThumbnail( newFile.path, 128, ); V2TimValueCallback createVideoMessageRes = await TencentImSDKPlugin.v2TIMManager .getMessageManager() .createVideoMessage( videoFilePath: entity.path, type: "mp4", duration: 0, snapshotPath: thumbImagePath ?? "", ); if (createVideoMessageRes.code == 0) { message = createVideoMessageRes.data?.messageInfo; id = createVideoMessageRes.data!.id!; } } // 消息设置 message?.isSelf = true; // message.conversation = conversation; message?.status = MessageStatus.V2TIM_MSG_STATUS_SENDING; print('组装完成,准备发送:${message?.toJson()}'); // String id = await FTIM.getMessageManager().sendMessage(message); // message.msgId = id; V2TimValueCallback sendMessageRes = await TencentImSDKPlugin.v2TIMManager .getMessageManager() .sendMessage( id: id, receiver: conversation.userID!, needReadReceipt: true, groupID: '', ); if (sendMessageRes.code == 0) { // 发送成功 _onNew1v1Message(sendMessageRes.data); } else { SCTts.show( 'create fail,code:${sendMessageRes.code},${sendMessageRes.desc}', ); } } } } notifyListeners(); } // 发送消息(房间) Future _sendRoomMessage(Msg msg) async { try { if (msg.type == SCRoomMsgType.luckGiftAnimOther) { _giftFxLog( 'send room msg start ' 'type=${msg.type} ' 'groupId=${msg.groupId} ' 'msg=${msg.msg} ' 'giftId=${msg.gift?.id} ' 'giftPhoto=${msg.gift?.giftPhoto}', ); } if (msg.type == SCRoomMsgType.roomSettingUpdate) { debugPrint( "[Room Cover Sync] send roomSettingUpdate groupId=${msg.groupId ?? ""} roomId=${msg.msg ?? ""}", ); } var user = msg.user?.copyWith(); var toUser = msg.toUser?.copyWith(); user?.cleanWearHonor(); user?.cleanWearBadge(); user?.cleanPhotos(); toUser?.cleanWearHonor(); toUser?.cleanWearBadge(); toUser?.cleanUseProps(); toUser?.cleanPhotos(); msg.needUpDataUserInfo = Provider.of( context!, listen: false, ).needUpDataUserInfo; msg.user = user; msg.toUser = toUser; final textMsg = await TencentImSDKPlugin.v2TIMManager .getMessageManager() .createCustomMessage(data: jsonEncode(msg.toJson())); if (msg.type == SCRoomMsgType.luckGiftAnimOther) { _giftFxLog( 'send room msg createCustomMessage ' 'type=${msg.type} ' 'code=${textMsg.code} ' 'id=${textMsg.data?.id} ' 'desc=${textMsg.desc}', ); } if (textMsg.code != 0) return; final sendResult = await TencentImSDKPlugin.v2TIMManager .getMessageManager() .sendMessage( id: textMsg.data!.id!, groupID: msg.groupId!, receiver: '', ); if (msg.type == SCRoomMsgType.luckGiftAnimOther) { _giftFxLog( 'send room msg result ' 'type=${msg.type} ' 'code=${sendResult.code} ' 'msgId=${sendResult.data?.msgID} ' 'groupId=${sendResult.data?.groupID} ' 'desc=${sendResult.desc}', ); } if (sendResult.code == 0) {} } catch (e) { throw Exception("create fail: $e"); } } /// 添加消息 void seedSystemRoomTips(String tips, {String groupId = ""}) { final normalizedTips = tips.trim(); if (normalizedTips.isEmpty) { return; } for (final msg in roomAllMsgList) { if (msg.type == SCRoomMsgType.systemTips && msg.msg == normalizedTips) { if ((msg.groupId ?? "").isEmpty && groupId.isNotEmpty) { msg.groupId = groupId; } return; } } addMsg( Msg( groupId: groupId, msg: normalizedTips, type: SCRoomMsgType.systemTips, ), ); } Future loadRoomHistoryMessages(String groupId, {int count = 20}) async { final normalizedGroupId = groupId.trim(); if (normalizedGroupId.isEmpty) { return; } try { final result = await TencentImSDKPlugin.v2TIMManager .getMessageManager() .getGroupHistoryMessageList( groupID: normalizedGroupId, count: count < 10 ? 10 : count, lastMsgID: null, ); final messages = result.data ?? const []; debugPrint( '[RoomHistory][IM] loaded group=$normalizedGroupId count=${messages.length}', ); for (final message in messages.reversed) { _addRoomHistoryMessage(normalizedGroupId, message); } } catch (error) { debugPrint( '[RoomHistory][IM] load failed group=$normalizedGroupId error=$error', ); } } void _addRoomHistoryMessage(String groupId, V2TimMessage message) { try { final customData = message.customElem?.data; if (customData == null || customData.trim().isEmpty) { return; } final decoded = json.decode(customData); if (decoded is! Map) { return; } final data = decoded.map((key, value) => MapEntry(key.toString(), value)); if (data['type'] == SCRoomMsgType.roomRedPacket) { final packetData = _broadcastPayloadMap(data['data']); if (packetData.isEmpty) { return; } if (!_isSameRoomRedPacketRegion(packetData)) { return; } RoomRedPacketPendingCache.instance.upsertPayload(packetData); addMsg(_buildRoomRedPacketMsg(groupId, packetData)); return; } final msg = Msg.fromJson(data); if ((msg.groupId ?? '').trim().isEmpty) { msg.groupId = groupId; } if (_shouldAddRoomHistoryMessage(msg)) { addMsg(msg); } } catch (error) { debugPrint( '[RoomHistory][IM] skip malformed msg=${message.msgID ?? ""} error=$error', ); } } bool _shouldAddRoomHistoryMessage(Msg msg) { switch (msg.type) { case SCRoomMsgType.text: case SCRoomMsgType.image: return true; case SCRoomMsgType.roomRedPacketClaim: return true; case SCRoomMsgType.emoticons: return (msg.number ?? -1) < 0; default: return false; } } addMsg(Msg msg) { final mergedGiftMsg = _mergeGiftMessageIfNeeded(msg); if (mergedGiftMsg != null) { msgAllListener?.call(mergedGiftMsg); msgGiftListener?.call(mergedGiftMsg); if (msg.type == SCRoomMsgType.gift) { msgFloatingGiftListener?.call(msg); } notifyListeners(); return; } if (msg.type == SCRoomMsgType.roomRedPacket) { _cacheRoomRedPacketMessage(msg); } if (msg.type == SCRoomMsgType.roomRedPacket && _hasRoomRedPacketMessage(msg)) { debugPrint( '[RoomRedPacket][Chat] skip duplicate packetId=${_roomRedPacketMessageKey(msg)}', ); return; } if (msg.type == SCRoomMsgType.roomRedPacketClaim && _hasRoomRedPacketClaimMessage(msg)) { debugPrint( '[RoomRedPacket][Claim] skip duplicate key=${_roomRedPacketClaimMessageKey(msg)}', ); return; } roomAllMsgList.insert(0, msg); if (roomAllMsgList.length > 250) { print('大于200条消息'); roomAllMsgList.removeAt(roomAllMsgList.length - 1); } msgAllListener?.call(msg); if (msg.type == SCRoomMsgType.text) { roomChatMsgList.insert(0, msg); if (roomChatMsgList.length > 250) { print('大于200条消息'); roomChatMsgList.removeAt(roomChatMsgList.length - 1); } msgChatListener?.call(msg); } else if (msg.type == SCRoomMsgType.image || (msg.type == SCRoomMsgType.emoticons && (msg.number ?? -1) < 0)) { roomChatMsgList.insert(0, msg); if (roomChatMsgList.length > 250) { print('大于200条消息'); roomChatMsgList.removeAt(roomChatMsgList.length - 1); } msgChatListener?.call(msg); } else if (msg.type == SCRoomMsgType.roomRedPacket) { roomChatMsgList.insert(0, msg); if (roomChatMsgList.length > 250) { print('大于200条消息'); roomChatMsgList.removeAt(roomChatMsgList.length - 1); } msgChatListener?.call(msg); } else if (msg.type == SCRoomMsgType.roomRedPacketClaim) { roomChatMsgList.insert(0, msg); if (roomChatMsgList.length > 250) { print('大于200条消息'); roomChatMsgList.removeAt(roomChatMsgList.length - 1); } msgChatListener?.call(msg); } else if (msg.type == SCRoomMsgType.gift || msg.type == SCRoomMsgType.luckGiftAnimOther) { roomGiftMsgList.insert(0, msg); if (roomGiftMsgList.length > 250) { print('大于200条消息'); roomGiftMsgList.removeAt(roomGiftMsgList.length - 1); } msgGiftListener?.call(msg); if (msg.type == SCRoomMsgType.gift) { msgFloatingGiftListener?.call(msg); } } } void _cacheRoomRedPacketMessage(Msg msg) { try { final decoded = jsonDecode(msg.msg ?? ''); if (decoded is Map) { RoomRedPacketPendingCache.instance.upsertPayload(decoded); } else if (decoded is Map) { RoomRedPacketPendingCache.instance.upsertPayload( decoded.map((key, value) => MapEntry(key.toString(), value)), ); } } catch (_) {} } Msg _buildRoomRedPacketMsg(String groupId, dynamic data) { final payload = data is Map ? data.map((key, value) => MapEntry(key.toString(), value)) : {}; final sender = SocialChatUserProfile( id: payload['actualAccount']?.toString() ?? payload['userId']?.toString(), userNickname: payload['userNickname']?.toString() ?? payload['userName']?.toString(), userAvatar: payload['userAvatar']?.toString(), ); return Msg( groupId: groupId, msg: jsonEncode(payload), type: SCRoomMsgType.roomRedPacket, user: sender, ); } bool _hasRoomRedPacketMessage(Msg msg) { final key = _roomRedPacketMessageKey(msg); if (key == null || key.isEmpty) { return false; } return roomAllMsgList.any( (item) => item.type == SCRoomMsgType.roomRedPacket && _roomRedPacketMessageKey(item) == key, ); } String? _roomRedPacketMessageKey(Msg msg) { if (msg.type != SCRoomMsgType.roomRedPacket) { return null; } try { final decoded = jsonDecode(msg.msg ?? ''); if (decoded is Map) { for (final key in const ['packetId', 'packetNo', 'id']) { final value = decoded[key]?.toString().trim(); if (value != null && value.isNotEmpty) { return value; } } } } catch (_) { return null; } return null; } bool _hasRoomRedPacketClaimMessage(Msg msg) { final key = _roomRedPacketClaimMessageKey(msg); if (key == null || key.isEmpty) { return false; } return roomAllMsgList.any( (item) => item.type == SCRoomMsgType.roomRedPacketClaim && _roomRedPacketClaimMessageKey(item) == key, ); } String? _roomRedPacketClaimMessageKey(Msg msg) { if (msg.type != SCRoomMsgType.roomRedPacketClaim) { return null; } final stableKey = (msg.giftBatchId ?? '').trim(); if (stableKey.isNotEmpty) { return stableKey; } final userId = (msg.user?.id ?? '').trim(); final amount = msg.number?.toString() ?? ''; final time = msg.time?.toString() ?? ''; if (userId.isEmpty || amount.isEmpty || time.isEmpty) { return null; } return ['legacy', userId, msg.msg ?? '', amount, time].join('|'); } Msg? _mergeGiftMessageIfNeeded(Msg incoming) { if (incoming.type != SCRoomMsgType.gift && incoming.type != SCRoomMsgType.luckGiftAnimOther) { return null; } final mergeTarget = _findMergeableGiftMessage(incoming); if (mergeTarget == null) { return null; } if (_shouldAccumulateGiftMessage(mergeTarget, incoming)) { mergeTarget.number = (mergeTarget.number ?? 0) + (incoming.number ?? 0); } else if ((mergeTarget.number ?? 0) <= 0) { mergeTarget.number = incoming.number; } mergeTarget.time = DateTime.now().millisecondsSinceEpoch; if ((incoming.msg ?? "").trim().isNotEmpty) { mergeTarget.msg = incoming.msg; } if ((mergeTarget.giftBatchId ?? "").trim().isEmpty && (incoming.giftBatchId ?? "").trim().isNotEmpty) { mergeTarget.giftBatchId = incoming.giftBatchId; } if ((mergeTarget.targetUserIds ?? const []).isEmpty && (incoming.targetUserIds ?? const []).isNotEmpty) { mergeTarget.targetUserIds = List.from(incoming.targetUserIds!); } _moveMessageToFront(roomGiftMsgList, mergeTarget); _moveMessageToFront(roomAllMsgList, mergeTarget); return mergeTarget; } Msg? _findMergeableGiftMessage(Msg incoming) { final now = DateTime.now().millisecondsSinceEpoch; for (final existing in roomGiftMsgList) { if ((existing.time ?? 0) <= 0 || now - (existing.time ?? 0) > _giftComboMergeWindowMs) { continue; } if (_isSameGiftComboMessage(existing, incoming)) { return existing; } } return null; } bool _isSameGiftComboMessage(Msg existing, Msg incoming) { if (existing.type != incoming.type || existing.groupId != incoming.groupId || existing.user?.id != incoming.user?.id || existing.gift?.id != incoming.gift?.id) { return false; } final existingBatchId = (existing.giftBatchId ?? "").trim(); final incomingBatchId = (incoming.giftBatchId ?? "").trim(); if (existingBatchId.isNotEmpty && existingBatchId == incomingBatchId) { return true; } if (existing.toUser?.id == incoming.toUser?.id) { return true; } final existingTargetKey = _giftTargetGroupKey(existing); final incomingTargetKey = _giftTargetGroupKey(incoming); return existingTargetKey.isNotEmpty && existingTargetKey == incomingTargetKey; } bool _shouldAccumulateGiftMessage(Msg existing, Msg incoming) { final existingBatchId = (existing.giftBatchId ?? "").trim(); final incomingBatchId = (incoming.giftBatchId ?? "").trim(); if (existingBatchId.isNotEmpty && existingBatchId == incomingBatchId) { return false; } final existingTargetKey = _giftTargetGroupKey(existing); final incomingTargetKey = _giftTargetGroupKey(incoming); if (existing.toUser?.id != incoming.toUser?.id && existingTargetKey.isNotEmpty && existingTargetKey == incomingTargetKey) { return false; } return true; } String _giftTargetGroupKey(Msg msg) { final targetUserIds = []; void addTargetUserId(String? userId) { final normalizedUserId = (userId ?? "").trim(); if (normalizedUserId.isEmpty || targetUserIds.contains(normalizedUserId)) { return; } targetUserIds.add(normalizedUserId); } for (final userId in msg.targetUserIds ?? const []) { addTargetUserId(userId); } addTargetUserId(msg.toUser?.id); if (targetUserIds.length <= 1) { return ""; } targetUserIds.sort(); return targetUserIds.join(","); } void _moveMessageToFront(List messages, Msg target) { final index = messages.indexOf(target); if (index <= 0) { return; } messages.removeAt(index); messages.insert(0, target); } bool _shouldHighlightLuckyGiftReward(SCBroadCastLuckGiftPush broadCastRes) { final rewardData = broadCastRes.data; if (rewardData == null) { return false; } if (rewardData.isBigReward) { return true; } return (rewardData.multiple ?? 0) >= 5; } bool _isLuckyGiftInCurrentRoom(SCBroadCastLuckGiftPush broadCastRes) { if (context == null) { return false; } final currentRoomId = Provider.of( context!, listen: false, ).currenRoom?.roomProfile?.roomProfile?.id ?? ''; final roomId = broadCastRes.data?.roomId ?? ''; return currentRoomId.isNotEmpty && roomId.isNotEmpty && currentRoomId == roomId; } SCFloatingMessage _buildLuckyGiftFloatingMessage( SCBroadCastLuckGiftPush broadCastRes, ) { final rewardData = broadCastRes.data; final resolvedGiftUrl = _resolveLuckyGiftGiftPhoto(rewardData); return SCFloatingMessage( type: 0, userId: rewardData?.sendUserId, roomId: rewardData?.roomId, toUserId: rewardData?.acceptUserId, userAvatarUrl: rewardData?.userAvatar, userName: rewardData?.nickname, toUserName: rewardData?.acceptNickname, giftUrl: resolvedGiftUrl, giftId: rewardData?.giftId, number: rewardData?.giftQuantity, coins: rewardData?.awardAmount, multiple: rewardData?.multiple, priority: 1000, ); } void _handleLuckyGiftGlobalNews( SCBroadCastLuckGiftPush broadCastRes, { required String source, }) { final rewardData = broadCastRes.data; if (rewardData == null) { return; } if (_isLuckyGiftInCurrentRoom(broadCastRes)) { addluckGiftPushQueue(broadCastRes); } if (!rewardData.shouldShowGlobalNews || (rewardData.multiple ?? 0) < _luckyGiftFloatMinMultiple) { return; } if (source == 'broadcast' && _isLuckyGiftInCurrentRoom(broadCastRes)) { _giftFxLog( 'skip global lucky gift overlay ' 'reason=current_room_already_receives_group_msg ' 'roomId=${rewardData.roomId} ' 'giftId=${rewardData.giftId}', ); return; } OverlayManager().addMessage(_buildLuckyGiftFloatingMessage(broadCastRes)); } void _handleRoomLuckyGiftMessage(SCBroadCastLuckGiftPush broadCastRes) { final rewardData = broadCastRes.data; if (rewardData == null) { return; } final resolvedGiftUrl = _resolveLuckyGiftGiftPhoto(rewardData); final roomMsg = Msg( groupId: '', msg: '', type: SCRoomMsgType.gameLuckyGift, ); roomMsg.gift = SocialChatGiftRes( id: rewardData.giftId, giftPhoto: resolvedGiftUrl, giftTab: 'LUCK', ); roomMsg.number = 0; roomMsg.awardAmount = rewardData.awardAmount; roomMsg.user = SocialChatUserProfile( id: rewardData.sendUserId, userNickname: rewardData.nickname, userAvatar: rewardData.userAvatar, ); roomMsg.toUser = SocialChatUserProfile( id: rewardData.acceptUserId, userNickname: rewardData.acceptNickname, ); addMsg(roomMsg); msgLuckyGiftRewardTickerListener?.call(roomMsg); if (_shouldHighlightLuckyGiftReward(broadCastRes)) { final highlightMsg = Msg( groupId: '', msg: '${rewardData.multiple ?? 0}', type: SCRoomMsgType.gameLuckyGift_5, ); highlightMsg.awardAmount = rewardData.awardAmount; highlightMsg.gift = SocialChatGiftRes( id: rewardData.giftId, giftPhoto: resolvedGiftUrl, giftTab: 'LUCK', ); highlightMsg.user = SocialChatUserProfile( id: rewardData.sendUserId, userNickname: rewardData.nickname, ); addMsg(highlightMsg); } addluckGiftPushQueue(broadCastRes); _handleLuckyGiftGlobalNews(broadCastRes, source: 'room_group'); if (rewardData.sendUserId == AccountStorage().getCurrentUser()?.userProfile?.id) { Provider.of( context!, listen: false, ).updateLuckyRewardAmount(roomMsg.awardAmount ?? 0); } } String _resolveLuckyGiftGiftPhoto(Data? rewardData) { final fallbackUrl = (rewardData?.giftCover ?? '').trim(); final giftId = (rewardData?.giftId ?? '').trim(); final currentContext = context; if (giftId.isEmpty || currentContext == null || !currentContext.mounted) { _giftFxLog( 'resolve lucky float gift photo skipped ' 'giftId=$giftId ' 'fallbackUrl=$fallbackUrl ' 'reason=${giftId.isEmpty ? "empty_gift_id" : "invalid_context"}', ); return fallbackUrl; } final appGeneralManager = Provider.of( currentContext, listen: false, ); final giftById = appGeneralManager.getGiftById(giftId); final giftByStandardId = appGeneralManager.getGiftByStandardId(giftId); final localGift = giftById ?? giftByStandardId; final localGiftPhoto = (localGift?.giftPhoto ?? '').trim(); _giftFxLog( 'resolve lucky float gift photo ' 'giftId=$giftId ' 'fallbackUrl=$fallbackUrl ' 'giftById.id=${giftById?.id} ' 'giftById.standardId=${giftById?.standardId} ' 'giftById.photo=${giftById?.giftPhoto} ' 'giftByStandardId.id=${giftByStandardId?.id} ' 'giftByStandardId.standardId=${giftByStandardId?.standardId} ' 'giftByStandardId.photo=${giftByStandardId?.giftPhoto} ' 'resolvedUrl=${localGiftPhoto.isNotEmpty ? localGiftPhoto : fallbackUrl}', ); if (localGiftPhoto.isNotEmpty) { return localGiftPhoto; } unawaited(appGeneralManager.giftList()); return fallbackUrl; } void _handleRegionGiftBroadcast(dynamic data) { final payload = _broadcastPayloadMap(data); if (payload.isEmpty) { debugPrint('[RoomRegionBroadcast][Gift] skip empty payload'); return; } if (!_isSameRegionBroadcastPayload(payload)) { final payloadRegion = _payloadText(payload['regionCode']); debugPrint( '[RoomRegionBroadcast][Gift] skip region mismatch ' 'payloadRegion=$payloadRegion currentRegion=$roomRedPacketBroadcastRegionCode', ); return; } final roomId = _payloadText(payload['roomId']); if (roomId.isEmpty) { debugPrint('[RoomRegionBroadcast][Gift] skip empty roomId'); return; } if (_isCurrentVoiceRoom(roomId)) { debugPrint( '[RoomRegionBroadcast][Gift] skip current room duplicate roomId=$roomId', ); return; } final giftId = _payloadText(payload['giftId']); final quantityValue = _payloadNum(payload['giftQuantity']) ?? _payloadNum(payload['quantity']) ?? _payloadNum(payload['giftCount']) ?? 1; final quantity = quantityValue <= 0 ? 1 : quantityValue; final giftCandy = _payloadNum(payload['giftCandy']) ?? 0; final actualAmount = _payloadNum(payload['actualAmount']); final amount = actualAmount != null && actualAmount > 0 ? actualAmount : giftCandy * quantity; final acceptUserIds = _payloadStringList(payload['acceptUserIds']); final toUserId = _firstNonBlank([ acceptUserIds.isNotEmpty ? acceptUserIds.first : '', _payloadText(payload['acceptUserId']), _payloadText(payload['acceptAccount']), _payloadText(payload['toUserId']), ]); final senderId = _firstNonBlank([ _payloadText(payload['actualAccount']), _payloadText(payload['sendUserId']), _payloadText(payload['userId']), _payloadText(payload['account']), ]); final senderName = _firstNonBlank([ _payloadText(payload['userNickname']), _payloadText(payload['sendUserName']), _payloadText(payload['nickname']), _payloadText(payload['userName']), ]); final receiverName = _firstNonBlank([ _payloadText(payload['acceptNickname']), _payloadText(payload['acceptUserNickname']), _payloadText(payload['toUserName']), ]); final giftUrl = _resolveRegionGiftPhoto(payload); final message = SCFloatingMessage( type: 1, roomId: roomId, userId: senderId, toUserId: toUserId, userName: senderName, toUserName: receiverName, userAvatarUrl: _payloadText(payload['userAvatar']), giftUrl: giftUrl, giftId: giftId, number: quantity, coins: amount, broadcastScope: SCFloatingMessage.broadcastScopeRegion, priority: 900, ); debugPrint( '[RoomRegionBroadcast][Gift] enqueue roomId=$roomId giftId=$giftId ' 'sender=$senderId amount=$amount quantity=$quantity giftUrl=$giftUrl', ); _enqueueGiftFloatingMessage( message, dedupKey: _giftFloatingDedupKey( roomId: roomId, senderId: senderId, receiverId: toUserId, giftId: giftId, quantity: quantity, ), ); } String _resolveRegionGiftPhoto(Map payload) { final giftId = _payloadText(payload['giftId']); final fallbackUrl = _firstNonBlank([ _payloadText(payload['giftPhoto']), _payloadText(payload['giftCover']), _payloadText(payload['giftUrl']), _payloadText(payload['giftIcon']), _payloadText(payload['giftImage']), _payloadText(payload['cover']), ]); final currentContext = context; if (giftId.isEmpty || currentContext == null || !currentContext.mounted) { return fallbackUrl; } final appGeneralManager = Provider.of( currentContext, listen: false, ); final localGift = appGeneralManager.getGiftByIdOrStandardId(giftId); final localGiftPhoto = (localGift?.giftPhoto ?? '').trim(); debugPrint( '[RoomRegionBroadcast][Gift] resolve gift photo giftId=$giftId ' 'localPhoto=$localGiftPhoto fallbackUrl=$fallbackUrl', ); if (localGiftPhoto.isNotEmpty) { return localGiftPhoto; } unawaited(appGeneralManager.giftList()); return fallbackUrl; } bool _isCurrentVoiceRoom(String roomId) { final currentRoomId = _currentVoiceRoomId(); return currentRoomId.isNotEmpty && currentRoomId == roomId.trim(); } bool _isCurrentVisibleVoiceRoom(String roomId) { final normalizedRoomId = roomId.trim(); final currentContext = context; if (normalizedRoomId.isEmpty || currentContext == null || !currentContext.mounted) { return false; } final rtcProvider = Provider.of( currentContext, listen: false, ); final currentRoomId = rtcProvider.currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? ''; return rtcProvider.shouldShowRoomVisualEffects && currentRoomId.isNotEmpty && currentRoomId == normalizedRoomId; } String _currentVoiceRoomId() { final currentContext = context; if (currentContext == null || !currentContext.mounted) { return ''; } return Provider.of( currentContext, listen: false, ).currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? ''; } String _currentVoiceRoomGroupId() { final currentContext = context; if (currentContext == null || !currentContext.mounted) { return ''; } return Provider.of( currentContext, listen: false, ).currenRoom?.roomProfile?.roomProfile?.roomAccount?.trim() ?? ''; } bool _shouldRouteUntrackedRoomRedPacketBroadcast( String groupId, V2TimMessage message, ) { if (groupId == _currentVoiceRoomGroupId()) { return false; } final customData = message.customElem?.data; if (customData == null || customData.isEmpty) { return false; } try { final decoded = json.decode(customData); final payload = _broadcastPayloadMap(decoded); return payload['type'] == SCRoomMsgType.roomRedPacket; } catch (_) { return false; } } Map _broadcastPayloadMap(dynamic data) { if (data is Map) { return data; } if (data is Map) { return data.map((key, value) => MapEntry(key.toString(), value)); } if (data is String && data.trim().isNotEmpty) { try { return _broadcastPayloadMap(json.decode(data)); } catch (_) { return {}; } } return {}; } String _payloadText(dynamic value) { if (value == null) { return ''; } return value.toString().trim(); } num? _payloadNum(dynamic value) { if (value is num) { return value; } if (value is String) { return num.tryParse(value.trim()); } return null; } List _payloadStringList(dynamic value) { if (value is Iterable) { return value .map((item) => _payloadText(item)) .where((item) => item.isNotEmpty) .toList(); } if (value is String) { final textValue = value.trim(); if (textValue.isEmpty) { return const []; } if (textValue.startsWith('[')) { try { final decoded = json.decode(textValue); if (decoded is Iterable) { return _payloadStringList(decoded); } } catch (_) {} } return textValue .split(',') .map((item) => item.trim()) .where((item) => item.isNotEmpty) .toList(); } return const []; } void _showCurrentRoomRedPacketDialog(dynamic payload) { final currentContext = context; if (currentContext == null || !currentContext.mounted) { return; } final payloadMap = _broadcastPayloadMap(payload); final payloadRoomId = _payloadText(payloadMap['roomId']); if (!_isCurrentVisibleVoiceRoom(payloadRoomId)) { return; } if (_isCurrentUserRoomRedPacketPayload(payload)) { return; } final dialogData = RoomRedPacketUiData.fromJson(payloadMap); if (!dialogData.hasPacketId || !dialogData.isOpenable || RoomRedPacketOpenDialog.isClaimedLocally(dialogData.packetId)) { return; } WidgetsBinding.instance.addPostFrameCallback((_) { if (!currentContext.mounted) { return; } RoomRedPacketOpenDialog.show(currentContext, dialogData); }); } bool _isCurrentUserRoomRedPacketPayload(dynamic payload) { final map = _broadcastPayloadMap(payload); final senderId = _firstNonBlank([ _payloadText(map['actualAccount']), _payloadText(map['userId']), _payloadText(map['sendUserId']), _payloadText(map['account']), ]); final currentUserId = AccountStorage().getCurrentUser()?.userProfile?.id?.trim() ?? ''; return senderId.isNotEmpty && currentUserId.isNotEmpty && senderId == currentUserId; } bool isLogout = false; logout() async { roomRedPacketBroadcastGroupId = null; roomRedPacketBroadcastRegionCode = null; RoomRedPacketOpenDialog.clearClaimedState(); RoomRedPacketPendingCache.instance.clear(); V2TimCallback logoutRes = await TencentImSDKPlugin.v2TIMManager.logout(); TencentImSDKPlugin.v2TIMManager .getMessageManager() .removeAdvancedMsgListener(); if (logoutRes.code == 0) { isLogout = true; } } ///全服广播消息 _newBroadCastMsgRecv( String groupID, V2TimMessage message, { bool forceRegionBroadcast = false, }) async { try { String? customData = message.customElem?.data; if (customData != null && customData.isNotEmpty) { final isRegionBroadcastGroup = forceRegionBroadcast || groupID == roomRedPacketBroadcastGroupId; final data = json.decode(customData); var type = data["type"]; if (type == "SYS_ACTIVITY") { if (onNewActivityMessageCurrentConversationListener != null) { var recode = Records.fromJson(data["data"]); onNewActivityMessageCurrentConversationListener?.call(recode); } else { activityUnReadCount = activityUnReadCount + 1; allUnReadCount = messageUnReadCount + notifcationUnReadCount + activityUnReadCount; notifyListeners(); } } else if (type == "SYS_ANNOUNCEMENT") { if (onNewNotifcationMessageCurrentConversationListener != null) { var recode = Records.fromJson(data["data"]); onNewNotifcationMessageCurrentConversationListener?.call(recode); } else { notifcationUnReadCount = notifcationUnReadCount + 1; allUnReadCount = messageUnReadCount + notifcationUnReadCount + activityUnReadCount; notifyListeners(); } } else if (type == "GAME_BAISHUN_WIN") { if (SCGlobalConfig.isReview) { ///审核状态不播放动画 return; } var fdata = data["data"]; var winCoins = fdata["currencyDiff"]; if (winCoins > 14999) { ///达到5000才飘屏 SCFloatingMessage msg = SCFloatingMessage( type: 2, userId: fdata["account"], userAvatarUrl: fdata["userAvatar"], userName: fdata["userNickname"], giftUrl: fdata["gameUrl"], roomId: fdata["roomId"], coins: fdata["currencyDiff"], ); OverlayManager().addMessage(msg); } } else if (type == "GAME_LUCKY_GIFT") { final broadCastRes = SCBroadCastLuckGiftPush.fromJson(data); _giftFxLog( 'recv GAME_LUCKY_GIFT broadcast ' 'giftId=${broadCastRes.data?.giftId} ' 'roomId=${broadCastRes.data?.roomId} ' 'sendUserId=${broadCastRes.data?.sendUserId} ' 'acceptUserId=${broadCastRes.data?.acceptUserId} ' 'giftQuantity=${broadCastRes.data?.giftQuantity} ' 'awardAmount=${broadCastRes.data?.awardAmount} ' 'multiple=${broadCastRes.data?.multiple} ' 'multipleType=${broadCastRes.data?.multipleType} ' 'globalNews=${broadCastRes.data?.globalNews}', ); _handleLuckyGiftGlobalNews(broadCastRes, source: 'broadcast'); } else if (type == "REGISTER_REWARD_GRANTED") { await DataPersistence.setPendingRegisterRewardDialog(true); await DataPersistence.clearAwaitRegisterRewardSocket(); eventBus.fire(RegisterRewardGrantedEvent(data: data["data"])); } else if (type == "ROCKET_ENERGY_LAUNCH") { ///火箭触发飘屏 var fdata = data["data"]; SCFloatingMessage msg = SCFloatingMessage( type: 3, roomId: fdata["roomId"], rocketLevel: fdata["fromLevel"], userAvatarUrl: fdata["userAvatar"], userName: fdata["nickname"], userId: fdata["actualAccount"], priority: 1000, ); OverlayManager().addMessage(msg); } else if (type == SCRoomMsgType.roomRedPacket) { ///红包触发飘屏 var fData = data["data"]; final payload = _broadcastPayloadMap(fData); final packetId = _payloadText(payload['packetId']); final packetRoomId = _payloadText(payload["roomId"]); debugPrint( '[RoomRedPacket][IM] recv broadcast packetId=$packetId ' 'roomId=$packetRoomId group=$groupID ' 'regionGroup=$roomRedPacketBroadcastGroupId ' 'isRegion=$isRegionBroadcastGroup ' 'payloadRegion=${_payloadText(payload['regionCode'])} ' 'currentRegion=${roomRedPacketBroadcastRegionCode ?? ''} ' 'currentRoom=${_currentVoiceRoomId()}', ); if (!_isSameRoomRedPacketRegion(fData)) { debugPrint( '[RoomRedPacket][IM] skip broadcast region mismatch ' 'packetId=$packetId ' 'payloadRegion=${_payloadText(payload['regionCode'])} ' 'currentRegion=${roomRedPacketBroadcastRegionCode ?? ''}', ); return; } RoomRedPacketPendingCache.instance.upsertPayload(fData); SCFloatingMessage msg = SCFloatingMessage( type: 4, roomId: fData["roomId"], userAvatarUrl: fData["userAvatar"], userName: fData["userNickname"], userId: fData["actualAccount"], toUserId: fData["packetId"], coins: fData["totalAmount"], number: fData["totalCount"], broadcastScope: isRegionBroadcastGroup ? SCFloatingMessage.broadcastScopeRegion : '', priority: 1000, ); if (_isCurrentVisibleVoiceRoom(packetRoomId)) { final rtcProvider = Provider.of( context!, listen: false, ); rtcProvider.loadRoomRedPacketList(1); addMsg( _buildRoomRedPacketMsg( rtcProvider.currenRoom?.roomProfile?.roomProfile?.roomAccount ?? "", fData, ), ); _showCurrentRoomRedPacketDialog(fData); } OverlayManager().addMessage(msg); } else if (type == SCRoomMsgType.sendGift) { if (isRegionBroadcastGroup) { _handleRegionGiftBroadcast(data["data"]); } } else if (type == SCRoomMsgType.inviteRoom) { ///邀请进入房间 var fdata = data["data"]; SCFloatingMessage msg = SCFloatingMessage.fromJson(fdata); if (msg.toUserId == AccountStorage().getCurrentUser()?.userProfile?.id && msg.roomId != Provider.of( context!, listen: false, ).currenRoom?.roomProfile?.roomProfile?.id) { SmartDialog.dismiss(tag: "showInviteRoom"); SmartDialog.show( tag: "showInviteRoom", alignment: Alignment.center, animationType: SmartAnimationType.fade, builder: (_) { return InviteRoomDialog(msg); }, ); } } } } catch (e) {} } _newGroupMsg(String groupID, V2TimMessage message) { if (groupID != Provider.of( context!, listen: false, ).currenRoom?.roomProfile?.roomProfile?.roomAccount) { return; } try { String? customData = message.customElem?.data; if (customData != null && customData.isNotEmpty) { // 直接处理字符串格式的自定义数据 final data = json.decode(customData); debugPrint(">>>>>>>>>>>>>>>>>>>消息类型${data["type"]}"); if (data["type"] == SCRoomMsgType.roomRedPacket) { ///房间红包 var fData = data["data"]; if (!_isSameRoomRedPacketRegion(fData)) { return; } RoomRedPacketPendingCache.instance.upsertPayload(fData); SCFloatingMessage msg = SCFloatingMessage( type: 4, roomId: fData["roomId"], userAvatarUrl: fData["userAvatar"], userName: fData["userNickname"], userId: fData["actualAccount"], toUserId: fData["packetId"], coins: fData["totalAmount"], number: fData["totalCount"], priority: 1000, ); final packetRoomId = _payloadText(fData["roomId"]); if (packetRoomId.isNotEmpty && packetRoomId != _currentVoiceRoomId()) { return; } final isCurrentVisibleRoom = _isCurrentVisibleVoiceRoom(packetRoomId); Provider.of( context!, listen: false, ).loadRoomRedPacketList(1); addMsg(_buildRoomRedPacketMsg(groupID, fData)); if (isCurrentVisibleRoom) { _showCurrentRoomRedPacketDialog(fData); OverlayManager().addMessage(msg); } return; } Msg msg = Msg.fromJson(data); if (msg.type == SCRoomMsgType.sendGift || msg.type == SCRoomMsgType.gameBurstCrystalSprint || msg.type == SCRoomMsgType.gameBurstCrystalBox) { ///这个消息暂时不监听 return; } if (msg.type == SCRoomMsgType.bsm) { if (msg.toUser?.id == AccountStorage().getCurrentUser()?.userProfile?.id) { SmartDialog.show( tag: "showConfirmDialog", alignment: Alignment.center, debounce: true, animationType: SmartAnimationType.fade, builder: (_) { return MsgDialog( title: SCAppLocalizations.of(context!)!.tips, msg: SCAppLocalizations.of( context!, )!.invitesYouToTheMicrophone(msg.msg ?? ""), btnText: SCAppLocalizations.of(context!)!.confirm, onEnsure: () { ///上麦 num index = Provider.of( context!, listen: false, ).findWheat(); if (index > -1) { Provider.of( context!, listen: false, ).shangMai( index, eventType: "INVITE", inviterId: msg.role, ); } }, ); }, ); } return; } if (msg.type == SCRoomMsgType.killXiaMai) { ///踢下麦 final rtcProvider = Provider.of( context!, listen: false, ); if (msg.msg == AccountStorage().getCurrentUser()?.userProfile?.id) { unawaited( rtcProvider.handleSelfMicRemovedByRemote(refreshMicList: false), ); } rtcProvider.retrieveMicrophoneList(notifyIfUnchanged: false); return; } if (msg.type == SCRoomMsgType.roomSettingUpdate) { debugPrint( "[Room Cover Sync] recv roomSettingUpdate groupId=$groupID roomId=${msg.msg ?? ""}", ); Provider.of( context!, listen: false, ).loadRoomInfo(msg.msg ?? ""); return; } if (msg.type == SCRoomMsgType.roomBGUpdate) { SCRoomThemeListRes res; if ((msg.msg ?? "").isNotEmpty) { res = SCRoomThemeListRes.fromJson(jsonDecode(msg.msg!)); } else { res = SCRoomThemeListRes(); } Provider.of( context!, listen: false, ).updateRoomBG(res); return; } if (msg.type == SCRoomMsgType.emoticons) { if ((msg.number ?? -1) >= 0) { Provider.of( context!, listen: false, ).starPlayEmoji(msg); return; } } if (msg.type == SCRoomMsgType.micChange) { Provider.of( context!, listen: false, ).micChange(BroadCastMicChangePush.fromJson(data).data?.mics); } else if (msg.type == SCRoomMsgType.roomMusic) { Provider.of( context!, listen: false, ).handleRoomMusicPublishingMessage(msg); } else if (msg.type == SCRoomMsgType.shangMai || msg.type == SCRoomMsgType.xiaMai || msg.type == SCRoomMsgType.fengMai || msg.type == SCRoomMsgType.jieFeng) { Provider.of( context!, listen: false, ).retrieveMicrophoneList(notifyIfUnchanged: false); } else if (msg.type == SCRoomMsgType.refreshOnlineUser) { Provider.of( context!, listen: false, ).fetchOnlineUsersList(notifyIfUnchanged: false); } else if (msg.type == SCRoomMsgType.gameLuckyGift) { var broadCastRes = SCBroadCastLuckGiftPush.fromJson(data); _giftFxLog( 'recv GAME_LUCKY_GIFT ' 'giftId=${broadCastRes.data?.giftId} ' 'roomId=${broadCastRes.data?.roomId} ' 'sendUserId=${broadCastRes.data?.sendUserId} ' 'acceptUserId=${broadCastRes.data?.acceptUserId} ' 'giftQuantity=${broadCastRes.data?.giftQuantity} ' 'awardAmount=${broadCastRes.data?.awardAmount} ' 'multiple=${broadCastRes.data?.multiple} ' 'multipleType=${broadCastRes.data?.multipleType} ' 'globalNews=${broadCastRes.data?.globalNews}', ); _handleRoomLuckyGiftMessage(broadCastRes); } else { if (msg.type == SCRoomMsgType.joinRoom) { final rtcProvider = Provider.of( context!, listen: false, ); final shouldShowRoomVisualEffects = rtcProvider.shouldShowRoomVisualEffects; if (msg.user != null) { Provider.of( context!, listen: false, ).addOnlineUser(msg.groupId ?? "", msg.user!); } rtcProvider.republishCurrentUserRoomMusicStateIfNeeded(); if (msgUserJoinListener != null) { msgUserJoinListener!(msg); } ///坐骑 final entryResource = msg.user?.getMountains(); if (entryResource != null) { if (SCGlobalConfig.allowsHighCostAnimations && SCGlobalConfig.isEntryVehicleAnimation && shouldShowRoomVisualEffects) { SCGiftVapSvgaManager().play( entryResource.sourceUrl ?? "", priority: SCGiftVapSvgaManager.entryEffectPriority, type: SCGiftVapSvgaManager.entryEffectType, ); } else if (SCGlobalConfig.isLowPerformanceDevice && shouldShowRoomVisualEffects) { final roomId = rtcProvider.currenRoom?.roomProfile?.roomProfile?.id ?? ""; final coverUrl = _resolveEntryCompensationCover(entryResource); OverlayManager().addLowPerformanceCompensationMessage( SCFloatingMessage( type: 5, priority: 950, userId: msg.user?.id ?? "", roomId: roomId, userAvatarUrl: msg.user?.userAvatar ?? "", userName: msg.user?.userNickname ?? "", giftUrl: coverUrl, fallbackUrl: coverUrl, ), dedupKey: _entryFloatingDedupKey( roomId: roomId, userId: msg.user?.id, resourceId: entryResource.id, ), ); } } } else if (msg.type == SCRoomMsgType.gift) { final gift = msg.gift; if (gift == null) { _giftFxLog( 'recv gift msg skipped reason=no_gift ' 'fromUserId=${msg.user?.id} ' 'toUserId=${msg.toUser?.id} ' 'quantity=${msg.number}', ); } else { final rtcProvider = Provider.of( context!, listen: false, ); final special = gift.special ?? ""; final giftSourceUrl = gift.giftSourceUrl ?? ""; final hasSource = giftSourceUrl.isNotEmpty; final hasAnimation = scGiftHasAnimationSpecial(special); final hasGlobalGift = special.contains( SCGiftType.GLOBAL_GIFT.name, ); final hasFullScreenEffect = scGiftHasFullScreenEffect(special); _giftFxLog( 'recv gift msg ' 'fromUserId=${msg.user?.id} ' 'fromUserName=${msg.user?.userNickname} ' 'toUserId=${msg.toUser?.id} ' 'toUserName=${msg.toUser?.userNickname} ' 'giftId=${gift.id} ' 'giftName=${gift.giftName} ' 'giftSourceUrl=$giftSourceUrl ' 'special=$special ' 'hasSource=$hasSource ' 'hasAnimation=$hasAnimation ' 'hasGlobalGift=$hasGlobalGift ' 'hasFullScreenEffect=$hasFullScreenEffect ' 'effectsEnabled=${SCGlobalConfig.isGiftSpecialEffects}', ); if (giftSourceUrl.isNotEmpty && special.isNotEmpty) { if (scGiftHasFullScreenEffect(special)) { if (SCGlobalConfig.allowsHighCostAnimations && SCGlobalConfig.isGiftSpecialEffects && rtcProvider.shouldShowRoomVisualEffects) { _giftFxLog( 'trigger player play path=$giftSourceUrl ' 'giftId=${gift.id} giftName=${gift.giftName}', ); SCGiftVapSvgaManager().play(giftSourceUrl); } else if (SCGlobalConfig.isLowPerformanceDevice && rtcProvider.shouldShowRoomVisualEffects) { final roomId = (msg.msg?.trim().isNotEmpty ?? false) ? msg.msg ?? "" : rtcProvider .currenRoom ?.roomProfile ?.roomProfile ?.id ?? ""; final quantity = msg.number ?? 0; final coins = quantity * (gift.giftCandy ?? 0); _enqueueGiftFloatingMessage( SCFloatingMessage( type: 1, priority: 900, userId: msg.user?.id ?? "", toUserId: msg.toUser?.id ?? "", userAvatarUrl: msg.user?.userAvatar ?? "", userName: msg.user?.userNickname ?? "", toUserName: msg.toUser?.userNickname ?? "", toUserAvatarUrl: msg.toUser?.userAvatar ?? "", giftUrl: gift.giftPhoto, giftId: gift.id, number: quantity, coins: coins, roomId: roomId, ), dedupKey: _giftFloatingDedupKey( roomId: roomId, senderId: msg.user?.id, receiverId: msg.toUser?.id, giftId: gift.id, quantity: quantity, ), ); } else { _giftFxLog( 'skip player play because visual effects disabled ' 'giftId=${gift.id} ' 'isGiftSpecialEffects=${SCGlobalConfig.isGiftSpecialEffects} ' 'roomVisible=${rtcProvider.shouldShowRoomVisualEffects}', ); } } else { _giftFxLog( 'skip player play because special does not include ' '${SCGiftType.ANIMSCION.name}/$kSCGiftAnimationSpecialAlias/${SCGiftType.GLOBAL_GIFT.name} ' 'giftId=${gift.id} special=${gift.special}', ); } } else { _giftFxLog( 'skip player play because giftSourceUrl or special is empty ' 'giftId=${gift.id} ' 'giftSourceUrl=${gift.giftSourceUrl} ' 'special=${gift.special}', ); } if (rtcProvider .currenRoom ?.roomProfile ?.roomSetting ?.showHeartbeat ?? false) { debouncer.debounce( duration: Duration(milliseconds: 350), onDebounce: () { rtcProvider.requestGiftTriggeredMicRefresh(); }, ); } final coins = (msg.number ?? 0) * (gift.giftCandy ?? 0); if (coins > 9999) { _enqueueGiftFloatingMessage( SCFloatingMessage( type: 1, userId: msg.user?.id ?? "", toUserId: msg.toUser?.id ?? "", userAvatarUrl: msg.user?.userAvatar ?? "", userName: msg.user?.userNickname ?? "", toUserName: msg.toUser?.userNickname ?? "", toUserAvatarUrl: msg.toUser?.userAvatar ?? "", giftUrl: gift.giftPhoto, giftId: gift.id, number: msg.number, coins: coins, roomId: msg.msg, ), dedupKey: _giftFloatingDedupKey( roomId: msg.msg, senderId: msg.user?.id, receiverId: msg.toUser?.id, giftId: gift.id, quantity: msg.number, ), ); } } } else if (msg.type == SCRoomMsgType.luckGiftAnimOther) { final hideLGiftAnimal = Provider.of( context!, listen: false, ).hideLGiftAnimal; if (hideLGiftAnimal) { _giftFxLog( 'recv LUCK_GIFT_ANIM_OTHER skipped ' 'reason=hideLGiftAnimal ' 'giftPhoto=${msg.gift?.giftPhoto}', ); } else { final targetUserIds = (jsonDecode(msg.msg ?? "") as List) .map((e) => e as String) .toList(); _giftFxLog( 'recv LUCK_GIFT_ANIM_OTHER ' 'giftPhoto=${msg.gift?.giftPhoto} ' 'sendUserId=${msg.user?.id} ' 'toUserId=${msg.toUser?.id} ' 'quantity=${msg.number} ' 'targetUserIds=${targetUserIds.join(",")}', ); eventBus.fire( GiveRoomLuckWithOtherEvent( msg.gift?.giftPhoto ?? "", targetUserIds, ), ); if (msg.user != null && msg.toUser != null && msg.gift != null) { _giftFxLog( 'trigger floating gift listener from LUCK_GIFT_ANIM_OTHER ' 'sendUserId=${msg.user?.id} ' 'toUserId=${msg.toUser?.id} ' 'quantity=${msg.number} ' 'giftId=${msg.gift?.id}', ); msgFloatingGiftListener?.call(msg); } else { _giftFxLog( 'skip floating gift listener from LUCK_GIFT_ANIM_OTHER ' 'reason=incomplete_msg ' 'sendUserId=${msg.user?.id} ' 'toUserId=${msg.toUser?.id} ' 'giftId=${msg.gift?.id} ' 'quantity=${msg.number}', ); } } } else if (msg.type == SCRoomMsgType.roomRoleChange) { ///房间身份变动 Provider.of( context!, listen: false, ).retrieveMicrophoneList(); if (msg.toUser?.id == AccountStorage().getCurrentUser()?.userProfile?.id) { Provider.of( context!, listen: false, ).currenRoom?.entrants?.setRoles(msg.msg); if (msg.msg == SCRoomRolesType.TOURIST.name && !(Provider.of( context!, listen: false, ).currenRoom?.roomProfile?.roomSetting?.touristMike ?? false)) { ///如果变成了游客,房间又是禁止游客上麦,需要下麦 num index = Provider.of( context!, listen: false, ).userOnMaiInIndex( AccountStorage().getCurrentUser()?.userProfile?.id ?? "", ); if (index > -1) { Provider.of( context!, listen: false, ).xiaMai(index); } } } } else if (msg.type == SCRoomMsgType.roomDice) { if ((msg.number ?? -1) > -1) { Provider.of( context!, listen: false, ).starPlayEmoji(msg); } } else if (msg.type == SCRoomMsgType.roomRPS) { if ((msg.number ?? -1) > -1) { Provider.of( context!, listen: false, ).starPlayEmoji(msg); } } addMsg(msg); } } } catch (e) { throw Exception("message parser fail: $e"); } } ///加入全服广播群 joinBigBroadcastGroup() async { bool joined = false; while (!isLogout && !joined) { await Future.delayed(Duration(milliseconds: 550)); try { var joinResult = await TencentImSDKPlugin.v2TIMManager.joinGroup( groupID: SCGlobalConfig.bigBroadcastGroup, message: "", ); if (joinResult.code == 0 || joinResult.code == 10013) { joined = true; } } catch (e) { //print('timm 登录异常:${e.toString()}'); SCTts.show('broadcastGroup join fail:${e.toString()}'); } } } ///发送全服消息 sendBigBroadcastGroup(BigBroadcastGroupMessage msg) async { try { final textMsg = await TencentImSDKPlugin.v2TIMManager .getMessageManager() .createCustomMessage(data: jsonEncode(msg.toJson())); if (textMsg.code != 0) return; final sendResult = await TencentImSDKPlugin.v2TIMManager .getMessageManager() .sendMessage( id: textMsg.data!.id!, groupID: SCGlobalConfig.bigBroadcastGroup, receiver: '', ); if (sendResult.code == 0) {} } catch (e) { throw Exception("create fail: $e"); } } Future sendRoomRedPacketBroadcast( Map payload, { String roomGroupId = '', }) async { if (payload.isEmpty) { return; } final targetGroupIds = {}; final normalizedRoomGroupId = roomGroupId.trim(); if (normalizedRoomGroupId.isNotEmpty) { targetGroupIds.add(normalizedRoomGroupId); } var regionGroupId = roomRedPacketBroadcastGroupId?.trim() ?? ''; if (regionGroupId.isEmpty) { await syncRoomRedPacketBroadcastGroup(); regionGroupId = roomRedPacketBroadcastGroupId?.trim() ?? ''; } if (regionGroupId.isNotEmpty) { targetGroupIds.add(regionGroupId); } if (targetGroupIds.isEmpty) { debugPrint( '[RoomRedPacket][IM] skip send broadcast because target groups are empty', ); return; } debugPrint( '[RoomRedPacket][IM] send broadcast packetId=${payload['packetId']} ' 'roomId=${payload['roomId']} mode=${payload['packetMode']} ' 'targets=${targetGroupIds.join(',')} regionGroup=$regionGroupId ' 'region=${payload['regionCode'] ?? roomRedPacketBroadcastRegionCode ?? ''}', ); final message = { 'type': SCRoomMsgType.roomRedPacket, 'data': payload, }; final data = jsonEncode(message); for (final groupId in targetGroupIds) { await _sendRoomRedPacketCustomMessage(groupId, data, payload); } } Future _sendRoomRedPacketCustomMessage( String groupId, String data, Map payload, ) async { try { final textMsg = await TencentImSDKPlugin.v2TIMManager .getMessageManager() .createCustomMessage(data: data); if (textMsg.code != 0 || textMsg.data?.id == null) { debugPrint( '[RoomRedPacket][IM] create message failed ' 'group=$groupId code=${textMsg.code} desc=${textMsg.desc}', ); return; } final sendResult = await TencentImSDKPlugin.v2TIMManager .getMessageManager() .sendMessage(id: textMsg.data!.id!, groupID: groupId, receiver: ''); debugPrint( '[RoomRedPacket][IM] send message packetId=${payload['packetId']} ' 'group=$groupId code=${sendResult.code} desc=${sendResult.desc}', ); } catch (error) { debugPrint( '[RoomRedPacket][IM] send message failed group=$groupId error=$error', ); } } Future quitGroup(String groupID) async { if (groupID.isEmpty) { return; } try { await TencentImSDKPlugin.v2TIMManager.quitGroup(groupID: groupID); } finally { await deleteConversationById("group_$groupID"); } } bool _isSameRoomRedPacketRegion(dynamic payload) { return _isSameRegionBroadcastPayload(payload); } bool _isSameRegionBroadcastPayload(dynamic payload) { final map = _broadcastPayloadMap(payload); final packetRegion = map['regionCode']?.toString().trim(); final currentRegion = roomRedPacketBroadcastRegionCode?.trim(); if (packetRegion == null || packetRegion.isEmpty || currentRegion == null || currentRegion.isEmpty) { return true; } return packetRegion.toUpperCase() == currentRegion.toUpperCase(); } ///清屏 void clearMessage() { roomAllMsgList.clear(); roomChatMsgList.clear(); roomGiftMsgList.clear(); msgChatListener?.call(Msg(groupId: "-1000", msg: "", type: "")); msgAllListener?.call(Msg(groupId: "-1000", msg: "", type: "")); msgGiftListener?.call(Msg(groupId: "-1000", msg: "", type: "")); notifyListeners(); } cleanRoomData() { roomAllMsgList.clear(); roomGiftMsgList.clear(); roomChatMsgList.clear(); _resetLuckGiftBurstQueue(); onNewMessageListenerGroupMap.forEach((k, v) { v = null; }); onNewMessageListenerGroupMap.clear(); } void addluckGiftPushQueue(SCBroadCastLuckGiftPush broadCastRes) { if (SCGlobalConfig.isLuckGiftSpecialEffects) { if (!shouldPlayLuckyGiftBurst(broadCastRes.data)) { return; } final now = DateTime.now().millisecondsSinceEpoch; _pruneRecentLuckGiftPushEvents(now); final eventKey = _luckyGiftPushEventKey(broadCastRes); if (_hasSeenLuckGiftPushEvent(eventKey, now)) { return; } final mergeKey = _luckyGiftPushMergeKey(broadCastRes); final mergeTarget = _findMergeableLuckGiftPushEntry(mergeKey, now); if (mergeTarget != null) { mergeTarget.merge(broadCastRes, eventKey); _rememberLuckGiftPushEvent(eventKey, now); _giftFxLog( 'merge lucky gift burst ' 'mergeKey=$mergeKey ' 'awardAmount=${mergeTarget.reward.data?.awardAmount} ' 'giftQuantity=${mergeTarget.reward.data?.giftQuantity} ' 'multiple=${mergeTarget.reward.data?.multiple} ' 'eventCount=${mergeTarget.eventKeys.length}', ); _scheduleLuckGiftBackCoins(restart: true); return; } while (_luckGiftPushQueue.length >= _maxLuckGiftPushQueueLength) { _luckGiftPushQueue.removeFirst(); } _luckGiftPushQueue.add( _LuckGiftPushQueueEntry( reward: broadCastRes, eventKey: eventKey, mergeKey: mergeKey, updatedAt: now, ), ); _rememberLuckGiftPushEvent(eventKey, now); _scheduleLuckGiftBackCoins(); } } static bool shouldPlayLuckyGiftBurst(Data? rewardData) { if (rewardData == null) { return false; } final awardAmount = rewardData.awardAmount ?? 0; final multiple = rewardData.multiple ?? 0; return awardAmount > _luckyGiftBurstMinAwardAmount || multiple >= _luckyGiftBurstMinMultiple; } void cleanLuckGiftBackCoins() { _resetLuckGiftBurstQueue(); } void _resetLuckGiftBurstQueue() { _luckGiftPushQueue.clear(); currentPlayingLuckGift = null; _currentLuckGiftPushKey = null; _currentLuckGiftPushEventKeys.clear(); _luckGiftPushPlayTimer?.cancel(); _luckGiftPushPlayTimer = null; _luckGiftPushPlaybackToken++; _recentLuckGiftPushEventTimes.clear(); } void _scheduleLuckGiftBackCoins({bool restart = false}) { if (currentPlayingLuckGift != null || _luckGiftPushQueue.isEmpty) { return; } if (restart) { _luckGiftPushPlayTimer?.cancel(); _luckGiftPushPlayTimer = null; } if (_luckGiftPushPlayTimer?.isActive ?? false) { return; } _luckGiftPushPlayTimer = Timer( Duration(milliseconds: _luckyGiftBurstMergeDelayMs), () { _luckGiftPushPlayTimer = null; playLuckGiftBackCoins(); }, ); } void playLuckGiftBackCoins() { if (currentPlayingLuckGift != null || _luckGiftPushQueue.isEmpty) { return; } _luckGiftPushPlayTimer?.cancel(); _luckGiftPushPlayTimer = null; final queueEntry = _luckGiftPushQueue.removeFirst(); currentPlayingLuckGift = queueEntry.reward; _currentLuckGiftPushKey = queueEntry.eventKey; _currentLuckGiftPushEventKeys = Set.from(queueEntry.eventKeys); final playbackToken = ++_luckGiftPushPlaybackToken; notifyListeners(); Future.delayed( Duration(milliseconds: _luckyGiftBurstPlaybackWatchdogMs), () { completeLuckGiftBackCoins(playbackToken); }, ); } void completeLuckGiftBackCoins(int playbackToken) { if (playbackToken != _luckGiftPushPlaybackToken || currentPlayingLuckGift == null) { return; } currentPlayingLuckGift = null; _currentLuckGiftPushKey = null; _currentLuckGiftPushEventKeys.clear(); notifyListeners(); _scheduleLuckGiftBackCoins(); } bool _hasSeenLuckGiftPushEvent(String eventKey, int now) { if (eventKey.isEmpty) { return false; } if (_currentLuckGiftPushKey == eventKey || _currentLuckGiftPushEventKeys.contains(eventKey)) { return true; } for (final queued in _luckGiftPushQueue) { if (queued.eventKeys.contains(eventKey)) { return true; } } final seenAt = _recentLuckGiftPushEventTimes[eventKey]; return seenAt != null && now - seenAt <= _luckyGiftBurstRecentEventTtlMs; } void _rememberLuckGiftPushEvent(String eventKey, int now) { if (eventKey.isEmpty) { return; } _recentLuckGiftPushEventTimes[eventKey] = now; } void _pruneRecentLuckGiftPushEvents(int now) { _recentLuckGiftPushEventTimes.removeWhere( (_, seenAt) => now - seenAt > _luckyGiftBurstRecentEventTtlMs, ); } _LuckGiftPushQueueEntry? _findMergeableLuckGiftPushEntry( String mergeKey, int now, ) { if (mergeKey.isEmpty) { return null; } final queuedEntries = _luckGiftPushQueue.toList(growable: false); for (final queued in queuedEntries.reversed) { if (queued.mergeKey == mergeKey && now - queued.updatedAt <= _luckyGiftBurstMergeWindowMs) { return queued; } } return null; } String _luckyGiftPushEventKey(SCBroadCastLuckGiftPush broadCastRes) { final rewardData = broadCastRes.data; if (rewardData == null) { return ''; } return '${rewardData.roomId ?? ""}|' '${rewardData.giftId ?? ""}|' '${rewardData.sendUserId ?? ""}|' '${rewardData.acceptUserId ?? ""}|' '${rewardData.giftQuantity ?? 0}|' '${rewardData.awardAmount ?? 0}|' '${rewardData.multiple ?? 0}|' '${rewardData.normalizedMultipleType}|' '${rewardData.balance ?? 0}'; } String _luckyGiftPushMergeKey(SCBroadCastLuckGiftPush broadCastRes) { final rewardData = broadCastRes.data; if (rewardData == null) { return ''; } return '${rewardData.roomId ?? ""}|' '${rewardData.giftId ?? ""}|' '${rewardData.sendUserId ?? ""}|' '${rewardData.acceptUserId ?? ""}'; } void updateNotificationCount(int count) { notifcationUnReadCount = 0; allUnReadCount = messageUnReadCount + notifcationUnReadCount + activityUnReadCount; notifyListeners(); } void updateActivityCount(int count) { activityUnReadCount = 0; allUnReadCount = messageUnReadCount + notifcationUnReadCount + activityUnReadCount; notifyListeners(); } void updateSystemCount(int count) { for (final conversation in conversationMap.values) { if (_isSystemConversation(conversation)) { conversation.unreadCount = 0; } } systemUnReadCount = 0; notifyListeners(); } void updateCustomerCount(int count) { conversationMap["c2c_${customerInfo?.id}"]?.unreadCount = 0; customerUnReadCount = 0; notifyListeners(); } Future deleteConversationById( String conversationID, { bool needShowToast = false, }) async { if (conversationID.isEmpty) { return; } V2TimCallback deleteConversationRes = await TencentImSDKPlugin.v2TIMManager .getConversationManager() .deleteConversation(conversationID: conversationID); if (deleteConversationRes.code == 0 && needShowToast && context != null) { SCTts.show(SCAppLocalizations.of(context!)!.operationSuccessful); } await initConversation(); } void clearC2CHistoryMessage(String conversationID, bool needShowToast) async { await deleteConversationById(conversationID, needShowToast: needShowToast); } }