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/foundation.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/room_rocket_launch_broadcast_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/sc_room_rocket_api_res.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/rocket/room_rocket_api_mapper.dart'; import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_reward_dialog_loader.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 _PendingRoomRocketRewardPopup { const _PendingRoomRocketRewardPopup({ required this.roomId, required this.receivedAt, this.initialRoomRecords = const [], }); final String roomId; final int receivedAt; final List initialRoomRecords; } class RealTimeMessagingManager extends ChangeNotifier { static const String _roomRocketRewardDialogTag = 'showRoomRocketRewardDialog'; 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; static const int _roomRocketLaunchRecentTtlMs = 90000; static const int _roomRocketLaunchLooseRecentTtlMs = 12000; static const int _roomRocketRegionBroadcastRecentTtlMs = 90000; static const int _roomRocketRewardPendingTtlMs = 120000; static const int _roomRocketRewardProbeTtlMs = 120000; static const int _roomRocketLaunchCountdownRetainMs = 10000; static const int _roomRocketLaunchEffectConsumedTtlMs = 120000; static const Duration _createRoomGroupTimeout = Duration(seconds: 12); static const List _roomRocketRewardPopupRetryDelays = [ Duration(milliseconds: 700), Duration(seconds: 2), Duration(seconds: 5), ]; static const Duration _roomRocketRewardAfterLaunchDelay = Duration( milliseconds: 350, ); static const List _roomRocketRoomReadyRewardRetryDelays = [ Duration(milliseconds: 300), Duration(milliseconds: 900), Duration(seconds: 2), Duration(seconds: 4), ]; BuildContext? context; void _giftFxLog(String 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 = {}; final Map _recentRoomRocketLaunchTimes = {}; final Map _recentRoomRocketLaunchLooseTimes = {}; final Map _recentRoomRocketRegionBroadcastTimes = {}; final _pendingRoomRocketRewardPopups = {}; final Map _recentRoomRocketRewardProbeTimes = {}; final Map _roomRocketLaunchCountdownDueTimes = {}; final Map _roomRocketLaunchCountdownKeysByRoom = {}; final _pendingRoomRocketCountdownLaunches = {}; final Map _roomRocketLaunchEffectTimers = {}; final Map _consumedRoomRocketLaunchEffectTimes = {}; final Set _hiddenCurrentRoomRocketLaunchKeys = {}; final Map _roomRocketRewardHoldUntilTimes = {}; final Map _roomRocketRewardHoldTimers = {}; Timer? _roomRocketRewardPopupRetryTimer; Timer? _roomRocketRoomReadyRewardRetryTimer; 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; 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) { 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 ?? "", ); if (res.code == 0) { isLogout = false; // 登录成功逻辑 logined = true; await initConversation(); unawaited(syncRoomRedPacketBroadcastGroup()); } else { // 登录失败逻辑 SCTts.show('tim login fail'); } } else { SCTts.show('tim login fail'); } } catch (e) { 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)) { _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, ) .timeout( _createRoomGroupTimeout, onTimeout: () => V2TimValueCallback( code: -10001, desc: 'Create IM group timeout', ), ); } ///加入房间的im群聊 Future joinRoomGroup(String groupID, String message) async { _resetLuckGiftBurstQueue(); var joinResult = await TencentImSDKPlugin.v2TIMManager.joinGroup( groupID: groupID, message: message, ); if (joinResult.code == 0 || joinResult.code == 10013) { onNewMessageListenerGroupMap[groupID] = _newGroupMsg; } return joinResult; } Future syncRoomRedPacketBroadcastGroup() async { for (var attempt = 0; attempt < 3 && !isLogout; attempt++) { try { final group = await SCChatRoomRepository().roomRedPacketImGroup(); if (!group.canJoin) { return; } final newGroupId = group.groupId.trim(); if (newGroupId == roomRedPacketBroadcastGroupId) { roomRedPacketBroadcastRegionCode = group.regionCode.trim(); 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)); } return; } } catch (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) { // 处理发送失败的情况 } } ///发送单聊文本消息 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; // 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) {} 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 []; if (!_isCurrentVoiceRoomGroup(normalizedGroupId)) { return; } for (final message in messages.reversed) { _addRoomHistoryMessage(normalizedGroupId, message); } } catch (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) {} } bool _shouldAddRoomHistoryMessage(Msg msg) { switch (msg.type) { case SCRoomMsgType.text: case SCRoomMsgType.image: return true; case SCRoomMsgType.roomRedPacketClaim: return true; case SCRoomMsgType.rocketRewardUser: case SCRoomMsgType.voiceRoomRocketRewardUser: return true; case SCRoomMsgType.rocketLaunchNotice: return true; case SCRoomMsgType.emoticons: return (msg.number ?? -1) < 0; default: return false; } } addMsg(Msg msg) { _scheduleRoomRocketStatusRefreshForGiftMessage(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)) { return; } if (msg.type == SCRoomMsgType.roomRedPacketClaim && _hasRoomRedPacketClaimMessage(msg)) { return; } if (SCRoomMsgType.isRocketRewardUserType(msg.type) && _hasRoomRocketRewardMessage(msg)) { return; } if (msg.type == SCRoomMsgType.rocketLaunchNotice && _hasRoomRocketNoticeMessage(msg)) { return; } roomAllMsgList.insert(0, msg); if (roomAllMsgList.length > 250) { roomAllMsgList.removeAt(roomAllMsgList.length - 1); } msgAllListener?.call(msg); if (msg.type == SCRoomMsgType.text) { roomChatMsgList.insert(0, msg); if (roomChatMsgList.length > 250) { 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) { roomChatMsgList.removeAt(roomChatMsgList.length - 1); } msgChatListener?.call(msg); } else if (msg.type == SCRoomMsgType.roomRedPacket) { roomChatMsgList.insert(0, msg); if (roomChatMsgList.length > 250) { roomChatMsgList.removeAt(roomChatMsgList.length - 1); } msgChatListener?.call(msg); } else if (msg.type == SCRoomMsgType.roomRedPacketClaim) { roomChatMsgList.insert(0, msg); if (roomChatMsgList.length > 250) { roomChatMsgList.removeAt(roomChatMsgList.length - 1); } msgChatListener?.call(msg); } else if (SCRoomMsgType.isRocketRewardUserType(msg.type)) { roomChatMsgList.insert(0, msg); if (roomChatMsgList.length > 250) { roomChatMsgList.removeAt(roomChatMsgList.length - 1); } msgChatListener?.call(msg); } else if (msg.type == SCRoomMsgType.rocketLaunchNotice) { roomChatMsgList.insert(0, msg); if (roomChatMsgList.length > 250) { 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) { roomGiftMsgList.removeAt(roomGiftMsgList.length - 1); } msgGiftListener?.call(msg); if (msg.type == SCRoomMsgType.gift) { msgFloatingGiftListener?.call(msg); } } } void _scheduleRoomRocketStatusRefreshForGiftMessage(Msg msg) { if (msg.type != SCRoomMsgType.gift && msg.type != SCRoomMsgType.luckGiftAnimOther) { return; } final currentContext = context; if (currentContext == null || !currentContext.mounted) { return; } final rtcProvider = Provider.of( currentContext, listen: false, ); final currentRoomId = (rtcProvider.currenRoom?.roomProfile?.roomProfile?.id ?? '').trim(); if (currentRoomId.isEmpty) { return; } final messageRoomId = msg.type == SCRoomMsgType.gift ? msg.msg?.trim() : ''; if (messageRoomId != null && messageRoomId.isNotEmpty && messageRoomId != currentRoomId) { return; } rtcProvider.scheduleRoomRocketStatusRefreshForGift(roomId: currentRoomId); } 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('|'); } bool _hasRoomRocketRewardMessage(Msg msg) { final key = _roomRocketRewardMessageKey(msg); if (key.isEmpty) { return false; } return roomAllMsgList.any( (item) => SCRoomMsgType.isRocketRewardUserType(item.type) && _roomRocketRewardMessageKey(item) == key, ); } bool _hasRoomRocketNoticeMessage(Msg msg) { final key = (msg.giftBatchId ?? '').trim(); if (key.isEmpty) { return false; } return roomAllMsgList.any( (item) => item.type == SCRoomMsgType.rocketLaunchNotice && (item.giftBatchId ?? '').trim() == key, ); } String _roomRocketRewardMessageKey(Msg msg) { final stableKey = (msg.giftBatchId ?? '').trim(); if (stableKey.isNotEmpty) { return stableKey; } return [ msg.groupId ?? '', msg.user?.id ?? '', msg.number?.toString() ?? '', msg.role ?? '', msg.msg ?? '', ].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) { return; } if (!_isSameRegionBroadcastPayload(payload)) { final payloadRegion = _payloadText(payload['regionCode']); return; } final roomId = _payloadText(payload['roomId']); if (roomId.isEmpty) { return; } if (_isCurrentVoiceRoom(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, ); _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(); 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 && rtcProvider.isVoiceRoomRouteVisible && currentRoomId.isNotEmpty && currentRoomId == normalizedRoomId; } bool _isCurrentHiddenVoiceRoom(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() ?? ''; if (currentRoomId.isEmpty || currentRoomId != normalizedRoomId) { return false; } return !rtcProvider.shouldShowRoomVisualEffects || !rtcProvider.isVoiceRoomRouteVisible; } 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 _isCurrentVoiceRoomGroup(String groupId) { final currentGroupId = _currentVoiceRoomGroupId(); return currentGroupId.isNotEmpty && currentGroupId == groupId.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(); } String _payloadAssetText(dynamic value) { if (value == null) { return ''; } if (value is Map) { final data = _broadcastPayloadMap(value); return _firstNonBlank([ _payloadAssetText(data['sourceUrl']), _payloadAssetText(data['url']), _payloadAssetText(data['resourceUrl']), _payloadAssetText(data['fileUrl']), _payloadAssetText(data['src']), _payloadAssetText(data['path']), _payloadAssetText(data['cover']), _payloadAssetText(data['imageUrl']), _payloadAssetText(data['svgUrl']), _payloadAssetText(data['svg']), _payloadAssetText(data['value']), ]); } if (value is Iterable && value is! String) { return _firstNonBlank(value.map(_payloadAssetText)); } final text = _payloadText(value); if (text == '{}' || text == '[]' || text.toLowerCase() == 'null') { return ''; } return text; } num? _payloadNum(dynamic value) { if (value is num) { return value; } if (value is String) { return num.tryParse(value.trim()); } return null; } void _handleVoiceRoomRocketStatusUpdate(dynamic payload) { final data = _broadcastPayloadMap(payload); final roomId = _payloadRoomId(data); if (!_isCurrentVoiceRoom(roomId)) { return; } final currentContext = context; if (currentContext == null || !currentContext.mounted) { return; } final rtcProvider = Provider.of( currentContext, listen: false, ); final isCurrentVisibleRoom = rtcProvider.shouldShowRoomVisualEffects && rtcProvider.isVoiceRoomRouteVisible; final launch = _buildRoomRocketLaunchFromStatusPayload( rtcProvider: rtcProvider, payload: data, roomId: roomId, ); if (_shouldHandleRoomRocketLaunchFromStatusPayload( rtcProvider: rtcProvider, payload: data, launch: launch, )) { if (isCurrentVisibleRoom) { _handleCurrentVisibleRoomRocketLaunch( rtcProvider: rtcProvider, launch: launch, publishRegionBroadcast: true, ); } else { _rememberRoomRocketLaunchCountdown( launch, suppressReplayAfterDue: true, ); _publishRoomRocketRegionBroadcastIfNeeded(launch); } } unawaited(rtcProvider.refreshRoomRocketStatus(roomId: roomId)); } void _handleVoiceRoomRocketLaunchBroadcast( dynamic payload, { bool isRegionBroadcast = false, }) { final launch = RoomRocketLaunchBroadcastMessage.fromJson( _broadcastPayloadMap(payload), ); if (!launch.isValid) { return; } var isCurrentRoom = false; var isCurrentVisibleRoom = false; final currentContext = context; if (currentContext != null && currentContext.mounted) { final rtcProvider = Provider.of( currentContext, listen: false, ); final currentRoomId = (rtcProvider.currenRoom?.roomProfile?.roomProfile?.id ?? '').trim(); if (currentRoomId == launch.roomId) { isCurrentRoom = true; isCurrentVisibleRoom = rtcProvider.shouldShowRoomVisualEffects && rtcProvider.isVoiceRoomRouteVisible; if (isCurrentVisibleRoom) { _handleCurrentVisibleRoomRocketLaunch( rtcProvider: rtcProvider, launch: launch, ); } else { _rememberRoomRocketLaunchCountdown( launch, suppressReplayAfterDue: true, ); } } } if (!isCurrentRoom) { _rememberRoomRocketLaunchCountdown(launch); } if (!isCurrentVisibleRoom && !isCurrentRoom) { _markRoomRocketRewardProbe(launch.roomId); } if (isCurrentRoom && !isCurrentVisibleRoom && !isRegionBroadcast) { return; } OverlayManager().addMessage( _buildRoomRocketLaunchFloatingMessage( launch, isRegionBroadcast: isRegionBroadcast, ), ); } void _handleVoiceRoomRocketRewardPopup(dynamic payload) { final data = _broadcastPayloadMap(payload); if (!_isCurrentUserInRoomRocketRewardPopup(data)) { return; } final roomId = _payloadRoomId(data); final currentRoomId = _currentVoiceRoomId(); final resolvedRoomId = roomId.isEmpty ? currentRoomId : roomId; if (resolvedRoomId.isEmpty) { return; } final initialRoomRecords = _roomRocketRewardRecordsFromPayload(data); if (currentRoomId.isNotEmpty && resolvedRoomId != currentRoomId) { _cachePendingRoomRocketRewardPopup( resolvedRoomId, initialRoomRecords: initialRoomRecords, ); return; } if (!_isCurrentVoiceRoom(resolvedRoomId)) { _cachePendingRoomRocketRewardPopup( resolvedRoomId, initialRoomRecords: initialRoomRecords, ); return; } if (!_isCurrentVisibleVoiceRoom(resolvedRoomId)) { _cachePendingRoomRocketRewardPopup( resolvedRoomId, initialRoomRecords: initialRoomRecords, ); return; } if (_isRoomRocketRewardPopupHeld(resolvedRoomId)) { _scheduleRoomRocketRewardPopupAfterHold( resolvedRoomId, initialRoomRecords: initialRoomRecords, ); return; } _pendingRoomRocketRewardPopups.remove(resolvedRoomId); _recentRoomRocketRewardProbeTimes.remove(resolvedRoomId); unawaited( _showRoomRocketRewardPopupDialog( resolvedRoomId, initialRoomRecords: initialRoomRecords, ), ); } bool _isCurrentUserInRoomRocketRewardPopup(Map payload) { final userIds = _roomRocketRewardPopupUserIds(payload); if (userIds == null) { return true; } if (userIds.isEmpty) { return false; } final profile = AccountStorage().getCurrentUser()?.userProfile; final currentKeys = { profile?.id?.trim() ?? '', profile?.account?.trim() ?? '', }..removeWhere((item) => item.isEmpty); if (currentKeys.isEmpty) { return false; } return currentKeys.any(userIds.contains); } Set? _roomRocketRewardPopupUserIds(Map payload) { for (final key in const [ 'userIds', 'user_ids', 'targetUserIds', 'target_user_ids', 'winnerUserIds', 'winner_user_ids', 'userId', 'user_id', 'winnerUserId', 'winner_user_id', 'rewardUserId', 'reward_user_id', 'receiverUserId', 'receiver_user_id', 'targetUserId', 'target_user_id', 'toUserId', 'to_user_id', ]) { if (payload.containsKey(key)) { return _payloadStringSet(payload[key]); } } return null; } Set _payloadStringSet(dynamic value) { if (value == null) { return {}; } if (value is Iterable && value is! String) { return value .map((item) => item?.toString().trim() ?? '') .where((item) => item.isNotEmpty) .toSet(); } if (value is Map) { return value.values .map((item) => item?.toString().trim() ?? '') .where((item) => item.isNotEmpty) .toSet(); } final raw = value.toString().trim(); if (raw.isEmpty) { return {}; } if (raw.startsWith('[') || raw.startsWith('{')) { try { return _payloadStringSet(jsonDecode(raw)); } catch (_) { return {}; } } return raw .split(',') .map((item) => item.trim()) .where((item) => item.isNotEmpty) .toSet(); } void handleVoiceRoomReadyForRocketRewards(String roomId, {int attempt = 0}) { final normalizedRoomId = roomId.trim(); if (normalizedRoomId.isEmpty) { return; } final now = DateTime.now().millisecondsSinceEpoch; _pruneRoomRocketRewardFallbacks(now); final pending = _pendingRoomRocketRewardPopups[normalizedRoomId]; final hasProbe = _recentRoomRocketRewardProbeTimes[normalizedRoomId] != null; final hasLaunch = _pendingRoomRocketCountdownLaunches[normalizedRoomId] != null; if (pending == null && !hasProbe && !hasLaunch) { return; } if (!_isCurrentVisibleVoiceRoom(normalizedRoomId)) { _scheduleRoomRocketReadyRewardRetry(normalizedRoomId, attempt); return; } final resumedLaunch = _resumeRoomRocketLaunchForCurrentRoom( normalizedRoomId, ); final pendingAfterResume = _pendingRoomRocketRewardPopups[normalizedRoomId]; final hasProbeAfterResume = _recentRoomRocketRewardProbeTimes[normalizedRoomId] != null; if (pendingAfterResume == null && !hasProbeAfterResume) { return; } if (!resumedLaunch && pendingAfterResume == null && attempt == 0) { _scheduleRoomRocketReadyRewardRetry(normalizedRoomId, attempt); return; } _pendingRoomRocketRewardPopups.remove(normalizedRoomId); _recentRoomRocketRewardProbeTimes.remove(normalizedRoomId); _roomRocketRoomReadyRewardRetryTimer?.cancel(); _roomRocketRoomReadyRewardRetryTimer = null; if (_isRoomRocketRewardPopupHeld(normalizedRoomId)) { _scheduleRoomRocketRewardPopupAfterHold( normalizedRoomId, initialRoomRecords: pending?.initialRoomRecords ?? const [], ); return; } unawaited( _showRoomRocketRewardPopupDialog( normalizedRoomId, initialRoomRecords: pendingAfterResume?.initialRoomRecords ?? const [], ), ); } void _scheduleRoomRocketReadyRewardRetry(String roomId, int attempt) { if (attempt >= _roomRocketRoomReadyRewardRetryDelays.length) { return; } _roomRocketRoomReadyRewardRetryTimer?.cancel(); _roomRocketRoomReadyRewardRetryTimer = Timer( _roomRocketRoomReadyRewardRetryDelays[attempt], () { _roomRocketRoomReadyRewardRetryTimer = null; handleVoiceRoomReadyForRocketRewards(roomId, attempt: attempt + 1); }, ); } void _cachePendingRoomRocketRewardPopup( String roomId, { List initialRoomRecords = const [], }) { final normalizedRoomId = roomId.trim(); if (normalizedRoomId.isEmpty) { return; } final now = DateTime.now().millisecondsSinceEpoch; _pruneRoomRocketRewardFallbacks(now); final existing = _pendingRoomRocketRewardPopups[normalizedRoomId]; final records = initialRoomRecords.isNotEmpty ? List.unmodifiable(initialRoomRecords) : existing?.initialRoomRecords ?? const []; _pendingRoomRocketRewardPopups[normalizedRoomId] = _PendingRoomRocketRewardPopup( roomId: normalizedRoomId, receivedAt: now, initialRoomRecords: records, ); } void _markRoomRocketRewardProbe(String roomId) { final normalizedRoomId = roomId.trim(); if (normalizedRoomId.isEmpty) { return; } final now = DateTime.now().millisecondsSinceEpoch; _pruneRoomRocketRewardFallbacks(now); _recentRoomRocketRewardProbeTimes[normalizedRoomId] = now; } void _holdRoomRocketRewardPopupUntilLaunchEnds( RoomRocketLaunchBroadcastMessage launch, ) { if (!launch.isValid) { return; } final normalizedRoomId = launch.roomId.trim(); if (normalizedRoomId.isEmpty) { return; } final now = DateTime.now().millisecondsSinceEpoch; final holdUntil = now + launch.displayDuration.inMilliseconds + _roomRocketRewardAfterLaunchDelay.inMilliseconds; final existing = _roomRocketRewardHoldUntilTimes[normalizedRoomId] ?? 0; if (holdUntil > existing) { _roomRocketRewardHoldUntilTimes[normalizedRoomId] = holdUntil; } } bool _isRoomRocketRewardPopupHeld(String roomId) { final normalizedRoomId = roomId.trim(); if (normalizedRoomId.isEmpty) { return false; } final now = DateTime.now().millisecondsSinceEpoch; final holdUntil = _roomRocketRewardHoldUntilTimes[normalizedRoomId] ?? 0; if (holdUntil > now) { return true; } if (holdUntil > 0) { _roomRocketRewardHoldUntilTimes.remove(normalizedRoomId); } return _pendingRoomRocketCountdownLaunches.containsKey(normalizedRoomId); } int _roomRocketRewardHoldUntil(String roomId) { final normalizedRoomId = roomId.trim(); if (normalizedRoomId.isEmpty) { return 0; } final now = DateTime.now().millisecondsSinceEpoch; var holdUntil = _roomRocketRewardHoldUntilTimes[normalizedRoomId] ?? 0; final launch = _pendingRoomRocketCountdownLaunches[normalizedRoomId]; if (launch != null) { final key = _roomRocketLaunchCountdownKeysByRoom[normalizedRoomId]; final launchDueAt = key == null ? now : _roomRocketLaunchCountdownDueTimes[key] ?? now; final countdownHoldUntil = launchDueAt + launch.displayDuration.inMilliseconds + _roomRocketRewardAfterLaunchDelay.inMilliseconds; if (countdownHoldUntil > holdUntil) { holdUntil = countdownHoldUntil; } } return holdUntil; } void _scheduleRoomRocketRewardPopupAfterHold( String roomId, { List initialRoomRecords = const [], }) { final normalizedRoomId = roomId.trim(); if (normalizedRoomId.isEmpty) { return; } _cachePendingRoomRocketRewardPopup( normalizedRoomId, initialRoomRecords: initialRoomRecords, ); final now = DateTime.now().millisecondsSinceEpoch; final holdUntil = _roomRocketRewardHoldUntil(normalizedRoomId); final delayMs = holdUntil <= now ? 120 : holdUntil - now; _roomRocketRewardHoldTimers[normalizedRoomId]?.cancel(); _roomRocketRewardHoldTimers[normalizedRoomId] = Timer( Duration(milliseconds: delayMs), () { _roomRocketRewardHoldTimers.remove(normalizedRoomId); if (_isRoomRocketRewardPopupHeld(normalizedRoomId)) { _scheduleRoomRocketRewardPopupAfterHold(normalizedRoomId); return; } if (!_isCurrentVisibleVoiceRoom(normalizedRoomId)) { _scheduleRoomRocketReadyRewardRetry(normalizedRoomId, 0); return; } final pending = _pendingRoomRocketRewardPopups.remove(normalizedRoomId); _recentRoomRocketRewardProbeTimes.remove(normalizedRoomId); unawaited( _showRoomRocketRewardPopupDialog( normalizedRoomId, initialRoomRecords: pending?.initialRoomRecords ?? const [], ), ); }, ); _roomRocketRewardDebug( 'defer popup roomId=$normalizedRoomId delayMs=$delayMs ' 'initialRoomRecords=${initialRoomRecords.length}', ); } void _pruneRoomRocketRewardFallbacks(int now) { _pendingRoomRocketRewardPopups.removeWhere( (_, pending) => now - pending.receivedAt > _roomRocketRewardPendingTtlMs, ); _recentRoomRocketRewardProbeTimes.removeWhere( (_, timestamp) => now - timestamp > _roomRocketRewardProbeTtlMs, ); } void _clearRoomRocketRewardFallbackState() { _pendingRoomRocketRewardPopups.clear(); _recentRoomRocketRewardProbeTimes.clear(); _consumedRoomRocketLaunchEffectTimes.clear(); _hiddenCurrentRoomRocketLaunchKeys.clear(); _roomRocketRoomReadyRewardRetryTimer?.cancel(); _roomRocketRoomReadyRewardRetryTimer = null; for (final timer in _roomRocketRewardHoldTimers.values) { timer.cancel(); } _roomRocketRewardHoldTimers.clear(); _roomRocketRewardHoldUntilTimes.clear(); _clearRoomRocketLaunchCountdownState(); } void _rememberRoomRocketLaunchCountdown( RoomRocketLaunchBroadcastMessage launch, { bool suppressReplayAfterDue = false, }) { if (!launch.isValid || _hasConsumedRoomRocketLaunchEffect(launch)) { return; } final now = DateTime.now().millisecondsSinceEpoch; _pruneRoomRocketLaunchCountdowns(now); final key = _roomRocketLaunchCountdownKey(launch); final existingDueAt = _roomRocketLaunchCountdownDueTimes[key]; final dueAt = existingDueAt != null && existingDueAt > now ? existingDueAt : now + _roomRocketLaunchCountdownDelay(launch).inMilliseconds; _roomRocketLaunchCountdownDueTimes[key] = dueAt; _roomRocketLaunchCountdownKeysByRoom[launch.roomId] = key; _pendingRoomRocketCountdownLaunches[launch.roomId] = launch; if (suppressReplayAfterDue) { _hiddenCurrentRoomRocketLaunchKeys.add(key); } } void _scheduleRoomRocketLaunchEffectsAfterCountdown( RoomRocketLaunchBroadcastMessage launch, ) { if (!launch.isValid || _hasConsumedRoomRocketLaunchEffect(launch)) { return; } _rememberRoomRocketLaunchCountdown(launch); final key = _roomRocketLaunchCountdownKey(launch); final delay = _roomRocketLaunchCountdownRemaining(launch.roomId); final dueAt = _roomRocketLaunchCountdownDueTimes[key] ?? DateTime.now().millisecondsSinceEpoch; _roomRocketLaunchEffectTimers[key]?.cancel(); _roomRocketLaunchEffectTimers[key] = Timer(delay, () { _roomRocketLaunchEffectTimers.remove(key); if (_roomRocketLaunchCountdownDueTimes[key] != dueAt) { return; } _roomRocketLaunchCountdownDueTimes.remove(key); if (_roomRocketLaunchCountdownKeysByRoom[launch.roomId] == key) { _roomRocketLaunchCountdownKeysByRoom.remove(launch.roomId); } final pendingLaunch = _pendingRoomRocketCountdownLaunches.remove(launch.roomId) ?? launch; _playRoomRocketLaunchEffectsAndReward(pendingLaunch); }); } void _playRoomRocketLaunchEffectsAndReward( RoomRocketLaunchBroadcastMessage launch, ) { if (_hasConsumedRoomRocketLaunchEffect(launch)) { _clearRoomRocketLaunchCountdownForRoom(launch.roomId); return; } _clearRoomRocketLaunchCountdownForRoom(launch.roomId); if (!_isCurrentVisibleVoiceRoom(launch.roomId)) { _consumeRoomRocketLaunchWithoutEffects(launch); return; } final currentContext = context; if (currentContext == null || !currentContext.mounted) { _consumeRoomRocketLaunchWithoutEffects(launch); return; } final rtcProvider = Provider.of( currentContext, listen: false, ); final shouldFetchReward = _pendingRoomRocketRewardPopups.containsKey(launch.roomId) || _recentRoomRocketRewardProbeTimes.containsKey(launch.roomId); final pending = _pendingRoomRocketRewardPopups.remove(launch.roomId); _recentRoomRocketRewardProbeTimes.remove(launch.roomId); _markRoomRocketLaunchEffectConsumed(launch); _holdRoomRocketRewardPopupUntilLaunchEnds(launch); rtcProvider.handleRoomRocketLaunchBroadcast(launch); if (shouldFetchReward) { _scheduleRoomRocketRewardPopupAfterHold( launch.roomId, initialRoomRecords: pending?.initialRoomRecords ?? const [], ); } } bool _resumeRoomRocketLaunchForCurrentRoom(String roomId) { final normalizedRoomId = roomId.trim(); final launch = _pendingRoomRocketCountdownLaunches[normalizedRoomId]; if (launch == null || !_isCurrentVisibleVoiceRoom(normalizedRoomId)) { return false; } if (_hasConsumedRoomRocketLaunchEffect(launch)) { _clearRoomRocketLaunchCountdownForRoom(normalizedRoomId); return false; } final currentContext = context; if (currentContext == null || !currentContext.mounted) { return false; } final rtcProvider = Provider.of( currentContext, listen: false, ); final remaining = _roomRocketLaunchCountdownRemaining(normalizedRoomId); final launchKey = _roomRocketLaunchCountdownKey(launch); if (remaining > Duration.zero) { _hiddenCurrentRoomRocketLaunchKeys.remove(launchKey); _addRoomRocketLaunchNoticeIfNeeded( rtcProvider: rtcProvider, launch: launch, secondsOverride: _roomRocketRemainingSeconds(remaining), ); _scheduleRoomRocketLaunchEffectsAfterCountdown(launch); } else { if (_hiddenCurrentRoomRocketLaunchKeys.remove(launchKey)) { _consumeRoomRocketLaunchWithoutEffects(launch); return false; } _playRoomRocketLaunchEffectsAndReward(launch); } return true; } int _roomRocketRemainingSeconds(Duration duration) { final seconds = (duration.inMilliseconds / 1000).ceil(); return seconds.clamp(1, 60).toInt(); } Duration _roomRocketLaunchCountdownRemaining(String roomId) { final normalizedRoomId = roomId.trim(); if (normalizedRoomId.isEmpty) { return Duration.zero; } final now = DateTime.now().millisecondsSinceEpoch; _pruneRoomRocketLaunchCountdowns(now); final key = _roomRocketLaunchCountdownKeysByRoom[normalizedRoomId]; final dueAt = key == null ? null : _roomRocketLaunchCountdownDueTimes[key]; if (dueAt == null || dueAt <= now) { return Duration.zero; } return Duration(milliseconds: dueAt - now); } Duration _roomRocketLaunchCountdownDelay( RoomRocketLaunchBroadcastMessage launch, ) { final seconds = launch.durationSeconds <= 0 ? 10 : launch.durationSeconds; return Duration(seconds: seconds.clamp(0, 60).toInt()); } String _roomRocketLaunchCountdownKey( RoomRocketLaunchBroadcastMessage launch, ) { final launchKey = _roomRocketLaunchNoticeKey(launch); if (launchKey.isNotEmpty) { return launchKey; } return 'rocket_launch_countdown|${launch.roomId}'; } void _pruneRoomRocketLaunchCountdowns(int now) { final expiredKeys = {}; _roomRocketLaunchCountdownDueTimes.removeWhere((key, dueAt) { final expired = now - dueAt > _roomRocketLaunchCountdownRetainMs; if (expired) { expiredKeys.add(key); } return expired; }); if (expiredKeys.isEmpty) { return; } _roomRocketLaunchCountdownKeysByRoom.removeWhere( (_, key) => expiredKeys.contains(key), ); _pendingRoomRocketCountdownLaunches.removeWhere( (roomId, _) => !_roomRocketLaunchCountdownKeysByRoom.containsKey(roomId), ); } void _clearRoomRocketLaunchCountdownState() { _roomRocketLaunchCountdownDueTimes.clear(); _roomRocketLaunchCountdownKeysByRoom.clear(); _pendingRoomRocketCountdownLaunches.clear(); _hiddenCurrentRoomRocketLaunchKeys.clear(); for (final timer in _roomRocketLaunchEffectTimers.values) { timer.cancel(); } _roomRocketLaunchEffectTimers.clear(); } void _clearRoomRocketLaunchCountdownForRoom(String roomId) { final normalizedRoomId = roomId.trim(); final key = _roomRocketLaunchCountdownKeysByRoom.remove(normalizedRoomId); if (key != null) { _roomRocketLaunchCountdownDueTimes.remove(key); _roomRocketLaunchEffectTimers.remove(key)?.cancel(); _hiddenCurrentRoomRocketLaunchKeys.remove(key); } _pendingRoomRocketCountdownLaunches.remove(normalizedRoomId); } bool _hasConsumedRoomRocketLaunchEffect( RoomRocketLaunchBroadcastMessage launch, ) { final key = _roomRocketLaunchCountdownKey(launch); if (key.isEmpty) { return false; } _pruneConsumedRoomRocketLaunchEffects( DateTime.now().millisecondsSinceEpoch, ); return _consumedRoomRocketLaunchEffectTimes.containsKey(key); } void _markRoomRocketLaunchEffectConsumed( RoomRocketLaunchBroadcastMessage launch, ) { final key = _roomRocketLaunchCountdownKey(launch); if (key.isEmpty) { return; } _consumedRoomRocketLaunchEffectTimes[key] = DateTime.now().millisecondsSinceEpoch; _hiddenCurrentRoomRocketLaunchKeys.remove(key); } void _consumeRoomRocketLaunchWithoutEffects( RoomRocketLaunchBroadcastMessage launch, ) { _clearRoomRocketLaunchCountdownForRoom(launch.roomId); _pendingRoomRocketRewardPopups.remove(launch.roomId); _recentRoomRocketRewardProbeTimes.remove(launch.roomId); _markRoomRocketLaunchEffectConsumed(launch); } void _pruneConsumedRoomRocketLaunchEffects(int now) { _consumedRoomRocketLaunchEffectTimes.removeWhere( (_, timestamp) => now - timestamp > _roomRocketLaunchEffectConsumedTtlMs, ); } void _handleCurrentVisibleRoomRocketLaunch({ required RealTimeCommunicationManager rtcProvider, required RoomRocketLaunchBroadcastMessage launch, bool publishRegionBroadcast = false, }) { if (_addRoomRocketLaunchNoticeIfNeeded( rtcProvider: rtcProvider, launch: launch, publishRegionBroadcast: publishRegionBroadcast, )) { _markRoomRocketRewardProbe(launch.roomId); _scheduleRoomRocketLaunchEffectsAfterCountdown(launch); } } bool _addRoomRocketLaunchNoticeIfNeeded({ required RealTimeCommunicationManager rtcProvider, required RoomRocketLaunchBroadcastMessage launch, bool publishRegionBroadcast = false, int? secondsOverride, }) { if (!_isCurrentVisibleVoiceRoom(launch.roomId)) { return false; } final launchKey = _roomRocketLaunchNoticeKey(launch); final looseLaunchKey = _roomRocketLaunchLooseKey(launch); final now = DateTime.now().millisecondsSinceEpoch; _pruneRecentRoomRocketLaunches(now); final alreadyHandled = (launchKey.isNotEmpty && _recentRoomRocketLaunchTimes.containsKey(launchKey)) || (looseLaunchKey.isNotEmpty && _recentRoomRocketLaunchLooseTimes.containsKey(looseLaunchKey)); if (alreadyHandled || launchKey.isEmpty) { return false; } _recentRoomRocketLaunchTimes[launchKey] = now; if (looseLaunchKey.isNotEmpty) { _recentRoomRocketLaunchLooseTimes[looseLaunchKey] = now; } addMsg( _buildRoomRocketLaunchNoticeMsg( _firstNonBlank([ rtcProvider.currenRoom?.roomProfile?.roomProfile?.roomAccount, launch.roomAccount, ]), launch, secondsOverride: secondsOverride, ), ); if (publishRegionBroadcast) { _publishRoomRocketRegionBroadcastIfNeeded(launch); } return true; } void _publishRoomRocketRegionBroadcastIfNeeded( RoomRocketLaunchBroadcastMessage launch, ) { if (!launch.isValid || !_shouldCurrentUserPublishRoomRocketRegionBroadcast(launch)) { return; } final key = _roomRocketLaunchNoticeKey(launch); if (key.isEmpty) { return; } final now = DateTime.now().millisecondsSinceEpoch; _recentRoomRocketRegionBroadcastTimes.removeWhere( (_, timestamp) => now - timestamp > _roomRocketRegionBroadcastRecentTtlMs, ); if (_recentRoomRocketRegionBroadcastTimes.containsKey(key)) { return; } _recentRoomRocketRegionBroadcastTimes[key] = now; unawaited(sendRoomRocketLaunchBroadcast(launch)); } bool _shouldCurrentUserPublishRoomRocketRegionBroadcast( RoomRocketLaunchBroadcastMessage launch, ) { final currentUser = AccountStorage().getCurrentUser()?.userProfile; final currentKeys = { currentUser?.id?.trim() ?? '', currentUser?.account?.trim() ?? '', }..removeWhere((item) => item.isEmpty); final triggerKeys = { launch.triggerUserId.trim(), launch.triggerUser.userId.trim(), launch.triggerUser.account.trim(), }..removeWhere((item) => item.isEmpty); if (triggerKeys.isNotEmpty) { return currentKeys.any(triggerKeys.contains); } return true; } String _rocketIconUrlForLevel(int level) { final currentContext = context; if (currentContext == null || !currentContext.mounted) { return ''; } final rtcProvider = Provider.of( currentContext, listen: false, ); final levels = rtcProvider.roomRocketStatus?.levels ?? const []; for (final item in levels) { if (item.level == level && item.rocketIconUrl.trim().isNotEmpty) { return item.rocketIconUrl; } } return ''; } Future _showRoomRocketRewardPopupDialog( String roomId, { int attempt = 0, List initialRoomRecords = const [], }) async { if (!_isCurrentVisibleVoiceRoom(roomId)) { return; } if (_isRoomRocketRewardPopupHeld(roomId)) { _scheduleRoomRocketRewardPopupAfterHold( roomId, initialRoomRecords: initialRoomRecords, ); return; } try { final res = await SCChatRoomRepository().roomRocketRewardPopups(roomId); if (!_isCurrentVisibleVoiceRoom(roomId)) { return; } if (res.records.isEmpty && initialRoomRecords.isEmpty && attempt < _roomRocketRewardPopupRetryDelays.length) { _scheduleRoomRocketRewardPopupRetry( roomId, attempt, initialRoomRecords: initialRoomRecords, ); return; } _roomRocketRewardPopupRetryTimer?.cancel(); _roomRocketRewardPopupRetryTimer = null; final recordIds = res.records .map((item) => item.id) .where((item) => item.isNotEmpty) .toList(growable: false); final userProfileResolver = _roomRocketRewardUserProfileResolver(); SmartDialog.dismiss(tag: _roomRocketRewardDialogTag); SmartDialog.show( tag: _roomRocketRewardDialogTag, alignment: Alignment.center, debounce: true, animationType: SmartAnimationType.fade, maskColor: Colors.transparent, clickMaskDismiss: false, builder: (_) => RoomRocketRewardDialogLoader( roomId: roomId, initialPopupRecords: res.records, initialRoomRecords: initialRoomRecords, userProfileResolver: userProfileResolver, onClose: () { SmartDialog.dismiss(tag: _roomRocketRewardDialogTag); _scheduleRoomRocketPostLaunchStatusRefresh(roomId); if (recordIds.isNotEmpty) { unawaited( SCChatRoomRepository().ackRoomRocketRewardPopups(recordIds), ); } }, ), ); } catch (_) { _scheduleRoomRocketRewardPopupRetry( roomId, attempt, initialRoomRecords: initialRoomRecords, ); } } void _scheduleRoomRocketRewardPopupRetry( String roomId, int attempt, { List initialRoomRecords = const [], }) { if (attempt >= _roomRocketRewardPopupRetryDelays.length || !_isCurrentVisibleVoiceRoom(roomId)) { return; } _roomRocketRewardPopupRetryTimer?.cancel(); _roomRocketRewardPopupRetryTimer = Timer( _roomRocketRewardPopupRetryDelays[attempt], () { _roomRocketRewardPopupRetryTimer = null; unawaited( _showRoomRocketRewardPopupDialog( roomId, attempt: attempt + 1, initialRoomRecords: initialRoomRecords, ), ); }, ); } void _roomRocketRewardDebug(String message) { if (!kDebugMode) { return; } debugPrint('[RoomRocketRewardPopup] $message'); } RoomRocketRewardUserProfileResolver? _roomRocketRewardUserProfileResolver() { final currentContext = context; if (currentContext == null || !currentContext.mounted) { return null; } try { final rtcProvider = Provider.of( currentContext, listen: false, ); return rtcProvider.roomRocketRewardUserProfileForRecord; } catch (_) { return null; } } void _scheduleRoomRocketPostLaunchStatusRefresh(String roomId) { final currentContext = context; if (currentContext == null || !currentContext.mounted) { return; } try { Provider.of( currentContext, listen: false, ).scheduleRoomRocketPostLaunchStatusRefresh(roomId: roomId); } catch (_) {} } List _roomRocketRewardRecordsFromPayload( Map payload, ) { final candidates = [ payload['roomRecords'], payload['roomRewardRecords'], payload['room_reward_records'], payload['allRecords'], payload['allRewardRecords'], payload['all_reward_records'], payload['rewardRecords'], payload['reward_records'], payload['rewardRecordList'], payload['recordList'], payload['records'], payload['list'], payload['items'], payload['rows'], ]; for (final candidate in candidates) { final records = SCRoomRocketRewardRecordRes.listFromJson(candidate); if (records.isNotEmpty) { return records; } } return const []; } SCFloatingMessage _buildRoomRocketLaunchFloatingMessage( RoomRocketLaunchBroadcastMessage launch, { bool isRegionBroadcast = false, }) { return SCFloatingMessage( type: 3, roomId: launch.roomId, rocketLevel: launch.safeLevel, userAvatarUrl: launch.roomCoverUrl, userName: _firstNonBlank([launch.roomName, launch.roomAccount]), userId: _firstNonBlank([ launch.triggerUser.account, launch.triggerUser.userId, launch.triggerUserId, ]), giftUrl: launch.rocketIconUrl, fallbackUrl: launch.rocketIconUrl, giftId: launch.launchNo, durationSeconds: launch.durationSeconds, broadcastScope: isRegionBroadcast ? SCFloatingMessage.broadcastScopeRegion : '', priority: 1000, ); } Msg _buildRoomRocketLaunchNoticeMsg( String groupId, RoomRocketLaunchBroadcastMessage launch, { int? secondsOverride, }) { final seconds = secondsOverride ?? (launch.durationSeconds <= 0 ? 10 : launch.durationSeconds); return Msg( groupId: groupId, msg: seconds.toString(), type: SCRoomMsgType.rocketLaunchNotice, number: seconds, rocketIconUrl: _firstNonBlank([ launch.rocketIconUrl, _rocketIconUrlForLevel(launch.safeLevel), ]), giftBatchId: _roomRocketLaunchNoticeKey(launch), ); } String _roomRocketLaunchNoticeKey(RoomRocketLaunchBroadcastMessage launch) { return _roomRocketLaunchNoticeKeyForValues( roomId: launch.roomId, level: launch.safeLevel, roundNo: launch.roundNo, launchNo: launch.launchNo, ); } String _roomRocketLaunchLooseKey(RoomRocketLaunchBroadcastMessage launch) { if (launch.roomId.isEmpty) { return ''; } return ['rocket_launch_loose', launch.roomId, launch.safeLevel].join('|'); } bool _shouldHandleRoomRocketLaunchFromStatusPayload({ required RealTimeCommunicationManager rtcProvider, required Map payload, required RoomRocketLaunchBroadcastMessage launch, }) { final currentPercent = _roomRocketPayloadPercent(payload); if (currentPercent < 99.5) { return false; } final previousPercent = _roomRocketStatusPercent( rtcProvider.roomRocketStatus, ); if (previousPercent < 99.5) { return true; } final launchKey = _roomRocketLaunchNoticeKey(launch); if (launchKey.isEmpty) { return false; } _pruneRecentRoomRocketLaunches(DateTime.now().millisecondsSinceEpoch); final looseLaunchKey = _roomRocketLaunchLooseKey(launch); return !_recentRoomRocketLaunchTimes.containsKey(launchKey) && (looseLaunchKey.isEmpty || !_recentRoomRocketLaunchLooseTimes.containsKey(looseLaunchKey)); } RoomRocketLaunchBroadcastMessage _buildRoomRocketLaunchFromStatusPayload({ required RealTimeCommunicationManager rtcProvider, required Map payload, required String roomId, }) { final roomProfile = rtcProvider.currenRoom?.roomProfile?.roomProfile; final level = _roomRocketPayloadLevel( payload, rtcProvider.roomRocketStatus?.currentLevel ?? rtcProvider.roomRocketStatus?.level?.toInt() ?? 1, ); return RoomRocketLaunchBroadcastMessage( roomId: roomId, roomAccount: _firstNonBlank([ roomProfile?.roomAccount, _payloadText(payload['roomAccount'] ?? payload['room_account']), ]), roomName: _firstNonBlank([ roomProfile?.roomName, _payloadText(payload['roomName'] ?? payload['room_name']), ]), roomCoverUrl: _firstNonBlank([ roomProfile?.roomCover, _payloadAssetText(payload['roomCoverUrl'] ?? payload['room_cover_url']), _payloadAssetText(payload['roomCover'] ?? payload['room_cover']), _payloadAssetText(payload['roomAvatar'] ?? payload['room_avatar']), _payloadAssetText(payload['roomIcon'] ?? payload['room_icon']), _payloadAssetText(payload['coverUrl'] ?? payload['cover_url']), _payloadAssetText(payload['cover']), _payloadAssetText(_broadcastPayloadMap(payload['room'])['coverUrl']), _payloadAssetText(_broadcastPayloadMap(payload['room'])['cover']), _payloadAssetText(_broadcastPayloadMap(payload['room'])['avatar']), _payloadAssetText( _broadcastPayloadMap(payload['roomInfo'])['coverUrl'], ), _payloadAssetText(_broadcastPayloadMap(payload['roomInfo'])['cover']), _payloadAssetText(_broadcastPayloadMap(payload['roomInfo'])['avatar']), ]), roundNo: _payloadNum(payload['roundNo'] ?? payload['round_no'])?.toInt() ?? 0, level: level, rocketIconUrl: _firstNonBlank([ _rocketIconFromPayload(payload), _rocketIconUrlForLevel(level), ]), rocketAnimationUrl: _rocketAnimationFromPayload(payload), durationSeconds: _roomRocketPayloadNoticeSeconds(payload), triggerUser: const RoomRocketLaunchTriggerUser( userId: '', nickname: '', avatar: '', account: '', countryCode: '', countryName: '', ), triggerUserId: _payloadText( payload['triggerUserId'] ?? payload['trigger_user_id'], ), launchNo: _payloadText(payload['launchNo'] ?? payload['launch_no']), ); } double _roomRocketStatusPercent(dynamic status) { if (status == null) { return 0; } final displayPercent = _payloadNum(status.displayPercent); if (displayPercent != null) { final raw = displayPercent.toDouble(); final percent = raw > 0 && raw <= 1 ? raw * 100 : raw; return percent.clamp(0, 100).toDouble(); } final energyPercent = _payloadNum(status.energyPercent); if (energyPercent != null) { final raw = energyPercent.toDouble(); final percent = raw > 0 && raw <= 1 ? raw * 100 : raw; return percent.clamp(0, 100).toDouble(); } final currentEnergy = _payloadNum(status.currentEnergy); final maxEnergy = _payloadNum(status.maxEnergy ?? status.needEnergy); if (currentEnergy == null || maxEnergy == null || maxEnergy <= 0) { return 0; } return ((currentEnergy / maxEnergy) * 100).clamp(0, 100).toDouble(); } double _roomRocketPayloadPercent(Map payload) { final value = _payloadNum( payload['displayPercent'] ?? payload['display_percent'] ?? payload['energyPercent'] ?? payload['energy_percent'] ?? payload['percent'], ); if (value != null) { final raw = value.toDouble(); final percent = raw > 0 && raw <= 1 ? raw * 100 : raw; return percent.clamp(0, 100).toDouble(); } final currentEnergy = _payloadNum( payload['currentEnergy'] ?? payload['current_energy'], ); final maxEnergy = _payloadNum( payload['maxEnergy'] ?? payload['max_energy'] ?? payload['needEnergy'] ?? payload['need_energy'], ); if (currentEnergy == null || maxEnergy == null || maxEnergy <= 0) { return 0; } return ((currentEnergy / maxEnergy) * 100).clamp(0, 100).toDouble(); } int _roomRocketPayloadLevel(Map payload, int fallback) { final value = _payloadNum( payload['currentLevel'] ?? payload['current_level'] ?? payload['level'] ?? payload['rocketLevel'] ?? payload['rocket_level'] ?? payload['from_level'] ?? payload['fromLevel'], ); return (value?.toInt() ?? fallback).clamp(1, 99).toInt(); } int _roomRocketPayloadNoticeSeconds(Map payload) { final seconds = _payloadNum( payload['countdownSeconds'] ?? payload['countdown_seconds'] ?? payload['launchDelaySeconds'] ?? payload['launch_delay_seconds'] ?? payload['delaySeconds'] ?? payload['delay_seconds'] ?? payload['durationSeconds'] ?? payload['duration_seconds'], )?.toInt() ?? 10; if (seconds > 0 && seconds <= 60) { return seconds; } return 10; } String _rocketIconFromPayload(Map payload) { return _firstNonBlank([ _payloadAssetText(payload['rocketIconUrl']), _payloadAssetText(payload['rocket_icon_url']), _payloadAssetText(payload['rocketIcon']), _payloadAssetText(payload['rocketUrl']), _payloadAssetText(payload['rocketImageUrl']), _payloadAssetText(payload['iconUrl']), ]); } String _rocketAnimationFromPayload(Map payload) { return _firstNonBlank([ _payloadAssetText(payload['rocketAnimationUrl']), _payloadAssetText(payload['rocket_animation_url']), _payloadAssetText(payload['rocketSvgaUrl']), _payloadAssetText(payload['rocket_svga_url']), _payloadAssetText(payload['animationUrl']), _payloadAssetText(payload['animation_url']), _payloadAssetText(payload['launchAnimationUrl']), _payloadAssetText(payload['launch_animation_url']), _payloadAssetText(payload['svgaUrl']), _payloadAssetText(payload['sourceUrl']), _payloadAssetText(payload['source_url']), ]); } String _payloadRoomId(Map payload) { return _firstNonBlank([ _payloadText(payload['roomId']), _payloadText(payload['room_id']), _payloadText(payload['roomID']), _payloadText(payload['voiceRoomId']), _payloadText(payload['voice_room_id']), _payloadText(payload['chatRoomId']), _payloadText(payload['chat_room_id']), _payloadText(_broadcastPayloadMap(payload['room'])['id']), _payloadText(_broadcastPayloadMap(payload['room'])['roomId']), _payloadText(_broadcastPayloadMap(payload['room'])['room_id']), _payloadText(_broadcastPayloadMap(payload['roomInfo'])['id']), _payloadText(_broadcastPayloadMap(payload['roomInfo'])['roomId']), _payloadText(_broadcastPayloadMap(payload['roomInfo'])['room_id']), ]); } void _pruneRecentRoomRocketLaunches(int now) { _recentRoomRocketLaunchTimes.removeWhere( (_, timestamp) => now - timestamp > _roomRocketLaunchRecentTtlMs, ); _recentRoomRocketLaunchLooseTimes.removeWhere( (_, timestamp) => now - timestamp > _roomRocketLaunchLooseRecentTtlMs, ); _pruneConsumedRoomRocketLaunchEffects(now); } String _roomRocketLaunchNoticeKeyForValues({ required String roomId, required int level, required int roundNo, String launchNo = '', }) { final roundKey = roundNo > 0 ? roundNo.toString() : (launchNo.trim().isNotEmpty ? launchNo : 'full'); return [ 'rocket_launch_notice', roomId, level.clamp(1, 99).toInt(), roundKey, ].join('|'); } 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) && _isDelayedRoomRedPacketPayload(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; _clearRoomRocketRewardFallbackState(); 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"]; final payload = _broadcastPayloadMap(fdata); final launchRoomId = _payloadRoomId(payload); if (!isRegionBroadcastGroup && _isCurrentHiddenVoiceRoom(launchRoomId)) { return; } final rocketIconUrl = _firstNonBlank([ _rocketIconFromPayload(payload), _payloadAssetText(payload["giftUrl"]), _payloadAssetText(payload["giftIcon"]), ]); SCFloatingMessage msg = SCFloatingMessage( type: 3, roomId: launchRoomId, rocketLevel: _roomRocketPayloadLevel(payload, 1), userAvatarUrl: _firstNonBlank([ _payloadAssetText(payload["roomCoverUrl"]), _payloadAssetText(payload["roomCover"]), _payloadAssetText(payload["roomAvatar"]), _payloadAssetText(payload["roomIcon"]), _payloadAssetText(payload["coverUrl"]), _payloadAssetText(payload["cover"]), _payloadAssetText(payload["userAvatar"]), ]), userName: _firstNonBlank([ _payloadText(payload["roomName"]), _payloadText(payload["roomAccount"]), _payloadText(payload["nickname"]), ]), userId: _payloadText(payload["actualAccount"]), giftUrl: rocketIconUrl, fallbackUrl: rocketIconUrl, broadcastScope: isRegionBroadcastGroup ? SCFloatingMessage.broadcastScopeRegion : '', priority: 1000, ); OverlayManager().addMessage(msg); } else if (type == SCRoomMsgType.voiceRoomRocketLaunchBroadcast) { _handleVoiceRoomRocketLaunchBroadcast( data["data"], isRegionBroadcast: isRegionBroadcastGroup, ); } else if (type == SCRoomMsgType.voiceRoomRocketStatusUpdate) { _handleVoiceRoomRocketStatusUpdate(data["data"]); } else if (type == SCRoomMsgType.voiceRoomRocketRewardPopup) { _handleVoiceRoomRocketRewardPopup(data["data"] ?? data); } else if (SCRoomMsgType.isRocketRewardUserType(type?.toString())) { _handleVoiceRoomRocketRewardPopup(data["data"] ?? data); } else if (type == SCRoomMsgType.roomRedPacket) { ///红包触发飘屏 var fData = data["data"]; final payload = _broadcastPayloadMap(fData); final packetRoomId = _payloadText(payload["roomId"]); if (isRegionBroadcastGroup && !_isDelayedRoomRedPacketPayload(fData)) { return; } 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"], 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); if (data["type"] == SCRoomMsgType.voiceRoomRocketLaunchBroadcast) { _handleVoiceRoomRocketLaunchBroadcast(data["data"]); return; } if (data["type"] == SCRoomMsgType.voiceRoomRocketStatusUpdate) { _handleVoiceRoomRocketStatusUpdate(data["data"]); return; } if (data["type"] == SCRoomMsgType.voiceRoomRocketRewardPopup) { _handleVoiceRoomRocketRewardPopup(data["data"] ?? data); return; } if (SCRoomMsgType.isRocketRewardUserType(data["type"]?.toString())) { _handleVoiceRoomRocketRewardPopup(data["data"] ?? data); } 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) { 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) { 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 = '', bool includeRegionBroadcast = false, }) async { if (payload.isEmpty) { return; } final targetGroupIds = {}; final normalizedRoomGroupId = roomGroupId.trim(); if (normalizedRoomGroupId.isNotEmpty) { targetGroupIds.add(normalizedRoomGroupId); } if (includeRegionBroadcast) { var regionGroupId = roomRedPacketBroadcastGroupId?.trim() ?? ''; if (regionGroupId.isEmpty) { await syncRoomRedPacketBroadcastGroup(); regionGroupId = roomRedPacketBroadcastGroupId?.trim() ?? ''; } if (regionGroupId.isNotEmpty) { targetGroupIds.add(regionGroupId); } } if (targetGroupIds.isEmpty) { return; } final message = { 'type': SCRoomMsgType.roomRedPacket, 'data': payload, }; final data = jsonEncode(message); for (final groupId in targetGroupIds) { await _sendRoomRedPacketCustomMessage(groupId, data, payload); } } Future sendRoomRocketLaunchBroadcast( RoomRocketLaunchBroadcastMessage launch, ) async { if (!launch.isValid) { return; } var regionGroupId = roomRedPacketBroadcastGroupId?.trim() ?? ''; if (regionGroupId.isEmpty) { await syncRoomRedPacketBroadcastGroup(); regionGroupId = roomRedPacketBroadcastGroupId?.trim() ?? ''; } if (regionGroupId.isEmpty) { return; } final message = { 'type': SCRoomMsgType.voiceRoomRocketLaunchBroadcast, 'data': _roomRocketLaunchBroadcastPayload(launch), }; await _sendRoomRocketLaunchCustomMessage( regionGroupId, jsonEncode(message), ); } Map _roomRocketLaunchBroadcastPayload( RoomRocketLaunchBroadcastMessage launch, ) { final launchNo = _firstNonBlank([ launch.launchNo, _roomRocketLaunchNoticeKey(launch), ]); return { 'roomId': launch.roomId, 'roomAccount': launch.roomAccount, 'roomName': launch.roomName, 'roomCoverUrl': launch.roomCoverUrl, 'roundNo': launch.roundNo, 'level': launch.safeLevel, 'rocketLevel': launch.safeLevel, 'rocketIconUrl': launch.rocketIconUrl, 'rocketAnimationUrl': launch.rocketAnimationUrl, 'durationSeconds': launch.durationSeconds, 'countdownSeconds': launch.durationSeconds, 'triggerUserId': launch.triggerUserId, 'triggerUser': { 'userId': launch.triggerUser.userId, 'nickname': launch.triggerUser.nickname, 'avatar': launch.triggerUser.avatar, 'account': launch.triggerUser.account, 'countryCode': launch.triggerUser.countryCode, 'countryName': launch.triggerUser.countryName, }, 'launchNo': launchNo, }; } Future _sendRoomRocketLaunchCustomMessage( String groupId, String data, ) async { try { final textMsg = await TencentImSDKPlugin.v2TIMManager .getMessageManager() .createCustomMessage(data: data); if (textMsg.code != 0 || textMsg.data?.id == null) { return; } await TencentImSDKPlugin.v2TIMManager.getMessageManager().sendMessage( id: textMsg.data!.id!, groupID: groupId, receiver: '', ); } catch (_) {} } 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) { return; } final sendResult = await TencentImSDKPlugin.v2TIMManager .getMessageManager() .sendMessage(id: textMsg.data!.id!, groupID: groupId, receiver: ''); } catch (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 _isDelayedRoomRedPacketPayload(dynamic payload) { final map = _broadcastPayloadMap(payload); final packetMode = _payloadText(map['packetMode']).toUpperCase(); if (packetMode == 'DELAYED') { return true; } if (packetMode == 'IMMEDIATE') { return false; } return _payloadText(map['claimStartTime']).isNotEmpty; } 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(); _recentRoomRocketLaunchTimes.clear(); _recentRoomRocketLaunchLooseTimes.clear(); _recentRoomRocketRegionBroadcastTimes.clear(); _consumedRoomRocketLaunchEffectTimes.clear(); _hiddenCurrentRoomRocketLaunchKeys.clear(); _roomRocketRewardPopupRetryTimer?.cancel(); _roomRocketRewardPopupRetryTimer = null; for (final timer in _roomRocketRewardHoldTimers.values) { timer.cancel(); } _roomRocketRewardHoldTimers.clear(); _roomRocketRewardHoldUntilTimes.clear(); 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); } }