From 74e8162c1fd49d9a82fe73c8433e8489bfc30f57 Mon Sep 17 00:00:00 2001 From: roxy Date: Fri, 15 May 2026 23:59:47 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E9=A3=98=E7=AA=97bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/modules/user/me_page2.dart | 36 ++++++- lib/services/audio/rtc_manager.dart | 91 +++++++++++++++-- lib/services/audio/rtm_manager.dart | 47 +++++++-- lib/services/auth/user_profile_manager.dart | 57 ++++++++--- .../business_logic/models/res/login_res.dart | 97 ++++++++++++++++++- .../models/res/sc_room_rocket_status_res.dart | 41 +++++--- .../local/floating_screen_manager.dart | 25 +++-- .../sources/local/user_manager.dart | 2 +- .../rocket/room_rocket_floating_entry.dart | 41 ++++---- test/room_rocket_status_test.dart | 32 ++++++ test/user_profile_vip_colored_id_test.dart | 61 ++++++++++++ 11 files changed, 457 insertions(+), 73 deletions(-) diff --git a/lib/modules/user/me_page2.dart b/lib/modules/user/me_page2.dart index 2e1c519..c40b3ee 100644 --- a/lib/modules/user/me_page2.dart +++ b/lib/modules/user/me_page2.dart @@ -11,6 +11,7 @@ import 'package:yumi/modules/user/settings/settings_route.dart'; import 'package:yumi/modules/user/vip/vip_route.dart'; import 'package:yumi/modules/wallet/wallet_route.dart'; import 'package:yumi/services/auth/user_profile_manager.dart'; +import 'package:yumi/shared/business_logic/models/res/login_res.dart'; import 'package:yumi/shared/business_logic/models/res/sc_user_counter_res.dart'; import 'package:yumi/shared/business_logic/models/res/sc_vip_res.dart'; import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; @@ -117,11 +118,36 @@ class _MePage2State extends State { status.active == false || status.levelInt <= 0 ? '0' : status.levelInt.toString(); - if (profile.vipLevel == nextVipLevel) return; - - profileManager.syncCurrentUserProfile( + final nextProfile = socialChatProfileWithAvatarFrameResource( profile.copyWith(vipLevel: nextVipLevel), + _avatarFramePropsFromVipStatus(status), + expireTime: _expireTimeFromVipStatus(status), ); + profileManager.syncCurrentUserProfile(nextProfile); + } + + PropsResources? _avatarFramePropsFromVipStatus(SCVipStatusRes status) { + if (!status.isActive) { + return null; + } + final resource = status.avatarFrame; + return socialChatAvatarFrameProps( + id: resource?.resourceId, + name: resource?.name, + sourceUrl: resource?.sourceResourceUrl, + cover: resource?.coverUrl ?? resource?.cover, + code: "VIP_AVATAR_FRAME", + ); + } + + String? _expireTimeFromVipStatus(SCVipStatusRes status) { + final expireAt = status.expireAt?.trim(); + if (expireAt == null || expireAt.isEmpty) { + return null; + } + final normalized = + expireAt.contains("T") ? expireAt : expireAt.replaceFirst(" ", "T"); + return DateTime.tryParse(normalized)?.millisecondsSinceEpoch.toString(); } void _refreshAfterVipReturn(SocialChatUserProfileManager profileManager) { @@ -783,7 +809,9 @@ class _MePage2State extends State { ), ); } else if (kDebugMode) { - debugPrint('[MePage2] hide Manager Center because yumiManager=$isYumiManager'); + debugPrint( + '[MePage2] hide Manager Center because yumiManager=$isYumiManager', + ); } return _buildMenuCard(items); } diff --git a/lib/services/audio/rtc_manager.dart b/lib/services/audio/rtc_manager.dart index 42bfe74..11c90f0 100644 --- a/lib/services/audio/rtc_manager.dart +++ b/lib/services/audio/rtc_manager.dart @@ -297,6 +297,12 @@ class RealTimeCommunicationManager extends ChangeNotifier { static const Duration _roomRocketGiftRefreshDelay = Duration( milliseconds: 700, ); + static const List _roomRocketPostLaunchRefreshDelays = [ + Duration(milliseconds: 800), + Duration(seconds: 2), + Duration(seconds: 5), + Duration(seconds: 10), + ]; static const int _roomPasswordNotTrueErrorCode = 9005; static const Duration _micListPollingInterval = Duration(seconds: 2); @@ -337,6 +343,7 @@ class RealTimeCommunicationManager extends ChangeNotifier { Timer? _roomEntryEffectTimer; Timer? _roomRocketLaunchAnimationTimer; Timer? _roomRocketGiftRefreshTimer; + final List _roomRocketPostLaunchRefreshTimers = []; String? _lastRoomRocketLaunchNoticeKey; Timer? _roomRedPacketPresenceTimer; Future? _rtcEnginePrewarmTask; @@ -485,6 +492,7 @@ class RealTimeCommunicationManager extends ChangeNotifier { _roomRocketLaunchAnimationTimer = null; _roomRocketGiftRefreshTimer?.cancel(); _roomRocketGiftRefreshTimer = null; + _cancelRoomRocketPostLaunchStatusRefresh(); } _roomVisualEffectsEnabled = enabled; notifyListeners(); @@ -2112,14 +2120,22 @@ class RealTimeCommunicationManager extends ChangeNotifier { } final existingVipLevel = _positiveProfileVipLevelHint(currentProfile); - final vipLevel = - existingVipLevel ?? await _loadCurrentVipLevelForColoredId(); + final needsVipStatus = + existingVipLevel == null || currentProfile.getHeaddress() == null; + final vipStatus = needsVipStatus ? await _loadCurrentVipStatus() : null; + final vipLevel = existingVipLevel ?? _vipLevelFromStatus(vipStatus); if (vipLevel == null || vipLevel.isEmpty) { return; } - final updatedProfile = currentProfile.copyWith(vipLevel: vipLevel); - if (!_profileHasPositiveVipLevelHint(currentProfile)) { + final updatedProfile = socialChatProfileWithAvatarFrameResource( + currentProfile.copyWith(vipLevel: vipLevel), + _avatarFramePropsFromVipStatus(vipStatus), + expireTime: _expireTimeFromVipStatus(vipStatus), + ); + if (!_profileHasPositiveVipLevelHint(currentProfile) || + currentProfile.getHeaddress()?.sourceUrl != + updatedProfile.getHeaddress()?.sourceUrl) { AccountStorage().setCurrentUser( currentUser.copyWith(userProfile: updatedProfile), ); @@ -2162,18 +2178,18 @@ class RealTimeCommunicationManager extends ChangeNotifier { } } - Future _loadCurrentVipLevelForColoredId() async { + Future _loadCurrentVipStatus() async { final repository = SCVipRepositoryImp(); try { final home = await repository.vipHome(); - final homeVipLevel = _vipLevelFromStatus(home.state); - if (homeVipLevel != null) { - return homeVipLevel; + final state = home.state; + if (state != null && state.levelInt > 0) { + return state; } } catch (_) {} try { - return _vipLevelFromStatus(await repository.vipStatus()); + return repository.vipStatus(); } catch (_) { return null; } @@ -2190,6 +2206,30 @@ class RealTimeCommunicationManager extends ChangeNotifier { return level > 0 ? level.toString() : '0'; } + PropsResources? _avatarFramePropsFromVipStatus(SCVipStatusRes? status) { + if (status?.isActive != true) { + return null; + } + final resource = status?.avatarFrame; + return socialChatAvatarFrameProps( + id: resource?.resourceId, + name: resource?.name, + sourceUrl: resource?.sourceResourceUrl, + cover: resource?.coverUrl ?? resource?.cover, + code: "VIP_AVATAR_FRAME", + ); + } + + String? _expireTimeFromVipStatus(SCVipStatusRes? status) { + final expireAt = status?.expireAt?.trim(); + if (expireAt == null || expireAt.isEmpty) { + return null; + } + final normalized = + expireAt.contains("T") ? expireAt : expireAt.replaceFirst(" ", "T"); + return DateTime.tryParse(normalized)?.millisecondsSinceEpoch.toString(); + } + JoinRoomRes? _mergeKnownVipIntoCurrentRoom( JoinRoomRes? room, SocialChatUserProfile currentProfile, @@ -2764,6 +2804,7 @@ class RealTimeCommunicationManager extends ChangeNotifier { userProfileResolver: roomRocketRewardUserProfileForRecord, onClose: () { SmartDialog.dismiss(tag: _roomRocketRewardDialogTag); + scheduleRoomRocketPostLaunchStatusRefresh(roomId: roomId); if (recordIds.isNotEmpty) { unawaited( SCChatRoomRepository().ackRoomRocketRewardPopups( @@ -2817,6 +2858,32 @@ class RealTimeCommunicationManager extends ChangeNotifier { }); } + void scheduleRoomRocketPostLaunchStatusRefresh({String? roomId}) { + final resolvedRoomId = + (roomId ?? currenRoom?.roomProfile?.roomProfile?.id ?? "").trim(); + if (resolvedRoomId.isEmpty) { + return; + } + _cancelRoomRocketPostLaunchStatusRefresh(); + for (final delay in _roomRocketPostLaunchRefreshDelays) { + final timer = Timer(delay, () { + if (resolvedRoomId != + (currenRoom?.roomProfile?.roomProfile?.id ?? "").trim()) { + return; + } + unawaited(refreshRoomRocketStatus(roomId: resolvedRoomId)); + }); + _roomRocketPostLaunchRefreshTimers.add(timer); + } + } + + void _cancelRoomRocketPostLaunchStatusRefresh() { + for (final timer in _roomRocketPostLaunchRefreshTimers) { + timer.cancel(); + } + _roomRocketPostLaunchRefreshTimers.clear(); + } + void handleRoomRocketLaunchBroadcast( RoomRocketLaunchBroadcastMessage launch, ) { @@ -2845,11 +2912,13 @@ class RealTimeCommunicationManager extends ChangeNotifier { _roomRocketLaunchAnimationTimer = null; SmartDialog.dismiss(tag: _roomRocketPagLaunchDialogTag); unawaited(refreshRoomRocketStatus(roomId: launch.roomId)); + scheduleRoomRocketPostLaunchStatusRefresh(roomId: launch.roomId); }); return; } unawaited(refreshRoomRocketStatus(roomId: launch.roomId)); + scheduleRoomRocketPostLaunchStatusRefresh(roomId: launch.roomId); } String? _rocketLaunchAnimationUrlForLevel(int level) { @@ -2992,6 +3061,9 @@ class RealTimeCommunicationManager extends ChangeNotifier { _roomRocketLaunchAnimationTimer?.cancel(); _roomRocketLaunchAnimationTimer = null; unawaited(refreshRoomRocketStatus(roomId: launch.roomId)); + scheduleRoomRocketPostLaunchStatusRefresh( + roomId: launch.roomId, + ); }, ), ); @@ -3572,6 +3644,7 @@ class RealTimeCommunicationManager extends ChangeNotifier { _roomRocketLaunchAnimationTimer = null; _roomRocketGiftRefreshTimer?.cancel(); _roomRocketGiftRefreshTimer = null; + _cancelRoomRocketPostLaunchStatusRefresh(); _lastRoomRocketLaunchNoticeKey = null; _stopRoomRedPacketPresenceHeartbeat(); _previewRoomSeatCount = null; diff --git a/lib/services/audio/rtm_manager.dart b/lib/services/audio/rtm_manager.dart index e313b60..a2d302c 100644 --- a/lib/services/audio/rtm_manager.dart +++ b/lib/services/audio/rtm_manager.dart @@ -1946,7 +1946,10 @@ class RealTimeMessagingManager extends ChangeNotifier { unawaited(rtcProvider.refreshRoomRocketStatus(roomId: roomId)); } - void _handleVoiceRoomRocketLaunchBroadcast(dynamic payload) { + void _handleVoiceRoomRocketLaunchBroadcast( + dynamic payload, { + bool isRegionBroadcast = false, + }) { final launch = RoomRocketLaunchBroadcastMessage.fromJson( _broadcastPayloadMap(payload), ); @@ -1978,10 +1981,15 @@ class RealTimeMessagingManager extends ChangeNotifier { } } - if (isCurrentRoom && !isCurrentVisibleRoom) { + if (isCurrentRoom && !isCurrentVisibleRoom && !isRegionBroadcast) { return; } - OverlayManager().addMessage(_buildRoomRocketLaunchFloatingMessage(launch)); + OverlayManager().addMessage( + _buildRoomRocketLaunchFloatingMessage( + launch, + isRegionBroadcast: isRegionBroadcast, + ), + ); } void _handleVoiceRoomRocketRewardPopup(dynamic payload) { @@ -2095,6 +2103,7 @@ class RealTimeMessagingManager extends ChangeNotifier { userProfileResolver: userProfileResolver, onClose: () { SmartDialog.dismiss(tag: _roomRocketRewardDialogTag); + _scheduleRoomRocketPostLaunchStatusRefresh(roomId); if (recordIds.isNotEmpty) { unawaited( SCChatRoomRepository().ackRoomRocketRewardPopups(recordIds), @@ -2154,6 +2163,19 @@ class RealTimeMessagingManager extends ChangeNotifier { } } + void _scheduleRoomRocketPostLaunchStatusRefresh(String roomId) { + final currentContext = context; + if (currentContext == null || !currentContext.mounted) { + return; + } + try { + Provider.of( + currentContext, + listen: false, + ).scheduleRoomRocketPostLaunchStatusRefresh(roomId: roomId); + } catch (_) {} + } + List _roomRocketRewardRecordsFromPayload( Map payload, ) { @@ -2183,8 +2205,9 @@ class RealTimeMessagingManager extends ChangeNotifier { } SCFloatingMessage _buildRoomRocketLaunchFloatingMessage( - RoomRocketLaunchBroadcastMessage launch, - ) { + RoomRocketLaunchBroadcastMessage launch, { + bool isRegionBroadcast = false, + }) { return SCFloatingMessage( type: 3, roomId: launch.roomId, @@ -2203,6 +2226,8 @@ class RealTimeMessagingManager extends ChangeNotifier { fallbackUrl: launch.rocketIconUrl, giftId: launch.launchNo, durationSeconds: launch.durationSeconds, + broadcastScope: + isRegionBroadcast ? SCFloatingMessage.broadcastScopeRegion : '', priority: 1000, ); } @@ -2637,7 +2662,8 @@ class RealTimeMessagingManager extends ChangeNotifier { var fdata = data["data"]; final payload = _broadcastPayloadMap(fdata); final launchRoomId = _payloadRoomId(payload); - if (_isCurrentHiddenVoiceRoom(launchRoomId)) { + if (!isRegionBroadcastGroup && + _isCurrentHiddenVoiceRoom(launchRoomId)) { return; } final rocketIconUrl = _firstNonBlank([ @@ -2666,11 +2692,18 @@ class RealTimeMessagingManager extends ChangeNotifier { userId: _payloadText(payload["actualAccount"]), giftUrl: rocketIconUrl, fallbackUrl: rocketIconUrl, + broadcastScope: + isRegionBroadcastGroup + ? SCFloatingMessage.broadcastScopeRegion + : '', priority: 1000, ); OverlayManager().addMessage(msg); } else if (type == SCRoomMsgType.voiceRoomRocketLaunchBroadcast) { - _handleVoiceRoomRocketLaunchBroadcast(data["data"]); + _handleVoiceRoomRocketLaunchBroadcast( + data["data"], + isRegionBroadcast: isRegionBroadcastGroup, + ); } else if (type == SCRoomMsgType.voiceRoomRocketStatusUpdate) { _handleVoiceRoomRocketStatusUpdate(data["data"]); } else if (type == SCRoomMsgType.voiceRoomRocketRewardPopup) { diff --git a/lib/services/auth/user_profile_manager.dart b/lib/services/auth/user_profile_manager.dart index 1c9c287..65f79d6 100644 --- a/lib/services/auth/user_profile_manager.dart +++ b/lib/services/auth/user_profile_manager.dart @@ -165,20 +165,31 @@ class SocialChatUserProfileManager extends ChangeNotifier { } final profileVipLevel = profile.vipLevel?.trim(); - if (profileVipLevel != null && profileVipLevel.isNotEmpty) { + if (profileVipLevel != null && + profileVipLevel.isNotEmpty && + profile.getHeaddress() != null) { return profile; } + final vipStatus = await _loadCurrentVipStatus(); final currentVipLevel = currentUserProfile?.vipLevel?.trim(); final resolvedVipLevel = - currentVipLevel != null && currentVipLevel.isNotEmpty + profileVipLevel != null && profileVipLevel.isNotEmpty + ? profileVipLevel + : currentVipLevel != null && currentVipLevel.isNotEmpty ? currentVipLevel - : await _loadCurrentVipLevel(); - if (resolvedVipLevel == null || resolvedVipLevel.isEmpty) { - return profile; + : _vipLevelFromStatus(vipStatus); + + var nextProfile = profile; + if (resolvedVipLevel != null && resolvedVipLevel.isNotEmpty) { + nextProfile = nextProfile.copyWith(vipLevel: resolvedVipLevel); } - return profile.copyWith(vipLevel: resolvedVipLevel); + return socialChatProfileWithAvatarFrameResource( + nextProfile, + _avatarFramePropsFromVipStatus(vipStatus), + expireTime: _expireTimeFromVipStatus(vipStatus), + ); } bool _isCurrentUserId(String userId) { @@ -188,23 +199,47 @@ class SocialChatUserProfileManager extends ChangeNotifier { currentUserId == userId; } - Future _loadCurrentVipLevel() async { + Future _loadCurrentVipStatus() async { final repository = SCVipRepositoryImp(); try { final home = await repository.vipHome(); - final homeVipLevel = _vipLevelFromStatus(home.state); - if (homeVipLevel != null) { - return homeVipLevel; + final state = home.state; + if (state != null && state.levelInt > 0) { + return state; } } catch (_) {} try { - return _vipLevelFromStatus(await repository.vipStatus()); + return repository.vipStatus(); } catch (_) { return null; } } + PropsResources? _avatarFramePropsFromVipStatus(SCVipStatusRes? status) { + if (status?.isActive != true) { + return null; + } + final resource = status?.avatarFrame; + return socialChatAvatarFrameProps( + id: resource?.resourceId, + name: resource?.name, + sourceUrl: resource?.sourceResourceUrl, + cover: resource?.coverUrl ?? resource?.cover, + code: "VIP_AVATAR_FRAME", + ); + } + + String? _expireTimeFromVipStatus(SCVipStatusRes? status) { + final expireAt = status?.expireAt?.trim(); + if (expireAt == null || expireAt.isEmpty) { + return null; + } + final normalized = + expireAt.contains("T") ? expireAt : expireAt.replaceFirst(" ", "T"); + return DateTime.tryParse(normalized)?.millisecondsSinceEpoch.toString(); + } + String? _vipLevelFromStatus(SCVipStatusRes? status) { if (status == null) { return null; diff --git a/lib/shared/business_logic/models/res/login_res.dart b/lib/shared/business_logic/models/res/login_res.dart index ade1790..8be2346 100644 --- a/lib/shared/business_logic/models/res/login_res.dart +++ b/lib/shared/business_logic/models/res/login_res.dart @@ -492,8 +492,9 @@ class SocialChatUserProfile { PropsResources? getHeaddress() { PropsResources? pr; _useProps?.forEach((value) { - if (value.propsResources?.type == SCPropsType.AVATAR_FRAME.name) { - pr = value.propsResources; + final resource = value.propsResources; + if (socialChatIsAvatarFrameResource(resource)) { + pr = resource; } }); return pr; @@ -1249,6 +1250,9 @@ class PropsResources { } PropsResources.fromJson(dynamic json) { + if (json is! Map) { + return; + } _amount = json['amount']; _code = json['code']; _cover = json['cover'] ?? json['coverUrl']; @@ -1264,9 +1268,12 @@ class PropsResources { _roomNotOpenedUrl = json['roomNotOpenedUrl']; _roomOpenedUrl = json['roomOpenedUrl']; _roomSendCoverUrl = json['roomSendCoverUrl']; - _sourceUrl = json['sourceUrl'] ?? json['resourceUrl'] ?? json['url']; _sysOrigin = json['sysOrigin']; _type = json['type']; + _sourceUrl = + _isAvatarFrameType(_type) + ? json['sourceUrl'] + : json['sourceUrl'] ?? json['resourceUrl'] ?? json['url']; _updateTime = json['updateTime']; } @@ -1395,6 +1402,90 @@ class PropsResources { } } +bool socialChatIsAvatarFrameResource(PropsResources? resource) { + if (!_isAvatarFrameType(resource?.type)) { + return false; + } + final sourceUrl = resource?.sourceUrl?.trim() ?? ""; + if (sourceUrl.isEmpty) { + return false; + } + return !_looksLikeEntryEffectResource(sourceUrl) && + !_looksLikeEntryEffectResource(resource?.cover ?? ""); +} + +PropsResources? socialChatAvatarFrameProps({ + String? id, + String? name, + String? sourceUrl, + String? cover, + String? code, +}) { + final resource = PropsResources( + id: id, + name: name, + code: code, + sourceUrl: sourceUrl, + cover: cover, + type: SCPropsType.AVATAR_FRAME.name, + ); + return socialChatIsAvatarFrameResource(resource) ? resource : null; +} + +SocialChatUserProfile socialChatProfileWithAvatarFrameResource( + SocialChatUserProfile profile, + PropsResources? avatarFrame, { + String? expireTime, +}) { + if (!socialChatIsAvatarFrameResource(avatarFrame)) { + return profile; + } + + final currentHeaddress = profile.getHeaddress(); + if (currentHeaddress != null) { + return profile; + } + + final useProps = [ + ...?profile.useProps?.where( + (item) => !_isAvatarFrameType(item.propsResources?.type), + ), + UseProps( + expireTime: expireTime ?? "4102444800000", + propsResources: avatarFrame, + userId: profile.id, + ), + ]; + + return profile.copyWith(useProps: useProps); +} + +bool _isAvatarFrameType(String? type) { + return type?.trim().toUpperCase() == SCPropsType.AVATAR_FRAME.name; +} + +bool _looksLikeEntryEffectResource(String value) { + if (value.trim().isEmpty) { + return false; + } + final text = _decodeResourceText(value).toLowerCase(); + final entryResourcePattern = RegExp( + r'(^|[\/_\-.?=&])(?:entry|entrance|ride|mountain|mountains)([\/_\-.?=&]|$)', + ); + return entryResourcePattern.hasMatch(text) || + text.contains('entryeffect') || + text.contains('进场') || + text.contains('坐骑'); +} + +String _decodeResourceText(String value) { + try { + return Uri.decodeFull(value); + } catch (_) { + return value; + } +} + /// account : "" /// expiredTime : 0 diff --git a/lib/shared/business_logic/models/res/sc_room_rocket_status_res.dart b/lib/shared/business_logic/models/res/sc_room_rocket_status_res.dart index 9a02f92..7584e17 100644 --- a/lib/shared/business_logic/models/res/sc_room_rocket_status_res.dart +++ b/lib/shared/business_logic/models/res/sc_room_rocket_status_res.dart @@ -87,19 +87,24 @@ class SCRoomRocketStatusRes { } SCRoomRocketStatusRes.fromJson(dynamic json) { - _roomId = json['roomId']; - _date = json['date'] ?? json['dayKey']; - _roundNo = _intValue(json['roundNo']); - _displayRound = _boolValue(json['displayRound']); - _currentLevel = _intValue(json['currentLevel']); - _needEnergy = _intValue(json['needEnergy']); - _displayPercent = _numValue(json['displayPercent']); + _roomId = json['roomId'] ?? json['room_id']; + _date = json['date'] ?? json['dayKey'] ?? json['day_key']; + _roundNo = _intValue(json['roundNo'] ?? json['round_no']); + _displayRound = _boolValue(json['displayRound'] ?? json['display_round']); + _currentLevel = _intValue(json['currentLevel'] ?? json['current_level']); + _needEnergy = _intValue(json['needEnergy'] ?? json['need_energy']); + _displayPercent = _numValue( + json['displayPercent'] ?? json['display_percent'], + ); _shake = _boolValue(json['shake']); - _level = json['level'] ?? _currentLevel; - _levelName = json['levelName']; - _currentEnergy = _intValue(json['currentEnergy']); - _maxEnergy = _intValue(json['maxEnergy']) ?? _needEnergy; - _energyPercent = _numValue(json['energyPercent']) ?? _displayPercent; + _level = json['level'] ?? json['rocket_level'] ?? _currentLevel; + _levelName = json['levelName'] ?? json['level_name']; + _currentEnergy = _intValue(json['currentEnergy'] ?? json['current_energy']); + _maxEnergy = + _intValue(json['maxEnergy'] ?? json['max_energy']) ?? _needEnergy; + _energyPercent = + _numValue(json['energyPercent'] ?? json['energy_percent']) ?? + _displayPercent; _status = json['status']; _statusDesc = json['statusDesc']; _levels = _levelList(json['levels']); @@ -124,6 +129,8 @@ class SCRoomRocketStatusRes { _firstPayload([ json['rocketKings'], json['rocket_kings'], + json['rocketKingUsers'], + json['rocket_king_users'], json['kings'], json['podium'], ]), @@ -146,18 +153,28 @@ class SCRoomRocketStatusRes { final resetRemainingMillis = _intValue( json['resetRemainingMillis'] ?? json['resetRemainMillis'] ?? + json['reset_remaining_millis'] ?? + json['reset_remain_millis'] ?? json['remainingMillis'] ?? + json['remaining_millis'] ?? json['remainMillis'], ); _resetRemainingSeconds = _intValue( json['resetRemainingSeconds'] ?? json['resetRemainSeconds'] ?? + json['reset_remaining_seconds'] ?? + json['reset_remain_seconds'] ?? json['remainingSeconds'] ?? + json['remaining_seconds'] ?? json['remainSeconds'] ?? + json['remain_seconds'] ?? json['countdownSeconds'] ?? + json['countdown_seconds'] ?? json['resetCountdownSeconds'] ?? + json['reset_countdown_seconds'] ?? json['secondsToReset'] ?? + json['seconds_to_reset'] ?? json['resetSeconds'], ) ?? (resetRemainingMillis == null 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 8d374cf..9c165fb 100644 --- a/lib/shared/data_sources/sources/local/floating_screen_manager.dart +++ b/lib/shared/data_sources/sources/local/floating_screen_manager.dart @@ -712,11 +712,20 @@ class OverlayManager { return message.type == 4 && message.isRegionBroadcast; } + bool _isRegionRoomRocketMessage(SCFloatingMessage message) { + return message.type == 3 && message.isRegionBroadcast; + } + + bool _isCrossRoomRegionFloatingMessage(SCFloatingMessage message) { + return _isRegionRedPacketMessage(message) || + _isRegionRoomRocketMessage(message); + } + bool _shouldDeferRegionRedPacketMessage( BuildContext context, SCFloatingMessage message, ) { - if (!_isRegionRedPacketMessage(message) || + if (!_isCrossRoomRegionFloatingMessage(message) || SCGlobalConfig.isFloatingBroadcastClosed) { return false; } @@ -828,20 +837,21 @@ class OverlayManager { messageRoomId.isNotEmpty && currentRoomId == messageRoomId; if (isCurrentVisibleRoom) { - // 区域红包在当前房间仍然需要展示;区域礼物当前房间由 RTM 层跳过,避免和房间 IM 礼物飘屏重复。 - return message.type == 4; + // 区域红包/火箭在当前房间仍然需要展示;区域礼物当前房间由 RTM 层跳过,避免和房间 IM 礼物飘屏重复。 + return message.type == 4 || message.type == 3; } final isCurrentRoomMessage = currentRoomId.isNotEmpty && messageRoomId.isNotEmpty && currentRoomId == messageRoomId; - if (message.type == 4 && isCurrentRoomMessage) { + if ((message.type == 4 || message.type == 3) && isCurrentRoomMessage) { return false; } - if (message.type == 4 && rtcProvider.shouldShowRoomVisualEffects) { + if ((message.type == 4 || message.type == 3) && + rtcProvider.shouldShowRoomVisualEffects) { return true; } - if (message.type == 4) { + if (message.type == 4 || message.type == 3) { return _homeRootTabsVisible; } if (SCGlobalConfig.isFloatingBroadcastRoomOnly) { @@ -860,7 +870,8 @@ class OverlayManager { while (_messageQueue.isNotEmpty) { final message = _messageQueue.removeFirst(); - if (!message.isRegionBroadcast || _isRegionRedPacketMessage(message)) { + if (!message.isRegionBroadcast || + _isCrossRoomRegionFloatingMessage(message)) { newQueue.add(message); } } diff --git a/lib/shared/data_sources/sources/local/user_manager.dart b/lib/shared/data_sources/sources/local/user_manager.dart index a81bb01..4b79300 100644 --- a/lib/shared/data_sources/sources/local/user_manager.dart +++ b/lib/shared/data_sources/sources/local/user_manager.dart @@ -60,7 +60,7 @@ class AccountStorage { PropsResources? getHeaddress() { PropsResources? pr; _currentUser?.userProfile?.useProps?.forEach((value) { - if (value.propsResources?.type == SCPropsType.AVATAR_FRAME.name) { + if (socialChatIsAvatarFrameResource(value.propsResources)) { ///判断有没有过期 if (int.parse(value.expireTime ?? "0") > DateTime.now().millisecondsSinceEpoch) { diff --git a/lib/ui_kit/widgets/room/rocket/room_rocket_floating_entry.dart b/lib/ui_kit/widgets/room/rocket/room_rocket_floating_entry.dart index 721a850..e718b09 100644 --- a/lib/ui_kit/widgets/room/rocket/room_rocket_floating_entry.dart +++ b/lib/ui_kit/widgets/room/rocket/room_rocket_floating_entry.dart @@ -198,17 +198,10 @@ class _RoomRocketEntryButtonState extends State<_RoomRocketEntryButton> SizedBox( width: 42.w, height: 8.w, - child: - widget.progressUrl.trim().isEmpty - ? _RocketEntryProgressBar(percent: widget.progressPercent) - : ClipRRect( - borderRadius: BorderRadius.circular(5.w), - child: _RocketEntryImage( - source: widget.progressUrl, - loading: false, - fit: BoxFit.fill, - ), - ), + child: _RocketEntryProgressBar( + percent: widget.progressPercent, + imageUrl: widget.progressUrl, + ), ), ], ), @@ -218,13 +211,29 @@ class _RoomRocketEntryButtonState extends State<_RoomRocketEntryButton> } class _RocketEntryProgressBar extends StatelessWidget { - const _RocketEntryProgressBar({required this.percent}); + const _RocketEntryProgressBar({required this.percent, this.imageUrl = ''}); final double percent; + final String imageUrl; @override Widget build(BuildContext context) { final value = (percent / 100).clamp(0.0, 1.0).toDouble(); + final progressImageUrl = imageUrl.trim(); + final fill = + progressImageUrl.isEmpty + ? const DecoratedBox( + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [Color(0xFFB8FFE6), Color(0xFF1BD091)], + ), + ), + ) + : _RocketEntryImage( + source: progressImageUrl, + loading: false, + fit: BoxFit.fill, + ); return DecoratedBox( decoration: BoxDecoration( borderRadius: BorderRadius.circular(5.w), @@ -241,13 +250,7 @@ class _RocketEntryProgressBar extends StatelessWidget { child: FractionallySizedBox( widthFactor: value, heightFactor: 1, - child: const DecoratedBox( - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [Color(0xFFB8FFE6), Color(0xFF1BD091)], - ), - ), - ), + child: fill, ), ), ), diff --git a/test/room_rocket_status_test.dart b/test/room_rocket_status_test.dart index de4cabb..f6790f3 100644 --- a/test/room_rocket_status_test.dart +++ b/test/room_rocket_status_test.dart @@ -90,6 +90,38 @@ void main() { expect(status.rocketKingsForLevel(2), hasLength(3)); }); + test('parses refreshed rocket status from snake case payloads', () { + final status = SCRoomRocketStatusRes.fromJson({ + 'room_id': 'room-1', + 'current_level': 2, + 'display_percent': 0, + 'current_energy': 0, + 'need_energy': 200, + 'round_no': 4, + 'levels': [ + { + 'level': 1, + 'rocket_icon_url': 'icon-1', + 'progress_bar_url': 'progress-1', + }, + { + 'level': 2, + 'rocket_icon_url': 'icon-2', + 'progress_bar_url': 'progress-2', + }, + ], + }); + final levels = RoomRocketApiMapper.levelsFromStatus(status); + + expect(status.roomId, 'room-1'); + expect(status.currentLevel, 2); + expect(status.displayPercent, 0); + expect(status.needEnergy, 200); + expect(status.roundNo, 4); + expect(levels?.last.imageUrl, 'icon-2'); + expect(levels?.last.progressImageUrl, 'progress-2'); + }); + test( 'uses top-level reward preview for the response level instead of current level', () { diff --git a/test/user_profile_vip_colored_id_test.dart b/test/user_profile_vip_colored_id_test.dart index 667557f..84a7a5f 100644 --- a/test/user_profile_vip_colored_id_test.dart +++ b/test/user_profile_vip_colored_id_test.dart @@ -6,6 +6,7 @@ import 'package:yumi/shared/business_logic/models/res/room_gift_rank_res.dart'; import 'package:yumi/shared/business_logic/models/res/room_member_res.dart'; import 'package:yumi/shared/business_logic/models/res/room_res.dart'; import 'package:yumi/shared/business_logic/models/res/room_user_card_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_vip_res.dart'; void main() { group('VIP colored ID', () { @@ -181,5 +182,65 @@ void main() { expect(mic.user?.vipLevelForColoredId, 5); expect(mic.user?.shouldShowColoredSpecialIdText(), isTrue); }); + + test('keeps avatar frame separate from entry effect resources', () { + final profile = SocialChatUserProfile.fromJson({ + 'account': '10001', + 'useProps': [ + { + 'propsResources': { + 'type': 'AVATAR_FRAME', + 'url': 'https://example.test/entry/effect.svga', + }, + }, + { + 'propsResources': { + 'type': 'RIDE', + 'sourceUrl': 'https://example.test/ride/effect.svga', + }, + }, + ], + }); + + expect(profile.getHeaddress(), isNull); + }); + + test('accepts avatar frame from explicit sourceUrl only', () { + final profile = SocialChatUserProfile.fromJson({ + 'account': '10001', + 'useProps': [ + { + 'propsResources': { + 'type': 'AVATAR_FRAME', + 'sourceUrl': 'https://example.test/avatar/frame.svga', + }, + }, + ], + }); + + expect(profile.getHeaddress()?.sourceUrl, contains('avatar/frame.svga')); + }); + + test('merges active VIP avatar frame into profile props', () { + final profile = SocialChatUserProfile.fromJson({'id': '10001'}); + final vipFrame = SCVipResourceRes.fromJson({ + 'resourceId': 'vip-frame-1', + 'url': 'https://example.test/vip/avatar_frame.svga', + 'coverUrl': 'https://example.test/vip/avatar_frame.png', + }); + + final merged = socialChatProfileWithAvatarFrameResource( + profile, + socialChatAvatarFrameProps( + id: vipFrame.resourceId, + sourceUrl: vipFrame.sourceResourceUrl, + cover: vipFrame.coverUrl, + code: 'VIP_AVATAR_FRAME', + ), + ); + + expect(merged.getHeaddress()?.sourceUrl, contains('avatar_frame.svga')); + expect(merged.getHeaddress()?.cover, contains('avatar_frame.png')); + }); }); }