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_top_layer_guard.dart'; import 'package:yumi/shared/tools/sc_room_utils.dart'; import 'package:yumi/shared/tools/sc_cp_gift_relation_utils.dart'; import 'package:yumi/shared/tools/sc_cp_relation_broadcast_assets.dart'; import 'package:yumi/shared/tools/sc_cp_relation_notice_utils.dart'; import 'package:yumi/shared/tools/sc_room_rocket_reward_popup_ack.dart'; import 'package:yumi/shared/tools/sc_user_id_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/shared/data_sources/sources/repositories/sc_user_repository_impl.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/sc_room_rocket_status_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/cp/room_cp_invite_dialog.dart'; import 'package:yumi/ui_kit/widgets/room/cp/room_cp_relation_formed_effect.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/shared/business_logic/models/res/sc_system_invit_message_res.dart'; import 'package:yumi/shared/data_sources/models/enum/sc_sysytem_message_type.dart'; import 'package:yumi/services/gift/gift_system_manager.dart'; import 'package:yumi/services/auth/user_profile_manager.dart'; import 'package:yumi/main.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; enum _CpInviteProcessResult { accepted, rejected, expired } 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; } bool roomRocketRewardPopupTargetsCurrentUser({ required Map payload, required Set currentUserKeys, }) { final audienceUserIds = _roomRocketRewardPopupAudienceUserIds(payload); if (audienceUserIds == null) { return true; } if (audienceUserIds.isEmpty || currentUserKeys.isEmpty) { return false; } return currentUserKeys.any(audienceUserIds.contains); } Set? _roomRocketRewardPopupAudienceUserIds( Map payload, ) { for (final key in const [ 'popupUserIds', 'popup_user_ids', 'notifyUserIds', 'notify_user_ids', 'audienceUserIds', 'audience_user_ids', 'visibleUserIds', 'visible_user_ids', ]) { if (payload.containsKey(key)) { return _roomRocketPayloadStringSet(payload[key]); } } return null; } Set _roomRocketPayloadStringSet(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 _roomRocketPayloadStringSet(jsonDecode(raw)); } catch (_) { return {}; } } return raw .split(',') .map((item) => item.trim()) .where((item) => item.isNotEmpty) .toSet(); } 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 _cpRelationGiftRefreshMinIntervalMs = 800; static const List _cpRelationGiftRefreshDelays = [ Duration(milliseconds: 300), Duration(milliseconds: 1500), Duration(milliseconds: 3500), ]; static const int _roomRocketLaunchRecentTtlMs = 90000; static const int _roomRocketLaunchLooseRecentTtlMs = 12000; static const int _roomRocketRegionBroadcastRecentTtlMs = 90000; static const int _roomRocketRewardPendingTtlMs = 3600000; static const int _roomRocketRewardProbeTtlMs = 3600000; static const int _roomRocketEmptyRewardPopupRecentTtlMs = 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) { debugPrint('[GiftFx][rtm] $message'); } void _enqueueGiftFloatingMessage( SCFloatingMessage message, { String? dedupKey, }) { if (SCGlobalConfig.isLowPerformanceDevice) { OverlayManager().addLowPerformanceCompensationMessage( message, dedupKey: dedupKey, ); return; } OverlayManager().addMessage(message); } String _giftFloatingDedupKey({ required String? roomId, required String? senderId, required String? receiverId, required String? giftId, required num? quantity, }) { return [ "gift", roomId ?? "", senderId ?? "", receiverId ?? "", giftId ?? "", quantity ?? 0, ].join("|"); } String _entryFloatingDedupKey({ required String? roomId, required String? userId, required String? resourceId, }) { return ["entry", roomId ?? "", userId ?? "", resourceId ?? ""].join("|"); } String _resolveEntryCompensationCover(PropsResources? resource) { return _firstNonBlank([ resource?.roomSendCoverUrl, resource?.roomOpenedUrl, resource?.cover, resource?.roomNotOpenedUrl, ]); } String _firstNonBlank(Iterable values) { for (final value in values) { final text = value?.trim(); if (text != null && text.isNotEmpty) { return text; } } return ""; } ///消息列表 List roomAllMsgList = []; List roomChatMsgList = []; List roomGiftMsgList = []; RoomNewMsgListener? msgAllListener; RoomNewMsgListener? msgChatListener; RoomNewMsgListener? msgGiftListener; RoomNewMsgListener? msgFloatingGiftListener; RoomNewMsgListener? msgLuckyGiftRewardTickerListener; RoomNewMsgListener? msgUserJoinListener; /// 当前会话 V2TimConversation? currentConversation; /// 会话列表缓存 Map conversationMap = {}; ///消息已读监听 OnMessageRecvC2CReadListener? onMessageRecvC2CReadListener; ///消息被撤回监听 OnRevokeMessageListener? onRevokeMessageListener; ///新消息监听 单聊 OnNewMessageListener? onNewMessageCurrentConversationListener; OnNewANMessageListener? onNewActivityMessageCurrentConversationListener; OnNewANMessageListener? onNewNotifcationMessageCurrentConversationListener; ///新消息监听 群聊 Map onNewMessageListenerGroupMap = {}; int allUnReadCount = 0; int messageUnReadCount = 0; int systemUnReadCount = 0; int customerUnReadCount = 0; int activityUnReadCount = 0; int notifcationUnReadCount = 0; SCBroadCastLuckGiftPush? currentPlayingLuckGift; final Queue<_LuckGiftPushQueueEntry> _luckGiftPushQueue = Queue(); String? _currentLuckGiftPushKey; Set _currentLuckGiftPushEventKeys = {}; Timer? _luckGiftPushPlayTimer; int _luckGiftPushPlaybackToken = 0; final Map _recentLuckGiftPushEventTimes = {}; 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 = {}; final Set _roomRocketRewardPopupLoadingRoomIds = {}; final Map _shownRoomRocketEmptyRewardPopupTimes = {}; Timer? _roomRocketRewardPopupRetryTimer; Timer? _roomRocketRoomReadyRewardRetryTimer; final Set _shownCpInviteMessageKeys = {}; final Set _shownCpInviteResultMessageKeys = {}; final Set _shownCpInviteRejectedDialogKeys = {}; final Map _recentCpRelationNoticeTimes = {}; int _lastCpRelationGiftRefreshAt = 0; 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); }, onConversationChanged: (conversationList) { _onRefreshConversation(conversationList); }, 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); final userId = conversation.userID?.trim() ?? ""; if (conversation.type == ConversationType.V2TIM_C2C && !_isSystemConversation(conversation) && conversation.conversationID != "customer" && conversation.conversationID != "article" && !scLooksLikeBackendUserId(userId)) { _cpInviteLog( 'skip startConversation reason=invalid_user_id ' 'userId=$userId conversationID=${conversation.conversationID}', ); return false; } 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) { if (conversation == null) { continue; } conversationMap[conversation.conversationID] = conversation; _handleCpBuildFromConversation(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, String? fallbackUserId, }) async { final cpData = _decodeCpSystemMessageData(message); unawaited(_handleIncomingCpBuildMessage(message)); final cpPeerUserId = _cpInviteConversationPeerIdFromData( cpData, message: message, ); final didSyncConversation = _upsertC2CConversationFromMessage( message, fallbackUserId: fallbackUserId ?? cpPeerUserId, preferFallbackUserId: cpPeerUserId != null, ); if (!didSyncConversation) { systemUnReadCount = _systemUnreadCount(); customerUnReadCount = conversationMap["c2c_${customerInfo?.id}"]?.unreadCount ?? 0; notifyListeners(); } onNewMessageCurrentConversationListener?.call(message, msgId: msgId); } bool _upsertC2CConversationFromMessage( V2TimMessage? message, { String? fallbackUserId, bool preferFallbackUserId = false, }) { if (message == null || message.groupID != null) { return false; } final peerId = _messagePeerId( message, fallbackUserId, preferFallbackUserId: preferFallbackUserId, ); if (peerId.isEmpty) { final cpData = _decodeCpSystemMessageData(message); if (cpData != null) { _cpInviteLog( 'skip C2C conversation sync reason=no_peer ' 'msgId=${message.msgID} sender=${message.sender}', ); } return false; } final cpData = _decodeCpSystemMessageData(message); if (_isCpRelationInviteNoticeData(cpData) && !scLooksLikeBackendUserId(peerId)) { _cpInviteLog( 'skip C2C conversation sync reason=invalid_peer ' 'peerId=$peerId msgId=${message.msgID} sender=${message.sender}', ); return false; } if ((message.userID?.trim() ?? "").isEmpty) { message.userID = peerId; } if ((message.timestamp ?? 0) <= 0) { message.timestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000; } final conversationId = "c2c_$peerId"; final conversation = conversationMap[conversationId] ?? V2TimConversation( type: ConversationType.V2TIM_C2C, userID: peerId, conversationID: conversationId, unreadCount: 0, ); final previousMsgId = conversation.lastMessage?.msgID ?? ""; final isSameMessage = previousMsgId.isNotEmpty && previousMsgId == (message.msgID ?? ""); final shouldIncrementUnread = message.isSelf != true && message.isExcludedFromUnreadCount != true && !isSameMessage && onNewMessageCurrentConversationListener == null; conversation.type ??= ConversationType.V2TIM_C2C; conversation.userID ??= peerId; conversation.lastMessage = message; conversation.showName = _c2cConversationDisplayName( conversation, message, peerId, ); conversation.faceUrl = _c2cConversationFaceUrl(conversation, message); conversation.unreadCount = (conversation.unreadCount ?? 0) + (shouldIncrementUnread ? 1 : 0); conversationMap[conversationId] = conversation; if (cpData != null) { _cpInviteLog( 'sync C2C conversation conversationId=$conversationId ' 'self=${message.isSelf} unread=${conversation.unreadCount} ' 'noticeType=${cpData["noticeType"] ?? cpData["type"]} ' 'msgId=${message.msgID}', ); } getConversationList(); return true; } String _messagePeerId( V2TimMessage message, String? fallbackUserId, { bool preferFallbackUserId = false, }) { final currentUserId = AccountStorage().getCurrentUser()?.userProfile?.id?.trim() ?? ""; final userId = message.userID?.trim() ?? ""; final sender = message.sender?.trim() ?? ""; final fallback = fallbackUserId?.trim() ?? ""; if (preferFallbackUserId && fallback.isNotEmpty && fallback != currentUserId) { return fallback; } if (message.isSelf == true) { if (userId.isNotEmpty) { return userId; } if (fallback.isNotEmpty) { return fallback; } if (sender.isNotEmpty && sender != currentUserId) { return sender; } return ""; } if (sender.isNotEmpty && sender != currentUserId) { return sender; } if (userId.isNotEmpty && userId != currentUserId) { return userId; } if (fallback.isNotEmpty) { return fallback; } if (userId.isNotEmpty) { return userId; } return ""; } String? _cpInviteConversationPeerIdFromData( Map? data, { V2TimMessage? message, }) { if (data == null) { return null; } final relationNoticePeerId = _cpRelationNoticePeerIdFromData( data, message: message, ); if (relationNoticePeerId != null) { return relationNoticePeerId; } final invite = _cpInviteFromMessageData(data); final currentProfile = AccountStorage().getCurrentUser()?.userProfile; if (invite == null || currentProfile == null) { return null; } final currentIds = [currentProfile.id, currentProfile.account] .map((value) => value?.trim() ?? "") .where((value) => value.isNotEmpty) .toSet(); final sender = message?.sender?.trim() ?? ""; final userId = message?.userID?.trim() ?? ""; final isSender = _cpInviteSenderMatchesCurrent( invite, currentProfile, data: data, ); if (isSender) { if (scLooksLikeBackendUserId(userId) && !currentIds.contains(userId)) { return userId; } return _cpInviteReceiverIdFromMessageData(data); } if (scLooksLikeBackendUserId(sender) && !currentIds.contains(sender)) { return sender; } return _cpInviteInviterIdFromMessageData(data); } String _c2cConversationDisplayName( V2TimConversation conversation, V2TimMessage message, String peerId, ) { final current = conversation.showName?.trim() ?? ""; if (current.isNotEmpty && current != peerId) { return current; } if (message.isSelf == true) { return current.isNotEmpty ? current : peerId; } final candidates = [ message.friendRemark, message.nickName, message.nameCard, peerId, ]; for (final candidate in candidates) { final text = candidate?.trim() ?? ""; if (text.isNotEmpty) { return text; } } return peerId; } String? _c2cConversationFaceUrl( V2TimConversation conversation, V2TimMessage message, ) { final current = conversation.faceUrl?.trim() ?? ""; if (current.isNotEmpty || message.isSelf == true) { return current.isNotEmpty ? current : conversation.faceUrl; } final faceUrl = message.faceUrl?.trim() ?? ""; return faceUrl.isNotEmpty ? faceUrl : conversation.faceUrl; } Future showLocalCpGiftInviteWaiting({ required SocialChatUserProfile? receiverProfile, required String? relationType, }) async { final normalizedRelationType = scNormalizeCpRelationType(relationType); final receiver = receiverProfile; final receiverId = receiver?.id?.trim() ?? ""; if (receiver == null || receiverId.isEmpty) { _cpInviteLog( 'skip local gift waiting reason=missing_receiver ' 'relationType=$normalizedRelationType', ); return; } final currentProfile = AccountStorage().getCurrentUser()?.userProfile; if (currentProfile == null) { _cpInviteLog( 'skip local gift waiting reason=no_current_user ' 'receiverId=$receiverId relationType=$normalizedRelationType', ); return; } final currentIds = _cpInviteProfileIdentityValues(currentProfile); final receiverIds = _cpInviteProfileIdentityValues(receiver); if (currentIds.any(receiverIds.contains)) { _cpInviteLog( 'skip local gift waiting reason=self_recipient ' 'receiverId=$receiverId relationType=$normalizedRelationType', ); return; } final context = navigatorKey.currentState?.context; if (context == null || !context.mounted) { _cpInviteLog( 'skip local gift waiting reason=no_context ' 'receiverId=$receiverId relationType=$normalizedRelationType', ); return; } if (!_canAutoShowCpInviteDialog(context)) { _cpInviteLog( 'skip local gift waiting reason=not_in_visible_voice_room ' 'receiverId=$receiverId relationType=$normalizedRelationType', ); return; } final applyId = await _latestCpGiftApplyId( receiverId, relationType: normalizedRelationType, ); if (applyId == null || applyId.isEmpty) { _cpInviteLog( 'skip local gift waiting reason=no_waiting_apply ' 'receiverId=$receiverId relationType=$normalizedRelationType', ); return; } if (!await _isCpInviteApplyWaitingForAutoPopup(applyId)) { return; } if (!context.mounted || !_canAutoShowCpInviteDialog(context)) { _cpInviteLog( 'skip local gift waiting reason=voice_room_hidden_after_status ' 'applyId=$applyId receiverId=$receiverId', ); return; } if (!_shownCpInviteMessageKeys.add(applyId)) { _cpInviteLog( 'skip duplicated local gift waiting applyId=$applyId ' 'receiverId=$receiverId relationType=$normalizedRelationType', ); return; } final currentName = _cpInviteDisplayName( nickname: currentProfile.userNickname, account: currentProfile.account, fallback: currentProfile.id, ); final receiverName = _cpInviteDisplayName( nickname: receiver.userNickname, account: receiver.account, fallback: receiverId, ); _cpInviteLog( 'show local gift waiting applyId=$applyId ' 'receiverId=$receiverId relationType=$normalizedRelationType', ); await RoomCpInviteDialog.show( context, inviter: RoomCpInviteDialogUser( name: currentName, avatarUrl: currentProfile.userAvatar ?? "", headdress: currentProfile.getHeaddress()?.sourceUrl ?? "", headdressCover: currentProfile.getHeaddress()?.cover ?? "", ), receiver: RoomCpInviteDialogUser( name: receiverName, avatarUrl: receiver.userAvatar ?? "", headdress: receiver.getHeaddress()?.sourceUrl ?? "", headdressCover: receiver.getHeaddress()?.cover ?? "", ), style: RoomCpInviteDialogStyle.waiting, countdownSeconds: 86399, descriptionText: _cpInviteWaitingDescriptionText(normalizedRelationType), onClose: () { _cpInviteLog('close local gift waiting applyId=$applyId'); }, onTimeout: () { _cpInviteLog('timeout local gift waiting applyId=$applyId'); }, ); } Future _latestCpGiftApplyId( String receiverId, { required String relationType, }) async { try { return await SCAccountRepository().cpRelationshipApplyId( receiverId, relationType: relationType, ); } catch (error) { _cpInviteLog( 'query local gift apply failed receiverId=$receiverId ' 'relationType=$relationType error=$error', ); return null; } } Future _handleIncomingCpBuildMessage(V2TimMessage? message) async { final data = _decodeCpSystemMessageData(message); if (data == null) { return; } final noticeType = (data["noticeType"] ?? data["type"])?.toString() ?? ""; if (_handleCpInviteResultMessage(data, msgId: message?.msgID)) { return; } if (_handleCpRelationRefreshNoticeMessage(data, message: message)) { return; } if (noticeType != SCSysytemMessageType.CP_BUILD.name) { return; } final applyId = _cpInviteApplyIdFromData(data); final invite = _cpInviteFromMessageData(data); if (applyId.isEmpty || invite == null) { _cpInviteLog( 'skip CP_BUILD reason=missing_apply_or_content ' 'applyId=$applyId ' 'hasInvite=${invite != null} ' 'raw=${message?.customElem?.data}', ); return; } final context = navigatorKey.currentState?.context; final currentProfile = AccountStorage().getCurrentUser()?.userProfile; if (context == null || currentProfile == null) { _cpInviteLog( 'skip CP_BUILD reason=no_context_or_current_user applyId=$applyId', ); return; } if (!context.mounted) { _cpInviteLog('skip CP_BUILD reason=context_unmounted applyId=$applyId'); return; } if (!_canAutoShowCpInviteDialog(context)) { _cpInviteLog( 'skip CP_BUILD reason=not_in_visible_voice_room applyId=$applyId', ); return; } if (!await _isCpInviteApplyWaitingForAutoPopup(applyId)) { return; } if (!context.mounted || !_canAutoShowCpInviteDialog(context)) { _cpInviteLog( 'skip CP_BUILD reason=voice_room_hidden_after_status ' 'applyId=$applyId', ); return; } final messageKey = applyId.isNotEmpty ? applyId : message?.msgID ?? ""; if (messageKey.isNotEmpty && !_shownCpInviteMessageKeys.add(messageKey)) { _cpInviteLog( 'skip duplicated CP_BUILD applyId=$applyId msgId=${message?.msgID}', ); return; } if (_cpInviteTargetsCurrentAsSenderAndReceiver( invite, currentProfile, data, )) { _cpInviteLog( 'skip CP_BUILD reason=self_recipient applyId=$applyId ' 'relationType=${invite.relationType}', ); await SmartDialog.dismiss(tag: RoomCpInviteDialog.dialogTag); return; } final isSender = _cpInviteSenderMatchesCurrent( invite, currentProfile, data: data, ); final inviterName = _cpInviteDisplayName( nickname: invite.userNickname, account: invite.account, actualAccount: invite.actualAccount, ); final currentName = _cpInviteDisplayName( nickname: currentProfile.userNickname, account: currentProfile.account, fallback: currentProfile.id, ); final relationType = scNormalizeCpRelationType(invite.relationType); if (isSender) { final receiver = _cpInviteReceiverFromMessageData( data, fallbackName: _cpInviteReceiverFallbackName(data, message), fallbackAvatar: _cpInviteReceiverFallbackAvatar(data, message), ); _cpInviteLog( 'show waiting CP_BUILD applyId=$applyId ' 'relationType=$relationType', ); await RoomCpInviteDialog.show( context, inviter: RoomCpInviteDialogUser( name: currentName, avatarUrl: currentProfile.userAvatar ?? "", headdress: currentProfile.getHeaddress()?.sourceUrl ?? "", headdressCover: currentProfile.getHeaddress()?.cover ?? "", ), receiver: receiver, style: RoomCpInviteDialogStyle.waiting, countdownSeconds: _cpInviteCountdownSeconds(invite), descriptionText: _cpInviteWaitingDescriptionText(relationType), onClose: () { _cpInviteLog('close waiting CP_BUILD applyId=$applyId'); }, onTimeout: () { _cpInviteLog('timeout waiting CP_BUILD applyId=$applyId'); }, ); return; } _cpInviteLog( 'show incoming CP_BUILD applyId=$applyId ' 'relationType=$relationType ' 'inviter=$inviterName', ); final currentReceiver = _cpInviteReceiverFromMessageData( data, fallbackName: _cpInviteReceiverFallbackName(data, null), fallbackAvatar: _cpInviteReceiverFallbackAvatar(data, null), ); await RoomCpInviteDialog.show( context, inviter: RoomCpInviteDialogUser( name: inviterName, avatarUrl: invite.userAvatar ?? "", headdress: invite.userHeaddress ?? "", headdressCover: invite.userHeaddressCover ?? "", ), receiver: RoomCpInviteDialogUser( name: currentName, avatarUrl: _firstCpInviteValue([ currentProfile.userAvatar, currentReceiver.avatarUrl, ]) ?? "", headdress: _firstCpInviteValue([ currentProfile.getHeaddress()?.sourceUrl, currentReceiver.headdress, ]) ?? "", headdressCover: _firstCpInviteValue([ currentProfile.getHeaddress()?.cover, currentReceiver.headdressCover, ]) ?? "", ), style: RoomCpInviteDialogStyle.incoming, countdownSeconds: _cpInviteCountdownSeconds(invite), descriptionText: _cpInviteDescriptionText(relationType), onAccept: () { unawaited( _processIncomingCpInvite(context, applyId, invite, true, data: data), ); }, onReject: () { unawaited( _processIncomingCpInvite(context, applyId, invite, false, data: data), ); }, onClose: () { _cpInviteLog('close incoming CP_BUILD applyId=$applyId'); }, onTimeout: () { _cpInviteLog('timeout incoming CP_BUILD applyId=$applyId'); }, ); } Map? _decodeCpSystemMessageData(V2TimMessage? message) { final content = message?.customElem?.data; if (content == null || content.trim().isEmpty) { return null; } try { final decoded = jsonDecode(content); if (decoded is Map) { return decoded.map((key, value) => MapEntry(key.toString(), value)); } } catch (error) { _cpInviteLog('decode CP system message failed error=$error raw=$content'); } return null; } SCSystemInvitMessageRes? _cpInviteFromMessageData(Map data) { try { return SCSystemInvitMessageRes.fromMessageData(data); } catch (error) { _cpInviteLog('parse CP_BUILD content failed error=$error data=$data'); } return null; } String _cpInviteApplyIdFromData(Map data) { final content = _cpInviteContentMap(data); return _firstCpInviteValue([ data["expand"]?.toString(), data["applyId"]?.toString(), data["apply_id"]?.toString(), data["id"]?.toString(), content["expand"]?.toString(), content["applyId"]?.toString(), content["apply_id"]?.toString(), content["id"]?.toString(), ]) ?? ""; } Future _isCpInviteApplyWaitingForAutoPopup(String applyId) async { try { final status = await SCAccountRepository().cpRelationshipApplyStatus( applyId, ); final normalizedStatus = status?.trim().toUpperCase() ?? ""; if (normalizedStatus == "WAIT") { return true; } _cpInviteLog( 'skip CP_BUILD reason=apply_status_not_wait ' 'applyId=$applyId status=$normalizedStatus', ); return false; } catch (error) { _cpInviteLog( 'skip CP_BUILD reason=apply_status_query_failed ' 'applyId=$applyId error=$error', ); return false; } } bool _handleCpInviteResultMessage( Map data, { String? msgId, }) { final result = _cpInviteResultFromMessageData(data); if (result == null) { return false; } final applyId = _cpInviteApplyIdFromData(data); final resultMessageKey = applyId.isNotEmpty ? applyId : msgId ?? ""; final resultKey = resultMessageKey.isNotEmpty ? '${result.name}_$resultMessageKey' : ""; if (resultKey.isNotEmpty && !_shownCpInviteResultMessageKeys.add(resultKey)) { _cpInviteLog('skip duplicated CP invite result key=$resultKey'); return true; } final invite = _cpInviteFromMessageData(data); final relationType = scNormalizeCpRelationType( _firstCpInviteValue([ invite?.relationType, data["relationType"]?.toString(), data["relation_type"]?.toString(), ]) ?? "", ); _cpInviteLog( 'handle CP invite result result=$result relationType=$relationType ' 'raw=$data', ); SmartDialog.dismiss(tag: RoomCpInviteDialog.dialogTag); final context = navigatorKey.currentState?.context; if (context != null) { unawaited( Provider.of( context, listen: false, ).refreshLoadedCpProfiles(), ); } final currentProfile = AccountStorage().getCurrentUser()?.userProfile; if (context != null && invite != null && currentProfile != null) { if (_cpInviteTargetsCurrentAsSenderAndReceiver( invite, currentProfile, data, )) { _cpInviteLog( 'skip CP invite result reason=self_recipient applyId=$applyId ' 'result=${result.name}', ); return true; } final isSender = _cpInviteSenderMatchesCurrent( invite, currentProfile, data: data, ); if (result == _CpInviteProcessResult.rejected) { if (isSender) { _showCpInviteRejectedDialog(relationType, dedupKey: resultKey); } return true; } if (result == _CpInviteProcessResult.accepted) { if (isSender) { _dispatchCpRelationEstablishedRoomNotice( groupId: _currentVisibleVoiceRoomGroupId(context), applyId: applyId, data: data, invite: invite, currentProfile: currentProfile, ); } return true; } if (result == _CpInviteProcessResult.expired) { _cpInviteLog( 'skip CP invite expired result dialog applyId=$applyId ' 'relationType=$relationType', ); return true; } unawaited( _showCpInviteResultDialog( context: context, data: data, invite: invite, currentProfile: currentProfile, result: result, relationType: relationType, ), ); } else { SmartDialog.dismiss(tag: RoomCpInviteDialog.dialogTag); } return true; } bool _handleCpRelationRefreshNoticeMessage( Map data, { V2TimMessage? message, }) { final noticeType = _cpRelationNoticeTypeFromData(data); if (!_isCpRelationRefreshNoticeType(noticeType)) { return false; } _cpInviteLog( 'handle CP relation refresh notice noticeType=$noticeType raw=$data', ); final context = navigatorKey.currentState?.context; if (context != null) { unawaited( Provider.of( context, listen: false, ).refreshLoadedCpProfiles(), ); if (noticeType == SCSysytemMessageType.CP_DISMISS.name) { final currentUserId = AccountStorage().getCurrentUser()?.userProfile?.id?.trim() ?? ""; final peerUserId = _cpRelationNoticePeerIdFromData( data, message: message, ); if (currentUserId.isNotEmpty && peerUserId != null) { Provider.of( context, listen: false, )?.invalidateRoomMicRelationBetween( userId: currentUserId, peerUserId: peerUserId, relationType: _cpRelationTypeFromNoticeData(data), ); } } } return true; } Future _showCpInviteResultDialog({ required BuildContext context, required Map data, required SCSystemInvitMessageRes invite, required SocialChatUserProfile currentProfile, required _CpInviteProcessResult result, required String relationType, }) async { if (!_canAutoShowCpInviteDialog(context)) { _cpInviteLog( 'skip CP invite result dialog reason=not_in_visible_voice_room ' 'result=${result.name} relationType=$relationType', ); return; } final isSender = _cpInviteSenderMatchesCurrent( invite, currentProfile, data: data, ); final currentName = _cpInviteDisplayName( nickname: currentProfile.userNickname, account: currentProfile.account, fallback: currentProfile.id, ); final inviterName = _cpInviteDisplayName( nickname: invite.userNickname, account: invite.account, actualAccount: invite.actualAccount, ); _cpInviteLog( 'show CP invite result dialog result=${result.name} ' 'relationType=$relationType isSender=$isSender', ); final currentReceiver = _cpInviteReceiverFromMessageData(data); await RoomCpInviteDialog.show( context, inviter: RoomCpInviteDialogUser( name: isSender ? currentName : inviterName, avatarUrl: isSender ? currentProfile.userAvatar ?? "" : invite.userAvatar ?? "", headdress: isSender ? currentProfile.getHeaddress()?.sourceUrl ?? "" : invite.userHeaddress ?? "", headdressCover: isSender ? currentProfile.getHeaddress()?.cover ?? "" : invite.userHeaddressCover ?? "", ), receiver: isSender ? _cpInviteReceiverFromMessageData( data, fallbackName: _cpInviteReceiverFallbackName(data, null), fallbackAvatar: _cpInviteReceiverFallbackAvatar(data, null), ) : RoomCpInviteDialogUser( name: currentName, avatarUrl: _firstCpInviteValue([ currentProfile.userAvatar, currentReceiver.avatarUrl, ]) ?? "", headdress: _firstCpInviteValue([ currentProfile.getHeaddress()?.sourceUrl, currentReceiver.headdress, ]) ?? "", headdressCover: _firstCpInviteValue([ currentProfile.getHeaddress()?.cover, currentReceiver.headdressCover, ]) ?? "", ), style: isSender ? RoomCpInviteDialogStyle.waiting : RoomCpInviteDialogStyle.incoming, actionState: _cpInviteActionStateForResult(result), countdownSeconds: _cpInviteCountdownSeconds(invite), descriptionText: isSender ? _cpInviteWaitingDescriptionText(relationType) : _cpInviteDescriptionText(relationType), ); } RoomCpInviteActionState _cpInviteActionStateForResult( _CpInviteProcessResult result, ) { switch (result) { case _CpInviteProcessResult.accepted: return RoomCpInviteActionState.accepted; case _CpInviteProcessResult.rejected: return RoomCpInviteActionState.rejected; case _CpInviteProcessResult.expired: return RoomCpInviteActionState.expired; } } _CpInviteProcessResult? _cpInviteResultFromMessageData( Map data, ) { final content = _cpInviteContentMap(data); final values = [ data["noticeType"]?.toString() ?? "", data["type"]?.toString() ?? "", data["noticeAction"]?.toString() ?? "", data["action"]?.toString() ?? "", data["status"]?.toString() ?? "", data["applyStatus"]?.toString() ?? "", data["processStatus"]?.toString() ?? "", data["result"]?.toString() ?? "", data["state"]?.toString() ?? "", content["noticeType"]?.toString() ?? "", content["type"]?.toString() ?? "", content["noticeAction"]?.toString() ?? "", content["action"]?.toString() ?? "", content["status"]?.toString() ?? "", content["applyStatus"]?.toString() ?? "", content["processStatus"]?.toString() ?? "", content["result"]?.toString() ?? "", content["state"]?.toString() ?? "", ].map((value) => value.trim().toUpperCase()).toList(); final expiredValue = _cpInviteBoolValue([ data["expired"], data["isExpired"], content["expired"], content["isExpired"], ]); if (expiredValue == true) { return _CpInviteProcessResult.expired; } final agreeValue = _cpInviteBoolValue([ data["agree"], data["accepted"], data["isAgree"], content["agree"], content["accepted"], content["isAgree"], ]); if (agreeValue == true) { return _CpInviteProcessResult.accepted; } if (agreeValue == false) { return _CpInviteProcessResult.rejected; } final joined = values.join("|"); if (joined.contains("TIMEOUT") || joined.contains("EXPIRE") || joined.contains("OVERDUE")) { return _CpInviteProcessResult.expired; } if (joined.contains("REJECT") || joined.contains("DECLIN") || joined.contains("REFUSE") || joined.contains("DENY")) { return _CpInviteProcessResult.rejected; } if (joined.contains("ACCEPT") || joined.contains("AGREE") || joined.contains("APPROVE") || joined.contains("ESTABLISH")) { return _CpInviteProcessResult.accepted; } return null; } bool? _cpInviteBoolValue(Iterable values) { for (final value in values) { if (value is bool) { return value; } final text = value?.toString().trim().toLowerCase() ?? ""; if (text.isEmpty) { continue; } if (text == "true" || text == "1" || text == "yes" || text == "agree") { return true; } if (text == "false" || text == "0" || text == "no" || text == "reject" || text == "decline" || text == "refuse") { return false; } } return null; } Map _cpInviteContentMap(Map data) { final content = data["content"]; try { if (content is String && content.trim().isNotEmpty) { final decoded = jsonDecode(content); if (decoded is Map) { return decoded.map((key, value) => MapEntry(key.toString(), value)); } } if (content is Map) { return content.map((key, value) => MapEntry(key.toString(), value)); } } catch (_) {} return const {}; } Future _processIncomingCpInvite( BuildContext context, String applyId, SCSystemInvitMessageRes invite, bool agree, { Map? data, }) async { _cpInviteLog('process CP_BUILD applyId=$applyId agree=$agree'); final profileManager = Provider.of( context, listen: false, ); final voiceRoomGroupId = _currentVisibleVoiceRoomGroupId(context); final success = await profileManager.cpRlationshipProcessApply( context, applyId, agree, ); if (!success) { _cpInviteLog( 'process CP_BUILD done applyId=$applyId agree=$agree success=$success', ); return; } _cpInviteLog( 'process CP_BUILD submitted applyId=$applyId agree=$agree ' 'wait_backend_result=true', ); SmartDialog.dismiss(tag: RoomCpInviteDialog.dialogTag); unawaited(profileManager.refreshLoadedCpProfiles()); if (agree) { _dispatchCpRelationEstablishedRoomNotice( groupId: voiceRoomGroupId, applyId: applyId, data: data, invite: invite, currentProfile: AccountStorage().getCurrentUser()?.userProfile, ); } else { _dispatchCpInviteRejectedRoomNotice( groupId: voiceRoomGroupId, applyId: applyId, data: data, invite: invite, currentProfile: AccountStorage().getCurrentUser()?.userProfile, ); } } Future sendCpRelationEstablishedSystemMessage({ required String applyId, required SCSystemInvitMessageRes invite, SocialChatUserProfile? currentProfile, }) async { _cpInviteLog( 'skip client established C2C send applyId=$applyId ' 'reason=backend_dispatches_cp_invite_results', ); } void _dispatchCpRelationEstablishedRoomNotice({ required String groupId, required String applyId, required SCSystemInvitMessageRes invite, required SocialChatUserProfile? currentProfile, Map? data, }) { if (currentProfile == null) { return; } if (groupId.isEmpty) { _cpInviteLog( 'skip room established notice reason=no_visible_voice_room ' 'applyId=$applyId', ); return; } final isSender = _cpInviteSenderMatchesCurrent( invite, currentProfile, data: data, ); final inviter = isSender ? currentProfile : _cpInviteInviterProfile(invite); final receiver = isSender ? _cpInviteReceiverProfile(data) : currentProfile; final relationType = scNormalizeCpRelationType( _firstCpInviteValue([ invite.relationType, data == null ? null : data["relationType"]?.toString(), data == null ? null : data["relation_type"]?.toString(), ]) ?? "", ); final targetUserIds = _compactCpNoticeTargetIds([inviter, receiver]); _cpInviteLog( 'dispatch room established notice applyId=$applyId ' 'relationType=$relationType groupId=$groupId ' 'isSender=$isSender targets=${targetUserIds.join(",")}', ); unawaited( dispatchMessage( Msg( groupId: groupId, msg: scBuildCpRelationEstablishedNotice( userName1: _roomMsgUserDisplayName(inviter), userName2: _roomMsgUserDisplayName(receiver), relationType: relationType, ), type: SCRoomMsgType.cpSystemNotice, noticeAction: "RELATION_ESTABLISHED", user: receiver, toUser: inviter, targetUserIds: targetUserIds, giftBatchId: applyId, role: relationType, ), addLocal: true, ), ); } void _dispatchCpInviteRejectedRoomNotice({ required String groupId, required String applyId, required SCSystemInvitMessageRes invite, required SocialChatUserProfile? currentProfile, Map? data, }) { if (currentProfile == null) { return; } if (groupId.isEmpty) { _cpInviteLog( 'skip room rejected notice reason=no_visible_voice_room ' 'applyId=$applyId', ); return; } final inviter = _cpInviteInviterProfile(invite); final targetUserIds = _compactCpNoticeTargetIds([inviter]); final relationType = scNormalizeCpRelationType( _firstCpInviteValue([ invite.relationType, data == null ? null : data["relationType"]?.toString(), data == null ? null : data["relation_type"]?.toString(), ]) ?? "", ); _cpInviteLog( 'dispatch room rejected notice applyId=$applyId ' 'relationType=$relationType groupId=$groupId ' 'targets=${targetUserIds.join(",")}', ); unawaited( dispatchMessage( Msg( groupId: groupId, msg: "", type: SCRoomMsgType.cpSystemNotice, noticeAction: "RELATION_REJECTED", user: currentProfile, toUser: inviter, targetUserIds: targetUserIds, giftBatchId: applyId, role: relationType, ), addLocal: true, ), ); } String _currentVisibleVoiceRoomGroupId(BuildContext context) { final rtcProvider = Provider.of( context, listen: false, ); if (!rtcProvider.isVoiceRoomRouteVisible) { return ""; } return (rtcProvider.currenRoom?.roomProfile?.roomProfile?.roomAccount ?? "") .trim(); } bool _canAutoShowCpInviteDialog(BuildContext context) { if (!context.mounted) { return false; } try { final rtcProvider = Provider.of( context, listen: false, ); final roomProfile = rtcProvider.currenRoom?.roomProfile?.roomProfile; final roomId = roomProfile?.id?.trim() ?? ""; final groupId = roomProfile?.roomAccount?.trim() ?? ""; return rtcProvider.isVoiceRoomRouteVisible && (roomId.isNotEmpty || groupId.isNotEmpty); } catch (error) { _cpInviteLog( 'skip auto CP invite dialog reason=voice_room_state_unavailable ' 'error=$error', ); return false; } } SocialChatUserProfile _cpInviteInviterProfile( SCSystemInvitMessageRes invite, ) { final userId = _firstCpInviteBackendUserId([invite.actualAccount, invite.account]) ?? ""; return SocialChatUserProfile( id: userId, account: _firstCpInviteValue([invite.account, invite.actualAccount]) ?? userId, userNickname: _cpInviteDisplayName( nickname: invite.userNickname, account: invite.account, actualAccount: invite.actualAccount, fallback: userId, ), userAvatar: invite.userAvatar ?? "", ); } SocialChatUserProfile _cpInviteReceiverProfile(Map? data) { final receiverData = data == null ? const {} : Map.from(data); final receiver = _cpInviteReceiverFromMessageData(receiverData); final userId = _cpInviteReceiverIdFromMessageData(receiverData); return SocialChatUserProfile( id: userId, account: _firstCpInviteValue([ receiverData["acceptAccount"]?.toString(), receiverData["receiverAccount"]?.toString(), userId, ]) ?? userId, userNickname: receiver.name, userAvatar: receiver.avatarUrl, ); } List _compactCpNoticeTargetIds( Iterable profiles, ) { final result = []; for (final profile in profiles) { for (final value in [profile?.id, profile?.account]) { final text = value?.trim() ?? ""; if (scLooksLikeBackendUserId(text) && !result.contains(text)) { result.add(text); } } } return result; } Future sendCpInviteRejectedSystemMessage({ required String applyId, required SCSystemInvitMessageRes invite, SocialChatUserProfile? currentProfile, }) async { _cpInviteLog( 'skip client rejected C2C send applyId=$applyId ' 'reason=backend_dispatches_cp_invite_results', ); } String _cpInviteInviterIdFromMessageData(Map data) { final content = _cpInviteContentMap(data); return _firstCpInviteBackendUserId([ data["userId"]?.toString(), data["inviterUserId"]?.toString(), data["inviterId"]?.toString(), data["senderUserId"]?.toString(), data["senderId"]?.toString(), data["fromUserId"]?.toString(), data["applyUserId"]?.toString(), content["userId"]?.toString(), content["inviterUserId"]?.toString(), content["inviterId"]?.toString(), content["senderUserId"]?.toString(), content["senderId"]?.toString(), content["fromUserId"]?.toString(), content["applyUserId"]?.toString(), ]) ?? ""; } String _cpInviteReceiverIdFromMessageData(Map data) { final content = _cpInviteContentMap(data); return _firstCpInviteBackendUserId([ data["acceptUserId"]?.toString(), data["receiverId"]?.toString(), data["receiverUserId"]?.toString(), data["targetUserId"]?.toString(), data["toUserId"]?.toString(), content["acceptUserId"]?.toString(), content["receiverId"]?.toString(), content["receiverUserId"]?.toString(), content["targetUserId"]?.toString(), content["toUserId"]?.toString(), ]) ?? ""; } int _cpInviteCountdownSeconds(SCSystemInvitMessageRes invite) { final endTime = invite.dismissEndTime; if (endTime == null || endTime <= 0) { return 86399; } final endMs = endTime > 100000000000 ? endTime : endTime * 1000; final seconds = ((endMs - DateTime.now().millisecondsSinceEpoch) / 1000).floor(); if (seconds <= 0) { return 0; } return seconds > 86399 ? 86399 : seconds; } String _cpInviteDescriptionText(String relationType) { switch (scNormalizeCpRelationType(relationType)) { case 'BROTHER': return 'Invite you to become Brother'; case 'SISTERS': return 'Invite you to become Sister'; case 'CP': default: return 'Invite you to become Couple'; } } String _cpInviteWaitingDescriptionText(String relationType) { switch (scNormalizeCpRelationType(relationType)) { case 'BROTHER': return 'We invite you to become Brother'; case 'SISTERS': return 'We invite you to become Sister'; case 'CP': default: return 'We invite you to become Couple'; } } String _cpInviteDisplayName({ String? nickname, String? account, String? actualAccount, String? fallback, }) { for (final value in [nickname, actualAccount, account, fallback]) { final text = value?.trim() ?? ""; if (text.isNotEmpty) { return text; } } return "User"; } bool _cpInviteSenderMatchesCurrent( SCSystemInvitMessageRes invite, SocialChatUserProfile currentProfile, { Map? data, }) { final currentIds = [currentProfile.id, currentProfile.account] .map((value) => value?.trim() ?? "") .where((value) => value.isNotEmpty) .toSet(); if (data != null) { final receiverIds = _cpInviteReceiverIdentityValues(data); if (currentIds.any(receiverIds.contains)) { return false; } final explicitInviteIds = _cpInviteInviterIdentityValues(data); if (explicitInviteIds.isNotEmpty) { return currentIds.any(explicitInviteIds.contains); } } final inviteIds = [invite.actualAccount, invite.account] .map((value) => value?.trim() ?? "") .where((value) => value.isNotEmpty) .toSet(); return currentIds.any(inviteIds.contains); } bool _cpInviteTargetsCurrentAsSenderAndReceiver( SCSystemInvitMessageRes invite, SocialChatUserProfile currentProfile, Map data, ) { final currentIds = _cpInviteProfileIdentityValues(currentProfile); if (currentIds.isEmpty) { return false; } final inviterIds = { ..._cpInviteInviterIdentityValues(data), invite.actualAccount?.trim() ?? "", invite.account?.trim() ?? "", }..remove(""); final receiverIds = _cpInviteReceiverIdentityValues(data); return inviterIds.any(currentIds.contains) && receiverIds.any(currentIds.contains); } Set _cpInviteProfileIdentityValues(SocialChatUserProfile profile) { return { profile.id?.trim() ?? "", profile.account?.trim() ?? "", profile.getID().trim(), }..remove(""); } Set _cpInviteInviterIdentityValues(Map data) { final content = _cpInviteContentMap(data); final values = [ data["account"]?.toString(), data["actualAccount"]?.toString(), data["applyUserId"]?.toString(), data["applyUserAccount"]?.toString(), data["applyActualAccount"]?.toString(), data["applyUserActualAccount"]?.toString(), data["inviterActualAccount"]?.toString(), data["inviterUserId"]?.toString(), data["inviterId"]?.toString(), data["senderUserId"]?.toString(), data["senderId"]?.toString(), data["fromUserId"]?.toString(), content["applyUserId"]?.toString(), content["applyUserAccount"]?.toString(), content["applyActualAccount"]?.toString(), content["applyUserActualAccount"]?.toString(), content["inviterActualAccount"]?.toString(), content["inviterUserId"]?.toString(), content["inviterId"]?.toString(), content["senderUserId"]?.toString(), content["senderId"]?.toString(), content["fromUserId"]?.toString(), ]; return values.map((value) => value?.trim() ?? "").where((value) { return value.isNotEmpty; }).toSet(); } Set _cpInviteReceiverIdentityValues(Map data) { final content = _cpInviteContentMap(data); return [ data["acceptUserId"]?.toString(), data["acceptAccount"]?.toString(), data["acceptActualAccount"]?.toString(), data["receiverId"]?.toString(), data["receiverUserId"]?.toString(), data["receiverAccount"]?.toString(), data["receiverActualAccount"]?.toString(), data["targetUserId"]?.toString(), data["toUserId"]?.toString(), content["acceptUserId"]?.toString(), content["acceptAccount"]?.toString(), content["acceptActualAccount"]?.toString(), content["receiverId"]?.toString(), content["receiverUserId"]?.toString(), content["receiverAccount"]?.toString(), content["receiverActualAccount"]?.toString(), content["targetUserId"]?.toString(), content["toUserId"]?.toString(), ].map((value) => value?.trim() ?? "").where((value) { return value.isNotEmpty; }).toSet(); } RoomCpInviteDialogUser _cpInviteReceiverFromMessageData( Map data, { String? fallbackName, String? fallbackAvatar, }) { final content = _cpInviteContentMap(data); final name = _cpInviteDisplayName( nickname: _firstCpInviteValue([ data["acceptNickname"]?.toString(), data["acceptUserNickname"]?.toString(), data["receiverNickname"]?.toString(), data["receiverUserNickname"]?.toString(), content["acceptNickname"]?.toString(), content["acceptUserNickname"]?.toString(), content["receiverNickname"]?.toString(), content["receiverUserNickname"]?.toString(), ]), account: _firstCpInviteValue([ data["acceptAccount"]?.toString(), data["receiverAccount"]?.toString(), content["acceptAccount"]?.toString(), content["receiverAccount"]?.toString(), ]), actualAccount: _firstCpInviteValue([ data["acceptActualAccount"]?.toString(), data["receiverActualAccount"]?.toString(), content["acceptActualAccount"]?.toString(), content["receiverActualAccount"]?.toString(), ]), fallback: _firstCpInviteValue([ fallbackName, data["acceptUserId"]?.toString(), data["receiverId"]?.toString(), data["receiverUserId"]?.toString(), content["acceptUserId"]?.toString(), content["receiverId"]?.toString(), content["receiverUserId"]?.toString(), ]), ); return RoomCpInviteDialogUser( name: name, avatarUrl: _firstCpInviteValue([ data["acceptUserAvatar"]?.toString(), data["receiverAvatar"]?.toString(), data["receiverUserAvatar"]?.toString(), content["acceptUserAvatar"]?.toString(), content["receiverAvatar"]?.toString(), content["receiverUserAvatar"]?.toString(), fallbackAvatar, ]) ?? "", headdress: _firstCpInviteValue([ data["acceptHeaddress"]?.toString(), data["acceptAvatarFrame"]?.toString(), data["acceptAvatarFrameUrl"]?.toString(), data["receiverHeaddress"]?.toString(), data["receiverAvatarFrame"]?.toString(), data["receiverAvatarFrameUrl"]?.toString(), content["acceptHeaddress"]?.toString(), content["acceptAvatarFrame"]?.toString(), content["acceptAvatarFrameUrl"]?.toString(), content["receiverHeaddress"]?.toString(), content["receiverAvatarFrame"]?.toString(), content["receiverAvatarFrameUrl"]?.toString(), ]) ?? "", headdressCover: _firstCpInviteValue([ data["acceptHeaddressCover"]?.toString(), data["acceptAvatarFrameCover"]?.toString(), data["receiverHeaddressCover"]?.toString(), data["receiverAvatarFrameCover"]?.toString(), content["acceptHeaddressCover"]?.toString(), content["acceptAvatarFrameCover"]?.toString(), content["receiverHeaddressCover"]?.toString(), content["receiverAvatarFrameCover"]?.toString(), ]) ?? "", ); } String? _cpInviteReceiverFallbackName( Map data, V2TimMessage? message, ) { final receiverId = _cpInviteReceiverIdFromMessageData(data); final conversation = receiverId.isNotEmpty ? conversationMap["c2c_$receiverId"] : null; return _firstCpInviteValue([ conversation?.showName, message?.friendRemark, message?.nickName, message?.nameCard, receiverId, message?.userID, ]); } String? _cpInviteReceiverFallbackAvatar( Map data, V2TimMessage? message, ) { final receiverId = _cpInviteReceiverIdFromMessageData(data); final conversation = receiverId.isNotEmpty ? conversationMap["c2c_$receiverId"] : null; return _firstCpInviteValue([conversation?.faceUrl, message?.faceUrl]); } String? _firstCpInviteValue(Iterable values) { for (final value in values) { final text = value?.trim() ?? ""; if (text.isNotEmpty) { return text; } } return null; } String? _firstCpInviteBackendUserId(Iterable values) { return scFirstBackendUserId(values); } bool _isCpRelationInviteNoticeData(Map? data) { if (data == null) { return false; } final content = _cpInviteContentMap(data); final noticeType = _firstCpInviteValue([ data["noticeType"]?.toString(), data["type"]?.toString(), content["noticeType"]?.toString(), content["type"]?.toString(), ])?.toUpperCase() ?? ""; if (noticeType == SCSysytemMessageType.CP_BUILD.name || _isCpRelationRefreshNoticeType(noticeType)) { return true; } return _cpInviteApplyIdFromData(data).isNotEmpty && _cpInviteResultFromMessageData(data) != null; } String _cpRelationNoticeTypeFromData(Map data) { final content = _cpInviteContentMap(data); return (_firstCpInviteValue([ data["noticeType"]?.toString(), data["type"]?.toString(), content["noticeType"]?.toString(), content["type"]?.toString(), ]) ?? "") .toUpperCase(); } bool _isCpRelationRefreshNoticeType(String noticeType) { return noticeType == SCSysytemMessageType.CP_DISMISS.name || noticeType == SCSysytemMessageType.CP_BLESS.name; } String _cpRelationTypeFromNoticeData(Map data) { final content = _cpInviteContentMap(data); return scNormalizeCpRelationType( _firstCpInviteValue([ data["relationType"]?.toString(), data["relation_type"]?.toString(), content["relationType"]?.toString(), content["relation_type"]?.toString(), ]) ?? "CP", ); } String? _cpRelationNoticePeerIdFromData( Map data, { V2TimMessage? message, }) { if (!_isCpRelationRefreshNoticeType(_cpRelationNoticeTypeFromData(data))) { return null; } final content = _cpInviteContentMap(data); final currentProfile = AccountStorage().getCurrentUser()?.userProfile; final currentIds = [currentProfile?.id, currentProfile?.account] .map((value) => value?.trim() ?? "") .where((value) => value.isNotEmpty) .toSet(); final candidates = [ data["expand"]?.toString(), content["expand"]?.toString(), data["peerUserId"]?.toString(), data["cpUserId"]?.toString(), data["targetUserId"]?.toString(), data["senderUserId"]?.toString(), data["fromUserId"]?.toString(), data["userId"]?.toString(), content["peerUserId"]?.toString(), content["cpUserId"]?.toString(), content["targetUserId"]?.toString(), content["senderUserId"]?.toString(), content["fromUserId"]?.toString(), content["userId"]?.toString(), message?.sender, message?.userID, ]; for (final candidate in candidates) { final userId = candidate?.trim() ?? ""; if (userId.isEmpty || currentIds.contains(userId) || !scLooksLikeBackendUserId(userId)) { continue; } return userId; } return null; } void _cpInviteLog(String message) { debugPrint('[CP][Invite] $message'); } void _onRefreshConversation(List conversations) { for (V2TimConversation conversation in conversations) { conversationMap[conversation.conversationID] = conversation; _handleCpBuildFromConversation(conversation); } getConversationList(); } void _handleCpBuildFromConversation(V2TimConversation conversation) { final message = conversation.lastMessage; if (message == null || message.groupID != null) { return; } final data = _decodeCpSystemMessageData(message); if (!_isCpRelationInviteNoticeData(data)) { return; } unawaited(_handleIncomingCpBuildMessage(message)); } ///创建房间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.cpSystemNotice: 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.rocketLaunchNotice: return true; case SCRoomMsgType.cpSystemNotice: return msg.noticeAction != "RELATION_REJECTED"; case SCRoomMsgType.emoticons: return (msg.number ?? -1) < 0; default: return false; } } addMsg(Msg msg) { _scheduleRoomRocketStatusRefreshForGiftMessage(msg); _scheduleCpRelationRefreshForGiftMessage(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)) { return; } if (msg.type == SCRoomMsgType.rocketLaunchNotice && _hasRoomRocketNoticeMessage(msg)) { return; } if (msg.type == SCRoomMsgType.cpSystemNotice && msg.noticeAction == "RELATION_REJECTED") { if (_isDuplicateCpRelationNotice(msg)) { return; } _showCpInviteRejectedDialogIfNeeded(msg); return; } if (msg.type == SCRoomMsgType.cpSystemNotice && _isDuplicateCpRelationNotice(msg)) { return; } roomAllMsgList.insert(0, msg); if (roomAllMsgList.length > 250) { roomAllMsgList.removeAt(roomAllMsgList.length - 1); } msgAllListener?.call(msg); _enqueueCpRelationBroadcastFloatingIfNeeded(msg); _showCpRelationFormedDialogIfNeeded(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 (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.cpSystemNotice) { roomChatMsgList.insert(0, msg); if (roomChatMsgList.length > 250) { roomChatMsgList.removeAt(roomChatMsgList.length - 1); } msgChatListener?.call(msg); roomGiftMsgList.insert(0, msg); if (roomGiftMsgList.length > 250) { roomGiftMsgList.removeAt(roomGiftMsgList.length - 1); } msgGiftListener?.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 _enqueueCpRelationBroadcastFloatingIfNeeded(Msg msg) { if (msg.type != SCRoomMsgType.cpSystemNotice) { return; } final action = msg.noticeAction?.trim() ?? ""; if (action != "RELATION_ESTABLISHED" && action != "RELATION_JOINED_ROOM") { return; } if (!_isLiveCpRelationBroadcastMessage(msg) || !_shouldShowCpRelationBroadcastFloating(msg.groupId ?? "")) { return; } final relationType = scNormalizeCpRelationType(msg.role); final isEntry = action == "RELATION_JOINED_ROOM"; final firstProfile = isEntry ? msg.user : (msg.toUser ?? msg.user); final secondProfile = isEntry ? msg.toUser : (msg.user ?? msg.toUser); OverlayManager().addMessage( SCFloatingMessage( type: scCpRelationBroadcastFloatingType, priority: 970, roomId: msg.groupId ?? "", userId: _roomMsgUserId(firstProfile), toUserId: _roomMsgUserId(secondProfile), userAvatarUrl: firstProfile?.userAvatar ?? "", toUserAvatarUrl: secondProfile?.userAvatar ?? "", userName: _roomMsgUserDisplayName(firstProfile), toUserName: _roomMsgUserDisplayName(secondProfile), giftUrl: scCpRelationBroadcastAsset(relationType), giftId: action, giftTab: relationType, displayText: msg.msg ?? "", durationSeconds: 5, ), ); } bool _isDuplicateCpRelationNotice(Msg msg) { if (msg.type != SCRoomMsgType.cpSystemNotice) { return false; } final action = msg.noticeAction?.trim() ?? ""; if (action != "RELATION_ESTABLISHED" && action != "RELATION_JOINED_ROOM" && action != "RELATION_REJECTED") { return false; } final key = _cpRelationNoticeDedupKey(msg); if (key.isEmpty) { return false; } final now = DateTime.now().millisecondsSinceEpoch; _recentCpRelationNoticeTimes.removeWhere((_, timestamp) { return now - timestamp > 60000; }); final lastShownAt = _recentCpRelationNoticeTimes[key]; if (lastShownAt != null && now - lastShownAt < 60000) { _cpInviteLog('skip duplicated room CP notice key=$key'); return true; } _recentCpRelationNoticeTimes[key] = now; return false; } String _cpRelationNoticeDedupKey(Msg msg) { final action = msg.noticeAction?.trim() ?? ""; final relationType = scNormalizeCpRelationType(msg.role); return scBuildCpRelationNoticeDedupKey( action: action, groupId: msg.groupId, relationType: relationType, explicitKey: msg.giftBatchId, userIds: [_roomMsgUserId(msg.user), _roomMsgUserId(msg.toUser)], userNames: [ _roomMsgUserDisplayName(msg.user), _roomMsgUserDisplayName(msg.toUser), ], ); } bool _isLiveCpRelationBroadcastMessage(Msg msg) { final timestamp = msg.time ?? 0; if (timestamp <= 0) { return true; } return DateTime.now().millisecondsSinceEpoch - timestamp < 60000; } bool _shouldShowCpRelationBroadcastFloating(String groupId) { final normalizedGroupId = groupId.trim(); final currentContext = context; if (normalizedGroupId.isEmpty || currentContext == null || !currentContext.mounted || !_isCurrentVoiceRoomGroup(normalizedGroupId)) { return false; } final rtcProvider = Provider.of( currentContext, listen: false, ); return rtcProvider.shouldShowRoomVisualEffects && rtcProvider.isVoiceRoomRouteVisible; } String _roomMsgUserId(SocialChatUserProfile? profile) { return _firstCpInviteValue([profile?.id, profile?.account]) ?? ""; } void _showCpRelationFormedDialogIfNeeded(Msg msg) { if (msg.type != SCRoomMsgType.cpSystemNotice || msg.noticeAction != "RELATION_ESTABLISHED") { return; } final currentContext = context; if (currentContext == null || !_canAutoShowCpInviteDialog(currentContext)) { return; } final currentProfile = AccountStorage().getCurrentUser()?.userProfile; if (currentProfile == null) { return; } final targetIds = msg.targetUserIds ?.map((value) => value.trim()) .where((value) => value.isNotEmpty) .toSet() ?? const {}; final isTarget = _isSameRoomMsgUser(currentProfile, msg.user) || _isSameRoomMsgUser(currentProfile, msg.toUser) || targetIds.contains(currentProfile.id?.trim() ?? "") || targetIds.contains(currentProfile.account?.trim() ?? ""); if (!isTarget) { return; } SmartDialog.dismiss(tag: RoomCpInviteDialog.dialogTag); RoomCpRelationFormedEffect.show( leftName: _roomMsgUserDisplayName(msg.toUser), rightName: _roomMsgUserDisplayName(msg.user), leftAvatar: msg.toUser?.userAvatar ?? "", rightAvatar: msg.user?.userAvatar ?? "", relationType: msg.role ?? "", ); } void _showCpInviteRejectedDialogIfNeeded(Msg msg) { final currentProfile = AccountStorage().getCurrentUser()?.userProfile; if (currentProfile == null || _isSameRoomMsgUser(currentProfile, msg.user)) { return; } final targetIds = msg.targetUserIds ?.map((value) => value.trim()) .where((value) => value.isNotEmpty) .toSet() ?? const {}; final isTarget = _isSameRoomMsgUser(currentProfile, msg.toUser) || targetIds.contains(currentProfile.id?.trim() ?? "") || targetIds.contains(currentProfile.account?.trim() ?? ""); if (!isTarget) { return; } SmartDialog.dismiss(tag: RoomCpInviteDialog.dialogTag); _showCpInviteRejectedDialog( msg.role ?? "", dedupKey: _cpRelationRejectedDialogDedupKey(msg), ); } String _cpRelationRejectedDialogDedupKey(Msg msg) { final explicitKey = msg.giftBatchId?.trim() ?? ""; if (explicitKey.isNotEmpty) { return "rejected_$explicitKey"; } return _cpRelationNoticeDedupKey(msg); } void _showCpInviteRejectedDialog(String relationType, {String? dedupKey}) { final context = navigatorKey.currentState?.context; if (context == null || !_canAutoShowCpInviteDialog(context)) { _cpInviteLog( 'skip rejected dialog reason=not_in_visible_voice_room ' 'relationType=$relationType', ); return; } final key = dedupKey?.trim() ?? ""; if (key.isNotEmpty && !_shownCpInviteRejectedDialogKeys.add(key)) { _cpInviteLog('skip duplicated rejected dialog key=$key'); return; } final localizations = SCAppLocalizations.of(context); SmartDialog.show( tag: "showConfirmDialog", alignment: Alignment.center, debounce: true, animationType: SmartAnimationType.fade, builder: (_) { return MsgDialog( title: localizations?.tips ?? "Tips", msg: localizations?.cpInviteRejectedByOther( _cpInviteLocalizedRelationName(context, relationType), ) ?? "The other party declined to become ${_cpInviteLocalizedRelationName(context, relationType)}.", btnText: localizations?.confirm ?? "Confirm", showCancel: false, onEnsure: () {}, ); }, ); } String _cpInviteLocalizedRelationName( BuildContext context, String relationType, ) { final localizations = SCAppLocalizations.of(context); switch (scNormalizeCpRelationType(relationType)) { case "BROTHER": return localizations?.cpRelationBrother ?? "Brother"; case "SISTERS": return localizations?.cpRelationSister ?? "Sister"; case "CP": default: return localizations?.cpRelationCp ?? "CP"; } } bool _isSameRoomMsgUser( SocialChatUserProfile currentProfile, SocialChatUserProfile? targetProfile, ) { if (targetProfile == null) { return false; } final currentId = currentProfile.id?.trim() ?? ""; final targetId = targetProfile.id?.trim() ?? ""; if (currentId.isNotEmpty && targetId.isNotEmpty && currentId == targetId) { return true; } final currentAccount = currentProfile.account?.trim() ?? ""; final targetAccount = targetProfile.account?.trim() ?? ""; return currentAccount.isNotEmpty && targetAccount.isNotEmpty && currentAccount == targetAccount; } String _roomMsgUserDisplayName(SocialChatUserProfile? profile) { for (final value in [ profile?.userNickname, profile?.account, profile?.id, ]) { final text = value?.trim() ?? ""; if (text.isNotEmpty) { return text; } } return "User"; } 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 _scheduleCpRelationRefreshForGiftMessage(Msg msg) { if (!_isCpRelationGiftMessage(msg)) { return; } final now = DateTime.now().millisecondsSinceEpoch; if (now - _lastCpRelationGiftRefreshAt < _cpRelationGiftRefreshMinIntervalMs) { return; } _lastCpRelationGiftRefreshAt = now; final userIds = Set.unmodifiable(_cpRelationGiftUserIds(msg)); for (final delay in _cpRelationGiftRefreshDelays) { unawaited( Future.delayed(delay, () { _refreshCpRelationDataAfterGift(userIds); }), ); } } bool _isCpRelationGiftMessage(Msg msg) { if (msg.type != SCRoomMsgType.gift && msg.type != SCRoomMsgType.luckGiftAnimOther) { return false; } return scIsCpGift(msg.gift); } Set _cpRelationGiftUserIds(Msg msg) { return { msg.user?.id?.trim() ?? '', msg.toUser?.id?.trim() ?? '', ...?msg.targetUserIds?.map((userId) => userId.trim()), }..removeWhere((userId) => userId.isEmpty); } void _refreshCpRelationDataAfterGift(Set userIds) { final currentContext = context?.mounted == true ? context : navigatorKey.currentState?.context; if (currentContext == null || !currentContext.mounted) { return; } try { unawaited( Provider.of( currentContext, listen: false, ).refreshLoadedCpProfiles(), ); } catch (_) {} try { Provider.of( currentContext, listen: false, ).refreshRoomMicRelationProfilesForUsers(userIds); } catch (_) {} } 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 _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, ); } 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 _handleGameWinBroadcast( dynamic rawData, { required bool isRegionBroadcast, }) { if (SCGlobalConfig.isReview) { return; } final wrapper = _broadcastPayloadMap(rawData); final payload = _broadcastPayloadMap(wrapper['data'] ?? rawData); if (payload.isEmpty) { return; } final winCoins = _payloadNum(payload['currencyDiff']) ?? _payloadNum(payload['winAmount']) ?? _payloadNum(payload['awardAmount']) ?? 0; if (winCoins <= 0) { return; } final roomId = _payloadRoomId(payload); final userId = _firstNonBlank([ _payloadText(payload['account']), _payloadText(payload['actualAccount']), _payloadText(payload['userId']), _payloadText(payload['sendUserId']), _payloadText(payload['senderUserId']), ]); final userName = _firstNonBlank([ _payloadText(payload['userNickname']), _payloadText(payload['nickname']), _payloadText(payload['userName']), userId, ]); final gameId = _firstNonBlank([ _payloadText(payload['currentGameId']), _payloadText(payload['gameId']), _payloadText(payload['providerGameId']), _payloadText(payload['vendorGameId']), _payloadText(payload['currentProviderGameId']), _payloadText(payload['currentVendorGameId']), ]); final gameName = _firstNonBlank([ _payloadText(payload['currentGameName']), _payloadText(payload['gameName']), _payloadText(payload['name']), 'game', ]); final gameIconUrl = _firstNonBlank([ _payloadAssetText(payload['gameUrl']), _payloadAssetText(payload['currentGameCover']), _payloadAssetText(payload['gameCover']), _payloadAssetText(payload['cover']), _payloadAssetText(payload['iconUrl']), _payloadAssetText(payload['icon']), ]); final roundId = _firstNonBlank([ _payloadText(payload['gameRoundId']), _payloadText(payload['game_round_id']), _payloadText(payload['roundId']), _payloadText(payload['roundNo']), ]); OverlayManager().addMessage( SCFloatingMessage( type: 2, userId: userId, userAvatarUrl: _payloadAssetText(payload['userAvatar']), userName: userName, toUserName: gameName, giftUrl: gameIconUrl, giftId: roundId, gameId: gameId, roomId: roomId, coins: winCoins, multiple: _payloadNum(payload['multiple']) ?? _payloadNum(payload['winMultiple']), displayText: _payloadText(payload['msg']), broadcastScope: isRegionBroadcast ? SCFloatingMessage.broadcastScopeRegion : '', priority: 1000, ), ); } 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 giftTab = _resolveRegionGiftTab(payload); final message = SCFloatingMessage( type: 1, roomId: roomId, userId: senderId, toUserId: toUserId, userName: senderName, toUserName: receiverName, userAvatarUrl: _payloadText(payload['userAvatar']), giftUrl: giftUrl, giftId: giftId, giftTab: giftTab, 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; } String _resolveRegionGiftTab(Map payload) { final payloadGiftTab = _payloadText(payload['giftTab']); final payloadRelationType = _firstNonBlank([ _payloadText(payload['relationType']), _payloadText(payload['cpRelationType']), ]); if (payloadGiftTab.isNotEmpty) { final gift = SocialChatGiftRes( giftTab: payloadGiftTab, relationType: payloadRelationType, ); return _floatingGiftTabFor(gift); } if (payloadRelationType.isNotEmpty) { final gift = SocialChatGiftRes(relationType: payloadRelationType); if (scIsCpGift(gift)) { return _floatingGiftTabFor(gift); } } final giftId = _payloadText(payload['giftId']); final currentContext = context; if (giftId.isEmpty || currentContext == null || !currentContext.mounted) { return ''; } final appGeneralManager = Provider.of( currentContext, listen: false, ); return _floatingGiftTabFor( appGeneralManager.getGiftByIdOrStandardId(giftId), ); } String _floatingGiftTabFor(SocialChatGiftRes? gift) { if (gift == null) { return ''; } if (scIsCpGift(gift)) { return scCpRelationTypeForGift(gift); } return gift.giftTab ?? ''; } 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; } bool _isRoomRocketStatusUpdateType(dynamic type) { final value = type?.toString().trim(); return value == SCRoomMsgType.voiceRoomRocketStatusUpdate || value == SCRoomMsgType.rocketEnergyUpdate || value == 'ROCKET_ENERGY_UPDATE'; } 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); } } rtcProvider.updateRoomRocketStatusFromRealtime( SCRoomRocketStatusRes.fromJson(data), roomId: roomId, ); rtcProvider.scheduleRoomRocketStatusRefreshForGift(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, allowEmptyPopup: true, ); return; } _pendingRoomRocketRewardPopups.remove(resolvedRoomId); _recentRoomRocketRewardProbeTimes.remove(resolvedRoomId); unawaited( _showRoomRocketRewardPopupDialog( resolvedRoomId, initialRoomRecords: initialRoomRecords, allowEmptyPopup: true, ), ); } bool _isCurrentUserInRoomRocketRewardPopup(Map payload) { final profile = AccountStorage().getCurrentUser()?.userProfile; final currentKeys = { profile?.id?.trim() ?? '', profile?.account?.trim() ?? '', }..removeWhere((item) => item.isEmpty); return roomRocketRewardPopupTargetsCurrentUser( payload: payload, currentUserKeys: currentKeys, ); } void queryVoiceRoomRocketRewardPopups(String roomId) { handleVoiceRoomReadyForRocketRewards(roomId, forceQuery: true); } void handleVoiceRoomReadyForRocketRewards( String roomId, { int attempt = 0, bool forceQuery = false, }) { 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 && !forceQuery) { return; } if (!_isCurrentVisibleVoiceRoom(normalizedRoomId)) { _scheduleRoomRocketReadyRewardRetry( normalizedRoomId, attempt, forceQuery: forceQuery, ); return; } final resumedLaunch = _resumeRoomRocketLaunchForCurrentRoom( normalizedRoomId, ); final pendingAfterResume = _pendingRoomRocketRewardPopups[normalizedRoomId]; final hasProbeAfterResume = _recentRoomRocketRewardProbeTimes[normalizedRoomId] != null; final allowEmptyPopup = pendingAfterResume != null || hasProbeAfterResume; if (pendingAfterResume == null && !hasProbeAfterResume && !forceQuery) { return; } if (!resumedLaunch && pendingAfterResume == null && attempt == 0 && !forceQuery) { _scheduleRoomRocketReadyRewardRetry( normalizedRoomId, attempt, forceQuery: forceQuery, ); return; } _pendingRoomRocketRewardPopups.remove(normalizedRoomId); _recentRoomRocketRewardProbeTimes.remove(normalizedRoomId); _roomRocketRoomReadyRewardRetryTimer?.cancel(); _roomRocketRoomReadyRewardRetryTimer = null; if (_isRoomRocketRewardPopupHeld(normalizedRoomId)) { _scheduleRoomRocketRewardPopupAfterHold( normalizedRoomId, initialRoomRecords: pending?.initialRoomRecords ?? const [], allowEmptyPopup: allowEmptyPopup, ); return; } unawaited( _showRoomRocketRewardPopupDialog( normalizedRoomId, initialRoomRecords: pendingAfterResume?.initialRoomRecords ?? const [], allowEmptyPopup: allowEmptyPopup, ), ); } void _scheduleRoomRocketReadyRewardRetry( String roomId, int attempt, { bool forceQuery = false, }) { if (attempt >= _roomRocketRoomReadyRewardRetryDelays.length) { return; } _roomRocketRoomReadyRewardRetryTimer?.cancel(); _roomRocketRoomReadyRewardRetryTimer = Timer( _roomRocketRoomReadyRewardRetryDelays[attempt], () { _roomRocketRoomReadyRewardRetryTimer = null; handleVoiceRoomReadyForRocketRewards( roomId, attempt: attempt + 1, forceQuery: forceQuery, ); }, ); } 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 existingRecords = existing?.initialRoomRecords ?? const []; final records = initialRoomRecords.isNotEmpty ? _mergeRoomRocketRewardRecords(existingRecords, initialRoomRecords) : existingRecords; _pendingRoomRocketRewardPopups[normalizedRoomId] = _PendingRoomRocketRewardPopup( roomId: normalizedRoomId, receivedAt: now, initialRoomRecords: records, ); } List _mergeRoomRocketRewardRecords( List existingRecords, List incomingRecords, ) { if (existingRecords.isEmpty) { return List.unmodifiable(incomingRecords); } if (incomingRecords.isEmpty) { return existingRecords; } final merged = [...existingRecords]; final seen = existingRecords.map(_roomRocketRewardRecordCacheKey).toSet(); for (final record in incomingRecords) { if (seen.add(_roomRocketRewardRecordCacheKey(record))) { merged.add(record); } } return List.unmodifiable(merged); } String _roomRocketRewardRecordCacheKey(SCRoomRocketRewardRecordRes record) { final id = record.id.trim(); if (id.isNotEmpty) { return 'id:$id'; } return [ record.launchNo.trim(), record.roomId.trim(), record.roundNo.toString(), record.level.toString(), record.userId.trim(), record.rewardScene.trim().toUpperCase(), record.rewardType.trim().toUpperCase(), record.rewardItemId.trim(), record.rewardName.trim(), record.rewardAmount.toString(), record.grantTime.trim(), record.createTime.trim(), ].join('|'); } 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 [], bool allowEmptyPopup = false, }) { 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, allowEmptyPopup: allowEmptyPopup, ); return; } if (!_isCurrentVisibleVoiceRoom(normalizedRoomId)) { _scheduleRoomRocketReadyRewardRetry(normalizedRoomId, 0); return; } final pending = _pendingRoomRocketRewardPopups.remove(normalizedRoomId); _recentRoomRocketRewardProbeTimes.remove(normalizedRoomId); unawaited( _showRoomRocketRewardPopupDialog( normalizedRoomId, initialRoomRecords: pending?.initialRoomRecords ?? const [], allowEmptyPopup: allowEmptyPopup, ), ); }, ); _roomRocketRewardDebug( 'defer popup roomId=$normalizedRoomId delayMs=$delayMs ' 'initialRoomRecords=${initialRoomRecords.length}', ); } bool _markRoomRocketEmptyRewardPopupForShow( String roomId, List roomRecords, ) { final normalizedRoomId = roomId.trim(); if (normalizedRoomId.isEmpty) { return false; } final now = DateTime.now().millisecondsSinceEpoch; _pruneRoomRocketRewardFallbacks(now); final key = _roomRocketEmptyRewardPopupKey(normalizedRoomId, roomRecords); if (_shownRoomRocketEmptyRewardPopupTimes.containsKey(key)) { return false; } _shownRoomRocketEmptyRewardPopupTimes[key] = now; return true; } String _roomRocketEmptyRewardPopupKey( String roomId, List roomRecords, ) { final launchNos = roomRecords .map((record) => record.launchNo.trim()) .where((launchNo) => launchNo.isNotEmpty) .toSet() .toList() ..sort(); if (launchNos.isNotEmpty) { return 'room_rocket_empty_reward|$roomId|launch:${launchNos.join(",")}'; } final roundKeys = roomRecords .map((record) { final level = record.level > 0 ? record.level.toString() : ''; final round = record.roundNo > 0 ? record.roundNo.toString() : ''; return [level, round].where((item) => item.isNotEmpty).join(':'); }) .where((key) => key.isNotEmpty) .toSet() .toList() ..sort(); if (roundKeys.isNotEmpty) { return 'room_rocket_empty_reward|$roomId|round:${roundKeys.join(",")}'; } return 'room_rocket_empty_reward|$roomId|recent'; } void _pruneRoomRocketRewardFallbacks(int now) { _pendingRoomRocketRewardPopups.removeWhere( (_, pending) => now - pending.receivedAt > _roomRocketRewardPendingTtlMs, ); _recentRoomRocketRewardProbeTimes.removeWhere( (_, timestamp) => now - timestamp > _roomRocketRewardProbeTtlMs, ); _shownRoomRocketEmptyRewardPopupTimes.removeWhere( (_, timestamp) => now - timestamp > _roomRocketEmptyRewardPopupRecentTtlMs, ); } void _clearRoomRocketRewardFallbackState() { _pendingRoomRocketRewardPopups.clear(); _recentRoomRocketRewardProbeTimes.clear(); _shownRoomRocketEmptyRewardPopupTimes.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.advanceRoomRocketStatusAfterLaunch(launch); rtcProvider.handleRoomRocketLaunchBroadcast(launch); if (shouldFetchReward) { _scheduleRoomRocketRewardPopupAfterHold( launch.roomId, initialRoomRecords: pending?.initialRoomRecords ?? const [], allowEmptyPopup: true, ); } } 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 [], bool allowEmptyPopup = false, }) async { final normalizedRoomId = roomId.trim(); if (normalizedRoomId.isEmpty || !_isCurrentVisibleVoiceRoom(normalizedRoomId)) { return; } if (_isRoomRocketRewardPopupHeld(normalizedRoomId)) { _scheduleRoomRocketRewardPopupAfterHold( normalizedRoomId, initialRoomRecords: initialRoomRecords, allowEmptyPopup: allowEmptyPopup, ); return; } if (SCRoomTopLayerGuard().isActive) { SCRoomTopLayerGuard().runWhenInactive(() { unawaited( _showRoomRocketRewardPopupDialog( normalizedRoomId, attempt: attempt, initialRoomRecords: initialRoomRecords, allowEmptyPopup: allowEmptyPopup, ), ); }); return; } if (!_roomRocketRewardPopupLoadingRoomIds.add(normalizedRoomId)) { return; } try { final res = await SCChatRoomRepository().roomRocketRewardPopups( normalizedRoomId, ); if (!_isCurrentVisibleVoiceRoom(normalizedRoomId)) { return; } if (res.records.isEmpty && initialRoomRecords.isEmpty && attempt < _roomRocketRewardPopupRetryDelays.length) { _scheduleRoomRocketRewardPopupRetry( normalizedRoomId, attempt, initialRoomRecords: initialRoomRecords, allowEmptyPopup: allowEmptyPopup, ); return; } final popupRecords = SCRoomRocketRewardPopupAck.filterUnshownRecords( res.records, ); if (res.records.isNotEmpty && popupRecords.isEmpty) { return; } if (popupRecords.isEmpty && initialRoomRecords.isEmpty && !allowEmptyPopup) { return; } if (popupRecords.isEmpty && allowEmptyPopup && !_markRoomRocketEmptyRewardPopupForShow( normalizedRoomId, initialRoomRecords, )) { return; } _roomRocketRewardPopupRetryTimer?.cancel(); _roomRocketRewardPopupRetryTimer = null; SCRoomRocketRewardPopupAck.markShownAndAck(popupRecords); final userProfileResolver = _roomRocketRewardUserProfileResolver(); final popupGroups = RoomRocketApiMapper.rewardPopupRecordGroups( popupRecords, ); final groupsToShow = popupGroups.isNotEmpty ? popupGroups : const >[ [], ]; _showRoomRocketRewardPopupGroups( roomId: normalizedRoomId, popupGroups: groupsToShow, groupIndex: 0, initialRoomRecords: initialRoomRecords, userProfileResolver: userProfileResolver, ); } catch (_) { _scheduleRoomRocketRewardPopupRetry( normalizedRoomId, attempt, initialRoomRecords: initialRoomRecords, allowEmptyPopup: allowEmptyPopup, ); } finally { _roomRocketRewardPopupLoadingRoomIds.remove(normalizedRoomId); } } void _showRoomRocketRewardPopupGroups({ required String roomId, required List> popupGroups, required int groupIndex, required List initialRoomRecords, required RoomRocketRewardUserProfileResolver? userProfileResolver, }) { if (!_isCurrentVisibleVoiceRoom(roomId) || groupIndex >= popupGroups.length) { return; } if (SCRoomTopLayerGuard().isActive) { SCRoomTopLayerGuard().runWhenInactive(() { _showRoomRocketRewardPopupGroups( roomId: roomId, popupGroups: popupGroups, groupIndex: groupIndex, initialRoomRecords: initialRoomRecords, userProfileResolver: userProfileResolver, ); }); return; } final popupRecords = popupGroups[groupIndex]; final roomRecords = RoomRocketApiMapper.rewardRoomRecordsForPopupGroup( initialRoomRecords, popupRecords, ); final nextGroupIndex = groupIndex + 1; SmartDialog.dismiss(tag: _roomRocketRewardDialogTag); SmartDialog.show( tag: _roomRocketRewardDialogTag, alignment: Alignment.center, debounce: true, animationType: SmartAnimationType.fade, maskColor: Colors.transparent, clickMaskDismiss: false, builder: (_) => SCRoomTopLayerSuppressor( reason: 'room_rocket_reward_dialog', keepDialogTags: const {_roomRocketRewardDialogTag}, child: RoomRocketRewardDialogLoader( roomId: roomId, initialPopupRecords: popupRecords, initialRoomRecords: roomRecords, userProfileResolver: userProfileResolver, onClose: () { SmartDialog.dismiss(tag: _roomRocketRewardDialogTag); _scheduleRoomRocketPostLaunchStatusRefresh(roomId); SCRoomRocketRewardPopupAck.markShownAndAck(popupRecords); if (nextGroupIndex < popupGroups.length) { unawaited( Future.delayed(const Duration(milliseconds: 160), () { _showRoomRocketRewardPopupGroups( roomId: roomId, popupGroups: popupGroups, groupIndex: nextGroupIndex, initialRoomRecords: initialRoomRecords, userProfileResolver: userProfileResolver, ); }), ); } }, ), ), ); } void _scheduleRoomRocketRewardPopupRetry( String roomId, int attempt, { List initialRoomRecords = const [], bool allowEmptyPopup = false, }) { if (attempt >= _roomRocketRewardPopupRetryDelays.length || !_isCurrentVisibleVoiceRoom(roomId)) { return; } _roomRocketRewardPopupRetryTimer?.cancel(); _roomRocketRewardPopupRetryTimer = Timer( _roomRocketRewardPopupRetryDelays[attempt], () { _roomRocketRewardPopupRetryTimer = null; unawaited( _showRoomRocketRewardPopupDialog( roomId, attempt: attempt + 1, initialRoomRecords: initialRoomRecords, allowEmptyPopup: allowEmptyPopup, ), ); }, ); } 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 (_) {} } void _scheduleRoomRocketStatusRefreshForCurrentRoom(String roomId) { final normalizedRoomId = roomId.trim(); if (normalizedRoomId.isEmpty || !_isCurrentVoiceRoom(normalizedRoomId)) { return; } final currentContext = context; if (currentContext == null || !currentContext.mounted) { return; } try { Provider.of( currentContext, listen: false, ).scheduleRoomRocketStatusRefreshForGift(roomId: normalizedRoomId); } 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, }) { final launchNo = _firstNonBlank([ launch.launchNo, _roomRocketLaunchNoticeKey(launch), ]); 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: 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('|'); } String _roomRocketLaunchFloatingIdFromPayload( Map payload, { required String roomId, required int level, }) { final explicitId = _firstNonBlank([ _payloadText(payload['launchNo'] ?? payload['launch_no']), _payloadText(payload['giftId'] ?? payload['gift_id']), _payloadText(payload['batchId'] ?? payload['batch_id']), ]); if (explicitId.isNotEmpty || roomId.isEmpty) { return explicitId; } return _roomRocketLaunchNoticeKeyForValues( roomId: roomId, level: level, roundNo: _payloadNum(payload['roundNo'] ?? payload['round_no'])?.toInt() ?? 0, ); } 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['currentRocketLevel'] ?? payload['current_rocket_level'] ?? payload['rocketCurrentLevel'] ?? payload['rocket_current_level'] ?? payload['levelNo'] ?? payload['level_no'] ?? 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") { _handleGameWinBroadcast( data, isRegionBroadcast: isRegionBroadcastGroup, ); } 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 rocketLevel = _roomRocketPayloadLevel(payload, 1); final launchNo = _roomRocketLaunchFloatingIdFromPayload( payload, roomId: launchRoomId, level: rocketLevel, ); final rocketIconUrl = _firstNonBlank([ _rocketIconFromPayload(payload), _payloadAssetText(payload["giftUrl"]), _payloadAssetText(payload["giftIcon"]), ]); SCFloatingMessage msg = SCFloatingMessage( type: 3, roomId: launchRoomId, rocketLevel: rocketLevel, 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, giftId: launchNo, broadcastScope: isRegionBroadcastGroup ? SCFloatingMessage.broadcastScopeRegion : '', priority: 1000, ); OverlayManager().addMessage(msg); _scheduleRoomRocketStatusRefreshForCurrentRoom(launchRoomId); } else if (type == SCRoomMsgType.voiceRoomRocketLaunchBroadcast) { _handleVoiceRoomRocketLaunchBroadcast( data["data"], isRegionBroadcast: isRegionBroadcastGroup, ); } else if (_isRoomRocketStatusUpdateType(type)) { _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 (_isRoomRocketStatusUpdateType(data["type"])) { _handleVoiceRoomRocketStatusUpdate(data["data"]); return; } if (data["type"] == SCRoomMsgType.voiceRoomRocketRewardPopup) { _handleVoiceRoomRocketRewardPopup(data["data"] ?? data); return; } if (SCRoomMsgType.isRocketRewardUserType(data["type"]?.toString())) { _handleVoiceRoomRocketRewardPopup(data["data"] ?? data); return; } 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; } if (data["type"] == "GAME_BAISHUN_WIN") { _handleGameWinBroadcast(data, isRegionBroadcast: false); 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.announceCpRelationEntryForRoomUser( groupId: msg.groupId ?? groupID, entrant: 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 && SCGlobalConfig.isEntryVehicleAnimation && 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 && SCGlobalConfig.isGiftSpecialEffects && 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, giftTab: _floatingGiftTabFor(gift), 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, giftTab: _floatingGiftTabFor(gift), 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); } }