6826 lines
226 KiB
Dart
6826 lines
226 KiB
Dart
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);
|
||
}
|
||
}
|