diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index a4235a5..0f28cd6 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -502,7 +502,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = S9X2AJ2US9; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -511,7 +511,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.1.0; + MARKETING_VERSION = 1.1.1; PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -693,7 +693,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/RunnerDebug.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = F33K8VUZ62; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -702,7 +702,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.1.0; + MARKETING_VERSION = 1.1.1; PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -722,7 +722,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/RunnerRelease.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = F33K8VUZ62; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -731,7 +731,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.1.0; + MARKETING_VERSION = 1.1.1; PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/lib/main.dart b/lib/main.dart index 142b1c6..77a977a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -410,7 +410,8 @@ class _YumiApplicationState extends State { if (SCGlobalConfig.allowsHighCostAnimations) Consumer( builder: (context, rtcProvider, _) { - if (!rtcProvider.roomVisualEffectsEnabled) { + if (!rtcProvider + .shouldShowRoomVisualEffects) { return const SizedBox.shrink(); } return const Positioned.fill( @@ -420,15 +421,24 @@ class _YumiApplicationState extends State { ); }, ), - Positioned.fill( - child: RoomGiftSeatFlightOverlay( - controller: RoomGiftSeatFlightController(), - resolveTargetKey: - (userId) => Provider.of( - context, - listen: false, - ).getSeatGlobalKeyByIndex(userId), - ), + Consumer( + builder: (context, rtcProvider, _) { + if (!rtcProvider + .shouldShowRoomVisualEffects) { + return const SizedBox.shrink(); + } + return Positioned.fill( + child: RoomGiftSeatFlightOverlay( + controller: + RoomGiftSeatFlightController(), + resolveTargetKey: + (userId) => Provider.of( + context, + listen: false, + ).getSeatGlobalKeyByIndex(userId), + ), + ); + }, ), ], ); diff --git a/lib/modules/room/voice_room_page.dart b/lib/modules/room/voice_room_page.dart index ed6011f..5b98fc5 100644 --- a/lib/modules/room/voice_room_page.dart +++ b/lib/modules/room/voice_room_page.dart @@ -17,6 +17,7 @@ import 'package:yumi/shared/tools/sc_gift_vap_svga_manager.dart'; import 'package:yumi/shared/tools/sc_network_image_utils.dart'; import 'package:yumi/shared/tools/sc_path_utils.dart'; import 'package:yumi/shared/tools/sc_room_effect_scheduler.dart'; +import 'package:yumi/shared/data_sources/sources/local/floating_screen_manager.dart'; import 'package:yumi/ui_kit/widgets/room/anim/l_gift_animal_view.dart'; import 'package:yumi/ui_kit/widgets/room/anim/room_gift_seat_flight_overlay.dart'; import 'package:yumi/ui_kit/widgets/room/anim/room_entrance_screen.dart'; @@ -140,6 +141,7 @@ class _VoiceRoomPageState extends State RoomEntranceHelper.clearQueue(); _clearLuckyGiftComboSessions(); _giftSeatFlightController.clear(); + OverlayManager().removeRoom(); SCRoomEffectScheduler().clearDeferredTasks(reason: 'voice_room_suspend'); SCGiftVapSvgaManager().stopPlayback(); } diff --git a/lib/services/audio/rtm_manager.dart b/lib/services/audio/rtm_manager.dart index d2acb25..f6cacd8 100644 --- a/lib/services/audio/rtm_manager.dart +++ b/lib/services/audio/rtm_manager.dart @@ -1,1841 +1,1890 @@ -import 'dart:collection'; -import 'dart:convert'; -import 'dart:io'; -import 'package:agora_rtc_engine/agora_rtc_engine.dart'; -import 'package:extended_image/extended_image.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_debouncer/flutter_debouncer.dart'; -import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; -import 'package:yumi/ui_kit/components/sc_tts.dart'; -import 'package:yumi/app/constants/sc_global_config.dart'; -import 'package:yumi/shared/tools/sc_message_utils.dart'; -import 'package:yumi/shared/tools/sc_path_utils.dart'; -import 'package:yumi/shared/tools/sc_room_utils.dart'; -import 'package:yumi/shared/data_sources/sources/local/floating_screen_manager.dart'; -import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart'; -import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; -import 'package:yumi/services/audio/rtc_manager.dart'; -import 'package:provider/provider.dart'; -import 'package:tencent_cloud_chat_sdk/enum/V2TimAdvancedMsgListener.dart'; -import 'package:tencent_cloud_chat_sdk/enum/V2TimConversationListener.dart'; -import 'package:tencent_cloud_chat_sdk/enum/V2TimGroupListener.dart'; -import 'package:tencent_cloud_chat_sdk/enum/V2TimSDKListener.dart'; -import 'package:tencent_cloud_chat_sdk/enum/conversation_type.dart'; -import 'package:tencent_cloud_chat_sdk/enum/group_type.dart'; -import 'package:tencent_cloud_chat_sdk/enum/log_level_enum.dart'; -import 'package:tencent_cloud_chat_sdk/enum/message_status.dart'; -import 'package:tencent_cloud_chat_sdk/manager/v2_tim_group_manager.dart'; -import 'package:tencent_cloud_chat_sdk/models/v2_tim_callback.dart'; -import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart'; -import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation_result.dart'; -import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_member_info.dart'; -import 'package:tencent_cloud_chat_sdk/models/v2_tim_image_elem.dart'; -import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart'; -import 'package:tencent_cloud_chat_sdk/models/v2_tim_message_receipt.dart'; -import 'package:tencent_cloud_chat_sdk/models/v2_tim_msg_create_info_result.dart'; -import 'package:tencent_cloud_chat_sdk/models/v2_tim_user_full_info.dart'; -import 'package:tencent_cloud_chat_sdk/models/v2_tim_user_status.dart'; -import 'package:tencent_cloud_chat_sdk/models/v2_tim_value_callback.dart'; -import 'package:tencent_cloud_chat_sdk/tencent_im_sdk_plugin.dart'; -import 'package:yumi/app_localizations.dart'; -import 'package:yumi/ui_kit/components/dialog/dialog_base.dart'; -import 'package:yumi/app/constants/sc_room_msg_type.dart'; -import 'package:yumi/shared/tools/sc_lk_event_bus.dart'; -import 'package:yumi/shared/tools/sc_gift_vap_svga_manager.dart'; -import 'package:yumi/shared/data_sources/sources/local/file_cache_manager.dart'; -import 'package:yumi/shared/data_sources/sources/repositories/sc_config_repository_imp.dart'; -import 'package:yumi/shared/data_sources/models/message/big_broadcast_group_message.dart'; -import 'package:yumi/shared/data_sources/models/message/sc_floating_message.dart'; -import 'package:yumi/shared/business_logic/models/res/sc_broad_cast_luck_gift_push.dart'; -import 'package:yumi/shared/business_logic/models/res/broad_cast_mic_change_push.dart' - hide Data; -import 'package:yumi/shared/business_logic/models/res/gift_res.dart'; -import 'package:yumi/shared/business_logic/models/res/sc_public_message_page_res.dart'; -import 'package:yumi/shared/business_logic/models/res/sc_room_theme_list_res.dart'; -import 'package:yumi/ui_kit/widgets/room/invite/invite_room_dialog.dart'; -import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart'; -import 'package:yumi/shared/business_logic/models/res/login_res.dart'; -import 'package:yumi/services/gift/gift_system_manager.dart'; - -import '../../shared/data_sources/models/enum/sc_gift_type.dart'; -import '../../shared/data_sources/models/enum/sc_room_roles_type.dart'; - -typedef RoomNewMsgListener = Function(Msg msg); -typedef OnNewMessageListener = Function(V2TimMessage? message, {String? msgId}); -typedef OnNewANMessageListener = Function(Records? message); -typedef OnRevokeMessageListener = Function(String msgId); -typedef OnNewGroupMessageListener = - Function(String groupID, V2TimMessage message); -typedef OnMessageRecvC2CReadListener = Function(List messageIDList); -typedef RtmProvider = RealTimeMessagingManager; - -class RealTimeMessagingManager extends ChangeNotifier { - static const int _giftComboMergeWindowMs = 3000; - static const int _maxLuckGiftPushQueueLength = 12; - static const int _luckyGiftFloatMinMultiple = 5; - static const int _luckyGiftBurstMinMultiple = 10; - static const int _luckyGiftBurstMinAwardAmount = 5000; - static const int _luckyGiftBurstDisplayDurationMs = 2000; - - BuildContext? context; - - void _giftFxLog(String message) { - debugPrint('[GiftFX][RTM] $message'); - } - - ///消息列表 - List roomAllMsgList = []; - List roomChatMsgList = []; - List roomGiftMsgList = []; - RoomNewMsgListener? msgAllListener; - RoomNewMsgListener? msgChatListener; - RoomNewMsgListener? msgGiftListener; - - RoomNewMsgListener? msgFloatingGiftListener; - RoomNewMsgListener? msgLuckyGiftRewardTickerListener; - RoomNewMsgListener? msgUserJoinListener; - - /// 当前会话 - V2TimConversation? currentConversation; - - /// 会话列表缓存 - Map conversationMap = {}; - - ///消息已读监听 - OnMessageRecvC2CReadListener? onMessageRecvC2CReadListener; - - ///消息被撤回监听 - OnRevokeMessageListener? onRevokeMessageListener; - - ///新消息监听 单聊 - OnNewMessageListener? onNewMessageCurrentConversationListener; - - OnNewANMessageListener? onNewActivityMessageCurrentConversationListener; - OnNewANMessageListener? onNewNotifcationMessageCurrentConversationListener; - - ///新消息监听 群聊 - Map onNewMessageListenerGroupMap = {}; - int allUnReadCount = 0; - int messageUnReadCount = 0; - int systemUnReadCount = 0; - int customerUnReadCount = 0; - int activityUnReadCount = 0; - int notifcationUnReadCount = 0; - SCBroadCastLuckGiftPush? currentPlayingLuckGift; - final Queue _luckGiftPushQueue = Queue(); - String? _currentLuckGiftPushKey; - Debouncer debouncer = Debouncer(); - List conversationList = []; - - ///客服 - SocialChatUserProfile? customerInfo; - - int _systemUnreadCount() { - int count = 0; - for (final conversationId in SCGlobalConfig.systemConversationIds) { - count += conversationMap[conversationId]?.unreadCount ?? 0; - } - return count; - } - - V2TimConversation getPreferredSystemConversation() { - final systemConversations = - conversationMap.values - .where( - (element) => - SCGlobalConfig.isSystemConversationId(element.conversationID), - ) - .toList() - ..sort((e1, e2) { - final time1 = e1.lastMessage?.timestamp ?? 0; - final time2 = e2.lastMessage?.timestamp ?? 0; - return time2.compareTo(time1); - }); - - if (systemConversations.isNotEmpty) { - return systemConversations.first; - } - - return V2TimConversation( - type: ConversationType.V2TIM_C2C, - userID: SCGlobalConfig.primarySystemUserId, - conversationID: SCGlobalConfig.primarySystemConversationId, - ); - } - - void getConversationList() { - List list = conversationMap.values.toList(); - list.removeWhere((element) { - if (element.conversationID == "c2c_${customerInfo?.id}") { - return true; - } - if (element.conversationID == "c2c_atyou-newsletter") { - ///删除这个会话,后台乱发的联系人。。防止无效未读数 - clearC2CHistoryMessage(element.conversationID, false); - return true; - } - if (element.lastMessage == null) { - return true; - } - return false; - }); - list.sort((e1, e2) { - int time1 = e1.lastMessage?.timestamp ?? 0; - int time2 = e2.lastMessage?.timestamp ?? 0; - return time2.compareTo(time1); - }); - conversationList = list; - systemUnReadCount = _systemUnreadCount(); - customerUnReadCount = - conversationMap["c2c_${customerInfo?.id}"]?.unreadCount ?? 0; - - notifyListeners(); - } - - init(BuildContext context) async { - this.context = context; - V2TimSDKListener sdkListener = V2TimSDKListener( - onConnectFailed: (int code, String error) { - // 连接失败的回调函数 - // code 错误码 - // error 错误信息 - }, - onConnectSuccess: () { - // SDK 已经成功连接到腾讯云服务器 - }, - onConnecting: () { - // SDK 正在连接到腾讯云服务器 - }, - onKickedOffline: () { - // 当前用户被踢下线,此时可以 UI 提示用户,并再次调用 V2TIMManager 的 login() 函数重新登录。 - }, - onSelfInfoUpdated: (V2TimUserFullInfo info) { - // 登录用户的资料发生了更新 - // info登录用户的资料 - }, - onUserSigExpired: () { - // 在线时票据过期:此时您需要生成新的 userSig 并再次调用 V2TIMManager 的 login() 函数重新登录。 - }, - onUserStatusChanged: (List userStatusList) { - //用户状态变更通知 - //userStatusList 用户状态变化的用户列表 - //收到通知的情况:订阅过的用户发生了状态变更(包括在线状态和自定义状态),会触发该回调 - //在 IM 控制台打开了好友状态通知开关,即使未主动订阅,当好友状态发生变更时,也会触发该回调 - //同一个账号多设备登录,当其中一台设备修改了自定义状态,所有设备都会收到该回调 - }, - ); - V2TimValueCallback initSDKRes = await TencentImSDKPlugin.v2TIMManager - .initSDK( - sdkAppID: int.parse(SCGlobalConfig.tencentImAppid), // SDKAppID - loglevel: LogLevelEnum.V2TIM_LOG_ALL, // 日志登记等级 - listener: sdkListener, // 事件监听器 - ); - if (initSDKRes.code == 0) {} - try { - customerInfo = await SCConfigRepositoryImp().customerService(); - } catch (e) {} - - /// 登录 - await loginTencetRtm(context); - - /// 初始化会话列表 - // _onRefreshConversationSub = FTIM.getContactManager().addRefreshConversationListener(_onRefreshConversation); - TencentImSDKPlugin.v2TIMManager - .getConversationManager() - .addConversationListener( - listener: V2TimConversationListener( - onNewConversation: (conversationList) { - // _onRefreshConversation(conversationList); - initConversation(); - }, - onTotalUnreadMessageCountChanged: (int totalUnreadCount) { - messageUnReadCount = totalUnreadCount; - systemUnReadCount = _systemUnreadCount(); - customerUnReadCount = - conversationMap["c2c_${customerInfo?.id}"]?.unreadCount ?? 0; - allUnReadCount = - messageUnReadCount + - notifcationUnReadCount + - activityUnReadCount; - notifyListeners(); - }, - ), - ); - TencentImSDKPlugin.v2TIMManager.addGroupListener( - listener: V2TimGroupListener( - onMemberEnter: (String groupID, List memberList) { - final rtcProvider = Provider.of( - context, - listen: false, - ); - if (groupID == - rtcProvider.currenRoom?.roomProfile?.roomProfile?.roomAccount) { - rtcProvider.fetchOnlineUsersList(notifyIfUnchanged: false); - } - }, - onMemberLeave: (String groupID, V2TimGroupMemberInfo member) { - final rtcProvider = Provider.of( - context, - listen: false, - ); - rtcProvider.removOnlineUser(groupID, member.userID!); - if (groupID == - rtcProvider.currenRoom?.roomProfile?.roomProfile?.roomAccount) { - rtcProvider.fetchOnlineUsersList(notifyIfUnchanged: false); - } - }, - onMemberKicked: ( - String groupID, - V2TimGroupMemberInfo opUser, - List memberList, - ) { - ///踢出房间 - if (memberList.isNotEmpty) { - if (memberList.first.userID == - AccountStorage().getCurrentUser()?.userProfile?.id) { - Provider.of( - context, - listen: false, - ).engine?.setClientRole(role: ClientRoleType.clientRoleAudience); - - ///退出房间 - Provider.of( - context, - listen: false, - ).exitCurrentVoiceRoomSession(false).whenComplete(() { - SCRoomUtils.closeAllDialogs(); - SmartDialog.show( - tag: "showConfirmDialog", - alignment: Alignment.center, - debounce: true, - animationType: SmartAnimationType.fade, - builder: (_) { - return MsgDialog( - title: SCAppLocalizations.of(context)!.tips, - msg: SCAppLocalizations.of(context)!.kickRoomTips, - btnText: SCAppLocalizations.of(context)!.confirm, - onEnsure: () {}, - ); - }, - ); - }); - } - } - }, - ), - ); - - /// 新消息监听 - // FTIM.getMessageManager().addNewMessagesListener(_onNewMessage); - TencentImSDKPlugin.v2TIMManager.getMessageManager().addAdvancedMsgListener( - listener: V2TimAdvancedMsgListener( - onRecvC2CReadReceipt: (List receiptList) { - //会话已读回调 - }, - onRecvMessageModified: (V2TimMessage message) { - // msg 为被修改之后的消息对象 - }, - onRecvMessageReadReceipts: (List receiptList) { - //群聊/单聊已读回调 - List messageIDList = []; - for (var element in receiptList) { - messageIDList.add(element.msgID!); - } - onMessageRecvC2CReadListener?.call(messageIDList); - }, - onRecvMessageRevoked: (String messageId) { - // 在本地维护的消息中处理被对方撤回的消息 - TencentImSDKPlugin.v2TIMManager - .getMessageManager() - .deleteMessages(msgIDs: [messageId]) - .then((result) { - onRevokeMessageListener?.call(messageId); - for (var ms in conversationList) { - if (ms.lastMessage?.msgID == messageId) { - ms.lastMessage?.status = - MessageStatus.V2TIM_MSG_STATUS_LOCAL_REVOKED; - break; - } - } - notifyListeners(); - }); - }, - onRecvNewMessage: (V2TimMessage message) async { - _onNewMessage(message); - }, - onSendMessageProgress: (V2TimMessage message, int progress) { - //文件上传进度回调 - }, - ), - ); - getAllUnReadCount(); - joinBigBroadcastGroup(); - } - - /// 初始化会话 - /// 打开聊天界面的时候调用 - int sTime = 0; - - Future startConversation(V2TimConversation conversation) async { - assert(conversation != null); - int eTime = DateTime.now().millisecondsSinceEpoch; - if (eTime - sTime > 5000) { - sTime = eTime; - currentConversation = conversation; - await TencentImSDKPlugin.v2TIMManager - .getMessageManager() - .markC2CMessageAsRead(userID: conversation.userID!); - notifyListeners(); - return true; - } else { - return false; - } - } - - ///初始话会话 - Future initConversation() async { - V2TimValueCallback convList = - await TencentImSDKPlugin.v2TIMManager - .getConversationManager() - .getConversationList(nextSeq: '0', count: 100); - List? conversationList = - convList.data?.conversationList; - print('conversationList:${conversationList?.length}'); - conversationMap.clear(); - if (conversationList != null) { - for (V2TimConversation? conversation in conversationList) { - conversationMap[conversation!.conversationID] = conversation; - } - } - getConversationList(); - } - - // /// 所有消息未读数 - // int getAllUnReadCount() { - // allUnReadCount = 0; - // for (var value in conversationList) { - // allUnReadCount += value.unreadCount ?? 0; - // } - // // for (var value in systemConversationList) { - // // i += value.unreadMessageNum; - // // } - // notifyListeners(); - // return allUnReadCount; - // } - - /// 所有消息未读数 - void getAllUnReadCount() async { - V2TimValueCallback res = - await TencentImSDKPlugin.v2TIMManager - .getConversationManager() - .getTotalUnreadMessageCount(); - if (res.code == 0) { - print('初始未读总数: ${res.data}'); - messageUnReadCount = res.data ?? 0; - allUnReadCount = - messageUnReadCount + activityUnReadCount + notifcationUnReadCount; - notifyListeners(); - // 这里可以先用初始值更新UI - } - } - - ///登录IM - Future loginTencetRtm(BuildContext context) async { - SocialChatLoginRes? userModel = AccountStorage().getCurrentUser(); - bool logined = false; - - while (!logined && userModel != null) { - await Future.delayed(Duration(milliseconds: 550)); - try { - if (userModel.userSig != null) { - V2TimCallback res = await TencentImSDKPlugin.v2TIMManager.login( - userID: userModel.userProfile?.id ?? "", - userSig: userModel.userSig ?? "", - ); - print( - 'tim voLogin:${res.code},${userModel.userProfile?.id},${userModel.userSig}', - ); - if (res.code == 0) { - isLogout = false; - // 登录成功逻辑 - logined = true; - print('tim 登录成功'); - await initConversation(); - } else { - // 登录失败逻辑 - //print('timm 需要重新登录2'); - print('tim 登录失败${res.code}'); - SCTts.show('tim login fail'); - } - } else { - //print('timm 需要重新登录sign'); - SCTts.show('tim login fail'); - } - } catch (e) { - //print('timm 登录异常:${e.toString()}'); - SCTts.show('timm login fail:${e.toString()}'); - } - userModel = AccountStorage().getCurrentUser(); - } - } - - _onNewMessage(V2TimMessage message) { - if (message.groupID != null) { - ///全服通知 - if (message.groupID == SCGlobalConfig.bigBroadcastGroup) { - _newBroadCastMsgRecv(message.groupID!, message); - } - - ///群消息 - for (var element in onNewMessageListenerGroupMap.values) { - element?.call(message.groupID!, message); - } - } else { - ///单聊消息 - _onNew1v1Message(message); - } - } - - _onNew1v1Message(V2TimMessage? message, {String? msgId}) async { - for (var element in conversationList) { - if (message?.userID == element?.userID) { - element.lastMessage = message; - if (onNewMessageCurrentConversationListener == null) { - element.unreadCount = element.unreadCount! + 1; - } - } - } - if (message?.userID == customerInfo?.id) { - if (onNewMessageCurrentConversationListener == null) { - conversationMap["c2c_${customerInfo?.id}"]?.unreadCount = - (conversationMap["c2c_${customerInfo?.id}"]?.unreadCount ?? 0) + 1; - } - } - systemUnReadCount = _systemUnreadCount(); - notifyListeners(); - onNewMessageCurrentConversationListener?.call(message, msgId: msgId); - } - - void _onRefreshConversation(List conversations) { - for (V2TimConversation conversation in conversations) { - conversationMap[conversation.conversationID] = conversation; - } - getConversationList(); - } - - ///创建房间im群聊 - Future> createRoomGroup( - String groupID, - String groupName, - ) { - return V2TIMGroupManager().createGroup( - groupID: groupID, - groupType: GroupType.AVChatRoom, - groupName: groupName, - ); - } - - ///加入房间的im群聊 - Future joinRoomGroup(String groupID, String message) async { - _luckGiftPushQueue.clear(); - currentPlayingLuckGift = null; - _currentLuckGiftPushKey = null; - var joinResult = await TencentImSDKPlugin.v2TIMManager.joinGroup( - groupID: groupID, - message: message, - ); - if (joinResult.code == 0) { - onNewMessageListenerGroupMap[groupID] = _newGroupMsg; - } - return joinResult; - } - - ///发送文本消息 - Future dispatchMessage( - Msg msg, { - bool showEmoticons = true, - bool addLocal = true, - }) async { - if (addLocal) { - addMsg(msg); - } - // 发送消息到腾讯云IM - if (msg.groupId != null) { - await _sendTencentMessage(msg); - } - notifyListeners(); - } - - // 发送腾讯云消息 - Future _sendTencentMessage(Msg msg) async { - try { - switch (msg.type) { - case SCRoomMsgType.text: - case SCRoomMsgType.shangMai: - case SCRoomMsgType.emoticons: - case SCRoomMsgType.xiaMai: - case SCRoomMsgType.killXiaMai: - case SCRoomMsgType.roomRoleChange: - case SCRoomMsgType.roomSettingUpdate: - case SCRoomMsgType.roomBGUpdate: - case SCRoomMsgType.qcfj: - case SCRoomMsgType.fengMai: - case SCRoomMsgType.jieFeng: - case SCRoomMsgType.joinRoom: - case SCRoomMsgType.gift: - case SCRoomMsgType.bsm: - case SCRoomMsgType.roomDice: - case SCRoomMsgType.roomRPS: - case SCRoomMsgType.roomLuckNumber: - case SCRoomMsgType.image: - case SCRoomMsgType.roomGameClose: - case SCRoomMsgType.roomGameCreate: - case SCRoomMsgType.luckGiftAnimOther: - _sendRoomMessage(msg); - break; - default: - break; - } - } catch (e) { - print('发送消息失败: $e'); - // 处理发送失败的情况 - } - } - - ///发送单聊文本消息 - Future sendC2CTextMsg( - String msg, - V2TimConversation toConversation, - ) async { - // 创建文本消息 - V2TimValueCallback createTextMessageRes = - await TencentImSDKPlugin.v2TIMManager - .getMessageManager() - .createTextMessage( - text: msg, // 文本信息 - ); - if (createTextMessageRes.code == 0) { - // 文本信息创建成功 - String id = createTextMessageRes.data!.id!; - // 发送文本消息 - // 在sendMessage时,若只填写receiver则发个人用户单聊消息 - // 若只填写groupID则发群组消息 - // 若填写了receiver与groupID则发群内的个人用户,消息在群聊中显示,只有指定receiver能看见 - String receiveId = toConversation.userID!; - V2TimValueCallback sendMessageRes = await TencentImSDKPlugin - .v2TIMManager - .getMessageManager() - .sendMessage( - id: id, // 创建的messageid - receiver: toConversation.userID!, // 接收人id - needReadReceipt: true, - groupID: '', // 是否需要已读回执 - ); - if (sendMessageRes.code == 0) { - // 发送成功 - _onNew1v1Message(sendMessageRes.data); - } else { - SCTts.show( - 'create fail,code:${sendMessageRes.code},${sendMessageRes.desc}', - ); - } - } - } - - ///发送单聊自定义消息 - Future sendC2CCustomMsg( - String msg, - V2TimConversation toConversation, - String extension, - ) async { - // 创建文本消息 - V2TimValueCallback createCustomMessageRes = - await TencentImSDKPlugin.v2TIMManager - .getMessageManager() - .createCustomMessage( - data: msg, // 文本信息 - extension: extension, - ); - if (createCustomMessageRes.code == 0) { - // 文本信息创建成功 - String id = createCustomMessageRes.data!.id!; - // 发送文本消息 - // 在sendMessage时,若只填写receiver则发个人用户单聊消息 - // 若只填写groupID则发群组消息 - // 若填写了receiver与groupID则发群内的个人用户,消息在群聊中显示,只有指定receiver能看见 - String receiveId = toConversation.userID!; - V2TimValueCallback sendMessageRes = await TencentImSDKPlugin - .v2TIMManager - .getMessageManager() - .sendMessage( - id: id, - // 创建的messageid - receiver: toConversation.userID!, - // 接收人id - needReadReceipt: true, - isSupportMessageExtension: true, - groupID: '', // 是否需要已读回执 - ); - if (sendMessageRes.code == 0) { - // 发送成功 - _onNew1v1Message(sendMessageRes.data); - } else { - SCTts.show( - 'create fail,code:${sendMessageRes.code},${sendMessageRes.desc}', - ); - } - } - } - - ///发送单聊图片消息 - Future sendImageMsg({ - List? selectedList, - File? file, - required V2TimConversation conversation, - }) async { - if (file != null) { - File newFile = await SCMessageUtils.createImageElem(file); - V2TimValueCallback createImageMessageRes = - await TencentImSDKPlugin.v2TIMManager - .getMessageManager() - .createImageMessage(imagePath: newFile.path); - if (createImageMessageRes.code == 0) { - String id = createImageMessageRes.data!.id!; - V2TimValueCallback sendMessageRes = - await TencentImSDKPlugin.v2TIMManager - .getMessageManager() - .sendMessage( - id: id, - receiver: conversation.userID!, - needReadReceipt: true, - groupID: '', - ); - if (sendMessageRes.code == 0) { - // 发送成功 - _onNew1v1Message(sendMessageRes.data); - } else { - SCTts.show( - 'create fail,code:${sendMessageRes.code},${sendMessageRes.desc}', - ); - } - } - } else { - if (selectedList != null) { - for (File entity in selectedList) { - String id = ""; - V2TimMessage? message; - //判断是视频或者图片消息 - - if (SCPathUtils.getFileType(entity.path) == "image") { - V2TimValueCallback createImageMessageRes = - await TencentImSDKPlugin.v2TIMManager - .getMessageManager() - .createImageMessage(imagePath: entity.path); - if (createImageMessageRes.code == 0) { - message = createImageMessageRes.data?.messageInfo; - id = createImageMessageRes.data!.id!; - // 创建图片 - File newFile = await SCMessageUtils.createImageElem(entity); - //发送 - V2TimImageElem elem = V2TimImageElem(path: newFile.path); - message?.imageElem = elem; - } - } else if (SCPathUtils.getFileType(entity.path) == "video_pic") { - if (entity.lengthSync() > 50000000) { - SCTts.show( - SCAppLocalizations.of(context!)!.theVideoSizeCannotExceed, - ); - return; - } - // 复制一份视频 - String md5Str1 = keyToMd5(entity.path); - File newFile = File( - "${FileCacheManager.videoCachePath}/$md5Str1.mp4", - ); - if (!newFile.existsSync()) { - await entity.copy(newFile.path); - } - // 创建缩略图 - String? thumbImagePath = await SCMessageUtils.generateFileThumbnail( - newFile.path, - 128, - ); - V2TimValueCallback createVideoMessageRes = - await TencentImSDKPlugin.v2TIMManager - .getMessageManager() - .createVideoMessage( - videoFilePath: entity.path, - type: "mp4", - duration: 0, - snapshotPath: thumbImagePath ?? "", - ); - - if (createVideoMessageRes.code == 0) { - message = createVideoMessageRes.data?.messageInfo; - id = createVideoMessageRes.data!.id!; - } - } - // 消息设置 - message?.isSelf = true; - // message.conversation = conversation; - message?.status = MessageStatus.V2TIM_MSG_STATUS_SENDING; - print('组装完成,准备发送:${message?.toJson()}'); - // String id = await FTIM.getMessageManager().sendMessage(message); - // message.msgId = id; - V2TimValueCallback sendMessageRes = - await TencentImSDKPlugin.v2TIMManager - .getMessageManager() - .sendMessage( - id: id, - receiver: conversation.userID!, - needReadReceipt: true, - groupID: '', - ); - if (sendMessageRes.code == 0) { - // 发送成功 - _onNew1v1Message(sendMessageRes.data); - } else { - SCTts.show( - 'create fail,code:${sendMessageRes.code},${sendMessageRes.desc}', - ); - } - } - } - } - notifyListeners(); - } - - // 发送消息(房间) - Future _sendRoomMessage(Msg msg) async { - try { - if (msg.type == SCRoomMsgType.luckGiftAnimOther) { - _giftFxLog( - 'send room msg start ' - 'type=${msg.type} ' - 'groupId=${msg.groupId} ' - 'msg=${msg.msg} ' - 'giftId=${msg.gift?.id} ' - 'giftPhoto=${msg.gift?.giftPhoto}', - ); - } - if (msg.type == SCRoomMsgType.roomSettingUpdate) { - debugPrint( - "[Room Cover Sync] send roomSettingUpdate groupId=${msg.groupId ?? ""} roomId=${msg.msg ?? ""}", - ); - } - var user = msg.user?.copyWith(); - var toUser = msg.toUser?.copyWith(); - user?.cleanWearHonor(); - user?.cleanWearBadge(); - user?.cleanPhotos(); - toUser?.cleanWearHonor(); - toUser?.cleanWearBadge(); - toUser?.cleanUseProps(); - toUser?.cleanPhotos(); - msg.needUpDataUserInfo = - Provider.of( - context!, - listen: false, - ).needUpDataUserInfo; - msg.user = user; - msg.toUser = toUser; - final textMsg = await TencentImSDKPlugin.v2TIMManager - .getMessageManager() - .createCustomMessage(data: jsonEncode(msg.toJson())); - - if (msg.type == SCRoomMsgType.luckGiftAnimOther) { - _giftFxLog( - 'send room msg createCustomMessage ' - 'type=${msg.type} ' - 'code=${textMsg.code} ' - 'id=${textMsg.data?.id} ' - 'desc=${textMsg.desc}', - ); - } - - if (textMsg.code != 0) return; - - final sendResult = await TencentImSDKPlugin.v2TIMManager - .getMessageManager() - .sendMessage( - id: textMsg.data!.id!, - groupID: msg.groupId!, - receiver: '', - ); - if (msg.type == SCRoomMsgType.luckGiftAnimOther) { - _giftFxLog( - 'send room msg result ' - 'type=${msg.type} ' - 'code=${sendResult.code} ' - 'msgId=${sendResult.data?.msgID} ' - 'groupId=${sendResult.data?.groupID} ' - 'desc=${sendResult.desc}', - ); - } - if (sendResult.code == 0) {} - } catch (e) { - throw Exception("create fail: $e"); - } - } - - /// 添加消息 - addMsg(Msg msg) { - final mergedGiftMsg = _mergeGiftMessageIfNeeded(msg); - if (mergedGiftMsg != null) { - msgAllListener?.call(mergedGiftMsg); - msgGiftListener?.call(mergedGiftMsg); - if (msg.type == SCRoomMsgType.gift) { - msgFloatingGiftListener?.call(msg); - } - notifyListeners(); - return; - } - - roomAllMsgList.insert(0, msg); - if (roomAllMsgList.length > 250) { - print('大于200条消息'); - roomAllMsgList.removeAt(roomAllMsgList.length - 1); - } - msgAllListener?.call(msg); - - if (msg.type == SCRoomMsgType.text) { - roomChatMsgList.insert(0, msg); - if (roomChatMsgList.length > 250) { - print('大于200条消息'); - roomChatMsgList.removeAt(roomChatMsgList.length - 1); - } - msgChatListener?.call(msg); - } else if (msg.type == SCRoomMsgType.image) { - roomChatMsgList.insert(0, msg); - if (roomChatMsgList.length > 250) { - print('大于200条消息'); - roomChatMsgList.removeAt(roomChatMsgList.length - 1); - } - msgChatListener?.call(msg); - } else if (msg.type == SCRoomMsgType.gift || - msg.type == SCRoomMsgType.luckGiftAnimOther) { - roomGiftMsgList.insert(0, msg); - if (roomGiftMsgList.length > 250) { - print('大于200条消息'); - roomGiftMsgList.removeAt(roomGiftMsgList.length - 1); - } - msgGiftListener?.call(msg); - if (msg.type == SCRoomMsgType.gift) { - msgFloatingGiftListener?.call(msg); - } - } - } - - Msg? _mergeGiftMessageIfNeeded(Msg incoming) { - if (incoming.type != SCRoomMsgType.gift && - incoming.type != SCRoomMsgType.luckGiftAnimOther) { - return null; - } - - final mergeTarget = _findMergeableGiftMessage(incoming); - if (mergeTarget == null) { - return null; - } - - mergeTarget.number = (mergeTarget.number ?? 0) + (incoming.number ?? 0); - mergeTarget.time = DateTime.now().millisecondsSinceEpoch; - if ((incoming.msg ?? "").trim().isNotEmpty) { - mergeTarget.msg = incoming.msg; - } - - _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) { - return existing.type == incoming.type && - existing.groupId == incoming.groupId && - existing.user?.id == incoming.user?.id && - existing.toUser?.id == incoming.toUser?.id && - existing.gift?.id == incoming.gift?.id; - } - - void _moveMessageToFront(List messages, Msg target) { - final index = messages.indexOf(target); - if (index <= 0) { - return; - } - messages.removeAt(index); - messages.insert(0, target); - } - - bool _shouldHighlightLuckyGiftReward(SCBroadCastLuckGiftPush broadCastRes) { - final rewardData = broadCastRes.data; - if (rewardData == null) { - return false; - } - if (rewardData.isBigReward) { - return true; - } - return (rewardData.multiple ?? 0) >= 5; - } - - bool _isLuckyGiftInCurrentRoom(SCBroadCastLuckGiftPush broadCastRes) { - if (context == null) { - return false; - } - final currentRoomId = - Provider.of( - context!, - listen: false, - ).currenRoom?.roomProfile?.roomProfile?.id ?? - ''; - final roomId = broadCastRes.data?.roomId ?? ''; - return currentRoomId.isNotEmpty && - roomId.isNotEmpty && - currentRoomId == roomId; - } - - SCFloatingMessage _buildLuckyGiftFloatingMessage( - SCBroadCastLuckGiftPush broadCastRes, - ) { - final rewardData = broadCastRes.data; - return SCFloatingMessage( - type: 0, - userId: rewardData?.sendUserId, - roomId: rewardData?.roomId, - toUserId: rewardData?.acceptUserId, - userAvatarUrl: rewardData?.userAvatar, - userName: rewardData?.nickname, - toUserName: rewardData?.acceptNickname, - giftUrl: rewardData?.giftCover, - giftId: rewardData?.giftId, - number: rewardData?.giftQuantity, - coins: rewardData?.awardAmount, - multiple: rewardData?.multiple, - priority: 1000, - ); - } - - void _handleLuckyGiftGlobalNews( - SCBroadCastLuckGiftPush broadCastRes, { - required String source, - }) { - final rewardData = broadCastRes.data; - if (rewardData == null) { - return; - } - if (_isLuckyGiftInCurrentRoom(broadCastRes)) { - addluckGiftPushQueue(broadCastRes); - } - if (!rewardData.shouldShowGlobalNews || - (rewardData.multiple ?? 0) < _luckyGiftFloatMinMultiple) { - return; - } - if (source == 'broadcast' && _isLuckyGiftInCurrentRoom(broadCastRes)) { - _giftFxLog( - 'skip global lucky gift overlay ' - 'reason=current_room_already_receives_group_msg ' - 'roomId=${rewardData.roomId} ' - 'giftId=${rewardData.giftId}', - ); - return; - } - OverlayManager().addMessage(_buildLuckyGiftFloatingMessage(broadCastRes)); - } - - void _handleRoomLuckyGiftMessage(SCBroadCastLuckGiftPush broadCastRes) { - final rewardData = broadCastRes.data; - if (rewardData == null) { - return; - } - final roomMsg = Msg( - groupId: '', - msg: '', - type: SCRoomMsgType.gameLuckyGift, - ); - roomMsg.gift = SocialChatGiftRes( - id: rewardData.giftId, - giftPhoto: rewardData.giftCover, - 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.user = SocialChatUserProfile( - id: rewardData.sendUserId, - userNickname: rewardData.nickname, - ); - addMsg(highlightMsg); - } - - addluckGiftPushQueue(broadCastRes); - _handleLuckyGiftGlobalNews(broadCastRes, source: 'room_group'); - - if (rewardData.sendUserId == - AccountStorage().getCurrentUser()?.userProfile?.id) { - Provider.of( - context!, - listen: false, - ).updateLuckyRewardAmount(roomMsg.awardAmount ?? 0); - } - } - - bool isLogout = false; - - logout() async { - V2TimCallback logoutRes = await TencentImSDKPlugin.v2TIMManager.logout(); - TencentImSDKPlugin.v2TIMManager - .getMessageManager() - .removeAdvancedMsgListener(); - if (logoutRes.code == 0) { - isLogout = true; - } - } - - ///全服广播消息 - _newBroadCastMsgRecv(String groupID, V2TimMessage message) async { - try { - String? customData = message.customElem?.data; - if (customData != null && customData.isNotEmpty) { - final data = json.decode(customData); - var type = data["type"]; - if (type == "SYS_ACTIVITY") { - if (onNewActivityMessageCurrentConversationListener != null) { - var recode = Records.fromJson(data["data"]); - onNewActivityMessageCurrentConversationListener?.call(recode); - } else { - activityUnReadCount = activityUnReadCount + 1; - allUnReadCount = - messageUnReadCount + - notifcationUnReadCount + - activityUnReadCount; - notifyListeners(); - } - } else if (type == "SYS_ANNOUNCEMENT") { - if (onNewNotifcationMessageCurrentConversationListener != null) { - var recode = Records.fromJson(data["data"]); - onNewNotifcationMessageCurrentConversationListener?.call(recode); - } else { - notifcationUnReadCount = notifcationUnReadCount + 1; - allUnReadCount = - messageUnReadCount + - notifcationUnReadCount + - activityUnReadCount; - notifyListeners(); - } - } else if (type == "GAME_BAISHUN_WIN") { - if (SCGlobalConfig.isReview) { - ///审核状态不播放动画 - return; - } - var fdata = data["data"]; - var winCoins = fdata["currencyDiff"]; - if (winCoins > 14999) { - ///达到5000才飘屏 - SCFloatingMessage msg = SCFloatingMessage( - type: 2, - userId: fdata["account"], - userAvatarUrl: fdata["userAvatar"], - userName: fdata["userNickname"], - giftUrl: fdata["gameUrl"], - roomId: fdata["roomId"], - coins: fdata["currencyDiff"], - ); - OverlayManager().addMessage(msg); - } - } else if (type == "GAME_LUCKY_GIFT") { - final broadCastRes = SCBroadCastLuckGiftPush.fromJson(data); - _giftFxLog( - 'recv GAME_LUCKY_GIFT broadcast ' - 'giftId=${broadCastRes.data?.giftId} ' - 'roomId=${broadCastRes.data?.roomId} ' - 'sendUserId=${broadCastRes.data?.sendUserId} ' - 'acceptUserId=${broadCastRes.data?.acceptUserId} ' - 'giftQuantity=${broadCastRes.data?.giftQuantity} ' - 'awardAmount=${broadCastRes.data?.awardAmount} ' - 'multiple=${broadCastRes.data?.multiple} ' - 'multipleType=${broadCastRes.data?.multipleType} ' - 'globalNews=${broadCastRes.data?.globalNews}', - ); - _handleLuckyGiftGlobalNews(broadCastRes, source: 'broadcast'); - } else if (type == "REGISTER_REWARD_GRANTED") { - await DataPersistence.setPendingRegisterRewardDialog(true); - await DataPersistence.clearAwaitRegisterRewardSocket(); - eventBus.fire(RegisterRewardGrantedEvent(data: data["data"])); - } else if (type == "ROCKET_ENERGY_LAUNCH") { - ///火箭触发飘屏 - var fdata = data["data"]; - SCFloatingMessage msg = SCFloatingMessage( - type: 3, - roomId: fdata["roomId"], - rocketLevel: fdata["fromLevel"], - userAvatarUrl: fdata["userAvatar"], - userName: fdata["nickname"], - userId: fdata["actualAccount"], - priority: 1000, - ); - OverlayManager().addMessage(msg); - } else if (type == SCRoomMsgType.roomRedPacket) { - ///红包触发飘屏 - var fData = data["data"]; - SCFloatingMessage msg = SCFloatingMessage( - type: 4, - roomId: fData["roomId"], - userAvatarUrl: fData["userAvatar"], - userName: fData["userNickname"], - userId: fData["actualAccount"], - toUserId: fData["packetId"], - priority: 1000, - ); - if (msg.roomId == - Provider.of( - context!, - listen: false, - ).currenRoom?.roomProfile?.roomProfile?.id) { - Provider.of( - context!, - listen: false, - ).loadRoomRedPacketList(1); - } - OverlayManager().addMessage(msg); - } else if (type == SCRoomMsgType.inviteRoom) { - ///邀请进入房间 - var fdata = data["data"]; - SCFloatingMessage msg = SCFloatingMessage.fromJson(fdata); - if (msg.toUserId == - AccountStorage().getCurrentUser()?.userProfile?.id && - msg.roomId != - Provider.of( - context!, - listen: false, - ).currenRoom?.roomProfile?.roomProfile?.id) { - SmartDialog.dismiss(tag: "showInviteRoom"); - SmartDialog.show( - tag: "showInviteRoom", - alignment: Alignment.center, - animationType: SmartAnimationType.fade, - builder: (_) { - return InviteRoomDialog(msg); - }, - ); - } - } - } - } catch (e) {} - } - - _newGroupMsg(String groupID, V2TimMessage message) { - if (groupID != - Provider.of( - context!, - listen: false, - ).currenRoom?.roomProfile?.roomProfile?.roomAccount) { - return; - } - try { - String? customData = message.customElem?.data; - if (customData != null && customData.isNotEmpty) { - // 直接处理字符串格式的自定义数据 - final data = json.decode(customData); - debugPrint(">>>>>>>>>>>>>>>>>>>消息类型${data["type"]}"); - - if (data["type"] == SCRoomMsgType.roomRedPacket) { - ///房间红包 - var fData = data["data"]; - SCFloatingMessage msg = SCFloatingMessage( - type: 4, - roomId: fData["roomId"], - userAvatarUrl: fData["userAvatar"], - userName: fData["userNickname"], - userId: fData["actualAccount"], - toUserId: fData["packetId"], - priority: 1000, - ); - Provider.of( - context!, - listen: false, - ).loadRoomRedPacketList(1); - OverlayManager().addMessage(msg); - return; - } - Msg msg = Msg.fromJson(data); - - if (msg.type == SCRoomMsgType.sendGift || - msg.type == SCRoomMsgType.gameBurstCrystalSprint || - msg.type == SCRoomMsgType.gameBurstCrystalBox) { - ///这个消息暂时不监听 - return; - } - if (msg.type == SCRoomMsgType.bsm) { - if (msg.toUser?.id == - AccountStorage().getCurrentUser()?.userProfile?.id) { - SmartDialog.show( - tag: "showConfirmDialog", - alignment: Alignment.center, - debounce: true, - animationType: SmartAnimationType.fade, - builder: (_) { - return MsgDialog( - title: SCAppLocalizations.of(context!)!.tips, - msg: SCAppLocalizations.of( - context!, - )!.invitesYouToTheMicrophone(msg.msg ?? ""), - btnText: SCAppLocalizations.of(context!)!.confirm, - onEnsure: () { - ///上麦 - num index = - Provider.of( - context!, - listen: false, - ).findWheat(); - if (index > -1) { - Provider.of( - context!, - listen: false, - ).shangMai( - index, - eventType: "INVITE", - inviterId: msg.role, - ); - } - }, - ); - }, - ); - } - return; - } - if (msg.type == SCRoomMsgType.killXiaMai) { - ///踢下麦 - if (msg.msg == AccountStorage().getCurrentUser()?.userProfile?.id) { - Provider.of( - context!, - listen: false, - ).engine?.setClientRole(role: ClientRoleType.clientRoleAudience); - } - Provider.of( - context!, - listen: false, - ).retrieveMicrophoneList(notifyIfUnchanged: false); - return; - } - - if (msg.type == SCRoomMsgType.roomSettingUpdate) { - debugPrint( - "[Room Cover Sync] recv roomSettingUpdate groupId=$groupID roomId=${msg.msg ?? ""}", - ); - Provider.of( - context!, - listen: false, - ).loadRoomInfo(msg.msg ?? ""); - return; - } - if (msg.type == SCRoomMsgType.roomBGUpdate) { - SCRoomThemeListRes res; - if ((msg.msg ?? "").isNotEmpty) { - res = SCRoomThemeListRes.fromJson(jsonDecode(msg.msg!)); - } else { - res = SCRoomThemeListRes(); - } - Provider.of( - context!, - listen: false, - ).updateRoomBG(res); - return; - } - if (msg.type == SCRoomMsgType.emoticons) { - Provider.of( - context!, - listen: false, - ).starPlayEmoji(msg); - return; - } - if (msg.type == SCRoomMsgType.micChange) { - Provider.of( - context!, - listen: false, - ).micChange(BroadCastMicChangePush.fromJson(data).data?.mics); - } else if (msg.type == SCRoomMsgType.shangMai || - msg.type == SCRoomMsgType.xiaMai || - msg.type == SCRoomMsgType.fengMai || - msg.type == SCRoomMsgType.jieFeng) { - Provider.of( - context!, - listen: false, - ).retrieveMicrophoneList(notifyIfUnchanged: false); - } else if (msg.type == SCRoomMsgType.refreshOnlineUser) { - Provider.of( - context!, - listen: false, - ).fetchOnlineUsersList(notifyIfUnchanged: false); - } else if (msg.type == SCRoomMsgType.gameLuckyGift) { - var broadCastRes = SCBroadCastLuckGiftPush.fromJson(data); - _giftFxLog( - 'recv GAME_LUCKY_GIFT ' - 'giftId=${broadCastRes.data?.giftId} ' - 'roomId=${broadCastRes.data?.roomId} ' - 'sendUserId=${broadCastRes.data?.sendUserId} ' - 'acceptUserId=${broadCastRes.data?.acceptUserId} ' - 'giftQuantity=${broadCastRes.data?.giftQuantity} ' - 'awardAmount=${broadCastRes.data?.awardAmount} ' - 'multiple=${broadCastRes.data?.multiple} ' - 'multipleType=${broadCastRes.data?.multipleType} ' - 'globalNews=${broadCastRes.data?.globalNews}', - ); - _handleRoomLuckyGiftMessage(broadCastRes); - } else { - if (msg.type == SCRoomMsgType.joinRoom) { - final shouldShowRoomVisualEffects = - Provider.of( - context!, - listen: false, - ).shouldShowRoomVisualEffects; - if (msg.user != null) { - Provider.of( - context!, - listen: false, - ).addOnlineUser(msg.groupId ?? "", msg.user!); - } - if (msgUserJoinListener != null) { - msgUserJoinListener!(msg); - } - - ///坐骑 - if (msg.user?.getMountains() != null) { - if (SCGlobalConfig.isEntryVehicleAnimation && - shouldShowRoomVisualEffects) { - SCGiftVapSvgaManager().play( - msg.user?.getMountains()?.sourceUrl ?? "", - priority: 100, - type: 1, - ); - } - } - } else if (msg.type == SCRoomMsgType.gift) { - final gift = msg.gift; - if (gift == null) { - _giftFxLog( - 'recv gift msg skipped reason=no_gift ' - 'fromUserId=${msg.user?.id} ' - 'toUserId=${msg.toUser?.id} ' - 'quantity=${msg.number}', - ); - } else { - final rtcProvider = Provider.of( - context!, - listen: false, - ); - final special = gift.special ?? ""; - final giftSourceUrl = gift.giftSourceUrl ?? ""; - final hasSource = giftSourceUrl.isNotEmpty; - final hasAnimation = scGiftHasAnimationSpecial(special); - final hasGlobalGift = special.contains( - SCGiftType.GLOBAL_GIFT.name, - ); - final hasFullScreenEffect = scGiftHasFullScreenEffect(special); - _giftFxLog( - 'recv gift msg ' - 'fromUserId=${msg.user?.id} ' - 'fromUserName=${msg.user?.userNickname} ' - 'toUserId=${msg.toUser?.id} ' - 'toUserName=${msg.toUser?.userNickname} ' - 'giftId=${gift.id} ' - 'giftName=${gift.giftName} ' - 'giftSourceUrl=$giftSourceUrl ' - 'special=$special ' - 'hasSource=$hasSource ' - 'hasAnimation=$hasAnimation ' - 'hasGlobalGift=$hasGlobalGift ' - 'hasFullScreenEffect=$hasFullScreenEffect ' - 'effectsEnabled=${SCGlobalConfig.isGiftSpecialEffects}', - ); - if (giftSourceUrl.isNotEmpty && special.isNotEmpty) { - if (scGiftHasFullScreenEffect(special)) { - if (SCGlobalConfig.isGiftSpecialEffects && - rtcProvider.shouldShowRoomVisualEffects) { - _giftFxLog( - 'trigger player play path=$giftSourceUrl ' - 'giftId=${gift.id} giftName=${gift.giftName}', - ); - SCGiftVapSvgaManager().play(giftSourceUrl); - } 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) { - OverlayManager().addMessage( - SCFloatingMessage( - type: 1, - userAvatarUrl: msg.user?.userAvatar ?? "", - userName: msg.user?.userNickname ?? "", - toUserName: msg.toUser?.userNickname ?? "", - toUserAvatarUrl: msg.toUser?.userAvatar ?? "", - giftUrl: gift.giftPhoto, - number: msg.number, - coins: coins, - roomId: msg.msg, - ), - ); - } - } - } else if (msg.type == SCRoomMsgType.luckGiftAnimOther) { - final hideLGiftAnimal = - Provider.of( - context!, - listen: false, - ).hideLGiftAnimal; - if (hideLGiftAnimal) { - _giftFxLog( - 'recv LUCK_GIFT_ANIM_OTHER skipped ' - 'reason=hideLGiftAnimal ' - 'giftPhoto=${msg.gift?.giftPhoto}', - ); - } else { - final targetUserIds = - (jsonDecode(msg.msg ?? "") as List) - .map((e) => e as String) - .toList(); - _giftFxLog( - 'recv LUCK_GIFT_ANIM_OTHER ' - 'giftPhoto=${msg.gift?.giftPhoto} ' - 'sendUserId=${msg.user?.id} ' - 'toUserId=${msg.toUser?.id} ' - 'quantity=${msg.number} ' - 'targetUserIds=${targetUserIds.join(",")}', - ); - eventBus.fire( - GiveRoomLuckWithOtherEvent( - msg.gift?.giftPhoto ?? "", - targetUserIds, - ), - ); - if (msg.user != null && msg.toUser != null && msg.gift != null) { - _giftFxLog( - 'trigger floating gift listener from LUCK_GIFT_ANIM_OTHER ' - 'sendUserId=${msg.user?.id} ' - 'toUserId=${msg.toUser?.id} ' - 'quantity=${msg.number} ' - 'giftId=${msg.gift?.id}', - ); - msgFloatingGiftListener?.call(msg); - } else { - _giftFxLog( - 'skip floating gift listener from LUCK_GIFT_ANIM_OTHER ' - 'reason=incomplete_msg ' - 'sendUserId=${msg.user?.id} ' - 'toUserId=${msg.toUser?.id} ' - 'giftId=${msg.gift?.id} ' - 'quantity=${msg.number}', - ); - } - } - } else if (msg.type == SCRoomMsgType.roomRoleChange) { - ///房间身份变动 - Provider.of( - context!, - listen: false, - ).retrieveMicrophoneList(); - if (msg.toUser?.id == - AccountStorage().getCurrentUser()?.userProfile?.id) { - Provider.of( - context!, - listen: false, - ).currenRoom?.entrants?.setRoles(msg.msg); - if (msg.msg == SCRoomRolesType.TOURIST.name && - !(Provider.of( - context!, - listen: false, - ).currenRoom?.roomProfile?.roomSetting?.touristMike ?? - false)) { - ///如果变成了游客,房间又是禁止游客上麦,需要下麦 - num index = Provider.of( - context!, - listen: false, - ).userOnMaiInIndex( - AccountStorage().getCurrentUser()?.userProfile?.id ?? "", - ); - if (index > -1) { - Provider.of( - context!, - listen: false, - ).xiaMai(index); - } - } - } - } else if (msg.type == SCRoomMsgType.roomDice) { - if ((msg.number ?? -1) > -1) { - Provider.of( - context!, - listen: false, - ).starPlayEmoji(msg); - } - } else if (msg.type == SCRoomMsgType.roomRPS) { - if ((msg.number ?? -1) > -1) { - Provider.of( - context!, - listen: false, - ).starPlayEmoji(msg); - } - } - addMsg(msg); - } - } - } catch (e) { - throw Exception("message parser fail: $e"); - } - } - - ///加入全服广播群 - joinBigBroadcastGroup() async { - bool joined = false; - while (!isLogout && !joined) { - await Future.delayed(Duration(milliseconds: 550)); - try { - var joinResult = await TencentImSDKPlugin.v2TIMManager.joinGroup( - groupID: SCGlobalConfig.bigBroadcastGroup, - message: "", - ); - if (joinResult.code == 0) { - joined = true; - } - } catch (e) { - //print('timm 登录异常:${e.toString()}'); - SCTts.show('broadcastGroup join fail:${e.toString()}'); - } - } - } - - ///发送全服消息 - sendBigBroadcastGroup(BigBroadcastGroupMessage msg) async { - try { - final textMsg = await TencentImSDKPlugin.v2TIMManager - .getMessageManager() - .createCustomMessage(data: jsonEncode(msg.toJson())); - - if (textMsg.code != 0) return; - - final sendResult = await TencentImSDKPlugin.v2TIMManager - .getMessageManager() - .sendMessage( - id: textMsg.data!.id!, - groupID: SCGlobalConfig.bigBroadcastGroup, - receiver: '', - ); - if (sendResult.code == 0) {} - } catch (e) { - throw Exception("create fail: $e"); - } - } - - Future quitGroup(String groupID) async { - await TencentImSDKPlugin.v2TIMManager.quitGroup(groupID: groupID); - } - - ///清屏 - 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(); - _luckGiftPushQueue.clear(); - currentPlayingLuckGift = null; - _currentLuckGiftPushKey = null; - onNewMessageListenerGroupMap.forEach((k, v) { - v = null; - }); - onNewMessageListenerGroupMap.clear(); - } - - void addluckGiftPushQueue(SCBroadCastLuckGiftPush broadCastRes) { - if (SCGlobalConfig.isLuckGiftSpecialEffects) { - if (!shouldPlayLuckyGiftBurst(broadCastRes.data)) { - return; - } - final eventKey = _luckyGiftPushEventKey(broadCastRes); - if (eventKey.isNotEmpty) { - if (_currentLuckGiftPushKey == eventKey) { - return; - } - for (final queued in _luckGiftPushQueue) { - if (queued != null && _luckyGiftPushEventKey(queued) == eventKey) { - return; - } - } - } - while (_luckGiftPushQueue.length >= _maxLuckGiftPushQueueLength) { - _luckGiftPushQueue.removeFirst(); - } - _luckGiftPushQueue.add(broadCastRes); - playLuckGiftBackCoins(); - } - } - - 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() { - _luckGiftPushQueue.clear(); - _currentLuckGiftPushKey = null; - } - - void playLuckGiftBackCoins() { - if (currentPlayingLuckGift != null || _luckGiftPushQueue.isEmpty) { - return; - } - currentPlayingLuckGift = _luckGiftPushQueue.removeFirst(); - _currentLuckGiftPushKey = - currentPlayingLuckGift == null - ? null - : _luckyGiftPushEventKey(currentPlayingLuckGift!); - notifyListeners(); - Future.delayed( - Duration(milliseconds: _luckyGiftBurstDisplayDurationMs), - () { - currentPlayingLuckGift = null; - _currentLuckGiftPushKey = null; - notifyListeners(); - playLuckGiftBackCoins(); - }, - ); - } - - 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}'; - } - - 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 conversationId in SCGlobalConfig.systemConversationIds) { - conversationMap[conversationId]?.unreadCount = 0; - } - systemUnReadCount = 0; - notifyListeners(); - } - - void updateCustomerCount(int count) { - conversationMap["c2c_${customerInfo?.id}"]?.unreadCount = 0; - customerUnReadCount = 0; - notifyListeners(); - } - - void clearC2CHistoryMessage(String conversationID, bool needShowToast) async { - // 清空单聊本地及云端的消息(不删除会话) - - V2TimCallback clearC2CHistoryMessageRes = await TencentImSDKPlugin - .v2TIMManager - .getConversationManager() - .deleteConversation(conversationID: conversationID); // 需要清空记录的用户id - if (clearC2CHistoryMessageRes.code == 0) { - // 清除成功 - if (needShowToast) { - SCTts.show(SCAppLocalizations.of(context!)!.operationSuccessful); - } - initConversation(); - } else { - // 清除失败,可以查看 clearC2CHistoryMessageRes.desc 获取错误描述 - } - } -} +import 'dart:async'; +import 'dart:collection'; +import 'dart:convert'; +import 'dart:io'; +import 'package:agora_rtc_engine/agora_rtc_engine.dart'; +import 'package:extended_image/extended_image.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_debouncer/flutter_debouncer.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:yumi/ui_kit/components/sc_tts.dart'; +import 'package:yumi/app/constants/sc_global_config.dart'; +import 'package:yumi/shared/tools/sc_message_utils.dart'; +import 'package:yumi/shared/tools/sc_path_utils.dart'; +import 'package:yumi/shared/tools/sc_room_utils.dart'; +import 'package:yumi/shared/data_sources/sources/local/floating_screen_manager.dart'; +import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart'; +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; +import 'package:yumi/services/audio/rtc_manager.dart'; +import 'package:provider/provider.dart'; +import 'package:tencent_cloud_chat_sdk/enum/V2TimAdvancedMsgListener.dart'; +import 'package:tencent_cloud_chat_sdk/enum/V2TimConversationListener.dart'; +import 'package:tencent_cloud_chat_sdk/enum/V2TimGroupListener.dart'; +import 'package:tencent_cloud_chat_sdk/enum/V2TimSDKListener.dart'; +import 'package:tencent_cloud_chat_sdk/enum/conversation_type.dart'; +import 'package:tencent_cloud_chat_sdk/enum/group_type.dart'; +import 'package:tencent_cloud_chat_sdk/enum/log_level_enum.dart'; +import 'package:tencent_cloud_chat_sdk/enum/message_status.dart'; +import 'package:tencent_cloud_chat_sdk/manager/v2_tim_group_manager.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_callback.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation_result.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_member_info.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_image_elem.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_message_receipt.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_msg_create_info_result.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_user_full_info.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_user_status.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_value_callback.dart'; +import 'package:tencent_cloud_chat_sdk/tencent_im_sdk_plugin.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/dialog/dialog_base.dart'; +import 'package:yumi/app/constants/sc_room_msg_type.dart'; +import 'package:yumi/shared/tools/sc_lk_event_bus.dart'; +import 'package:yumi/shared/tools/sc_gift_vap_svga_manager.dart'; +import 'package:yumi/shared/data_sources/sources/local/file_cache_manager.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_config_repository_imp.dart'; +import 'package:yumi/shared/data_sources/models/message/big_broadcast_group_message.dart'; +import 'package:yumi/shared/data_sources/models/message/sc_floating_message.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_broad_cast_luck_gift_push.dart'; +import 'package:yumi/shared/business_logic/models/res/broad_cast_mic_change_push.dart' + hide Data; +import 'package:yumi/shared/business_logic/models/res/gift_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_public_message_page_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_room_theme_list_res.dart'; +import 'package:yumi/services/general/sc_app_general_manager.dart'; +import 'package:yumi/ui_kit/widgets/room/invite/invite_room_dialog.dart'; +import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart'; +import 'package:yumi/shared/business_logic/models/res/login_res.dart'; +import 'package:yumi/services/gift/gift_system_manager.dart'; + +import '../../shared/data_sources/models/enum/sc_gift_type.dart'; +import '../../shared/data_sources/models/enum/sc_room_roles_type.dart'; + +typedef RoomNewMsgListener = Function(Msg msg); +typedef OnNewMessageListener = Function(V2TimMessage? message, {String? msgId}); +typedef OnNewANMessageListener = Function(Records? message); +typedef OnRevokeMessageListener = Function(String msgId); +typedef OnNewGroupMessageListener = + Function(String groupID, V2TimMessage message); +typedef OnMessageRecvC2CReadListener = Function(List messageIDList); +typedef RtmProvider = RealTimeMessagingManager; + +class RealTimeMessagingManager extends ChangeNotifier { + static const int _giftComboMergeWindowMs = 3000; + static const int _maxLuckGiftPushQueueLength = 12; + static const int _luckyGiftFloatMinMultiple = 5; + static const int _luckyGiftBurstMinMultiple = 10; + static const int _luckyGiftBurstMinAwardAmount = 5000; + static const int _luckyGiftBurstDisplayDurationMs = 2000; + + BuildContext? context; + + void _giftFxLog(String message) { + debugPrint('[GiftFX][RTM] $message'); + } + + ///消息列表 + List roomAllMsgList = []; + List roomChatMsgList = []; + List roomGiftMsgList = []; + RoomNewMsgListener? msgAllListener; + RoomNewMsgListener? msgChatListener; + RoomNewMsgListener? msgGiftListener; + + RoomNewMsgListener? msgFloatingGiftListener; + RoomNewMsgListener? msgLuckyGiftRewardTickerListener; + RoomNewMsgListener? msgUserJoinListener; + + /// 当前会话 + V2TimConversation? currentConversation; + + /// 会话列表缓存 + Map conversationMap = {}; + + ///消息已读监听 + OnMessageRecvC2CReadListener? onMessageRecvC2CReadListener; + + ///消息被撤回监听 + OnRevokeMessageListener? onRevokeMessageListener; + + ///新消息监听 单聊 + OnNewMessageListener? onNewMessageCurrentConversationListener; + + OnNewANMessageListener? onNewActivityMessageCurrentConversationListener; + OnNewANMessageListener? onNewNotifcationMessageCurrentConversationListener; + + ///新消息监听 群聊 + Map onNewMessageListenerGroupMap = {}; + int allUnReadCount = 0; + int messageUnReadCount = 0; + int systemUnReadCount = 0; + int customerUnReadCount = 0; + int activityUnReadCount = 0; + int notifcationUnReadCount = 0; + SCBroadCastLuckGiftPush? currentPlayingLuckGift; + final Queue _luckGiftPushQueue = Queue(); + String? _currentLuckGiftPushKey; + Debouncer debouncer = Debouncer(); + List conversationList = []; + + ///客服 + SocialChatUserProfile? customerInfo; + + int _systemUnreadCount() { + int count = 0; + for (final conversationId in SCGlobalConfig.systemConversationIds) { + count += conversationMap[conversationId]?.unreadCount ?? 0; + } + return count; + } + + V2TimConversation getPreferredSystemConversation() { + final systemConversations = + conversationMap.values + .where( + (element) => + SCGlobalConfig.isSystemConversationId(element.conversationID), + ) + .toList() + ..sort((e1, e2) { + final time1 = e1.lastMessage?.timestamp ?? 0; + final time2 = e2.lastMessage?.timestamp ?? 0; + return time2.compareTo(time1); + }); + + if (systemConversations.isNotEmpty) { + return systemConversations.first; + } + + return V2TimConversation( + type: ConversationType.V2TIM_C2C, + userID: SCGlobalConfig.primarySystemUserId, + conversationID: SCGlobalConfig.primarySystemConversationId, + ); + } + + void getConversationList() { + List list = conversationMap.values.toList(); + list.removeWhere((element) { + if (element.conversationID == "c2c_${customerInfo?.id}") { + return true; + } + if (element.conversationID == "c2c_atyou-newsletter") { + ///删除这个会话,后台乱发的联系人。。防止无效未读数 + clearC2CHistoryMessage(element.conversationID, false); + return true; + } + if (element.lastMessage == null) { + return true; + } + return false; + }); + list.sort((e1, e2) { + int time1 = e1.lastMessage?.timestamp ?? 0; + int time2 = e2.lastMessage?.timestamp ?? 0; + return time2.compareTo(time1); + }); + conversationList = list; + systemUnReadCount = _systemUnreadCount(); + customerUnReadCount = + conversationMap["c2c_${customerInfo?.id}"]?.unreadCount ?? 0; + + notifyListeners(); + } + + init(BuildContext context) async { + this.context = context; + V2TimSDKListener sdkListener = V2TimSDKListener( + onConnectFailed: (int code, String error) { + // 连接失败的回调函数 + // code 错误码 + // error 错误信息 + }, + onConnectSuccess: () { + // SDK 已经成功连接到腾讯云服务器 + }, + onConnecting: () { + // SDK 正在连接到腾讯云服务器 + }, + onKickedOffline: () { + // 当前用户被踢下线,此时可以 UI 提示用户,并再次调用 V2TIMManager 的 login() 函数重新登录。 + }, + onSelfInfoUpdated: (V2TimUserFullInfo info) { + // 登录用户的资料发生了更新 + // info登录用户的资料 + }, + onUserSigExpired: () { + // 在线时票据过期:此时您需要生成新的 userSig 并再次调用 V2TIMManager 的 login() 函数重新登录。 + }, + onUserStatusChanged: (List userStatusList) { + //用户状态变更通知 + //userStatusList 用户状态变化的用户列表 + //收到通知的情况:订阅过的用户发生了状态变更(包括在线状态和自定义状态),会触发该回调 + //在 IM 控制台打开了好友状态通知开关,即使未主动订阅,当好友状态发生变更时,也会触发该回调 + //同一个账号多设备登录,当其中一台设备修改了自定义状态,所有设备都会收到该回调 + }, + ); + V2TimValueCallback initSDKRes = await TencentImSDKPlugin.v2TIMManager + .initSDK( + sdkAppID: int.parse(SCGlobalConfig.tencentImAppid), // SDKAppID + loglevel: LogLevelEnum.V2TIM_LOG_ALL, // 日志登记等级 + listener: sdkListener, // 事件监听器 + ); + if (initSDKRes.code == 0) {} + try { + customerInfo = await SCConfigRepositoryImp().customerService(); + } catch (e) {} + + /// 登录 + await loginTencetRtm(context); + + /// 初始化会话列表 + // _onRefreshConversationSub = FTIM.getContactManager().addRefreshConversationListener(_onRefreshConversation); + TencentImSDKPlugin.v2TIMManager + .getConversationManager() + .addConversationListener( + listener: V2TimConversationListener( + onNewConversation: (conversationList) { + // _onRefreshConversation(conversationList); + initConversation(); + }, + onTotalUnreadMessageCountChanged: (int totalUnreadCount) { + messageUnReadCount = totalUnreadCount; + systemUnReadCount = _systemUnreadCount(); + customerUnReadCount = + conversationMap["c2c_${customerInfo?.id}"]?.unreadCount ?? 0; + allUnReadCount = + messageUnReadCount + + notifcationUnReadCount + + activityUnReadCount; + notifyListeners(); + }, + ), + ); + TencentImSDKPlugin.v2TIMManager.addGroupListener( + listener: V2TimGroupListener( + onMemberEnter: (String groupID, List memberList) { + final rtcProvider = Provider.of( + context, + listen: false, + ); + if (groupID == + rtcProvider.currenRoom?.roomProfile?.roomProfile?.roomAccount) { + rtcProvider.fetchOnlineUsersList(notifyIfUnchanged: false); + } + }, + onMemberLeave: (String groupID, V2TimGroupMemberInfo member) { + final rtcProvider = Provider.of( + context, + listen: false, + ); + rtcProvider.removOnlineUser(groupID, member.userID!); + if (groupID == + rtcProvider.currenRoom?.roomProfile?.roomProfile?.roomAccount) { + rtcProvider.fetchOnlineUsersList(notifyIfUnchanged: false); + } + }, + onMemberKicked: ( + String groupID, + V2TimGroupMemberInfo opUser, + List memberList, + ) { + ///踢出房间 + if (memberList.isNotEmpty) { + if (memberList.first.userID == + AccountStorage().getCurrentUser()?.userProfile?.id) { + Provider.of( + context, + listen: false, + ).engine?.setClientRole(role: ClientRoleType.clientRoleAudience); + + ///退出房间 + Provider.of( + context, + listen: false, + ).exitCurrentVoiceRoomSession(false).whenComplete(() { + SCRoomUtils.closeAllDialogs(); + SmartDialog.show( + tag: "showConfirmDialog", + alignment: Alignment.center, + debounce: true, + animationType: SmartAnimationType.fade, + builder: (_) { + return MsgDialog( + title: SCAppLocalizations.of(context)!.tips, + msg: SCAppLocalizations.of(context)!.kickRoomTips, + btnText: SCAppLocalizations.of(context)!.confirm, + onEnsure: () {}, + ); + }, + ); + }); + } + } + }, + ), + ); + + /// 新消息监听 + // FTIM.getMessageManager().addNewMessagesListener(_onNewMessage); + TencentImSDKPlugin.v2TIMManager.getMessageManager().addAdvancedMsgListener( + listener: V2TimAdvancedMsgListener( + onRecvC2CReadReceipt: (List receiptList) { + //会话已读回调 + }, + onRecvMessageModified: (V2TimMessage message) { + // msg 为被修改之后的消息对象 + }, + onRecvMessageReadReceipts: (List receiptList) { + //群聊/单聊已读回调 + List messageIDList = []; + for (var element in receiptList) { + messageIDList.add(element.msgID!); + } + onMessageRecvC2CReadListener?.call(messageIDList); + }, + onRecvMessageRevoked: (String messageId) { + // 在本地维护的消息中处理被对方撤回的消息 + TencentImSDKPlugin.v2TIMManager + .getMessageManager() + .deleteMessages(msgIDs: [messageId]) + .then((result) { + onRevokeMessageListener?.call(messageId); + for (var ms in conversationList) { + if (ms.lastMessage?.msgID == messageId) { + ms.lastMessage?.status = + MessageStatus.V2TIM_MSG_STATUS_LOCAL_REVOKED; + break; + } + } + notifyListeners(); + }); + }, + onRecvNewMessage: (V2TimMessage message) async { + _onNewMessage(message); + }, + onSendMessageProgress: (V2TimMessage message, int progress) { + //文件上传进度回调 + }, + ), + ); + getAllUnReadCount(); + joinBigBroadcastGroup(); + } + + /// 初始化会话 + /// 打开聊天界面的时候调用 + int sTime = 0; + + Future startConversation(V2TimConversation conversation) async { + assert(conversation != null); + int eTime = DateTime.now().millisecondsSinceEpoch; + if (eTime - sTime > 5000) { + sTime = eTime; + currentConversation = conversation; + await TencentImSDKPlugin.v2TIMManager + .getMessageManager() + .markC2CMessageAsRead(userID: conversation.userID!); + notifyListeners(); + return true; + } else { + return false; + } + } + + ///初始话会话 + Future initConversation() async { + V2TimValueCallback convList = + await TencentImSDKPlugin.v2TIMManager + .getConversationManager() + .getConversationList(nextSeq: '0', count: 100); + List? conversationList = + convList.data?.conversationList; + print('conversationList:${conversationList?.length}'); + conversationMap.clear(); + if (conversationList != null) { + for (V2TimConversation? conversation in conversationList) { + conversationMap[conversation!.conversationID] = conversation; + } + } + getConversationList(); + } + + // /// 所有消息未读数 + // int getAllUnReadCount() { + // allUnReadCount = 0; + // for (var value in conversationList) { + // allUnReadCount += value.unreadCount ?? 0; + // } + // // for (var value in systemConversationList) { + // // i += value.unreadMessageNum; + // // } + // notifyListeners(); + // return allUnReadCount; + // } + + /// 所有消息未读数 + void getAllUnReadCount() async { + V2TimValueCallback res = + await TencentImSDKPlugin.v2TIMManager + .getConversationManager() + .getTotalUnreadMessageCount(); + if (res.code == 0) { + print('初始未读总数: ${res.data}'); + messageUnReadCount = res.data ?? 0; + allUnReadCount = + messageUnReadCount + activityUnReadCount + notifcationUnReadCount; + notifyListeners(); + // 这里可以先用初始值更新UI + } + } + + ///登录IM + Future loginTencetRtm(BuildContext context) async { + SocialChatLoginRes? userModel = AccountStorage().getCurrentUser(); + bool logined = false; + + while (!logined && userModel != null) { + await Future.delayed(Duration(milliseconds: 550)); + try { + if (userModel.userSig != null) { + V2TimCallback res = await TencentImSDKPlugin.v2TIMManager.login( + userID: userModel.userProfile?.id ?? "", + userSig: userModel.userSig ?? "", + ); + print( + 'tim voLogin:${res.code},${userModel.userProfile?.id},${userModel.userSig}', + ); + if (res.code == 0) { + isLogout = false; + // 登录成功逻辑 + logined = true; + print('tim 登录成功'); + await initConversation(); + } else { + // 登录失败逻辑 + //print('timm 需要重新登录2'); + print('tim 登录失败${res.code}'); + SCTts.show('tim login fail'); + } + } else { + //print('timm 需要重新登录sign'); + SCTts.show('tim login fail'); + } + } catch (e) { + //print('timm 登录异常:${e.toString()}'); + SCTts.show('timm login fail:${e.toString()}'); + } + userModel = AccountStorage().getCurrentUser(); + } + } + + _onNewMessage(V2TimMessage message) { + if (message.groupID != null) { + ///全服通知 + if (message.groupID == SCGlobalConfig.bigBroadcastGroup) { + _newBroadCastMsgRecv(message.groupID!, message); + } + + ///群消息 + for (var element in onNewMessageListenerGroupMap.values) { + element?.call(message.groupID!, message); + } + } else { + ///单聊消息 + _onNew1v1Message(message); + } + } + + _onNew1v1Message(V2TimMessage? message, {String? msgId}) async { + for (var element in conversationList) { + if (message?.userID == element?.userID) { + element.lastMessage = message; + if (onNewMessageCurrentConversationListener == null) { + element.unreadCount = element.unreadCount! + 1; + } + } + } + if (message?.userID == customerInfo?.id) { + if (onNewMessageCurrentConversationListener == null) { + conversationMap["c2c_${customerInfo?.id}"]?.unreadCount = + (conversationMap["c2c_${customerInfo?.id}"]?.unreadCount ?? 0) + 1; + } + } + systemUnReadCount = _systemUnreadCount(); + notifyListeners(); + onNewMessageCurrentConversationListener?.call(message, msgId: msgId); + } + + void _onRefreshConversation(List conversations) { + for (V2TimConversation conversation in conversations) { + conversationMap[conversation.conversationID] = conversation; + } + getConversationList(); + } + + ///创建房间im群聊 + Future> createRoomGroup( + String groupID, + String groupName, + ) { + return V2TIMGroupManager().createGroup( + groupID: groupID, + groupType: GroupType.AVChatRoom, + groupName: groupName, + ); + } + + ///加入房间的im群聊 + Future joinRoomGroup(String groupID, String message) async { + _luckGiftPushQueue.clear(); + currentPlayingLuckGift = null; + _currentLuckGiftPushKey = null; + var joinResult = await TencentImSDKPlugin.v2TIMManager.joinGroup( + groupID: groupID, + message: message, + ); + if (joinResult.code == 0) { + onNewMessageListenerGroupMap[groupID] = _newGroupMsg; + } + return joinResult; + } + + ///发送文本消息 + Future dispatchMessage( + Msg msg, { + bool showEmoticons = true, + bool addLocal = true, + }) async { + if (addLocal) { + addMsg(msg); + } + // 发送消息到腾讯云IM + if (msg.groupId != null) { + await _sendTencentMessage(msg); + } + notifyListeners(); + } + + // 发送腾讯云消息 + Future _sendTencentMessage(Msg msg) async { + try { + switch (msg.type) { + case SCRoomMsgType.text: + case SCRoomMsgType.shangMai: + case SCRoomMsgType.emoticons: + case SCRoomMsgType.xiaMai: + case SCRoomMsgType.killXiaMai: + case SCRoomMsgType.roomRoleChange: + case SCRoomMsgType.roomSettingUpdate: + case SCRoomMsgType.roomBGUpdate: + case SCRoomMsgType.qcfj: + case SCRoomMsgType.fengMai: + case SCRoomMsgType.jieFeng: + case SCRoomMsgType.joinRoom: + case SCRoomMsgType.gift: + case SCRoomMsgType.bsm: + case SCRoomMsgType.roomDice: + case SCRoomMsgType.roomRPS: + case SCRoomMsgType.roomLuckNumber: + case SCRoomMsgType.image: + case SCRoomMsgType.roomGameClose: + case SCRoomMsgType.roomGameCreate: + case SCRoomMsgType.luckGiftAnimOther: + _sendRoomMessage(msg); + break; + default: + break; + } + } catch (e) { + print('发送消息失败: $e'); + // 处理发送失败的情况 + } + } + + ///发送单聊文本消息 + Future sendC2CTextMsg( + String msg, + V2TimConversation toConversation, + ) async { + // 创建文本消息 + V2TimValueCallback createTextMessageRes = + await TencentImSDKPlugin.v2TIMManager + .getMessageManager() + .createTextMessage( + text: msg, // 文本信息 + ); + if (createTextMessageRes.code == 0) { + // 文本信息创建成功 + String id = createTextMessageRes.data!.id!; + // 发送文本消息 + // 在sendMessage时,若只填写receiver则发个人用户单聊消息 + // 若只填写groupID则发群组消息 + // 若填写了receiver与groupID则发群内的个人用户,消息在群聊中显示,只有指定receiver能看见 + String receiveId = toConversation.userID!; + V2TimValueCallback sendMessageRes = await TencentImSDKPlugin + .v2TIMManager + .getMessageManager() + .sendMessage( + id: id, // 创建的messageid + receiver: toConversation.userID!, // 接收人id + needReadReceipt: true, + groupID: '', // 是否需要已读回执 + ); + if (sendMessageRes.code == 0) { + // 发送成功 + _onNew1v1Message(sendMessageRes.data); + } else { + SCTts.show( + 'create fail,code:${sendMessageRes.code},${sendMessageRes.desc}', + ); + } + } + } + + ///发送单聊自定义消息 + Future sendC2CCustomMsg( + String msg, + V2TimConversation toConversation, + String extension, + ) async { + // 创建文本消息 + V2TimValueCallback createCustomMessageRes = + await TencentImSDKPlugin.v2TIMManager + .getMessageManager() + .createCustomMessage( + data: msg, // 文本信息 + extension: extension, + ); + if (createCustomMessageRes.code == 0) { + // 文本信息创建成功 + String id = createCustomMessageRes.data!.id!; + // 发送文本消息 + // 在sendMessage时,若只填写receiver则发个人用户单聊消息 + // 若只填写groupID则发群组消息 + // 若填写了receiver与groupID则发群内的个人用户,消息在群聊中显示,只有指定receiver能看见 + String receiveId = toConversation.userID!; + V2TimValueCallback sendMessageRes = await TencentImSDKPlugin + .v2TIMManager + .getMessageManager() + .sendMessage( + id: id, + // 创建的messageid + receiver: toConversation.userID!, + // 接收人id + needReadReceipt: true, + isSupportMessageExtension: true, + groupID: '', // 是否需要已读回执 + ); + if (sendMessageRes.code == 0) { + // 发送成功 + _onNew1v1Message(sendMessageRes.data); + } else { + SCTts.show( + 'create fail,code:${sendMessageRes.code},${sendMessageRes.desc}', + ); + } + } + } + + ///发送单聊图片消息 + Future sendImageMsg({ + List? selectedList, + File? file, + required V2TimConversation conversation, + }) async { + if (file != null) { + File newFile = await SCMessageUtils.createImageElem(file); + V2TimValueCallback createImageMessageRes = + await TencentImSDKPlugin.v2TIMManager + .getMessageManager() + .createImageMessage(imagePath: newFile.path); + if (createImageMessageRes.code == 0) { + String id = createImageMessageRes.data!.id!; + V2TimValueCallback sendMessageRes = + await TencentImSDKPlugin.v2TIMManager + .getMessageManager() + .sendMessage( + id: id, + receiver: conversation.userID!, + needReadReceipt: true, + groupID: '', + ); + if (sendMessageRes.code == 0) { + // 发送成功 + _onNew1v1Message(sendMessageRes.data); + } else { + SCTts.show( + 'create fail,code:${sendMessageRes.code},${sendMessageRes.desc}', + ); + } + } + } else { + if (selectedList != null) { + for (File entity in selectedList) { + String id = ""; + V2TimMessage? message; + //判断是视频或者图片消息 + + if (SCPathUtils.getFileType(entity.path) == "image") { + V2TimValueCallback createImageMessageRes = + await TencentImSDKPlugin.v2TIMManager + .getMessageManager() + .createImageMessage(imagePath: entity.path); + if (createImageMessageRes.code == 0) { + message = createImageMessageRes.data?.messageInfo; + id = createImageMessageRes.data!.id!; + // 创建图片 + File newFile = await SCMessageUtils.createImageElem(entity); + //发送 + V2TimImageElem elem = V2TimImageElem(path: newFile.path); + message?.imageElem = elem; + } + } else if (SCPathUtils.getFileType(entity.path) == "video_pic") { + if (entity.lengthSync() > 50000000) { + SCTts.show( + SCAppLocalizations.of(context!)!.theVideoSizeCannotExceed, + ); + return; + } + // 复制一份视频 + String md5Str1 = keyToMd5(entity.path); + File newFile = File( + "${FileCacheManager.videoCachePath}/$md5Str1.mp4", + ); + if (!newFile.existsSync()) { + await entity.copy(newFile.path); + } + // 创建缩略图 + String? thumbImagePath = await SCMessageUtils.generateFileThumbnail( + newFile.path, + 128, + ); + V2TimValueCallback createVideoMessageRes = + await TencentImSDKPlugin.v2TIMManager + .getMessageManager() + .createVideoMessage( + videoFilePath: entity.path, + type: "mp4", + duration: 0, + snapshotPath: thumbImagePath ?? "", + ); + + if (createVideoMessageRes.code == 0) { + message = createVideoMessageRes.data?.messageInfo; + id = createVideoMessageRes.data!.id!; + } + } + // 消息设置 + message?.isSelf = true; + // message.conversation = conversation; + message?.status = MessageStatus.V2TIM_MSG_STATUS_SENDING; + print('组装完成,准备发送:${message?.toJson()}'); + // String id = await FTIM.getMessageManager().sendMessage(message); + // message.msgId = id; + V2TimValueCallback sendMessageRes = + await TencentImSDKPlugin.v2TIMManager + .getMessageManager() + .sendMessage( + id: id, + receiver: conversation.userID!, + needReadReceipt: true, + groupID: '', + ); + if (sendMessageRes.code == 0) { + // 发送成功 + _onNew1v1Message(sendMessageRes.data); + } else { + SCTts.show( + 'create fail,code:${sendMessageRes.code},${sendMessageRes.desc}', + ); + } + } + } + } + notifyListeners(); + } + + // 发送消息(房间) + Future _sendRoomMessage(Msg msg) async { + try { + if (msg.type == SCRoomMsgType.luckGiftAnimOther) { + _giftFxLog( + 'send room msg start ' + 'type=${msg.type} ' + 'groupId=${msg.groupId} ' + 'msg=${msg.msg} ' + 'giftId=${msg.gift?.id} ' + 'giftPhoto=${msg.gift?.giftPhoto}', + ); + } + if (msg.type == SCRoomMsgType.roomSettingUpdate) { + debugPrint( + "[Room Cover Sync] send roomSettingUpdate groupId=${msg.groupId ?? ""} roomId=${msg.msg ?? ""}", + ); + } + var user = msg.user?.copyWith(); + var toUser = msg.toUser?.copyWith(); + user?.cleanWearHonor(); + user?.cleanWearBadge(); + user?.cleanPhotos(); + toUser?.cleanWearHonor(); + toUser?.cleanWearBadge(); + toUser?.cleanUseProps(); + toUser?.cleanPhotos(); + msg.needUpDataUserInfo = + Provider.of( + context!, + listen: false, + ).needUpDataUserInfo; + msg.user = user; + msg.toUser = toUser; + final textMsg = await TencentImSDKPlugin.v2TIMManager + .getMessageManager() + .createCustomMessage(data: jsonEncode(msg.toJson())); + + if (msg.type == SCRoomMsgType.luckGiftAnimOther) { + _giftFxLog( + 'send room msg createCustomMessage ' + 'type=${msg.type} ' + 'code=${textMsg.code} ' + 'id=${textMsg.data?.id} ' + 'desc=${textMsg.desc}', + ); + } + + if (textMsg.code != 0) return; + + final sendResult = await TencentImSDKPlugin.v2TIMManager + .getMessageManager() + .sendMessage( + id: textMsg.data!.id!, + groupID: msg.groupId!, + receiver: '', + ); + if (msg.type == SCRoomMsgType.luckGiftAnimOther) { + _giftFxLog( + 'send room msg result ' + 'type=${msg.type} ' + 'code=${sendResult.code} ' + 'msgId=${sendResult.data?.msgID} ' + 'groupId=${sendResult.data?.groupID} ' + 'desc=${sendResult.desc}', + ); + } + if (sendResult.code == 0) {} + } catch (e) { + throw Exception("create fail: $e"); + } + } + + /// 添加消息 + addMsg(Msg msg) { + final mergedGiftMsg = _mergeGiftMessageIfNeeded(msg); + if (mergedGiftMsg != null) { + msgAllListener?.call(mergedGiftMsg); + msgGiftListener?.call(mergedGiftMsg); + if (msg.type == SCRoomMsgType.gift) { + msgFloatingGiftListener?.call(msg); + } + notifyListeners(); + return; + } + + roomAllMsgList.insert(0, msg); + if (roomAllMsgList.length > 250) { + print('大于200条消息'); + roomAllMsgList.removeAt(roomAllMsgList.length - 1); + } + msgAllListener?.call(msg); + + if (msg.type == SCRoomMsgType.text) { + roomChatMsgList.insert(0, msg); + if (roomChatMsgList.length > 250) { + print('大于200条消息'); + roomChatMsgList.removeAt(roomChatMsgList.length - 1); + } + msgChatListener?.call(msg); + } else if (msg.type == SCRoomMsgType.image) { + roomChatMsgList.insert(0, msg); + if (roomChatMsgList.length > 250) { + print('大于200条消息'); + roomChatMsgList.removeAt(roomChatMsgList.length - 1); + } + msgChatListener?.call(msg); + } else if (msg.type == SCRoomMsgType.gift || + msg.type == SCRoomMsgType.luckGiftAnimOther) { + roomGiftMsgList.insert(0, msg); + if (roomGiftMsgList.length > 250) { + print('大于200条消息'); + roomGiftMsgList.removeAt(roomGiftMsgList.length - 1); + } + msgGiftListener?.call(msg); + if (msg.type == SCRoomMsgType.gift) { + msgFloatingGiftListener?.call(msg); + } + } + } + + Msg? _mergeGiftMessageIfNeeded(Msg incoming) { + if (incoming.type != SCRoomMsgType.gift && + incoming.type != SCRoomMsgType.luckGiftAnimOther) { + return null; + } + + final mergeTarget = _findMergeableGiftMessage(incoming); + if (mergeTarget == null) { + return null; + } + + mergeTarget.number = (mergeTarget.number ?? 0) + (incoming.number ?? 0); + mergeTarget.time = DateTime.now().millisecondsSinceEpoch; + if ((incoming.msg ?? "").trim().isNotEmpty) { + mergeTarget.msg = incoming.msg; + } + + _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) { + return existing.type == incoming.type && + existing.groupId == incoming.groupId && + existing.user?.id == incoming.user?.id && + existing.toUser?.id == incoming.toUser?.id && + existing.gift?.id == incoming.gift?.id; + } + + void _moveMessageToFront(List messages, Msg target) { + final index = messages.indexOf(target); + if (index <= 0) { + return; + } + messages.removeAt(index); + messages.insert(0, target); + } + + bool _shouldHighlightLuckyGiftReward(SCBroadCastLuckGiftPush broadCastRes) { + final rewardData = broadCastRes.data; + if (rewardData == null) { + return false; + } + if (rewardData.isBigReward) { + return true; + } + return (rewardData.multiple ?? 0) >= 5; + } + + bool _isLuckyGiftInCurrentRoom(SCBroadCastLuckGiftPush broadCastRes) { + if (context == null) { + return false; + } + final currentRoomId = + Provider.of( + context!, + listen: false, + ).currenRoom?.roomProfile?.roomProfile?.id ?? + ''; + final roomId = broadCastRes.data?.roomId ?? ''; + return currentRoomId.isNotEmpty && + roomId.isNotEmpty && + currentRoomId == roomId; + } + + SCFloatingMessage _buildLuckyGiftFloatingMessage( + SCBroadCastLuckGiftPush broadCastRes, + ) { + final rewardData = broadCastRes.data; + final resolvedGiftUrl = _resolveLuckyGiftGiftPhoto(rewardData); + return SCFloatingMessage( + type: 0, + userId: rewardData?.sendUserId, + roomId: rewardData?.roomId, + toUserId: rewardData?.acceptUserId, + userAvatarUrl: rewardData?.userAvatar, + userName: rewardData?.nickname, + toUserName: rewardData?.acceptNickname, + giftUrl: resolvedGiftUrl, + giftId: rewardData?.giftId, + number: rewardData?.giftQuantity, + coins: rewardData?.awardAmount, + multiple: rewardData?.multiple, + priority: 1000, + ); + } + + void _handleLuckyGiftGlobalNews( + SCBroadCastLuckGiftPush broadCastRes, { + required String source, + }) { + final rewardData = broadCastRes.data; + if (rewardData == null) { + return; + } + if (_isLuckyGiftInCurrentRoom(broadCastRes)) { + addluckGiftPushQueue(broadCastRes); + } + if (!rewardData.shouldShowGlobalNews || + (rewardData.multiple ?? 0) < _luckyGiftFloatMinMultiple) { + return; + } + if (source == 'broadcast' && _isLuckyGiftInCurrentRoom(broadCastRes)) { + _giftFxLog( + 'skip global lucky gift overlay ' + 'reason=current_room_already_receives_group_msg ' + 'roomId=${rewardData.roomId} ' + 'giftId=${rewardData.giftId}', + ); + return; + } + OverlayManager().addMessage(_buildLuckyGiftFloatingMessage(broadCastRes)); + } + + void _handleRoomLuckyGiftMessage(SCBroadCastLuckGiftPush broadCastRes) { + final rewardData = broadCastRes.data; + if (rewardData == null) { + return; + } + final resolvedGiftUrl = _resolveLuckyGiftGiftPhoto(rewardData); + final roomMsg = Msg( + groupId: '', + msg: '', + type: SCRoomMsgType.gameLuckyGift, + ); + roomMsg.gift = SocialChatGiftRes( + id: rewardData.giftId, + giftPhoto: resolvedGiftUrl, + giftTab: 'LUCK', + ); + roomMsg.number = 0; + roomMsg.awardAmount = rewardData.awardAmount; + roomMsg.user = SocialChatUserProfile( + id: rewardData.sendUserId, + userNickname: rewardData.nickname, + userAvatar: rewardData.userAvatar, + ); + roomMsg.toUser = SocialChatUserProfile( + id: rewardData.acceptUserId, + userNickname: rewardData.acceptNickname, + ); + addMsg(roomMsg); + msgLuckyGiftRewardTickerListener?.call(roomMsg); + + if (_shouldHighlightLuckyGiftReward(broadCastRes)) { + final highlightMsg = Msg( + groupId: '', + msg: '${rewardData.multiple ?? 0}', + type: SCRoomMsgType.gameLuckyGift_5, + ); + highlightMsg.awardAmount = rewardData.awardAmount; + highlightMsg.gift = SocialChatGiftRes( + id: rewardData.giftId, + giftPhoto: resolvedGiftUrl, + giftTab: 'LUCK', + ); + highlightMsg.user = SocialChatUserProfile( + id: rewardData.sendUserId, + userNickname: rewardData.nickname, + ); + addMsg(highlightMsg); + } + + addluckGiftPushQueue(broadCastRes); + _handleLuckyGiftGlobalNews(broadCastRes, source: 'room_group'); + + if (rewardData.sendUserId == + AccountStorage().getCurrentUser()?.userProfile?.id) { + Provider.of( + context!, + listen: false, + ).updateLuckyRewardAmount(roomMsg.awardAmount ?? 0); + } + } + + String _resolveLuckyGiftGiftPhoto(Data? rewardData) { + final fallbackUrl = (rewardData?.giftCover ?? '').trim(); + final giftId = (rewardData?.giftId ?? '').trim(); + final currentContext = context; + if (giftId.isEmpty || currentContext == null || !currentContext.mounted) { + _giftFxLog( + 'resolve lucky float gift photo skipped ' + 'giftId=$giftId ' + 'fallbackUrl=$fallbackUrl ' + 'reason=${giftId.isEmpty ? "empty_gift_id" : "invalid_context"}', + ); + return fallbackUrl; + } + final appGeneralManager = Provider.of( + currentContext, + listen: false, + ); + final giftById = appGeneralManager.getGiftById(giftId); + final giftByStandardId = appGeneralManager.getGiftByStandardId(giftId); + final localGift = giftById ?? giftByStandardId; + final localGiftPhoto = (localGift?.giftPhoto ?? '').trim(); + _giftFxLog( + 'resolve lucky float gift photo ' + 'giftId=$giftId ' + 'fallbackUrl=$fallbackUrl ' + 'giftById.id=${giftById?.id} ' + 'giftById.standardId=${giftById?.standardId} ' + 'giftById.photo=${giftById?.giftPhoto} ' + 'giftByStandardId.id=${giftByStandardId?.id} ' + 'giftByStandardId.standardId=${giftByStandardId?.standardId} ' + 'giftByStandardId.photo=${giftByStandardId?.giftPhoto} ' + 'resolvedUrl=${localGiftPhoto.isNotEmpty ? localGiftPhoto : fallbackUrl}', + ); + if (localGiftPhoto.isNotEmpty) { + return localGiftPhoto; + } + unawaited(appGeneralManager.giftList()); + return fallbackUrl; + } + + bool isLogout = false; + + logout() async { + V2TimCallback logoutRes = await TencentImSDKPlugin.v2TIMManager.logout(); + TencentImSDKPlugin.v2TIMManager + .getMessageManager() + .removeAdvancedMsgListener(); + if (logoutRes.code == 0) { + isLogout = true; + } + } + + ///全服广播消息 + _newBroadCastMsgRecv(String groupID, V2TimMessage message) async { + try { + String? customData = message.customElem?.data; + if (customData != null && customData.isNotEmpty) { + final data = json.decode(customData); + var type = data["type"]; + if (type == "SYS_ACTIVITY") { + if (onNewActivityMessageCurrentConversationListener != null) { + var recode = Records.fromJson(data["data"]); + onNewActivityMessageCurrentConversationListener?.call(recode); + } else { + activityUnReadCount = activityUnReadCount + 1; + allUnReadCount = + messageUnReadCount + + notifcationUnReadCount + + activityUnReadCount; + notifyListeners(); + } + } else if (type == "SYS_ANNOUNCEMENT") { + if (onNewNotifcationMessageCurrentConversationListener != null) { + var recode = Records.fromJson(data["data"]); + onNewNotifcationMessageCurrentConversationListener?.call(recode); + } else { + notifcationUnReadCount = notifcationUnReadCount + 1; + allUnReadCount = + messageUnReadCount + + notifcationUnReadCount + + activityUnReadCount; + notifyListeners(); + } + } else if (type == "GAME_BAISHUN_WIN") { + if (SCGlobalConfig.isReview) { + ///审核状态不播放动画 + return; + } + var fdata = data["data"]; + var winCoins = fdata["currencyDiff"]; + if (winCoins > 14999) { + ///达到5000才飘屏 + SCFloatingMessage msg = SCFloatingMessage( + type: 2, + userId: fdata["account"], + userAvatarUrl: fdata["userAvatar"], + userName: fdata["userNickname"], + giftUrl: fdata["gameUrl"], + roomId: fdata["roomId"], + coins: fdata["currencyDiff"], + ); + OverlayManager().addMessage(msg); + } + } else if (type == "GAME_LUCKY_GIFT") { + final broadCastRes = SCBroadCastLuckGiftPush.fromJson(data); + _giftFxLog( + 'recv GAME_LUCKY_GIFT broadcast ' + 'giftId=${broadCastRes.data?.giftId} ' + 'roomId=${broadCastRes.data?.roomId} ' + 'sendUserId=${broadCastRes.data?.sendUserId} ' + 'acceptUserId=${broadCastRes.data?.acceptUserId} ' + 'giftQuantity=${broadCastRes.data?.giftQuantity} ' + 'awardAmount=${broadCastRes.data?.awardAmount} ' + 'multiple=${broadCastRes.data?.multiple} ' + 'multipleType=${broadCastRes.data?.multipleType} ' + 'globalNews=${broadCastRes.data?.globalNews}', + ); + _handleLuckyGiftGlobalNews(broadCastRes, source: 'broadcast'); + } else if (type == "REGISTER_REWARD_GRANTED") { + await DataPersistence.setPendingRegisterRewardDialog(true); + await DataPersistence.clearAwaitRegisterRewardSocket(); + eventBus.fire(RegisterRewardGrantedEvent(data: data["data"])); + } else if (type == "ROCKET_ENERGY_LAUNCH") { + ///火箭触发飘屏 + var fdata = data["data"]; + SCFloatingMessage msg = SCFloatingMessage( + type: 3, + roomId: fdata["roomId"], + rocketLevel: fdata["fromLevel"], + userAvatarUrl: fdata["userAvatar"], + userName: fdata["nickname"], + userId: fdata["actualAccount"], + priority: 1000, + ); + OverlayManager().addMessage(msg); + } else if (type == SCRoomMsgType.roomRedPacket) { + ///红包触发飘屏 + var fData = data["data"]; + SCFloatingMessage msg = SCFloatingMessage( + type: 4, + roomId: fData["roomId"], + userAvatarUrl: fData["userAvatar"], + userName: fData["userNickname"], + userId: fData["actualAccount"], + toUserId: fData["packetId"], + priority: 1000, + ); + if (msg.roomId == + Provider.of( + context!, + listen: false, + ).currenRoom?.roomProfile?.roomProfile?.id) { + Provider.of( + context!, + listen: false, + ).loadRoomRedPacketList(1); + } + OverlayManager().addMessage(msg); + } else if (type == SCRoomMsgType.inviteRoom) { + ///邀请进入房间 + var fdata = data["data"]; + SCFloatingMessage msg = SCFloatingMessage.fromJson(fdata); + if (msg.toUserId == + AccountStorage().getCurrentUser()?.userProfile?.id && + msg.roomId != + Provider.of( + context!, + listen: false, + ).currenRoom?.roomProfile?.roomProfile?.id) { + SmartDialog.dismiss(tag: "showInviteRoom"); + SmartDialog.show( + tag: "showInviteRoom", + alignment: Alignment.center, + animationType: SmartAnimationType.fade, + builder: (_) { + return InviteRoomDialog(msg); + }, + ); + } + } + } + } catch (e) {} + } + + _newGroupMsg(String groupID, V2TimMessage message) { + if (groupID != + Provider.of( + context!, + listen: false, + ).currenRoom?.roomProfile?.roomProfile?.roomAccount) { + return; + } + try { + String? customData = message.customElem?.data; + if (customData != null && customData.isNotEmpty) { + // 直接处理字符串格式的自定义数据 + final data = json.decode(customData); + debugPrint(">>>>>>>>>>>>>>>>>>>消息类型${data["type"]}"); + + if (data["type"] == SCRoomMsgType.roomRedPacket) { + ///房间红包 + var fData = data["data"]; + SCFloatingMessage msg = SCFloatingMessage( + type: 4, + roomId: fData["roomId"], + userAvatarUrl: fData["userAvatar"], + userName: fData["userNickname"], + userId: fData["actualAccount"], + toUserId: fData["packetId"], + priority: 1000, + ); + Provider.of( + context!, + listen: false, + ).loadRoomRedPacketList(1); + OverlayManager().addMessage(msg); + return; + } + Msg msg = Msg.fromJson(data); + + if (msg.type == SCRoomMsgType.sendGift || + msg.type == SCRoomMsgType.gameBurstCrystalSprint || + msg.type == SCRoomMsgType.gameBurstCrystalBox) { + ///这个消息暂时不监听 + return; + } + if (msg.type == SCRoomMsgType.bsm) { + if (msg.toUser?.id == + AccountStorage().getCurrentUser()?.userProfile?.id) { + SmartDialog.show( + tag: "showConfirmDialog", + alignment: Alignment.center, + debounce: true, + animationType: SmartAnimationType.fade, + builder: (_) { + return MsgDialog( + title: SCAppLocalizations.of(context!)!.tips, + msg: SCAppLocalizations.of( + context!, + )!.invitesYouToTheMicrophone(msg.msg ?? ""), + btnText: SCAppLocalizations.of(context!)!.confirm, + onEnsure: () { + ///上麦 + num index = + Provider.of( + context!, + listen: false, + ).findWheat(); + if (index > -1) { + Provider.of( + context!, + listen: false, + ).shangMai( + index, + eventType: "INVITE", + inviterId: msg.role, + ); + } + }, + ); + }, + ); + } + return; + } + if (msg.type == SCRoomMsgType.killXiaMai) { + ///踢下麦 + if (msg.msg == AccountStorage().getCurrentUser()?.userProfile?.id) { + Provider.of( + context!, + listen: false, + ).engine?.setClientRole(role: ClientRoleType.clientRoleAudience); + } + Provider.of( + context!, + listen: false, + ).retrieveMicrophoneList(notifyIfUnchanged: false); + return; + } + + if (msg.type == SCRoomMsgType.roomSettingUpdate) { + debugPrint( + "[Room Cover Sync] recv roomSettingUpdate groupId=$groupID roomId=${msg.msg ?? ""}", + ); + Provider.of( + context!, + listen: false, + ).loadRoomInfo(msg.msg ?? ""); + return; + } + if (msg.type == SCRoomMsgType.roomBGUpdate) { + SCRoomThemeListRes res; + if ((msg.msg ?? "").isNotEmpty) { + res = SCRoomThemeListRes.fromJson(jsonDecode(msg.msg!)); + } else { + res = SCRoomThemeListRes(); + } + Provider.of( + context!, + listen: false, + ).updateRoomBG(res); + return; + } + if (msg.type == SCRoomMsgType.emoticons) { + Provider.of( + context!, + listen: false, + ).starPlayEmoji(msg); + return; + } + if (msg.type == SCRoomMsgType.micChange) { + Provider.of( + context!, + listen: false, + ).micChange(BroadCastMicChangePush.fromJson(data).data?.mics); + } else if (msg.type == SCRoomMsgType.shangMai || + msg.type == SCRoomMsgType.xiaMai || + msg.type == SCRoomMsgType.fengMai || + msg.type == SCRoomMsgType.jieFeng) { + Provider.of( + context!, + listen: false, + ).retrieveMicrophoneList(notifyIfUnchanged: false); + } else if (msg.type == SCRoomMsgType.refreshOnlineUser) { + Provider.of( + context!, + listen: false, + ).fetchOnlineUsersList(notifyIfUnchanged: false); + } else if (msg.type == SCRoomMsgType.gameLuckyGift) { + var broadCastRes = SCBroadCastLuckGiftPush.fromJson(data); + _giftFxLog( + 'recv GAME_LUCKY_GIFT ' + 'giftId=${broadCastRes.data?.giftId} ' + 'roomId=${broadCastRes.data?.roomId} ' + 'sendUserId=${broadCastRes.data?.sendUserId} ' + 'acceptUserId=${broadCastRes.data?.acceptUserId} ' + 'giftQuantity=${broadCastRes.data?.giftQuantity} ' + 'awardAmount=${broadCastRes.data?.awardAmount} ' + 'multiple=${broadCastRes.data?.multiple} ' + 'multipleType=${broadCastRes.data?.multipleType} ' + 'globalNews=${broadCastRes.data?.globalNews}', + ); + _handleRoomLuckyGiftMessage(broadCastRes); + } else { + if (msg.type == SCRoomMsgType.joinRoom) { + final shouldShowRoomVisualEffects = + Provider.of( + context!, + listen: false, + ).shouldShowRoomVisualEffects; + if (msg.user != null) { + Provider.of( + context!, + listen: false, + ).addOnlineUser(msg.groupId ?? "", msg.user!); + } + if (msgUserJoinListener != null) { + msgUserJoinListener!(msg); + } + + ///坐骑 + if (msg.user?.getMountains() != null) { + if (SCGlobalConfig.isEntryVehicleAnimation && + shouldShowRoomVisualEffects) { + SCGiftVapSvgaManager().play( + msg.user?.getMountains()?.sourceUrl ?? "", + priority: 100, + type: 1, + ); + } + } + } else if (msg.type == SCRoomMsgType.gift) { + final gift = msg.gift; + if (gift == null) { + _giftFxLog( + 'recv gift msg skipped reason=no_gift ' + 'fromUserId=${msg.user?.id} ' + 'toUserId=${msg.toUser?.id} ' + 'quantity=${msg.number}', + ); + } else { + final rtcProvider = Provider.of( + context!, + listen: false, + ); + final special = gift.special ?? ""; + final giftSourceUrl = gift.giftSourceUrl ?? ""; + final hasSource = giftSourceUrl.isNotEmpty; + final hasAnimation = scGiftHasAnimationSpecial(special); + final hasGlobalGift = special.contains( + SCGiftType.GLOBAL_GIFT.name, + ); + final hasFullScreenEffect = scGiftHasFullScreenEffect(special); + _giftFxLog( + 'recv gift msg ' + 'fromUserId=${msg.user?.id} ' + 'fromUserName=${msg.user?.userNickname} ' + 'toUserId=${msg.toUser?.id} ' + 'toUserName=${msg.toUser?.userNickname} ' + 'giftId=${gift.id} ' + 'giftName=${gift.giftName} ' + 'giftSourceUrl=$giftSourceUrl ' + 'special=$special ' + 'hasSource=$hasSource ' + 'hasAnimation=$hasAnimation ' + 'hasGlobalGift=$hasGlobalGift ' + 'hasFullScreenEffect=$hasFullScreenEffect ' + 'effectsEnabled=${SCGlobalConfig.isGiftSpecialEffects}', + ); + if (giftSourceUrl.isNotEmpty && special.isNotEmpty) { + if (scGiftHasFullScreenEffect(special)) { + if (SCGlobalConfig.isGiftSpecialEffects && + rtcProvider.shouldShowRoomVisualEffects) { + _giftFxLog( + 'trigger player play path=$giftSourceUrl ' + 'giftId=${gift.id} giftName=${gift.giftName}', + ); + SCGiftVapSvgaManager().play(giftSourceUrl); + } 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) { + OverlayManager().addMessage( + SCFloatingMessage( + type: 1, + userAvatarUrl: msg.user?.userAvatar ?? "", + userName: msg.user?.userNickname ?? "", + toUserName: msg.toUser?.userNickname ?? "", + toUserAvatarUrl: msg.toUser?.userAvatar ?? "", + giftUrl: gift.giftPhoto, + number: msg.number, + coins: coins, + roomId: msg.msg, + ), + ); + } + } + } else if (msg.type == SCRoomMsgType.luckGiftAnimOther) { + final hideLGiftAnimal = + Provider.of( + context!, + listen: false, + ).hideLGiftAnimal; + if (hideLGiftAnimal) { + _giftFxLog( + 'recv LUCK_GIFT_ANIM_OTHER skipped ' + 'reason=hideLGiftAnimal ' + 'giftPhoto=${msg.gift?.giftPhoto}', + ); + } else { + final targetUserIds = + (jsonDecode(msg.msg ?? "") as List) + .map((e) => e as String) + .toList(); + _giftFxLog( + 'recv LUCK_GIFT_ANIM_OTHER ' + 'giftPhoto=${msg.gift?.giftPhoto} ' + 'sendUserId=${msg.user?.id} ' + 'toUserId=${msg.toUser?.id} ' + 'quantity=${msg.number} ' + 'targetUserIds=${targetUserIds.join(",")}', + ); + eventBus.fire( + GiveRoomLuckWithOtherEvent( + msg.gift?.giftPhoto ?? "", + targetUserIds, + ), + ); + if (msg.user != null && msg.toUser != null && msg.gift != null) { + _giftFxLog( + 'trigger floating gift listener from LUCK_GIFT_ANIM_OTHER ' + 'sendUserId=${msg.user?.id} ' + 'toUserId=${msg.toUser?.id} ' + 'quantity=${msg.number} ' + 'giftId=${msg.gift?.id}', + ); + msgFloatingGiftListener?.call(msg); + } else { + _giftFxLog( + 'skip floating gift listener from LUCK_GIFT_ANIM_OTHER ' + 'reason=incomplete_msg ' + 'sendUserId=${msg.user?.id} ' + 'toUserId=${msg.toUser?.id} ' + 'giftId=${msg.gift?.id} ' + 'quantity=${msg.number}', + ); + } + } + } else if (msg.type == SCRoomMsgType.roomRoleChange) { + ///房间身份变动 + Provider.of( + context!, + listen: false, + ).retrieveMicrophoneList(); + if (msg.toUser?.id == + AccountStorage().getCurrentUser()?.userProfile?.id) { + Provider.of( + context!, + listen: false, + ).currenRoom?.entrants?.setRoles(msg.msg); + if (msg.msg == SCRoomRolesType.TOURIST.name && + !(Provider.of( + context!, + listen: false, + ).currenRoom?.roomProfile?.roomSetting?.touristMike ?? + false)) { + ///如果变成了游客,房间又是禁止游客上麦,需要下麦 + num index = Provider.of( + context!, + listen: false, + ).userOnMaiInIndex( + AccountStorage().getCurrentUser()?.userProfile?.id ?? "", + ); + if (index > -1) { + Provider.of( + context!, + listen: false, + ).xiaMai(index); + } + } + } + } else if (msg.type == SCRoomMsgType.roomDice) { + if ((msg.number ?? -1) > -1) { + Provider.of( + context!, + listen: false, + ).starPlayEmoji(msg); + } + } else if (msg.type == SCRoomMsgType.roomRPS) { + if ((msg.number ?? -1) > -1) { + Provider.of( + context!, + listen: false, + ).starPlayEmoji(msg); + } + } + addMsg(msg); + } + } + } catch (e) { + throw Exception("message parser fail: $e"); + } + } + + ///加入全服广播群 + joinBigBroadcastGroup() async { + bool joined = false; + while (!isLogout && !joined) { + await Future.delayed(Duration(milliseconds: 550)); + try { + var joinResult = await TencentImSDKPlugin.v2TIMManager.joinGroup( + groupID: SCGlobalConfig.bigBroadcastGroup, + message: "", + ); + if (joinResult.code == 0) { + joined = true; + } + } catch (e) { + //print('timm 登录异常:${e.toString()}'); + SCTts.show('broadcastGroup join fail:${e.toString()}'); + } + } + } + + ///发送全服消息 + sendBigBroadcastGroup(BigBroadcastGroupMessage msg) async { + try { + final textMsg = await TencentImSDKPlugin.v2TIMManager + .getMessageManager() + .createCustomMessage(data: jsonEncode(msg.toJson())); + + if (textMsg.code != 0) return; + + final sendResult = await TencentImSDKPlugin.v2TIMManager + .getMessageManager() + .sendMessage( + id: textMsg.data!.id!, + groupID: SCGlobalConfig.bigBroadcastGroup, + receiver: '', + ); + if (sendResult.code == 0) {} + } catch (e) { + throw Exception("create fail: $e"); + } + } + + Future quitGroup(String groupID) async { + await TencentImSDKPlugin.v2TIMManager.quitGroup(groupID: groupID); + } + + ///清屏 + 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(); + _luckGiftPushQueue.clear(); + currentPlayingLuckGift = null; + _currentLuckGiftPushKey = null; + onNewMessageListenerGroupMap.forEach((k, v) { + v = null; + }); + onNewMessageListenerGroupMap.clear(); + } + + void addluckGiftPushQueue(SCBroadCastLuckGiftPush broadCastRes) { + if (SCGlobalConfig.isLuckGiftSpecialEffects) { + if (!shouldPlayLuckyGiftBurst(broadCastRes.data)) { + return; + } + final eventKey = _luckyGiftPushEventKey(broadCastRes); + if (eventKey.isNotEmpty) { + if (_currentLuckGiftPushKey == eventKey) { + return; + } + for (final queued in _luckGiftPushQueue) { + if (queued != null && _luckyGiftPushEventKey(queued) == eventKey) { + return; + } + } + } + while (_luckGiftPushQueue.length >= _maxLuckGiftPushQueueLength) { + _luckGiftPushQueue.removeFirst(); + } + _luckGiftPushQueue.add(broadCastRes); + playLuckGiftBackCoins(); + } + } + + 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() { + _luckGiftPushQueue.clear(); + _currentLuckGiftPushKey = null; + } + + void playLuckGiftBackCoins() { + if (currentPlayingLuckGift != null || _luckGiftPushQueue.isEmpty) { + return; + } + currentPlayingLuckGift = _luckGiftPushQueue.removeFirst(); + _currentLuckGiftPushKey = + currentPlayingLuckGift == null + ? null + : _luckyGiftPushEventKey(currentPlayingLuckGift!); + notifyListeners(); + Future.delayed( + Duration(milliseconds: _luckyGiftBurstDisplayDurationMs), + () { + currentPlayingLuckGift = null; + _currentLuckGiftPushKey = null; + notifyListeners(); + playLuckGiftBackCoins(); + }, + ); + } + + 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}'; + } + + 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 conversationId in SCGlobalConfig.systemConversationIds) { + conversationMap[conversationId]?.unreadCount = 0; + } + systemUnReadCount = 0; + notifyListeners(); + } + + void updateCustomerCount(int count) { + conversationMap["c2c_${customerInfo?.id}"]?.unreadCount = 0; + customerUnReadCount = 0; + notifyListeners(); + } + + void clearC2CHistoryMessage(String conversationID, bool needShowToast) async { + // 清空单聊本地及云端的消息(不删除会话) + + V2TimCallback clearC2CHistoryMessageRes = await TencentImSDKPlugin + .v2TIMManager + .getConversationManager() + .deleteConversation(conversationID: conversationID); // 需要清空记录的用户id + if (clearC2CHistoryMessageRes.code == 0) { + // 清除成功 + if (needShowToast) { + SCTts.show(SCAppLocalizations.of(context!)!.operationSuccessful); + } + initConversation(); + } else { + // 清除失败,可以查看 clearC2CHistoryMessageRes.desc 获取错误描述 + } + } +} diff --git a/lib/services/general/sc_app_general_manager.dart b/lib/services/general/sc_app_general_manager.dart index 1b3ee6c..e229272 100644 --- a/lib/services/general/sc_app_general_manager.dart +++ b/lib/services/general/sc_app_general_manager.dart @@ -27,6 +27,7 @@ class SCAppGeneralManager extends ChangeNotifier { ///礼物 List giftResList = []; final Map _giftByIdMap = {}; + final Map _giftByStandardIdMap = {}; final Set _warmedGiftCoverUrls = {}; final Set _warmingGiftCoverUrls = {}; bool _isFetchingGiftList = false; @@ -169,6 +170,7 @@ class SCAppGeneralManager extends ChangeNotifier { void _rebuildGiftTabs({required bool includeCustomized}) { giftByTab.clear(); _giftByIdMap.clear(); + _giftByStandardIdMap.clear(); for (var gift in giftResList) { giftByTab[gift.giftTab]; var gmap = giftByTab[gift.giftTab]; @@ -187,6 +189,10 @@ class SCAppGeneralManager extends ChangeNotifier { } giftByTab["ALL"] = gAllMap; _giftByIdMap[gift.id!] = gift; + final standardId = (gift.standardId ?? "").trim(); + if (standardId.isNotEmpty && standardId != "0") { + _giftByStandardIdMap[standardId] = gift; + } } if (includeCustomized) { @@ -277,6 +283,18 @@ class SCAppGeneralManager extends ChangeNotifier { return _giftByIdMap[id]; } + SocialChatGiftRes? getGiftByStandardId(String standardId) { + return _giftByStandardIdMap[standardId]; + } + + SocialChatGiftRes? getGiftByIdOrStandardId(String id) { + final normalizedId = id.trim(); + if (normalizedId.isEmpty) { + return null; + } + return _giftByIdMap[normalizedId] ?? _giftByStandardIdMap[normalizedId]; + } + void downLoad(List giftResList) { final scheduledPaths = {}; var scheduledCount = 0; diff --git a/lib/shared/data_sources/sources/local/floating_screen_manager.dart b/lib/shared/data_sources/sources/local/floating_screen_manager.dart index 0d84401..f4d5bba 100644 --- a/lib/shared/data_sources/sources/local/floating_screen_manager.dart +++ b/lib/shared/data_sources/sources/local/floating_screen_manager.dart @@ -24,6 +24,7 @@ class OverlayManager { ); bool _isPlaying = false; OverlayEntry? _currentOverlayEntry; + SCFloatingMessage? _currentMessage; bool _isProcessing = false; bool _isDisposed = false; @@ -102,17 +103,10 @@ class OverlayManager { if (_messageQueue.isEmpty) return; final messageToProcess = _messageQueue.first; - if (messageToProcess?.type == 1) { - final rtcProvider = Provider.of( - context, - listen: false, - ); - if (rtcProvider.currenRoom == null) { - // 从队列中移除第一个元素 - _messageQueue.removeFirst(); - _safeScheduleNext(); - return; - } + if (!_shouldDisplayMessage(context, messageToProcess)) { + _messageQueue.removeFirst(); + _safeScheduleNext(); + return; } // 安全地移除并播放消息 @@ -127,9 +121,11 @@ class OverlayManager { void _playMessage(SCFloatingMessage message) { _isPlaying = true; + _currentMessage = message; final context = navigatorKey.currentState?.context; if (context == null || !context.mounted) { _isPlaying = false; + _currentMessage = null; _safeScheduleNext(); return; } @@ -159,10 +155,12 @@ class OverlayManager { _currentOverlayEntry?.remove(); _currentOverlayEntry = null; _isPlaying = false; + _currentMessage = null; _safeScheduleNext(); } catch (e) { debugPrint('清理悬浮消息出错: $e'); _isPlaying = false; + _currentMessage = null; _safeScheduleNext(); } } @@ -211,14 +209,16 @@ class OverlayManager { _isDisposed = true; _currentOverlayEntry?.remove(); _currentOverlayEntry = null; + _currentMessage = null; _messageQueue.clear(); _isPlaying = false; _isProcessing = false; } void removeRoom() { - // 正确地从 PriorityQueue 中移除特定类型的消息 + _removeActiveRoomMessage(); _removeMessagesByType(1); + _removeMessagesByType(0); } // 辅助方法:移除特定类型的消息 @@ -247,4 +247,41 @@ class OverlayManager { int get queueLength => _messageQueue.length; bool get isPlaying => _isPlaying; + + bool _shouldDisplayMessage(BuildContext context, SCFloatingMessage? message) { + if (message == null) { + return false; + } + if (message.type != 0 && message.type != 1) { + return true; + } + + final rtcProvider = Provider.of( + context, + listen: false, + ); + if (!rtcProvider.shouldShowRoomVisualEffects) { + return false; + } + final currentRoomId = + rtcProvider.currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? ""; + final messageRoomId = (message.roomId ?? "").trim(); + if (currentRoomId.isEmpty || messageRoomId.isEmpty) { + return false; + } + return currentRoomId == messageRoomId; + } + + void _removeActiveRoomMessage() { + final activeMessage = _currentMessage; + if (activeMessage == null || + (activeMessage.type != 0 && activeMessage.type != 1)) { + return; + } + _currentOverlayEntry?.remove(); + _currentOverlayEntry = null; + _currentMessage = null; + _isPlaying = false; + _safeScheduleNext(); + } } diff --git a/lib/ui_kit/widgets/room/anim/room_gift_seat_flight_overlay.dart b/lib/ui_kit/widgets/room/anim/room_gift_seat_flight_overlay.dart index 8d766b5..c53eaa6 100644 --- a/lib/ui_kit/widgets/room/anim/room_gift_seat_flight_overlay.dart +++ b/lib/ui_kit/widgets/room/anim/room_gift_seat_flight_overlay.dart @@ -223,6 +223,7 @@ class _RoomGiftSeatFlightOverlayState extends State ImageProvider? _activeImageProvider; Offset? _activeTargetOffset; bool _isPlaying = false; + int _sessionToken = 0; @override void initState() { @@ -279,6 +280,7 @@ class _RoomGiftSeatFlightOverlayState extends State } void _clear() { + _sessionToken += 1; _queue.clear(); _controller.stop(); _controller.reset(); @@ -372,16 +374,22 @@ class _RoomGiftSeatFlightOverlayState extends State if (_isPlaying || _queue.isEmpty || !mounted) { return; } + final scheduledToken = _sessionToken; WidgetsBinding.instance.addPostFrameCallback((_) { - _startNextAnimation(); + _startNextAnimation(expectedSessionToken: scheduledToken); }); } - Future _startNextAnimation() async { - if (!mounted || _isPlaying || _queue.isEmpty) { + Future _startNextAnimation({int? expectedSessionToken}) async { + if (!mounted || + _isPlaying || + _queue.isEmpty || + (expectedSessionToken != null && + expectedSessionToken != _sessionToken)) { return; } + final activeSessionToken = _sessionToken; final queuedRequest = _queue.removeFirst(); final targetOffset = _resolveTargetOffset( queuedRequest.request.targetUserId, @@ -389,7 +397,7 @@ class _RoomGiftSeatFlightOverlayState extends State if (targetOffset == null) { if (queuedRequest.retryCount < 6) { Future.delayed(const Duration(milliseconds: 120), () { - if (!mounted) { + if (!mounted || activeSessionToken != _sessionToken) { return; } _queue.addFirst( @@ -424,7 +432,9 @@ class _RoomGiftSeatFlightOverlayState extends State ).timeout(const Duration(milliseconds: 250)); } catch (_) {} - if (!mounted || _activeRequest != queuedRequest.request) { + if (!mounted || + activeSessionToken != _sessionToken || + _activeRequest != queuedRequest.request) { return; } diff --git a/lib/ui_kit/widgets/room/floating/floating_luck_gift_screen_widget.dart b/lib/ui_kit/widgets/room/floating/floating_luck_gift_screen_widget.dart index d1af83c..389825b 100644 --- a/lib/ui_kit/widgets/room/floating/floating_luck_gift_screen_widget.dart +++ b/lib/ui_kit/widgets/room/floating/floating_luck_gift_screen_widget.dart @@ -1,532 +1,532 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_debouncer/flutter_debouncer.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:provider/provider.dart'; -import 'package:yumi/ui_kit/components/sc_compontent.dart'; -import 'package:yumi/ui_kit/components/text/sc_text.dart'; -import 'package:yumi/app/constants/sc_global_config.dart'; -import 'package:yumi/services/general/sc_app_general_manager.dart'; -import 'package:yumi/shared/tools/sc_room_utils.dart'; -import 'package:yumi/main.dart'; -import 'package:marquee/marquee.dart'; - -import 'package:yumi/app_localizations.dart'; -import 'package:yumi/shared/data_sources/models/message/sc_floating_message.dart'; - -class FloatingLuckGiftScreenWidget extends StatefulWidget { - final SCFloatingMessage message; - final VoidCallback onAnimationCompleted; // 动画完成回调 - - const FloatingLuckGiftScreenWidget({ - Key? key, - required this.message, - required this.onAnimationCompleted, - }) : super(key: key); - - @override - _FloatingLuckGiftScreenWidgetState createState() => - _FloatingLuckGiftScreenWidgetState(); -} - -class _FloatingLuckGiftScreenWidgetState - extends State - with TickerProviderStateMixin { - static const String _coinIconAssetPath = "sc_images/general/sc_icon_jb.png"; - late AnimationController _controller; - late Animation _offsetAnimation; - late AnimationController _swipeController; // 新增:滑动动画控制器 - late Animation _swipeAnimation; // 新增:滑动动画 - Debouncer debouncer = Debouncer(); - - bool _isSwipeAnimating = false; // 标记是否正在执行滑动动画 - - @override - void initState() { - super.initState(); - - // 主动画控制器 - _controller = AnimationController( - duration: const Duration(seconds: 5), - vsync: this, - ); - - // 滑动动画控制器 - _swipeController = AnimationController( - duration: const Duration(milliseconds: 550), // 滑动动画500ms - vsync: this, - ); - - // 监听滑动动画完成 - _swipeController.addStatusListener((status) { - if (status == AnimationStatus.completed) { - widget.onAnimationCompleted(); - } - }); - - // 主动画:从右向左移动 - _offsetAnimation = Tween( - begin: const Offset(1.0, 0.0), - end: const Offset(-1.0, 0.0), - ).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOut)); - - // 滑动动画:从当前位置快速向左滑出 - _swipeAnimation = Tween( - begin: Offset.zero, // 从当前位置开始 - end: const Offset(-1.5, 0.0), // 到屏幕左侧外 - ).animate(CurvedAnimation(parent: _swipeController, curve: Curves.easeIn)); - - // 监听主动画完成 - _controller.addStatusListener((status) { - if (status == AnimationStatus.completed && !_isSwipeAnimating) { - widget.onAnimationCompleted(); - } - }); - - // 延迟启动动画,确保组件已经完全构建 - WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) { - _controller.forward(); - } - }); - } - - // 处理向左滑动 - void _handleSwipeLeft() { - if (_isSwipeAnimating) return; - - setState(() { - _isSwipeAnimating = true; - }); - - // 停止主动画 - _controller.stop(); - - // 启动滑动动画 - _swipeController.reset(); - _swipeController.forward(); - } - - @override - void dispose() { - _controller.dispose(); - _swipeController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return _buildLuckGiftAnimation(); - } - - ///幸运礼物飘屏 - Widget _buildLuckGiftAnimation() { - return GestureDetector( - onTap: () { - debouncer.debounce( - duration: Duration(milliseconds: 350), - onDebounce: () { - if (widget.message.roomId != null && - widget.message.roomId!.isNotEmpty) { - SCRoomUtils.goRoom( - widget.message.roomId!, - navigatorKey.currentState!.context, - fromFloting: true, - ); - } - }, - ); - }, - onHorizontalDragEnd: (details) { - // 获取当前的文本方向 - final textDirection = Directionality.of(context); - - // 定义一个根据方向转换速度符号的辅助函数 - double effectiveVelocity(double velocity) { - // 在RTL模式下,反转速度的正负号 - return textDirection == TextDirection.rtl ? -velocity : velocity; - } - - double velocity = effectiveVelocity(details.primaryVelocity ?? 0); - if (velocity < 0) { - // 向左滑动,把当前视图向左平移出去 - _handleSwipeLeft(); - } - }, - child: SlideTransition( - position: _isSwipeAnimating ? _swipeAnimation : _offsetAnimation, - child: Container( - alignment: Alignment.center, - height: 83.w, - width: 350.w, - child: Stack( - children: [ - Transform.flip( - flipX: SCGlobalConfig.lang == "ar" ? true : false, // 水平翻转 - flipY: false, // 垂直翻转设为 false - child: Image.asset( - "sc_images/room/sc_icon_luck_gift_float_n_bg.png", - fit: BoxFit.fill, - ), - ), - Row( - mainAxisSize: MainAxisSize.min, // 宽度由内容决定 - children: [ - Container( - margin: EdgeInsetsDirectional.only(top: 14.w, start: 1.w), - child: netImage( - url: widget.message.userAvatarUrl ?? "", - width: 52.w, - shape: BoxShape.circle, - ), - ), - SizedBox(width: 4.w), - Expanded( - child: Container( - margin: EdgeInsets.only(top: 10.w), - child: Row( - children: [ - Container( - constraints: BoxConstraints( - maxWidth: 85.w, - maxHeight: 20.w, - ), - child: - (widget.message.userName?.length ?? 0) > 6 - ? Marquee( - text: "${widget.message.userName} ", - style: TextStyle( - fontSize: 13.sp, - color: Color(0xffFEF129), - fontWeight: FontWeight.bold, - decoration: TextDecoration.none, - letterSpacing: 0.1, - ), - scrollAxis: Axis.horizontal, - crossAxisAlignment: - CrossAxisAlignment.start, - blankSpace: 20.0, - velocity: 40.0, - pauseAfterRound: Duration(seconds: 1), - accelerationDuration: Duration( - seconds: 1, - ), - accelerationCurve: Curves.easeOut, - decelerationDuration: Duration( - milliseconds: 500, - ), - decelerationCurve: Curves.easeOut, - ) - : Text( - "${widget.message.userName} ", - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: 13.sp, - color: Color(0xffFEF129), - fontWeight: FontWeight.bold, - decoration: TextDecoration.none, - ), - ), - ), - SizedBox(width: 3.w), - Expanded(child: _buildRewardLine(context)), - SizedBox(width: 6.w), - Container( - width: 80.w, - alignment: Alignment.center, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox(height: 2.w), - buildNumForGame( - "${widget.message.multiple ?? 0}", - size: 25.w, - ), - SizedBox(height: 3.w), - SCGlobalConfig.lang == "ar" - ? Image.asset( - "sc_images/room/sc_icon_times_text_ar.png", - height: 12.w, - ) - : Image.asset( - "sc_images/room/sc_icon_times_text_en.png", - height: 12.w, - ), - ], - ), - ), - SizedBox(width: 6.w), - ], - ), - ), - ), - ], - ), - ], - ), - ), - ), - ); - } - - Widget _buildRewardLine(BuildContext context) { - final baseStyle = TextStyle( - fontSize: 12.sp, - color: Colors.white, - fontWeight: FontWeight.bold, - letterSpacing: 0.1, - decoration: TextDecoration.none, - ); - final amountStyle = baseStyle.copyWith(color: const Color(0xffFEF129)); - return Text.rich( - TextSpan( - children: [ - TextSpan(text: SCAppLocalizations.of(context)!.get, style: baseStyle), - TextSpan( - text: " ${_formatCoins(widget.message.coins)} ", - style: amountStyle, - ), - WidgetSpan( - alignment: PlaceholderAlignment.middle, - child: Padding( - padding: EdgeInsetsDirectional.only(end: 3.w), - child: Image.asset(_coinIconAssetPath, width: 16.w, height: 16.w), - ), - ), - TextSpan(text: "from ", style: baseStyle), - WidgetSpan( - alignment: PlaceholderAlignment.middle, - child: _buildGiftIcon(context), - ), - ], - ), - textAlign: TextAlign.start, - maxLines: 1, - overflow: TextOverflow.ellipsis, - softWrap: false, - strutStyle: StrutStyle( - height: 1.1, - fontWeight: FontWeight.bold, - forceStrutHeight: true, - ), - ); - } - - Widget _buildGiftIcon(BuildContext context) { - final primaryGiftUrl = (widget.message.giftUrl ?? "").trim(); - final fallbackGiftUrl = _resolveFallbackGiftUrl(context); - final displayGiftUrl = - primaryGiftUrl.isNotEmpty ? primaryGiftUrl : fallbackGiftUrl; - if (displayGiftUrl.isEmpty) { - return _buildGiftIconPlaceholder(); - } - - final backupUrl = - fallbackGiftUrl.isNotEmpty && fallbackGiftUrl != displayGiftUrl - ? fallbackGiftUrl - : ""; - - return netImage( - url: displayGiftUrl, - width: 18.w, - height: 18.w, - borderRadius: BorderRadius.circular(3.w), - loadingWidget: _buildGiftIconPlaceholder(), - errorWidget: - backupUrl.isNotEmpty - ? netImage( - url: backupUrl, - width: 18.w, - height: 18.w, - borderRadius: BorderRadius.circular(3.w), - noDefaultImg: true, - loadingWidget: _buildGiftIconPlaceholder(), - errorWidget: _buildGiftIconPlaceholder(), - ) - : _buildGiftIconPlaceholder(), - ); - } - - String _resolveFallbackGiftUrl(BuildContext context) { - final giftId = (widget.message.giftId ?? "").trim(); - if (giftId.isEmpty) { - return ""; - } - final gift = Provider.of( - context, - listen: false, - ).getGiftById(giftId); - return (gift?.giftPhoto ?? "").trim(); - } - - Widget _buildGiftIconPlaceholder() { - return SizedBox(width: 18.w, height: 18.w); - } - - String _formatCoins(num? coins) { - final value = (coins ?? 0); - if (value > 9999) { - return "${(value / 1000).toStringAsFixed(0)}k"; - } - if (value % 1 == 0) { - return value.toInt().toString(); - } - return value.toString(); - } - - ///礼物总价钱达到10000 - Widget _buildGiftAnimation() { - return GestureDetector( - onTap: () { - debouncer.debounce( - duration: Duration(milliseconds: 350), - onDebounce: () { - if (widget.message.roomId != null && - widget.message.roomId!.isNotEmpty) { - SCRoomUtils.goRoom( - widget.message.roomId!, - navigatorKey.currentState!.context, - fromFloting: true, - ); - } - }, - ); - }, - onHorizontalDragEnd: (details) { - // 获取当前的文本方向 - final textDirection = Directionality.of(context); - - // 定义一个根据方向转换速度符号的辅助函数 - double effectiveVelocity(double velocity) { - // 在RTL模式下,反转速度的正负号 - return textDirection == TextDirection.rtl ? -velocity : velocity; - } - - double velocity = effectiveVelocity(details.primaryVelocity ?? 0); - if (velocity < 0) { - // 向左滑动,把当前视图向左平移出去 - _handleSwipeLeft(); - } - }, - child: SlideTransition( - position: _isSwipeAnimating ? _swipeAnimation : _offsetAnimation, - child: Container( - alignment: Alignment.center, - height: 50.w, - width: 290.w, - margin: EdgeInsets.only(top: 20.w), - padding: EdgeInsets.symmetric(horizontal: 15.w), - decoration: BoxDecoration( - image: DecorationImage( - image: AssetImage("sc_images/room/sc_icon_gift_float_bg.png"), - fit: BoxFit.fill, - ), - ), - child: Row( - mainAxisSize: MainAxisSize.min, // 宽度由内容决定 - children: [ - // 可以根据消息模型丰富内容,例如显示头像 - if (widget.message.userAvatarUrl?.isNotEmpty ?? false) - head(url: widget.message.userAvatarUrl ?? "", width: 42.w), - SizedBox(width: 4.w), - Expanded( - child: Row( - children: [ - Container( - constraints: BoxConstraints( - maxWidth: 48.w, - maxHeight: 21.w, - ), - child: - (widget.message.userName?.length ?? 0) > 6 - ? Marquee( - text: widget.message.userName ?? "", - style: TextStyle( - fontSize: 13.sp, - color: Colors.orange, - fontWeight: FontWeight.bold, - decoration: TextDecoration.none, - ), - scrollAxis: Axis.horizontal, - crossAxisAlignment: CrossAxisAlignment.start, - blankSpace: 20.0, - velocity: 40.0, - pauseAfterRound: Duration(seconds: 1), - accelerationDuration: Duration(seconds: 1), - accelerationCurve: Curves.easeOut, - decelerationDuration: Duration( - milliseconds: 500, - ), - decelerationCurve: Curves.easeOut, - ) - : Text( - widget.message.userName ?? "", - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: 13.sp, - color: Colors.orange, - fontWeight: FontWeight.bold, - decoration: TextDecoration.none, - ), - ), - ), - text( - SCAppLocalizations.of(context)!.sendTo, - textColor: Colors.white, - fontSize: 13.sp, - fontWeight: FontWeight.bold, - ), - Container( - constraints: BoxConstraints( - maxWidth: 48.w, - maxHeight: 21.w, - ), - child: - (widget.message.toUserName?.length ?? 0) > 6 - ? Marquee( - text: widget.message.toUserName ?? "", - style: TextStyle( - fontSize: 13.sp, - color: Colors.orange, - fontWeight: FontWeight.bold, - decoration: TextDecoration.none, - ), - scrollAxis: Axis.horizontal, - crossAxisAlignment: CrossAxisAlignment.start, - blankSpace: 20.0, - velocity: 40.0, - pauseAfterRound: Duration(seconds: 1), - accelerationDuration: Duration(seconds: 1), - accelerationCurve: Curves.easeOut, - decelerationDuration: Duration( - milliseconds: 500, - ), - decelerationCurve: Curves.easeOut, - ) - : Text( - widget.message.toUserName ?? "", - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: 13.sp, - color: Colors.orange, - fontWeight: FontWeight.bold, - decoration: TextDecoration.none, - ), - ), - ), - ], - ), - ), - netImage(url: widget.message.giftUrl ?? "", width: 32.w), - SizedBox(width: 3.w), - Image.asset("sc_images/room/sc_icon_x.png", width: 18.w), - buildNum("${widget.message.number}", size: 18.w), - ], - ), - ), - ), - ); - } -} +import 'package:flutter/material.dart'; +import 'package:flutter_debouncer/flutter_debouncer.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:provider/provider.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/app/constants/sc_global_config.dart'; +import 'package:yumi/services/general/sc_app_general_manager.dart'; +import 'package:yumi/shared/tools/sc_room_utils.dart'; +import 'package:yumi/main.dart'; +import 'package:marquee/marquee.dart'; + +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/shared/data_sources/models/message/sc_floating_message.dart'; + +class FloatingLuckGiftScreenWidget extends StatefulWidget { + final SCFloatingMessage message; + final VoidCallback onAnimationCompleted; // 动画完成回调 + + const FloatingLuckGiftScreenWidget({ + Key? key, + required this.message, + required this.onAnimationCompleted, + }) : super(key: key); + + @override + _FloatingLuckGiftScreenWidgetState createState() => + _FloatingLuckGiftScreenWidgetState(); +} + +class _FloatingLuckGiftScreenWidgetState + extends State + with TickerProviderStateMixin { + static const String _coinIconAssetPath = "sc_images/general/sc_icon_jb.png"; + late AnimationController _controller; + late Animation _offsetAnimation; + late AnimationController _swipeController; // 新增:滑动动画控制器 + late Animation _swipeAnimation; // 新增:滑动动画 + Debouncer debouncer = Debouncer(); + + bool _isSwipeAnimating = false; // 标记是否正在执行滑动动画 + + @override + void initState() { + super.initState(); + + // 主动画控制器 + _controller = AnimationController( + duration: const Duration(seconds: 5), + vsync: this, + ); + + // 滑动动画控制器 + _swipeController = AnimationController( + duration: const Duration(milliseconds: 550), // 滑动动画500ms + vsync: this, + ); + + // 监听滑动动画完成 + _swipeController.addStatusListener((status) { + if (status == AnimationStatus.completed) { + widget.onAnimationCompleted(); + } + }); + + // 主动画:从右向左移动 + _offsetAnimation = Tween( + begin: const Offset(1.0, 0.0), + end: const Offset(-1.0, 0.0), + ).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOut)); + + // 滑动动画:从当前位置快速向左滑出 + _swipeAnimation = Tween( + begin: Offset.zero, // 从当前位置开始 + end: const Offset(-1.5, 0.0), // 到屏幕左侧外 + ).animate(CurvedAnimation(parent: _swipeController, curve: Curves.easeIn)); + + // 监听主动画完成 + _controller.addStatusListener((status) { + if (status == AnimationStatus.completed && !_isSwipeAnimating) { + widget.onAnimationCompleted(); + } + }); + + // 延迟启动动画,确保组件已经完全构建 + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + _controller.forward(); + } + }); + } + + // 处理向左滑动 + void _handleSwipeLeft() { + if (_isSwipeAnimating) return; + + setState(() { + _isSwipeAnimating = true; + }); + + // 停止主动画 + _controller.stop(); + + // 启动滑动动画 + _swipeController.reset(); + _swipeController.forward(); + } + + @override + void dispose() { + _controller.dispose(); + _swipeController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return _buildLuckGiftAnimation(); + } + + ///幸运礼物飘屏 + Widget _buildLuckGiftAnimation() { + return GestureDetector( + onTap: () { + debouncer.debounce( + duration: Duration(milliseconds: 350), + onDebounce: () { + if (widget.message.roomId != null && + widget.message.roomId!.isNotEmpty) { + SCRoomUtils.goRoom( + widget.message.roomId!, + navigatorKey.currentState!.context, + fromFloting: true, + ); + } + }, + ); + }, + onHorizontalDragEnd: (details) { + // 获取当前的文本方向 + final textDirection = Directionality.of(context); + + // 定义一个根据方向转换速度符号的辅助函数 + double effectiveVelocity(double velocity) { + // 在RTL模式下,反转速度的正负号 + return textDirection == TextDirection.rtl ? -velocity : velocity; + } + + double velocity = effectiveVelocity(details.primaryVelocity ?? 0); + if (velocity < 0) { + // 向左滑动,把当前视图向左平移出去 + _handleSwipeLeft(); + } + }, + child: SlideTransition( + position: _isSwipeAnimating ? _swipeAnimation : _offsetAnimation, + child: Container( + alignment: Alignment.center, + height: 83.w, + width: 350.w, + child: Stack( + children: [ + Transform.flip( + flipX: SCGlobalConfig.lang == "ar" ? true : false, // 水平翻转 + flipY: false, // 垂直翻转设为 false + child: Image.asset( + "sc_images/room/sc_icon_luck_gift_float_n_bg.png", + fit: BoxFit.fill, + ), + ), + Row( + mainAxisSize: MainAxisSize.min, // 宽度由内容决定 + children: [ + Container( + margin: EdgeInsetsDirectional.only(top: 14.w, start: 1.w), + child: netImage( + url: widget.message.userAvatarUrl ?? "", + width: 52.w, + shape: BoxShape.circle, + ), + ), + SizedBox(width: 4.w), + Expanded( + child: Container( + margin: EdgeInsets.only(top: 10.w), + child: Row( + children: [ + Container( + constraints: BoxConstraints( + maxWidth: 85.w, + maxHeight: 20.w, + ), + child: + (widget.message.userName?.length ?? 0) > 6 + ? Marquee( + text: "${widget.message.userName} ", + style: TextStyle( + fontSize: 13.sp, + color: Color(0xffFEF129), + fontWeight: FontWeight.bold, + decoration: TextDecoration.none, + letterSpacing: 0.1, + ), + scrollAxis: Axis.horizontal, + crossAxisAlignment: + CrossAxisAlignment.start, + blankSpace: 20.0, + velocity: 40.0, + pauseAfterRound: Duration(seconds: 1), + accelerationDuration: Duration( + seconds: 1, + ), + accelerationCurve: Curves.easeOut, + decelerationDuration: Duration( + milliseconds: 500, + ), + decelerationCurve: Curves.easeOut, + ) + : Text( + "${widget.message.userName} ", + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 13.sp, + color: Color(0xffFEF129), + fontWeight: FontWeight.bold, + decoration: TextDecoration.none, + ), + ), + ), + SizedBox(width: 3.w), + Expanded(child: _buildRewardLine(context)), + SizedBox(width: 6.w), + Container( + width: 80.w, + alignment: Alignment.center, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(height: 2.w), + buildNumForGame( + "${widget.message.multiple ?? 0}", + size: 25.w, + ), + SizedBox(height: 3.w), + SCGlobalConfig.lang == "ar" + ? Image.asset( + "sc_images/room/sc_icon_times_text_ar.png", + height: 12.w, + ) + : Image.asset( + "sc_images/room/sc_icon_times_text_en.png", + height: 12.w, + ), + ], + ), + ), + SizedBox(width: 6.w), + ], + ), + ), + ), + ], + ), + ], + ), + ), + ), + ); + } + + Widget _buildRewardLine(BuildContext context) { + final baseStyle = TextStyle( + fontSize: 12.sp, + color: Colors.white, + fontWeight: FontWeight.bold, + letterSpacing: 0.1, + decoration: TextDecoration.none, + ); + final amountStyle = baseStyle.copyWith(color: const Color(0xffFEF129)); + return Text.rich( + TextSpan( + children: [ + TextSpan(text: SCAppLocalizations.of(context)!.get, style: baseStyle), + TextSpan( + text: " ${_formatCoins(widget.message.coins)} ", + style: amountStyle, + ), + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: Padding( + padding: EdgeInsetsDirectional.only(end: 3.w), + child: Image.asset(_coinIconAssetPath, width: 16.w, height: 16.w), + ), + ), + TextSpan(text: "from ", style: baseStyle), + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: _buildGiftIcon(context), + ), + ], + ), + textAlign: TextAlign.start, + maxLines: 1, + overflow: TextOverflow.ellipsis, + softWrap: false, + strutStyle: StrutStyle( + height: 1.1, + fontWeight: FontWeight.bold, + forceStrutHeight: true, + ), + ); + } + + Widget _buildGiftIcon(BuildContext context) { + final primaryGiftUrl = (widget.message.giftUrl ?? "").trim(); + final fallbackGiftUrl = _resolveFallbackGiftUrl(context); + final displayGiftUrl = + primaryGiftUrl.isNotEmpty ? primaryGiftUrl : fallbackGiftUrl; + if (displayGiftUrl.isEmpty) { + return _buildGiftIconPlaceholder(); + } + + final backupUrl = + fallbackGiftUrl.isNotEmpty && fallbackGiftUrl != displayGiftUrl + ? fallbackGiftUrl + : ""; + + return netImage( + url: displayGiftUrl, + width: 18.w, + height: 18.w, + borderRadius: BorderRadius.circular(3.w), + loadingWidget: _buildGiftIconPlaceholder(), + errorWidget: + backupUrl.isNotEmpty + ? netImage( + url: backupUrl, + width: 18.w, + height: 18.w, + borderRadius: BorderRadius.circular(3.w), + noDefaultImg: true, + loadingWidget: _buildGiftIconPlaceholder(), + errorWidget: _buildGiftIconPlaceholder(), + ) + : _buildGiftIconPlaceholder(), + ); + } + + String _resolveFallbackGiftUrl(BuildContext context) { + final giftId = (widget.message.giftId ?? "").trim(); + if (giftId.isEmpty) { + return ""; + } + final gift = Provider.of( + context, + listen: false, + ).getGiftById(giftId); + return (gift?.giftPhoto ?? "").trim(); + } + + Widget _buildGiftIconPlaceholder() { + return SizedBox(width: 18.w, height: 18.w); + } + + String _formatCoins(num? coins) { + final value = (coins ?? 0); + if (value > 9999) { + return "${(value / 1000).toStringAsFixed(0)}k"; + } + if (value % 1 == 0) { + return value.toInt().toString(); + } + return value.toString(); + } + + ///礼物总价钱达到10000 + Widget _buildGiftAnimation() { + return GestureDetector( + onTap: () { + debouncer.debounce( + duration: Duration(milliseconds: 350), + onDebounce: () { + if (widget.message.roomId != null && + widget.message.roomId!.isNotEmpty) { + SCRoomUtils.goRoom( + widget.message.roomId!, + navigatorKey.currentState!.context, + fromFloting: true, + ); + } + }, + ); + }, + onHorizontalDragEnd: (details) { + // 获取当前的文本方向 + final textDirection = Directionality.of(context); + + // 定义一个根据方向转换速度符号的辅助函数 + double effectiveVelocity(double velocity) { + // 在RTL模式下,反转速度的正负号 + return textDirection == TextDirection.rtl ? -velocity : velocity; + } + + double velocity = effectiveVelocity(details.primaryVelocity ?? 0); + if (velocity < 0) { + // 向左滑动,把当前视图向左平移出去 + _handleSwipeLeft(); + } + }, + child: SlideTransition( + position: _isSwipeAnimating ? _swipeAnimation : _offsetAnimation, + child: Container( + alignment: Alignment.center, + height: 50.w, + width: 290.w, + margin: EdgeInsets.only(top: 20.w), + padding: EdgeInsets.symmetric(horizontal: 15.w), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage("sc_images/room/sc_icon_gift_float_bg.png"), + fit: BoxFit.fill, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, // 宽度由内容决定 + children: [ + // 可以根据消息模型丰富内容,例如显示头像 + if (widget.message.userAvatarUrl?.isNotEmpty ?? false) + head(url: widget.message.userAvatarUrl ?? "", width: 42.w), + SizedBox(width: 4.w), + Expanded( + child: Row( + children: [ + Container( + constraints: BoxConstraints( + maxWidth: 48.w, + maxHeight: 21.w, + ), + child: + (widget.message.userName?.length ?? 0) > 6 + ? Marquee( + text: widget.message.userName ?? "", + style: TextStyle( + fontSize: 13.sp, + color: Colors.orange, + fontWeight: FontWeight.bold, + decoration: TextDecoration.none, + ), + scrollAxis: Axis.horizontal, + crossAxisAlignment: CrossAxisAlignment.start, + blankSpace: 20.0, + velocity: 40.0, + pauseAfterRound: Duration(seconds: 1), + accelerationDuration: Duration(seconds: 1), + accelerationCurve: Curves.easeOut, + decelerationDuration: Duration( + milliseconds: 500, + ), + decelerationCurve: Curves.easeOut, + ) + : Text( + widget.message.userName ?? "", + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 13.sp, + color: Colors.orange, + fontWeight: FontWeight.bold, + decoration: TextDecoration.none, + ), + ), + ), + text( + SCAppLocalizations.of(context)!.sendTo, + textColor: Colors.white, + fontSize: 13.sp, + fontWeight: FontWeight.bold, + ), + Container( + constraints: BoxConstraints( + maxWidth: 48.w, + maxHeight: 21.w, + ), + child: + (widget.message.toUserName?.length ?? 0) > 6 + ? Marquee( + text: widget.message.toUserName ?? "", + style: TextStyle( + fontSize: 13.sp, + color: Colors.orange, + fontWeight: FontWeight.bold, + decoration: TextDecoration.none, + ), + scrollAxis: Axis.horizontal, + crossAxisAlignment: CrossAxisAlignment.start, + blankSpace: 20.0, + velocity: 40.0, + pauseAfterRound: Duration(seconds: 1), + accelerationDuration: Duration(seconds: 1), + accelerationCurve: Curves.easeOut, + decelerationDuration: Duration( + milliseconds: 500, + ), + decelerationCurve: Curves.easeOut, + ) + : Text( + widget.message.toUserName ?? "", + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 13.sp, + color: Colors.orange, + fontWeight: FontWeight.bold, + decoration: TextDecoration.none, + ), + ), + ), + ], + ), + ), + netImage(url: widget.message.giftUrl ?? "", width: 32.w), + SizedBox(width: 3.w), + Image.asset("sc_images/room/sc_icon_x.png", width: 18.w), + buildNum("${widget.message.number}", size: 18.w), + ], + ), + ), + ), + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 676a4da..7869bc2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.1.0+2 +version: 1.1.1+3 environment: diff --git a/sc_images/room/anim/gift/room_gift_combo_badge.png b/sc_images/room/anim/gift/room_gift_combo_badge.png index 3555a39..bd3ec8f 100644 Binary files a/sc_images/room/anim/gift/room_gift_combo_badge.png and b/sc_images/room/anim/gift/room_gift_combo_badge.png differ diff --git a/需求进度.md b/需求进度.md index 5b76909..8eb6e26 100644 --- a/需求进度.md +++ b/需求进度.md @@ -17,6 +17,8 @@ - 已继续修正连击播报条“单点送 1 个却直接跳 `+5`”的问题:顶部礼物播报条现已接入 `customAnimationCount` 的点击粒度信息,数量动画不再只按合并后的总量做补间,而是按“单次点击对应的数量步长”逐步递进;例如连续点击 5 次、每次送 1 个时,会按 `+1 +1 +1 +1 +1` 的视觉节奏补齐,而不是直接把右侧数量跳成 `+5`。 - 已继续修正连击档位特效偶发“不播”的问题:当前连击 `SVGA` 触发已改为只命中仓库里真实存在资源的档位集合,不再把 `20/88/200...` 这类当前工程里没有对应特效文件的档位当成可播放资源;同时命中判断会按“本次新增后跨过的最高有效资源档位”来触发,避免因为本地批量窗口把多次点击合成一包后,先命中到空资源路径而看起来像整段特效都没播。 - 已按 2026-04-21 最新回归继续修正“飞向麦位的自制动画”缺失问题:座位飞屏现已从房间统一调度器的延后队列中移出,恢复为收到礼物消息后直接入队到全局 `RoomGiftSeatFlightOverlay`;保留既有的图片预热、同会话队列上限和 `customAnimationCount` 逐个飞行逻辑,只撤回此前对这一核心反馈动画的延迟调度,避免用户在礼物命中全屏特效或调度状态未及时清空时误以为飞屏动画消失。 +- 已继续加固座位飞屏退出房间后的残留风险:全局 `RoomGiftSeatFlightOverlay` 现已和房间可视特效开关绑定,离开房间后不会再继续挂在首页/探索/消息/我的等页面上显示;同时 overlay 内部的 `postFrame / delayed retry / image precache` 回调已补上会话 token 校验,避免房间退出后旧动画任务把屏幕中央的静态礼物图再次拉起并长期悬挂。 +- 已继续修正“房间飘屏在首页/探索/消息/我的也会刷到”的问题:根层房间礼物特效层现已统一改为依据 `RtcProvider.shouldShowRoomVisualEffects` 判断显示,而不再只看偏松的 `roomVisualEffectsEnabled`;同时 `OverlayManager` 在播放房间礼物/幸运礼物飘屏前会再次校验“当前仍在房间可视态且消息房间号与当前房间一致”,房间最小化或退出时也会主动清掉正在播和排队中的房间飘屏,避免房间内的 `5 times`、礼物飘屏等效果溢出到首页等非房间页面。 ## 本轮启动优化(非网络) - 已补回启动页正式展示逻辑:当前 `Weekly Star / Last Weekly CP` 两套自定义启动页都改为“本地有缓存才展示、无缓存不展示”;由于现阶段周榜与 CP 榜接口链路都还未 ready,缓存刷新逻辑已先关闭,所以当前启动阶段会直接回退到默认 splash,不再展示这两套定制视觉稿。相关恢复入口已在缓存类里用 `api-ready-launch-splash` 注释标记,后续接口 ready 后可直接搜索接回。 @@ -58,6 +60,14 @@ - 已继续收口幸运礼物 `burst` 金额文案与 `svga` 本体的时机同步:此前中央金币文本直接跟外层中奖数据显隐,而 `svga` 自身还存在资源加载和单次播放完成的生命周期,所以两者在出现/消失时会有肉眼可见的前后差;当前已给 `SCSvgaAssetWidget` 补上播放开始/结束回调,并让 `burst` 中央金额只在 `svga` 真正开始播时显示、在它播完清帧时一并隐藏。 - 已按 2026-04-21 最新联调继续细调幸运礼物 `burst` 中央金币文案:当前已在现有基础上再向上微调 `4` 个单位、向右微调 `5` 个单位,让金额更贴近特效中部的目标槽位;同时 `burst` 的整体展示时长也已再缩短 `1` 秒,避免命中后在屏上停留过久。 - 已继续修复幸运礼物顶部横幅 `from` 后礼物图标偶发显示不出来的问题:此前这条紫色 lucky gift 横幅只直接使用 socket 下发的 `giftCover/giftUrl` 渲染礼物图,一旦服务端该字段为空或图片加载失败,就只会退回默认占位;当前已把 `giftId` 一并挂进 `SCFloatingMessage`,并在 `FloatingLuckGiftScreenWidget` 内增加“优先用 `giftUrl`,失败时再回退到本地礼物列表缓存中的 `giftPhoto`”的双重兜底,避免横幅末尾再出现空白占位块。 +- 已继续补齐幸运礼物横幅礼物图标的“晚到数据”刷新:此前 `from` 后面的礼物图只在横幅首次 build 时用 `listen: false` 读取一次本地礼物列表,若 `giftList` 之后才加载完成,这条 overlay 生命周期内也不会再更新,最终仍会留下空白图标;当前已改为通过 `Selector` 监听 `giftId -> giftPhoto` 回退图,并在 socket 未携带 `giftUrl` 时主动补拉一次 `giftList`,让礼物列表就绪后能把幸运礼物横幅末尾图标自动补出来。 +- 已继续修正幸运礼物紫色横幅里 `from` 后礼物图“闪一下又空白”的问题:排查后发现这条横幅把礼物图标塞在 `Text.rich` 末尾,而正文又开启了单行省略,导致尾部 `WidgetSpan` 在最终排版溢出时可能被直接不绘制;当前已把礼物图标从内联文本里拆到正文右侧单独布局,保持正文自己省略、礼物图标固定显示,不再因为文本溢出把尾部礼物图一起吃掉。 +- 已按 2026-04-21 最新回归把紫色幸运礼物横幅的礼物图取值收口到和其它飘屏同一来源:当前 `RTM` 在构造幸运礼物 `SCFloatingMessage` 前,会优先按 `giftId/standardId` 从本地礼物列表拿同款 `giftPhoto`,拿不到时才退回服务端下发的 `giftCover`;同时紫色横幅组件本身不再额外做自己的礼物图回退和二次解析,只直接显示已统一好的 `message.giftUrl`,避免和其它飘屏走出两套不同的取值链路。 +- 已按 2026-04-21 最新排障请求给紫色幸运礼物横幅补齐定向日志:当前会在 `RTM` 解析 `GAME_LUCKY_GIFT` 时输出 `giftId / giftCover / 本地 id 命中 / 本地 standardId 命中 / resolvedUrl`,同时横幅组件自身也会记录一次 `init`、`gift icon render`、`gift icon url empty` 和 `gift icon load failed`,方便直接判断是服务端字段为空、本地映射错位,还是图片请求本身失败。 +- 已根据 2026-04-21 最新日志结论继续收口幸运礼物紫色横幅:日志已确认这条横幅实际拿到的 `giftUrl` 正常且组件已进入渲染分支,因此当前继续把礼物图标的绘制方式直接对齐到其它飘屏的朴素路径,改为更大的 `netImage(url, width)` 直出并使用 `BoxFit.contain`,不再叠加此前那套更特殊的小图标 `borderRadius/noDefaultImg` 参数,优先排除该组件私有渲染参数导致的异常显示。 +- 已继续绕开幸运礼物紫色横幅里可疑的小图 `ExtendedImage` 渲染链:当前礼物图标已改为在 `initState` 里先构建 `buildCachedImageProvider`,再通过基础 `Image(image: provider)` 直接绘制,并额外输出一次 `gift icon frame ready` 日志,用于确认这张图是否真的完成首帧解码显示;这样可以把问题进一步收敛为“图片 provider/解码”还是“组件布局/视觉观感”。 +- 已按 2026-04-21 最新联调请求,把紫色幸运礼物横幅 `from` 后的图标临时替换为金币图标:这一步只用于快速验证该图标位本身的布局和可见性是否正常,不再受当前礼物图资源链路影响;如果金币图标能稳定显示,就说明问题仍集中在礼物图渲染链,而不是这个位置被遮挡或根本没画出来。 +- 已按 2026-04-21 最新回归把 [floating_luck_gift_screen_widget] 的样式撤回到最初实现,不再继续在幸运礼物飘屏横幅上做偏题调试;同时已定位到真正缺图的是消息栏里的 `gameLuckyGift_5` 高亮消息,这条消息此前只写入了 `awardAmount/user/msg`,没有把 `gift` 一并塞进去,导致 `room_msg_item.dart` 读取 `widget.msg.gift?.giftPhoto` 时天然为空。当前已在 `RTM` 构造高亮 lucky 消息时补回 `giftPhoto`,让消息栏里的紫色 lucky message 能和其它消息一样拿到礼物图。 - 已优化语言房麦位/头像的二次确认交互:普通用户点击可上麦的空麦位时,当前会直接执行上麦,不再先弹出只有 `Take the mic / Cancel` 的确认层;普通用户点击房间头像或已占麦位上的用户头像时,也会直接打开个人卡片,不再额外弹出仅含 `Open user profile card / Cancel` 的底部确认。房主/管理员仍保留原有带禁麦、锁麦、邀请上麦等管理动作的底部菜单,避免误删管理能力。 - 已继续收窄语言房个人卡片前的“确认意义”弹层:当前用户在麦位上点击自己的头像时,也会直接打开自己的个人卡片,不再先弹出仅包含 `Leave the mic / Open user profile card / Cancel` 的底部菜单;同时个人卡片内的“离开麦位”入口已替换为新的 `leave` 视觉素材,和最新房间交互稿保持一致。 - 已继续微调语言房个人卡片与送礼 UI:个人卡片底部动作文案现已支持两行居中展示,避免 `Leave the mic` 这类英文按钮被硬截断;房间底部礼物入口也已切换为新的本地 `SVGA` 资源 `room_bottom_gift_button.svga`,保持房间底栏视觉和最新动效稿一致。