yumi-flutter/lib/services/audio/rtm_manager.dart
2026-05-23 19:12:58 +08:00

6826 lines
226 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<String> 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<String> eventKeys = <String>{};
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 <SCRoomRocketRewardRecordRes>[],
});
final String roomId;
final int receivedAt;
final List<SCRoomRocketRewardRecordRes> initialRoomRecords;
}
bool roomRocketRewardPopupTargetsCurrentUser({
required Map<String, dynamic> payload,
required Set<String> currentUserKeys,
}) {
final audienceUserIds = _roomRocketRewardPopupAudienceUserIds(payload);
if (audienceUserIds == null) {
return true;
}
if (audienceUserIds.isEmpty || currentUserKeys.isEmpty) {
return false;
}
return currentUserKeys.any(audienceUserIds.contains);
}
Set<String>? _roomRocketRewardPopupAudienceUserIds(
Map<String, dynamic> 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<String> _roomRocketPayloadStringSet(dynamic value) {
if (value == null) {
return <String>{};
}
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 <String>{};
}
if (raw.startsWith('[') || raw.startsWith('{')) {
try {
return _roomRocketPayloadStringSet(jsonDecode(raw));
} catch (_) {
return <String>{};
}
}
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<Duration> _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<Duration> _roomRocketRewardPopupRetryDelays = [
Duration(milliseconds: 700),
Duration(seconds: 2),
Duration(seconds: 5),
];
static const Duration _roomRocketRewardAfterLaunchDelay = Duration(
milliseconds: 350,
);
static const List<Duration> _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<String?> values) {
for (final value in values) {
final text = value?.trim();
if (text != null && text.isNotEmpty) {
return text;
}
}
return "";
}
///消息列表
List<Msg> roomAllMsgList = [];
List<Msg> roomChatMsgList = [];
List<Msg> roomGiftMsgList = [];
RoomNewMsgListener? msgAllListener;
RoomNewMsgListener? msgChatListener;
RoomNewMsgListener? msgGiftListener;
RoomNewMsgListener? msgFloatingGiftListener;
RoomNewMsgListener? msgLuckyGiftRewardTickerListener;
RoomNewMsgListener? msgUserJoinListener;
/// 当前会话
V2TimConversation? currentConversation;
/// 会话列表缓存
Map<String, V2TimConversation> conversationMap = {};
///消息已读监听
OnMessageRecvC2CReadListener? onMessageRecvC2CReadListener;
///消息被撤回监听
OnRevokeMessageListener? onRevokeMessageListener;
///新消息监听 单聊
OnNewMessageListener? onNewMessageCurrentConversationListener;
OnNewANMessageListener? onNewActivityMessageCurrentConversationListener;
OnNewANMessageListener? onNewNotifcationMessageCurrentConversationListener;
///新消息监听 群聊
Map<String, OnNewGroupMessageListener?> 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<String> _currentLuckGiftPushEventKeys = <String>{};
Timer? _luckGiftPushPlayTimer;
int _luckGiftPushPlaybackToken = 0;
final Map<String, int> _recentLuckGiftPushEventTimes = <String, int>{};
final Map<String, int> _recentRoomRocketLaunchTimes = <String, int>{};
final Map<String, int> _recentRoomRocketLaunchLooseTimes = <String, int>{};
final Map<String, int> _recentRoomRocketRegionBroadcastTimes =
<String, int>{};
final _pendingRoomRocketRewardPopups =
<String, _PendingRoomRocketRewardPopup>{};
final Map<String, int> _recentRoomRocketRewardProbeTimes = <String, int>{};
final Map<String, int> _roomRocketLaunchCountdownDueTimes = <String, int>{};
final Map<String, String> _roomRocketLaunchCountdownKeysByRoom =
<String, String>{};
final _pendingRoomRocketCountdownLaunches =
<String, RoomRocketLaunchBroadcastMessage>{};
final Map<String, Timer> _roomRocketLaunchEffectTimers = <String, Timer>{};
final Map<String, int> _consumedRoomRocketLaunchEffectTimes = <String, int>{};
final Set<String> _hiddenCurrentRoomRocketLaunchKeys = <String>{};
final Map<String, int> _roomRocketRewardHoldUntilTimes = <String, int>{};
final Map<String, Timer> _roomRocketRewardHoldTimers = <String, Timer>{};
final Set<String> _roomRocketRewardPopupLoadingRoomIds = <String>{};
final Map<String, int> _shownRoomRocketEmptyRewardPopupTimes =
<String, int>{};
Timer? _roomRocketRewardPopupRetryTimer;
Timer? _roomRocketRoomReadyRewardRetryTimer;
final Set<String> _shownCpInviteMessageKeys = <String>{};
final Set<String> _shownCpInviteResultMessageKeys = <String>{};
final Set<String> _shownCpInviteRejectedDialogKeys = <String>{};
final Map<String, int> _recentCpRelationNoticeTimes = <String, int>{};
int _lastCpRelationGiftRefreshAt = 0;
int get currentLuckGiftBurstPlaybackToken => _luckGiftPushPlaybackToken;
String? roomRedPacketBroadcastGroupId;
String? roomRedPacketBroadcastRegionCode;
Debouncer debouncer = Debouncer();
List<V2TimConversation> 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<V2TimConversation> 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<V2TimUserStatus> userStatusList) {
//用户状态变更通知
//userStatusList 用户状态变化的用户列表
//收到通知的情况:订阅过的用户发生了状态变更(包括在线状态和自定义状态),会触发该回调
//在 IM 控制台打开了好友状态通知开关,即使未主动订阅,当好友状态发生变更时,也会触发该回调
//同一个账号多设备登录,当其中一台设备修改了自定义状态,所有设备都会收到该回调
},
);
V2TimValueCallback<bool> 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<V2TimGroupMemberInfo> memberList) {
final rtcProvider = Provider.of<RealTimeCommunicationManager>(
context,
listen: false,
);
if (groupID ==
rtcProvider.currenRoom?.roomProfile?.roomProfile?.roomAccount) {
rtcProvider.fetchOnlineUsersList(notifyIfUnchanged: false);
}
},
onMemberLeave: (String groupID, V2TimGroupMemberInfo member) {
final rtcProvider = Provider.of<RealTimeCommunicationManager>(
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<V2TimGroupMemberInfo> memberList,
) {
///踢出房间
if (memberList.isNotEmpty) {
if (memberList.first.userID ==
AccountStorage().getCurrentUser()?.userProfile?.id) {
final rtcProvider = Provider.of<RealTimeCommunicationManager>(
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<V2TimMessageReceipt> receiptList) {
//会话已读回调
},
onRecvMessageModified: (V2TimMessage message) {
// msg 为被修改之后的消息对象
},
onRecvMessageReadReceipts: (List<V2TimMessageReceipt> receiptList) {
//群聊/单聊已读回调
List<String> 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<bool> 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<V2TimConversationResult> convList =
await TencentImSDKPlugin.v2TIMManager
.getConversationManager()
.getConversationList(nextSeq: '0', count: 100);
List<V2TimConversation?>? 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<int> 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<String, dynamic>? 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<void> _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;
}
if (!await _isCpInviteApplyWaitingForAutoPopup(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;
}
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 (_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<String, dynamic>? _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<String, dynamic> data) {
try {
return SCSystemInvitMessageRes.fromMessageData(data);
} catch (error) {
_cpInviteLog('parse CP_BUILD content failed error=$error data=$data');
}
return null;
}
String _cpInviteApplyIdFromData(Map<String, dynamic> 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<bool> _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<String, dynamic> 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<SocialChatUserProfileManager>(
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;
}
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<String, dynamic> 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<SocialChatUserProfileManager>(
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<RtcProvider?>(
context,
listen: false,
)?.invalidateRoomMicRelationBetween(
userId: currentUserId,
peerUserId: peerUserId,
relationType: _cpRelationTypeFromNoticeData(data),
);
}
}
}
return true;
}
Future<void> _showCpInviteResultDialog({
required BuildContext context,
required Map<String, dynamic> data,
required SCSystemInvitMessageRes invite,
required SocialChatUserProfile currentProfile,
required _CpInviteProcessResult result,
required String relationType,
}) async {
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<String, dynamic> data,
) {
final content = _cpInviteContentMap(data);
final values =
<String>[
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<dynamic> 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<String, dynamic> _cpInviteContentMap(Map<String, dynamic> 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 <String, dynamic>{};
}
Future<void> _processIncomingCpInvite(
BuildContext context,
String applyId,
SCSystemInvitMessageRes invite,
bool agree, {
Map<String, dynamic>? data,
}) async {
_cpInviteLog('process CP_BUILD applyId=$applyId agree=$agree');
final profileManager = Provider.of<SocialChatUserProfileManager>(
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<void> 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<String, dynamic>? 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<String, dynamic>? 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<RealTimeCommunicationManager>(
context,
listen: false,
);
if (!rtcProvider.isVoiceRoomRouteVisible) {
return "";
}
return (rtcProvider.currenRoom?.roomProfile?.roomProfile?.roomAccount ?? "")
.trim();
}
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<String, dynamic>? data) {
final receiverData =
data == null
? const <String, dynamic>{}
: Map<String, dynamic>.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<String> _compactCpNoticeTargetIds(
Iterable<SocialChatUserProfile?> profiles,
) {
final result = <String>[];
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<void> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic>? 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<String, dynamic> data,
) {
final currentIds = _cpInviteProfileIdentityValues(currentProfile);
if (currentIds.isEmpty) {
return false;
}
final inviterIds = <String>{
..._cpInviteInviterIdentityValues(data),
invite.actualAccount?.trim() ?? "",
invite.account?.trim() ?? "",
}..remove("");
final receiverIds = _cpInviteReceiverIdentityValues(data);
return inviterIds.any(currentIds.contains) &&
receiverIds.any(currentIds.contains);
}
Set<String> _cpInviteProfileIdentityValues(SocialChatUserProfile profile) {
return <String>{
profile.id?.trim() ?? "",
profile.account?.trim() ?? "",
profile.getID().trim(),
}..remove("");
}
Set<String> _cpInviteInviterIdentityValues(Map<String, dynamic> data) {
final content = _cpInviteContentMap(data);
final values = <String?>[
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<String> _cpInviteReceiverIdentityValues(Map<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> data,
V2TimMessage? message,
) {
final receiverId = _cpInviteReceiverIdFromMessageData(data);
final conversation =
receiverId.isNotEmpty ? conversationMap["c2c_$receiverId"] : null;
return _firstCpInviteValue([conversation?.faceUrl, message?.faceUrl]);
}
String? _firstCpInviteValue(Iterable<String?> values) {
for (final value in values) {
final text = value?.trim() ?? "";
if (text.isNotEmpty) {
return text;
}
}
return null;
}
String? _firstCpInviteBackendUserId(Iterable<String?> values) {
return scFirstBackendUserId(values);
}
bool _isCpRelationInviteNoticeData(Map<String, dynamic>? 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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 = <String?>[
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<V2TimConversation> 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<V2TimValueCallback<String>> createRoomGroup(
String groupID,
String groupName,
) {
return V2TIMGroupManager()
.createGroup(
groupID: groupID,
groupType: GroupType.AVChatRoom,
groupName: groupName,
)
.timeout(
_createRoomGroupTimeout,
onTimeout:
() => V2TimValueCallback<String>(
code: -10001,
desc: 'Create IM group timeout',
),
);
}
///加入房间的im群聊
Future<V2TimCallback> 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<void> 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<void> _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<void> sendC2CTextMsg(
String msg,
V2TimConversation toConversation,
) async {
// 创建文本消息
V2TimValueCallback<V2TimMsgCreateInfoResult> 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<V2TimMessage> 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<void> sendC2CCustomMsg(
String msg,
V2TimConversation toConversation,
String extension,
) async {
// 创建文本消息
V2TimValueCallback<V2TimMsgCreateInfoResult> 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<V2TimMessage> 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<void> sendImageMsg({
List<File>? selectedList,
File? file,
required V2TimConversation conversation,
}) async {
if (file != null) {
File newFile = await SCMessageUtils.createImageElem(file);
V2TimValueCallback<V2TimMsgCreateInfoResult> createImageMessageRes =
await TencentImSDKPlugin.v2TIMManager
.getMessageManager()
.createImageMessage(imagePath: newFile.path);
if (createImageMessageRes.code == 0) {
String id = createImageMessageRes.data!.id!;
V2TimValueCallback<V2TimMessage> 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<V2TimMsgCreateInfoResult> 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<V2TimMsgCreateInfoResult> 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<V2TimMessage> 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<void> _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<RealTimeCommunicationManager>(
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<void> 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 <V2TimMessage>[];
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<RealTimeCommunicationManager>(
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 currentProfile = AccountStorage().getCurrentUser()?.userProfile;
if (currentProfile == null) {
return;
}
final targetIds =
msg.targetUserIds
?.map((value) => value.trim())
.where((value) => value.isNotEmpty)
.toSet() ??
const <String>{};
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 <String>{};
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 key = dedupKey?.trim() ?? "";
if (key.isNotEmpty && !_shownCpInviteRejectedDialogKeys.add(key)) {
_cpInviteLog('skip duplicated rejected dialog key=$key');
return;
}
final context = navigatorKey.currentState?.context;
if (context == null) {
_cpInviteLog(
'skip rejected dialog reason=no_context relationType=$relationType',
);
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<RealTimeCommunicationManager>(
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<String>.unmodifiable(_cpRelationGiftUserIds(msg));
for (final delay in _cpRelationGiftRefreshDelays) {
unawaited(
Future<void>.delayed(delay, () {
_refreshCpRelationDataAfterGift(userIds);
}),
);
}
}
bool _isCpRelationGiftMessage(Msg msg) {
if (msg.type != SCRoomMsgType.gift &&
msg.type != SCRoomMsgType.luckGiftAnimOther) {
return false;
}
return scIsCpGift(msg.gift);
}
Set<String> _cpRelationGiftUserIds(Msg msg) {
return <String>{
msg.user?.id?.trim() ?? '',
msg.toUser?.id?.trim() ?? '',
...?msg.targetUserIds?.map((userId) => userId.trim()),
}..removeWhere((userId) => userId.isEmpty);
}
void _refreshCpRelationDataAfterGift(Set<String> userIds) {
final currentContext =
context?.mounted == true ? context : navigatorKey.currentState?.context;
if (currentContext == null || !currentContext.mounted) {
return;
}
try {
unawaited(
Provider.of<SocialChatUserProfileManager>(
currentContext,
listen: false,
).refreshLoadedCpProfiles(),
);
} catch (_) {}
try {
Provider.of<RealTimeCommunicationManager>(
currentContext,
listen: false,
).refreshRoomMicRelationProfilesForUsers(userIds);
} catch (_) {}
}
void _cacheRoomRedPacketMessage(Msg msg) {
try {
final decoded = jsonDecode(msg.msg ?? '');
if (decoded is Map<String, dynamic>) {
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))
: <String, dynamic>{};
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 <String>[]).isEmpty &&
(incoming.targetUserIds ?? const <String>[]).isNotEmpty) {
mergeTarget.targetUserIds = List<String>.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 = <String>[];
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 <String>[]) {
addTargetUserId(userId);
}
addTargetUserId(msg.toUser?.id);
if (targetUserIds.length <= 1) {
return "";
}
targetUserIds.sort();
return targetUserIds.join(",");
}
void _moveMessageToFront(List<Msg> 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<RealTimeCommunicationManager>(
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<GiftProvider>(
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<SCAppGeneralManager>(
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<String, dynamic> 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<SCAppGeneralManager>(
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<String, dynamic> payload) {
final payloadGiftTab = _payloadText(payload['giftTab']);
if (payloadGiftTab.isNotEmpty) {
return payloadGiftTab;
}
final giftId = _payloadText(payload['giftId']);
final currentContext = context;
if (giftId.isEmpty || currentContext == null || !currentContext.mounted) {
return '';
}
final appGeneralManager = Provider.of<SCAppGeneralManager>(
currentContext,
listen: false,
);
return appGeneralManager.getGiftByIdOrStandardId(giftId)?.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<RealTimeCommunicationManager>(
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<RealTimeCommunicationManager>(
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<RealTimeCommunicationManager>(
currentContext,
listen: false,
).currenRoom?.roomProfile?.roomProfile?.id?.trim() ??
'';
}
String _currentVoiceRoomGroupId() {
final currentContext = context;
if (currentContext == null || !currentContext.mounted) {
return '';
}
return Provider.of<RealTimeCommunicationManager>(
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<String, dynamic> _broadcastPayloadMap(dynamic data) {
if (data is Map<String, dynamic>) {
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 <String, dynamic>{};
}
}
return <String, dynamic>{};
}
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<RealTimeCommunicationManager>(
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<RealTimeCommunicationManager>(
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<String, dynamic> payload) {
final profile = AccountStorage().getCurrentUser()?.userProfile;
final currentKeys = <String>{
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 <SCRoomRocketRewardRecordRes>[],
allowEmptyPopup: allowEmptyPopup,
);
return;
}
unawaited(
_showRoomRocketRewardPopupDialog(
normalizedRoomId,
initialRoomRecords:
pendingAfterResume?.initialRoomRecords ??
const <SCRoomRocketRewardRecordRes>[],
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<SCRoomRocketRewardRecordRes> initialRoomRecords =
const <SCRoomRocketRewardRecordRes>[],
}) {
final normalizedRoomId = roomId.trim();
if (normalizedRoomId.isEmpty) {
return;
}
final now = DateTime.now().millisecondsSinceEpoch;
_pruneRoomRocketRewardFallbacks(now);
final existing = _pendingRoomRocketRewardPopups[normalizedRoomId];
final existingRecords =
existing?.initialRoomRecords ?? const <SCRoomRocketRewardRecordRes>[];
final records =
initialRoomRecords.isNotEmpty
? _mergeRoomRocketRewardRecords(existingRecords, initialRoomRecords)
: existingRecords;
_pendingRoomRocketRewardPopups[normalizedRoomId] =
_PendingRoomRocketRewardPopup(
roomId: normalizedRoomId,
receivedAt: now,
initialRoomRecords: records,
);
}
List<SCRoomRocketRewardRecordRes> _mergeRoomRocketRewardRecords(
List<SCRoomRocketRewardRecordRes> existingRecords,
List<SCRoomRocketRewardRecordRes> incomingRecords,
) {
if (existingRecords.isEmpty) {
return List<SCRoomRocketRewardRecordRes>.unmodifiable(incomingRecords);
}
if (incomingRecords.isEmpty) {
return existingRecords;
}
final merged = <SCRoomRocketRewardRecordRes>[...existingRecords];
final seen = existingRecords.map(_roomRocketRewardRecordCacheKey).toSet();
for (final record in incomingRecords) {
if (seen.add(_roomRocketRewardRecordCacheKey(record))) {
merged.add(record);
}
}
return List<SCRoomRocketRewardRecordRes>.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<SCRoomRocketRewardRecordRes> initialRoomRecords =
const <SCRoomRocketRewardRecordRes>[],
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 <SCRoomRocketRewardRecordRes>[],
allowEmptyPopup: allowEmptyPopup,
),
);
},
);
_roomRocketRewardDebug(
'defer popup roomId=$normalizedRoomId delayMs=$delayMs '
'initialRoomRecords=${initialRoomRecords.length}',
);
}
bool _markRoomRocketEmptyRewardPopupForShow(
String roomId,
List<SCRoomRocketRewardRecordRes> 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<SCRoomRocketRewardRecordRes> 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<RealTimeCommunicationManager>(
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 <SCRoomRocketRewardRecordRes>[],
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<RealTimeCommunicationManager>(
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 = <String>{};
_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 = <String>{
currentUser?.id?.trim() ?? '',
currentUser?.account?.trim() ?? '',
}..removeWhere((item) => item.isEmpty);
final triggerKeys = <String>{
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<RealTimeCommunicationManager>(
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<void> _showRoomRocketRewardPopupDialog(
String roomId, {
int attempt = 0,
List<SCRoomRocketRewardRecordRes> initialRoomRecords =
const <SCRoomRocketRewardRecordRes>[],
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 <List<SCRoomRocketRewardRecordRes>>[
<SCRoomRocketRewardRecordRes>[],
];
_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<List<SCRoomRocketRewardRecordRes>> popupGroups,
required int groupIndex,
required List<SCRoomRocketRewardRecordRes> 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 <String>{_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<void>.delayed(const Duration(milliseconds: 160), () {
_showRoomRocketRewardPopupGroups(
roomId: roomId,
popupGroups: popupGroups,
groupIndex: nextGroupIndex,
initialRoomRecords: initialRoomRecords,
userProfileResolver: userProfileResolver,
);
}),
);
}
},
),
),
);
}
void _scheduleRoomRocketRewardPopupRetry(
String roomId,
int attempt, {
List<SCRoomRocketRewardRecordRes> initialRoomRecords =
const <SCRoomRocketRewardRecordRes>[],
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<RealTimeCommunicationManager>(
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<RealTimeCommunicationManager>(
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<RealTimeCommunicationManager>(
currentContext,
listen: false,
).scheduleRoomRocketStatusRefreshForGift(roomId: normalizedRoomId);
} catch (_) {}
}
List<SCRoomRocketRewardRecordRes> _roomRocketRewardRecordsFromPayload(
Map<String, dynamic> 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 <SCRoomRocketRewardRecordRes>[];
}
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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String> _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<RealTimeCommunicationManager>(
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<RealTimeCommunicationManager>(
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<RealTimeCommunicationManager>(
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<RealTimeCommunicationManager>(
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<RealTimeCommunicationManager>(
context!,
listen: false,
).findWheat();
if (index > -1) {
Provider.of<RealTimeCommunicationManager>(
context!,
listen: false,
).shangMai(
index,
eventType: "INVITE",
inviterId: msg.role,
);
}
},
);
},
);
}
return;
}
if (msg.type == SCRoomMsgType.killXiaMai) {
///踢下麦
final rtcProvider = Provider.of<RealTimeCommunicationManager>(
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<RealTimeCommunicationManager>(
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<RealTimeCommunicationManager>(
context!,
listen: false,
).updateRoomBG(res);
return;
}
if (msg.type == SCRoomMsgType.emoticons) {
if ((msg.number ?? -1) >= 0) {
Provider.of<RealTimeCommunicationManager>(
context!,
listen: false,
).starPlayEmoji(msg);
return;
}
}
if (msg.type == SCRoomMsgType.micChange) {
Provider.of<RealTimeCommunicationManager>(
context!,
listen: false,
).micChange(BroadCastMicChangePush.fromJson(data).data?.mics);
} else if (msg.type == SCRoomMsgType.roomMusic) {
Provider.of<RealTimeCommunicationManager>(
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<RealTimeCommunicationManager>(
context!,
listen: false,
).retrieveMicrophoneList(notifyIfUnchanged: false);
} else if (msg.type == SCRoomMsgType.refreshOnlineUser) {
Provider.of<RealTimeCommunicationManager>(
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<RealTimeCommunicationManager>(
context!,
listen: false,
);
final shouldShowRoomVisualEffects =
rtcProvider.shouldShowRoomVisualEffects;
if (msg.user != null) {
Provider.of<RealTimeCommunicationManager>(
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<RealTimeCommunicationManager>(
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: gift.giftTab,
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: gift.giftTab,
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<GiftProvider>(
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<RealTimeCommunicationManager>(
context!,
listen: false,
).retrieveMicrophoneList();
if (msg.toUser?.id ==
AccountStorage().getCurrentUser()?.userProfile?.id) {
Provider.of<RealTimeCommunicationManager>(
context!,
listen: false,
).currenRoom?.entrants?.setRoles(msg.msg);
if (msg.msg == SCRoomRolesType.TOURIST.name &&
!(Provider.of<RealTimeCommunicationManager>(
context!,
listen: false,
).currenRoom?.roomProfile?.roomSetting?.touristMike ??
false)) {
///如果变成了游客,房间又是禁止游客上麦,需要下麦
num index = Provider.of<RealTimeCommunicationManager>(
context!,
listen: false,
).userOnMaiInIndex(
AccountStorage().getCurrentUser()?.userProfile?.id ?? "",
);
if (index > -1) {
Provider.of<RealTimeCommunicationManager>(
context!,
listen: false,
).xiaMai(index);
}
}
}
} else if (msg.type == SCRoomMsgType.roomDice) {
if ((msg.number ?? -1) > -1) {
Provider.of<RealTimeCommunicationManager>(
context!,
listen: false,
).starPlayEmoji(msg);
}
} else if (msg.type == SCRoomMsgType.roomRPS) {
if ((msg.number ?? -1) > -1) {
Provider.of<RealTimeCommunicationManager>(
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<void> sendRoomRedPacketBroadcast(
Map<String, dynamic> payload, {
String roomGroupId = '',
bool includeRegionBroadcast = false,
}) async {
if (payload.isEmpty) {
return;
}
final targetGroupIds = <String>{};
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 = <String, dynamic>{
'type': SCRoomMsgType.roomRedPacket,
'data': payload,
};
final data = jsonEncode(message);
for (final groupId in targetGroupIds) {
await _sendRoomRedPacketCustomMessage(groupId, data, payload);
}
}
Future<void> 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 = <String, dynamic>{
'type': SCRoomMsgType.voiceRoomRocketLaunchBroadcast,
'data': _roomRocketLaunchBroadcastPayload(launch),
};
await _sendRoomRocketLaunchCustomMessage(
regionGroupId,
jsonEncode(message),
);
}
Map<String, dynamic> _roomRocketLaunchBroadcastPayload(
RoomRocketLaunchBroadcastMessage launch,
) {
final launchNo = _firstNonBlank([
launch.launchNo,
_roomRocketLaunchNoticeKey(launch),
]);
return <String, dynamic>{
'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': <String, dynamic>{
'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<void> _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<void> _sendRoomRedPacketCustomMessage(
String groupId,
String data,
Map<String, dynamic> 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<String>.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<void> 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);
}
}