diff --git a/lib/modules/user/profile/person_detail_page.dart b/lib/modules/user/profile/person_detail_page.dart index 7ab0ff1..eca4385 100644 --- a/lib/modules/user/profile/person_detail_page.dart +++ b/lib/modules/user/profile/person_detail_page.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:async'; import 'dart:ui'; import 'package:carousel_slider/carousel_slider.dart'; import 'package:flutter/material.dart'; @@ -9,6 +10,7 @@ import 'package:yumi/ui_kit/components/sc_debounce_widget.dart'; import 'package:yumi/ui_kit/components/text/sc_text.dart'; import 'package:yumi/shared/tools/sc_user_utils.dart'; import 'package:yumi/modules/index/main_route.dart'; +import 'package:yumi/main.dart' show routeObserver; import 'package:provider/provider.dart'; import 'package:tencent_cloud_chat_sdk/enum/conversation_type.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart'; @@ -61,7 +63,7 @@ class PersonDetailPage extends StatefulWidget { _PersonDetailPageState createState() => _PersonDetailPageState(); } -class _PersonDetailPageState extends State { +class _PersonDetailPageState extends State with RouteAware { static const Color _profileBg = Color(0xff072121); static const Color _cardBg = Color(0xff08251E); static const Color _profileBorder = Color(0xffB2FBCC); @@ -86,6 +88,7 @@ class _PersonDetailPageState extends State { // 添加滚动控制器 final ScrollController _scrollController = ScrollController(); double _opacity = 0.0; + PageRoute? _routeObserverRoute; @override void initState() { @@ -155,6 +158,52 @@ class _PersonDetailPageState extends State { _loadGiftWall(); } + @override + void didChangeDependencies() { + super.didChangeDependencies(); + final route = ModalRoute.of(context); + if (route is PageRoute && _routeObserverRoute != route) { + if (_routeObserverRoute != null) { + routeObserver.unsubscribe(this); + } + _routeObserverRoute = route; + routeObserver.subscribe(this, route); + } + } + + @override + void didPopNext() { + _refreshProfileRelationsAfterResume(); + } + + void _refreshProfileRelationsAfterResume() { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) { + return; + } + final ref = Provider.of( + context, + listen: false, + ); + final profile = ref.userProfile; + final loadedUserId = profile?.id?.trim() ?? ""; + final targetUserId = widget.tageId.trim(); + if (targetUserId.isEmpty) { + return; + } + final hasRelationCards = + (profile?.cpList?.isNotEmpty ?? false) || + (profile?.closeFriendList?.isNotEmpty ?? false); + if (loadedUserId.isEmpty || + loadedUserId != targetUserId || + !hasRelationCards) { + unawaited(ref.reloadUserInfoById(targetUserId)); + return; + } + unawaited(ref.refreshLoadedCpProfiles()); + }); + } + void userCounter(String userId) async { try { var userCounterList = await SCAccountRepository().userCounter(userId); @@ -317,6 +366,8 @@ class _PersonDetailPageState extends State { @override void dispose() { + routeObserver.unsubscribe(this); + _routeObserverRoute = null; _scrollController.dispose(); super.dispose(); } diff --git a/lib/services/audio/rtc_manager.dart b/lib/services/audio/rtc_manager.dart index c74cccc..4a19454 100644 --- a/lib/services/audio/rtc_manager.dart +++ b/lib/services/audio/rtc_manager.dart @@ -9,6 +9,7 @@ import 'package:yumi/app/constants/sc_room_msg_type.dart'; import 'package:yumi/shared/tools/sc_permission_utils.dart'; import 'package:yumi/shared/tools/sc_room_utils.dart'; import 'package:yumi/shared/tools/sc_room_top_layer_guard.dart'; +import 'package:yumi/shared/tools/sc_cp_relation_level_utils.dart'; import 'package:yumi/shared/tools/sc_cp_relation_notice_utils.dart'; import 'package:yumi/shared/data_sources/models/message/room_rocket_launch_broadcast_message.dart'; import 'package:yumi/shared/data_sources/models/message/sc_floating_message.dart'; @@ -1710,6 +1711,44 @@ class RealTimeCommunicationManager extends ChangeNotifier { ); } + void refreshRoomMicRelationProfilesForUsers(Set userIds) { + final roomId = (currenRoom?.roomProfile?.roomProfile?.id ?? "").trim(); + if (roomId.isEmpty || roomWheatMap.isEmpty) { + return; + } + final normalizedUserIds = + userIds.map((userId) => userId.trim()).where((userId) { + return userId.isNotEmpty; + }).toSet(); + final refreshAll = normalizedUserIds.isEmpty; + if (refreshAll) { + _roomMicRelationCache.clear(); + _roomMicRelationLoadingUserIds.clear(); + } else { + _roomMicRelationCache.removeWhere((userId, _) { + return normalizedUserIds.contains(userId); + }); + _roomMicRelationLoadingUserIds.removeWhere(normalizedUserIds.contains); + } + + final queuedUserIds = {}; + for (final mic in roomWheatMap.values) { + final user = mic.user; + final userId = _roomMicRelationUserId(user); + if (user == null || userId.isEmpty) { + continue; + } + if (!refreshAll && !normalizedUserIds.contains(userId)) { + continue; + } + if (!queuedUserIds.add(userId) || + !_roomMicRelationLoadingUserIds.add(userId)) { + continue; + } + unawaited(_hydrateRoomMicRelationProfile(roomId, userId, user)); + } + } + bool _mergeCachedRoomMicRelationProfiles() { final now = DateTime.now(); _roomMicRelationCache.removeWhere( @@ -1806,7 +1845,11 @@ class RealTimeCommunicationManager extends ChangeNotifier { relationType: normalizedType, cpValue: pair.cpValue ?? cabin?.cpVal?.toDouble(), levelText: - pair.levelText ?? _roomMicRelationLevelTextFromCabin(cabin), + scCpRelationLevelTextFromCabin( + cabin, + fallbackText: pair.levelText, + ) ?? + _roomMicRelationLevelTextFromCabin(cabin), ); if (_isRoomMicRelationLocallyDismissed(enrichedPair)) { return; @@ -1954,6 +1997,10 @@ class RealTimeCommunicationManager extends ChangeNotifier { pair.cpAccount?.trim() ?? "", pair.relationType?.trim() ?? "", pair.status?.trim() ?? "", + pair.days?.trim() ?? "", + pair.levelText?.trim() ?? "", + pair.cpValue?.toString() ?? "", + pair.dismissCost?.toString() ?? "", ].join("|"); } @@ -4101,6 +4148,67 @@ class RealTimeCommunicationManager extends ChangeNotifier { _roomRocketPostLaunchRefreshTimers.clear(); } + void advanceRoomRocketStatusAfterLaunch( + RoomRocketLaunchBroadcastMessage launch, + ) { + final currentRoomId = + (currenRoom?.roomProfile?.roomProfile?.id ?? "").trim(); + if (!launch.isValid || launch.roomId != currentRoomId) { + return; + } + final currentStatus = roomRocketStatus; + if (currentStatus == null || !currentStatus.isEnabled) { + return; + } + final nextLevel = _nextRoomRocketLevelAfterLaunch(launch.safeLevel); + final nextLevelInfo = _roomRocketLevelFor(nextLevel); + final update = SCRoomRocketStatusRes( + roomId: launch.roomId, + level: nextLevel, + currentLevel: nextLevel, + currentEnergy: 0, + maxEnergy: nextLevelInfo?.needEnergy, + needEnergy: nextLevelInfo?.needEnergy, + energyPercent: 0, + displayPercent: 0, + configured: currentStatus.configured, + enabled: currentStatus.enabled, + levels: currentStatus.levels, + ); + roomRocketStatus = currentStatus.mergeRealtimeUpdate( + update, + roomId: launch.roomId, + ); + notifyListeners(); + } + + int _nextRoomRocketLevelAfterLaunch(int launchedLevel) { + final normalizedLevel = launchedLevel.clamp(1, 99).toInt(); + final levels = [ + ...(roomRocketStatus?.levels ?? const []), + ]..sort((a, b) { + final sortCompare = a.sort.compareTo(b.sort); + return sortCompare != 0 ? sortCompare : a.level.compareTo(b.level); + }); + for (final level in levels) { + if (level.enabled && level.level > normalizedLevel) { + return level.level.clamp(1, 99).toInt(); + } + } + return normalizedLevel; + } + + SCRoomRocketLevelRes? _roomRocketLevelFor(int level) { + final normalizedLevel = level.clamp(1, 99).toInt(); + for (final item + in roomRocketStatus?.levels ?? const []) { + if (item.level == normalizedLevel) { + return item; + } + } + return null; + } + void handleRoomRocketLaunchBroadcast( RoomRocketLaunchBroadcastMessage launch, ) { diff --git a/lib/services/audio/rtm_manager.dart b/lib/services/audio/rtm_manager.dart index c1cb34a..f8933d5 100644 --- a/lib/services/audio/rtm_manager.dart +++ b/lib/services/audio/rtm_manager.dart @@ -14,6 +14,7 @@ import 'package:yumi/shared/tools/sc_message_utils.dart'; import 'package:yumi/shared/tools/sc_path_utils.dart'; import 'package:yumi/shared/tools/sc_room_top_layer_guard.dart'; import 'package:yumi/shared/tools/sc_room_utils.dart'; +import 'package:yumi/shared/tools/sc_cp_gift_relation_utils.dart'; import 'package:yumi/shared/tools/sc_cp_relation_broadcast_assets.dart'; import 'package:yumi/shared/tools/sc_cp_relation_notice_utils.dart'; import 'package:yumi/shared/tools/sc_room_rocket_reward_popup_ack.dart'; @@ -134,6 +135,74 @@ class _PendingRoomRocketRewardPopup { final List initialRoomRecords; } +bool roomRocketRewardPopupTargetsCurrentUser({ + required Map payload, + required Set currentUserKeys, +}) { + final audienceUserIds = _roomRocketRewardPopupAudienceUserIds(payload); + if (audienceUserIds == null) { + return true; + } + if (audienceUserIds.isEmpty || currentUserKeys.isEmpty) { + return false; + } + return currentUserKeys.any(audienceUserIds.contains); +} + +Set? _roomRocketRewardPopupAudienceUserIds( + Map payload, +) { + for (final key in const [ + 'popupUserIds', + 'popup_user_ids', + 'notifyUserIds', + 'notify_user_ids', + 'audienceUserIds', + 'audience_user_ids', + 'visibleUserIds', + 'visible_user_ids', + ]) { + if (payload.containsKey(key)) { + return _roomRocketPayloadStringSet(payload[key]); + } + } + return null; +} + +Set _roomRocketPayloadStringSet(dynamic value) { + if (value == null) { + return {}; + } + if (value is Iterable && value is! String) { + return value + .map((item) => item?.toString().trim() ?? '') + .where((item) => item.isNotEmpty) + .toSet(); + } + if (value is Map) { + return value.values + .map((item) => item?.toString().trim() ?? '') + .where((item) => item.isNotEmpty) + .toSet(); + } + final raw = value.toString().trim(); + if (raw.isEmpty) { + return {}; + } + if (raw.startsWith('[') || raw.startsWith('{')) { + try { + return _roomRocketPayloadStringSet(jsonDecode(raw)); + } catch (_) { + return {}; + } + } + return raw + .split(',') + .map((item) => item.trim()) + .where((item) => item.isNotEmpty) + .toSet(); +} + class RealTimeMessagingManager extends ChangeNotifier { static const String _roomRocketRewardDialogTag = 'showRoomRocketRewardDialog'; @@ -146,11 +215,18 @@ class RealTimeMessagingManager extends ChangeNotifier { static const int _luckyGiftBurstMergeWindowMs = 1800; static const int _luckyGiftBurstRecentEventTtlMs = 5000; static const int _luckyGiftBurstPlaybackWatchdogMs = 8000; + static const int _cpRelationGiftRefreshMinIntervalMs = 800; + static const List _cpRelationGiftRefreshDelays = [ + Duration(milliseconds: 300), + Duration(milliseconds: 1500), + Duration(milliseconds: 3500), + ]; static const int _roomRocketLaunchRecentTtlMs = 90000; static const int _roomRocketLaunchLooseRecentTtlMs = 12000; static const int _roomRocketRegionBroadcastRecentTtlMs = 90000; static const int _roomRocketRewardPendingTtlMs = 3600000; static const int _roomRocketRewardProbeTtlMs = 3600000; + static const int _roomRocketEmptyRewardPopupRecentTtlMs = 120000; static const int _roomRocketLaunchCountdownRetainMs = 10000; static const int _roomRocketLaunchEffectConsumedTtlMs = 120000; static const Duration _createRoomGroupTimeout = Duration(seconds: 12); @@ -296,12 +372,15 @@ class RealTimeMessagingManager extends ChangeNotifier { final Map _roomRocketRewardHoldUntilTimes = {}; final Map _roomRocketRewardHoldTimers = {}; final Set _roomRocketRewardPopupLoadingRoomIds = {}; + final Map _shownRoomRocketEmptyRewardPopupTimes = + {}; Timer? _roomRocketRewardPopupRetryTimer; Timer? _roomRocketRoomReadyRewardRetryTimer; final Set _shownCpInviteMessageKeys = {}; final Set _shownCpInviteResultMessageKeys = {}; final Set _shownCpInviteRejectedDialogKeys = {}; final Map _recentCpRelationNoticeTimes = {}; + int _lastCpRelationGiftRefreshAt = 0; int get currentLuckGiftBurstPlaybackToken => _luckGiftPushPlaybackToken; String? roomRedPacketBroadcastGroupId; String? roomRedPacketBroadcastRegionCode; @@ -2693,6 +2772,7 @@ class RealTimeMessagingManager extends ChangeNotifier { addMsg(Msg msg) { _scheduleRoomRocketStatusRefreshForGiftMessage(msg); + _scheduleCpRelationRefreshForGiftMessage(msg); final mergedGiftMsg = _mergeGiftMessageIfNeeded(msg); if (mergedGiftMsg != null) { msgAllListener?.call(mergedGiftMsg); @@ -3083,6 +3163,64 @@ class RealTimeMessagingManager extends ChangeNotifier { rtcProvider.scheduleRoomRocketStatusRefreshForGift(roomId: currentRoomId); } + void _scheduleCpRelationRefreshForGiftMessage(Msg msg) { + if (!_isCpRelationGiftMessage(msg)) { + return; + } + final now = DateTime.now().millisecondsSinceEpoch; + if (now - _lastCpRelationGiftRefreshAt < + _cpRelationGiftRefreshMinIntervalMs) { + return; + } + _lastCpRelationGiftRefreshAt = now; + final userIds = Set.unmodifiable(_cpRelationGiftUserIds(msg)); + for (final delay in _cpRelationGiftRefreshDelays) { + unawaited( + Future.delayed(delay, () { + _refreshCpRelationDataAfterGift(userIds); + }), + ); + } + } + + bool _isCpRelationGiftMessage(Msg msg) { + if (msg.type != SCRoomMsgType.gift && + msg.type != SCRoomMsgType.luckGiftAnimOther) { + return false; + } + return scIsCpGift(msg.gift); + } + + Set _cpRelationGiftUserIds(Msg msg) { + return { + msg.user?.id?.trim() ?? '', + msg.toUser?.id?.trim() ?? '', + ...?msg.targetUserIds?.map((userId) => userId.trim()), + }..removeWhere((userId) => userId.isEmpty); + } + + void _refreshCpRelationDataAfterGift(Set userIds) { + final currentContext = + context?.mounted == true ? context : navigatorKey.currentState?.context; + if (currentContext == null || !currentContext.mounted) { + return; + } + try { + unawaited( + Provider.of( + currentContext, + listen: false, + ).refreshLoadedCpProfiles(), + ); + } catch (_) {} + try { + Provider.of( + currentContext, + listen: false, + ).refreshRoomMicRelationProfilesForUsers(userIds); + } catch (_) {} + } + void _cacheRoomRedPacketMessage(Msg msg) { try { final decoded = jsonDecode(msg.msg ?? ''); @@ -3865,6 +4003,13 @@ class RealTimeMessagingManager extends ChangeNotifier { return null; } + bool _isRoomRocketStatusUpdateType(dynamic type) { + final value = type?.toString().trim(); + return value == SCRoomMsgType.voiceRoomRocketStatusUpdate || + value == SCRoomMsgType.rocketEnergyUpdate || + value == 'ROCKET_ENERGY_UPDATE'; + } + void _handleVoiceRoomRocketStatusUpdate(dynamic payload) { final data = _broadcastPayloadMap(payload); final roomId = _payloadRoomId(data); @@ -4006,6 +4151,7 @@ class RealTimeMessagingManager extends ChangeNotifier { _scheduleRoomRocketRewardPopupAfterHold( resolvedRoomId, initialRoomRecords: initialRoomRecords, + allowEmptyPopup: true, ); return; } @@ -4016,89 +4162,21 @@ class RealTimeMessagingManager extends ChangeNotifier { _showRoomRocketRewardPopupDialog( resolvedRoomId, initialRoomRecords: initialRoomRecords, + allowEmptyPopup: true, ), ); } bool _isCurrentUserInRoomRocketRewardPopup(Map payload) { - final userIds = _roomRocketRewardPopupUserIds(payload); - if (userIds == null) { - return true; - } - if (userIds.isEmpty) { - return false; - } final profile = AccountStorage().getCurrentUser()?.userProfile; final currentKeys = { profile?.id?.trim() ?? '', profile?.account?.trim() ?? '', }..removeWhere((item) => item.isEmpty); - if (currentKeys.isEmpty) { - return false; - } - return currentKeys.any(userIds.contains); - } - - Set? _roomRocketRewardPopupUserIds(Map payload) { - for (final key in const [ - 'userIds', - 'user_ids', - 'targetUserIds', - 'target_user_ids', - 'winnerUserIds', - 'winner_user_ids', - 'userId', - 'user_id', - 'winnerUserId', - 'winner_user_id', - 'rewardUserId', - 'reward_user_id', - 'receiverUserId', - 'receiver_user_id', - 'targetUserId', - 'target_user_id', - 'toUserId', - 'to_user_id', - ]) { - if (payload.containsKey(key)) { - return _payloadStringSet(payload[key]); - } - } - return null; - } - - Set _payloadStringSet(dynamic value) { - if (value == null) { - return {}; - } - if (value is Iterable && value is! String) { - return value - .map((item) => item?.toString().trim() ?? '') - .where((item) => item.isNotEmpty) - .toSet(); - } - if (value is Map) { - return value.values - .map((item) => item?.toString().trim() ?? '') - .where((item) => item.isNotEmpty) - .toSet(); - } - final raw = value.toString().trim(); - if (raw.isEmpty) { - return {}; - } - if (raw.startsWith('[') || raw.startsWith('{')) { - try { - return _payloadStringSet(jsonDecode(raw)); - } catch (_) { - return {}; - } - } - return raw - .split(',') - .map((item) => item.trim()) - .where((item) => item.isNotEmpty) - .toSet(); + return roomRocketRewardPopupTargetsCurrentUser( + payload: payload, + currentUserKeys: currentKeys, + ); } void queryVoiceRoomRocketRewardPopups(String roomId) { @@ -4138,6 +4216,7 @@ class RealTimeMessagingManager extends ChangeNotifier { final pendingAfterResume = _pendingRoomRocketRewardPopups[normalizedRoomId]; final hasProbeAfterResume = _recentRoomRocketRewardProbeTimes[normalizedRoomId] != null; + final allowEmptyPopup = pendingAfterResume != null || hasProbeAfterResume; if (pendingAfterResume == null && !hasProbeAfterResume && !forceQuery) { return; } @@ -4162,6 +4241,7 @@ class RealTimeMessagingManager extends ChangeNotifier { initialRoomRecords: pending?.initialRoomRecords ?? const [], + allowEmptyPopup: allowEmptyPopup, ); return; } @@ -4171,6 +4251,7 @@ class RealTimeMessagingManager extends ChangeNotifier { initialRoomRecords: pendingAfterResume?.initialRoomRecords ?? const [], + allowEmptyPopup: allowEmptyPopup, ), ); } @@ -4338,6 +4419,7 @@ class RealTimeMessagingManager extends ChangeNotifier { String roomId, { List initialRoomRecords = const [], + bool allowEmptyPopup = false, }) { final normalizedRoomId = roomId.trim(); if (normalizedRoomId.isEmpty) { @@ -4356,7 +4438,10 @@ class RealTimeMessagingManager extends ChangeNotifier { () { _roomRocketRewardHoldTimers.remove(normalizedRoomId); if (_isRoomRocketRewardPopupHeld(normalizedRoomId)) { - _scheduleRoomRocketRewardPopupAfterHold(normalizedRoomId); + _scheduleRoomRocketRewardPopupAfterHold( + normalizedRoomId, + allowEmptyPopup: allowEmptyPopup, + ); return; } if (!_isCurrentVisibleVoiceRoom(normalizedRoomId)) { @@ -4371,6 +4456,7 @@ class RealTimeMessagingManager extends ChangeNotifier { initialRoomRecords: pending?.initialRoomRecords ?? const [], + allowEmptyPopup: allowEmptyPopup, ), ); }, @@ -4381,6 +4467,55 @@ class RealTimeMessagingManager extends ChangeNotifier { ); } + bool _markRoomRocketEmptyRewardPopupForShow( + String roomId, + List roomRecords, + ) { + final normalizedRoomId = roomId.trim(); + if (normalizedRoomId.isEmpty) { + return false; + } + final now = DateTime.now().millisecondsSinceEpoch; + _pruneRoomRocketRewardFallbacks(now); + final key = _roomRocketEmptyRewardPopupKey(normalizedRoomId, roomRecords); + if (_shownRoomRocketEmptyRewardPopupTimes.containsKey(key)) { + return false; + } + _shownRoomRocketEmptyRewardPopupTimes[key] = now; + return true; + } + + String _roomRocketEmptyRewardPopupKey( + String roomId, + List roomRecords, + ) { + final launchNos = + roomRecords + .map((record) => record.launchNo.trim()) + .where((launchNo) => launchNo.isNotEmpty) + .toSet() + .toList() + ..sort(); + if (launchNos.isNotEmpty) { + return 'room_rocket_empty_reward|$roomId|launch:${launchNos.join(",")}'; + } + final roundKeys = + roomRecords + .map((record) { + final level = record.level > 0 ? record.level.toString() : ''; + final round = record.roundNo > 0 ? record.roundNo.toString() : ''; + return [level, round].where((item) => item.isNotEmpty).join(':'); + }) + .where((key) => key.isNotEmpty) + .toSet() + .toList() + ..sort(); + if (roundKeys.isNotEmpty) { + return 'room_rocket_empty_reward|$roomId|round:${roundKeys.join(",")}'; + } + return 'room_rocket_empty_reward|$roomId|recent'; + } + void _pruneRoomRocketRewardFallbacks(int now) { _pendingRoomRocketRewardPopups.removeWhere( (_, pending) => now - pending.receivedAt > _roomRocketRewardPendingTtlMs, @@ -4388,11 +4523,16 @@ class RealTimeMessagingManager extends ChangeNotifier { _recentRoomRocketRewardProbeTimes.removeWhere( (_, timestamp) => now - timestamp > _roomRocketRewardProbeTtlMs, ); + _shownRoomRocketEmptyRewardPopupTimes.removeWhere( + (_, timestamp) => + now - timestamp > _roomRocketEmptyRewardPopupRecentTtlMs, + ); } void _clearRoomRocketRewardFallbackState() { _pendingRoomRocketRewardPopups.clear(); _recentRoomRocketRewardProbeTimes.clear(); + _shownRoomRocketEmptyRewardPopupTimes.clear(); _consumedRoomRocketLaunchEffectTimes.clear(); _hiddenCurrentRoomRocketLaunchKeys.clear(); _roomRocketRoomReadyRewardRetryTimer?.cancel(); @@ -4484,6 +4624,7 @@ class RealTimeMessagingManager extends ChangeNotifier { _recentRoomRocketRewardProbeTimes.remove(launch.roomId); _markRoomRocketLaunchEffectConsumed(launch); _holdRoomRocketRewardPopupUntilLaunchEnds(launch); + rtcProvider.advanceRoomRocketStatusAfterLaunch(launch); rtcProvider.handleRoomRocketLaunchBroadcast(launch); if (shouldFetchReward) { _scheduleRoomRocketRewardPopupAfterHold( @@ -4491,6 +4632,7 @@ class RealTimeMessagingManager extends ChangeNotifier { initialRoomRecords: pending?.initialRoomRecords ?? const [], + allowEmptyPopup: true, ); } } @@ -4772,6 +4914,7 @@ class RealTimeMessagingManager extends ChangeNotifier { int attempt = 0, List initialRoomRecords = const [], + bool allowEmptyPopup = false, }) async { final normalizedRoomId = roomId.trim(); if (normalizedRoomId.isEmpty || @@ -4782,6 +4925,7 @@ class RealTimeMessagingManager extends ChangeNotifier { _scheduleRoomRocketRewardPopupAfterHold( normalizedRoomId, initialRoomRecords: initialRoomRecords, + allowEmptyPopup: allowEmptyPopup, ); return; } @@ -4792,6 +4936,7 @@ class RealTimeMessagingManager extends ChangeNotifier { normalizedRoomId, attempt: attempt, initialRoomRecords: initialRoomRecords, + allowEmptyPopup: allowEmptyPopup, ), ); }); @@ -4814,6 +4959,7 @@ class RealTimeMessagingManager extends ChangeNotifier { normalizedRoomId, attempt, initialRoomRecords: initialRoomRecords, + allowEmptyPopup: allowEmptyPopup, ); return; } @@ -4823,7 +4969,17 @@ class RealTimeMessagingManager extends ChangeNotifier { if (res.records.isNotEmpty && popupRecords.isEmpty) { return; } - if (popupRecords.isEmpty && initialRoomRecords.isEmpty) { + if (popupRecords.isEmpty && + initialRoomRecords.isEmpty && + !allowEmptyPopup) { + return; + } + if (popupRecords.isEmpty && + allowEmptyPopup && + !_markRoomRocketEmptyRewardPopupForShow( + normalizedRoomId, + initialRoomRecords, + )) { return; } _roomRocketRewardPopupRetryTimer?.cancel(); @@ -4851,6 +5007,7 @@ class RealTimeMessagingManager extends ChangeNotifier { normalizedRoomId, attempt, initialRoomRecords: initialRoomRecords, + allowEmptyPopup: allowEmptyPopup, ); } finally { _roomRocketRewardPopupLoadingRoomIds.remove(normalizedRoomId); @@ -4931,6 +5088,7 @@ class RealTimeMessagingManager extends ChangeNotifier { int attempt, { List initialRoomRecords = const [], + bool allowEmptyPopup = false, }) { if (attempt >= _roomRocketRewardPopupRetryDelays.length || !_isCurrentVisibleVoiceRoom(roomId)) { @@ -4946,6 +5104,7 @@ class RealTimeMessagingManager extends ChangeNotifier { roomId, attempt: attempt + 1, initialRoomRecords: initialRoomRecords, + allowEmptyPopup: allowEmptyPopup, ), ); }, @@ -4988,6 +5147,23 @@ class RealTimeMessagingManager extends ChangeNotifier { } catch (_) {} } + void _scheduleRoomRocketStatusRefreshForCurrentRoom(String roomId) { + final normalizedRoomId = roomId.trim(); + if (normalizedRoomId.isEmpty || !_isCurrentVoiceRoom(normalizedRoomId)) { + return; + } + final currentContext = context; + if (currentContext == null || !currentContext.mounted) { + return; + } + try { + Provider.of( + currentContext, + listen: false, + ).scheduleRoomRocketStatusRefreshForGift(roomId: normalizedRoomId); + } catch (_) {} + } + List _roomRocketRewardRecordsFromPayload( Map payload, ) { @@ -5248,6 +5424,12 @@ class RealTimeMessagingManager extends ChangeNotifier { final value = _payloadNum( payload['currentLevel'] ?? payload['current_level'] ?? + payload['currentRocketLevel'] ?? + payload['current_rocket_level'] ?? + payload['rocketCurrentLevel'] ?? + payload['rocket_current_level'] ?? + payload['levelNo'] ?? + payload['level_no'] ?? payload['level'] ?? payload['rocketLevel'] ?? payload['rocket_level'] ?? @@ -5548,12 +5730,13 @@ class RealTimeMessagingManager extends ChangeNotifier { priority: 1000, ); OverlayManager().addMessage(msg); + _scheduleRoomRocketStatusRefreshForCurrentRoom(launchRoomId); } else if (type == SCRoomMsgType.voiceRoomRocketLaunchBroadcast) { _handleVoiceRoomRocketLaunchBroadcast( data["data"], isRegionBroadcast: isRegionBroadcastGroup, ); - } else if (type == SCRoomMsgType.voiceRoomRocketStatusUpdate) { + } else if (_isRoomRocketStatusUpdateType(type)) { _handleVoiceRoomRocketStatusUpdate(data["data"]); } else if (type == SCRoomMsgType.voiceRoomRocketRewardPopup) { _handleVoiceRoomRocketRewardPopup(data["data"] ?? data); @@ -5651,7 +5834,7 @@ class RealTimeMessagingManager extends ChangeNotifier { _handleVoiceRoomRocketLaunchBroadcast(data["data"]); return; } - if (data["type"] == SCRoomMsgType.voiceRoomRocketStatusUpdate) { + if (_isRoomRocketStatusUpdateType(data["type"])) { _handleVoiceRoomRocketStatusUpdate(data["data"]); return; } diff --git a/lib/services/auth/user_profile_manager.dart b/lib/services/auth/user_profile_manager.dart index 7b4a980..6bfde1c 100644 --- a/lib/services/auth/user_profile_manager.dart +++ b/lib/services/auth/user_profile_manager.dart @@ -132,14 +132,32 @@ class SocialChatUserProfileManager extends ChangeNotifier { /// 当前正在查看的个人详情资料,不用于承载登录用户自己的 Me 页数据。 SocialChatUserProfile? userProfile; + int _userInfoRequestSerial = 0; - void getUserInfoById(String userId) async { - userProfile = null; + Future getUserInfoById(String userId) { + return _loadUserInfoById(userId, clearBeforeLoad: true); + } + + Future reloadUserInfoById(String userId) { + return _loadUserInfoById(userId, clearBeforeLoad: false); + } + + Future _loadUserInfoById( + String userId, { + required bool clearBeforeLoad, + }) async { + final requestSerial = ++_userInfoRequestSerial; + if (clearBeforeLoad) { + userProfile = null; + notifyListeners(); + } final loadedProfile = await SCAccountRepository().loadUserInfo(userId); var mergedProfile = await _withCurrentVipLevel(loadedProfile, userId); mergedProfile = await _withCpCabinProfiles(mergedProfile, userId); + if (requestSerial != _userInfoRequestSerial) { + return; + } userProfile = mergedProfile; - notifyListeners(); } @@ -193,7 +211,41 @@ class SocialChatUserProfileManager extends ChangeNotifier { ); final cpPairs = []; final closeFriendPairs = []; + _addCpRelationPairs( + cpPairs: cpPairs, + closeFriendPairs: closeFriendPairs, + relations: fallbackCpPairs, + ); + _addCpRelationPairs( + cpPairs: cpPairs, + closeFriendPairs: closeFriendPairs, + relations: fallbackCloseFriendPairs, + ); var loadedAnyCabin = false; + final shouldLoadCurrentUserPairs = _isCurrentUserId(userId); + if (kDebugMode) { + final currentUserId = + AccountStorage().getCurrentUser()?.userProfile?.id?.trim() ?? ""; + debugPrint( + '[CP][Profile] source userId=$userId currentUserId=$currentUserId ' + 'profileCp=${profile.cpList?.length ?? 0} ' + 'profileCloseFriends=${profile.closeFriendList?.length ?? 0} ' + 'loadCurrentPairs=$shouldLoadCurrentUserPairs', + ); + } + if (shouldLoadCurrentUserPairs) { + await Future.wait( + _cpProfileRelationTypes.map((relationType) async { + final pairs = await _safeLoadCpRelationshipPairs(relationType); + _addCpRelationPairs( + cpPairs: cpPairs, + closeFriendPairs: closeFriendPairs, + relations: pairs, + fallbackRelationType: relationType, + ); + }), + ); + } await Future.wait( _cpProfileRelationTypes.map((relationType) async { final cabin = await _safeLoadCpCabin(userId, relationType); @@ -239,8 +291,9 @@ class SocialChatUserProfileManager extends ChangeNotifier { } if (!loadedAnyCabin) { return profile.copyWith( - cpList: fallbackCpPairs, - closeFriendList: fallbackCloseFriendPairs, + cpList: cpPairs, + closeFriendList: closeFriendPairs, + isCpRelation: cpPairs.isNotEmpty, ); } return profile.copyWith( @@ -325,6 +378,28 @@ class SocialChatUserProfileManager extends ChangeNotifier { ].join('|'); } + Future> _safeLoadCpRelationshipPairs(String relationType) async { + try { + final pairs = await SCAccountRepository().cpRelationshipPairs( + relationType: relationType, + ); + if (kDebugMode) { + debugPrint( + '[CP][Profile] pair loaded relationType=$relationType ' + 'count=${pairs.length}', + ); + } + return pairs; + } catch (error) { + if (kDebugMode) { + debugPrint( + '[CP][Profile] pair failed relationType=$relationType error=$error', + ); + } + return const []; + } + } + Future _safeLoadCpCabin( String userId, String relationType, @@ -493,10 +568,12 @@ class SocialChatUserProfileManager extends ChangeNotifier { } bool _isCurrentUserId(String userId) { - final currentUserId = AccountStorage().getCurrentUser()?.userProfile?.id; + final currentUserId = + AccountStorage().getCurrentUser()?.userProfile?.id?.trim(); + final normalizedUserId = userId.trim(); return currentUserId != null && currentUserId.isNotEmpty && - currentUserId == userId; + currentUserId == normalizedUserId; } Future _loadCurrentVipStatus() async { diff --git a/lib/shared/business_logic/models/res/login_res.dart b/lib/shared/business_logic/models/res/login_res.dart index 59fa569..4de9d1e 100644 --- a/lib/shared/business_logic/models/res/login_res.dart +++ b/lib/shared/business_logic/models/res/login_res.dart @@ -1187,9 +1187,7 @@ class CPRes { source['level'] ?? source['cpLevel'] ?? source['cabinLevel'] ?? - source['lv'] ?? - source['name'] ?? - source['title'], + source['lv'], ); _dismissEndTime = _asInt(source['dismissEndTime']); _dismissRemainSeconds = _asInt(source['dismissRemainSeconds']); 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 b0e0a39..821da50 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 @@ -107,6 +107,12 @@ class SCRoomRocketStatusRes { _currentLevel = _intValue( json['currentLevel'] ?? json['current_level'] ?? + json['currentRocketLevel'] ?? + json['current_rocket_level'] ?? + json['rocketCurrentLevel'] ?? + json['rocket_current_level'] ?? + json['levelNo'] ?? + json['level_no'] ?? json['toLevel'] ?? json['to_level'] ?? json['nextLevel'] ?? @@ -120,6 +126,12 @@ class SCRoomRocketStatusRes { _level = json['level'] ?? json['rocket_level'] ?? + json['currentRocketLevel'] ?? + json['current_rocket_level'] ?? + json['rocketCurrentLevel'] ?? + json['rocket_current_level'] ?? + json['levelNo'] ?? + json['level_no'] ?? json['toLevel'] ?? json['to_level'] ?? json['nextLevel'] ?? diff --git a/lib/shared/business_logic/repositories/room_repository.dart b/lib/shared/business_logic/repositories/room_repository.dart index f4baef4..5f61b75 100644 --- a/lib/shared/business_logic/repositories/room_repository.dart +++ b/lib/shared/business_logic/repositories/room_repository.dart @@ -60,7 +60,11 @@ abstract class SocialChatRoomRepository { Future micGoDown(String roomId, num mickIndex); ///这个用户是否可以被踢下麦 - Future kickOffMicrophone(String roomId, String kickUserId); + Future kickOffMicrophone( + String roomId, + String kickUserId, { + num? mickIndex, + }); ///锁麦 Future micLock(String roomId, num mickIndex, bool lock); diff --git a/lib/shared/data_sources/sources/repositories/sc_room_repository_imp.dart b/lib/shared/data_sources/sources/repositories/sc_room_repository_imp.dart index b45c4f7..602b4bb 100644 --- a/lib/shared/data_sources/sources/repositories/sc_room_repository_imp.dart +++ b/lib/shared/data_sources/sources/repositories/sc_room_repository_imp.dart @@ -952,6 +952,7 @@ class SCChatRoomRepository implements SocialChatRoomRepository { final result = await http.post( "a4465894e21be08d833ef701566ffe1b6a835338046b35fb94271445c996f6e3574aff257ce7e668d08f4caccd1c6232", data: data, + extra: const {BaseNetworkClient.silentErrorToastKey: true}, fromJson: (json) => json as bool, ); return result; diff --git a/lib/shared/tools/sc_cp_relation_level_utils.dart b/lib/shared/tools/sc_cp_relation_level_utils.dart index 796c695..c8b13df 100644 --- a/lib/shared/tools/sc_cp_relation_level_utils.dart +++ b/lib/shared/tools/sc_cp_relation_level_utils.dart @@ -22,6 +22,9 @@ String? scCpRelationLevelTextFromCabin( if (computedText != null) { return computedText; } + if (currentValue != null && currentValue <= 0) { + return 'Lv.0'; + } final currentConfig = _CpRelationLevelSource.fromDynamic( cabin.currentCpCabinConfigCO, @@ -51,7 +54,13 @@ String? scCpRelationLevelTextFromRightsConfig( level: computedLevel?.level, name: computedLevel?.levelName, ); - return computedText ?? _blankAsNull(fallbackText); + if (computedText != null) { + return computedText; + } + if (currentValue != null && currentValue <= 0) { + return 'Lv.0'; + } + return _blankAsNull(fallbackText); } String scCpRelationLevelDisplayText(String? rawText) { @@ -86,9 +95,9 @@ int? scCpRelationLevelIndexOrNull(String? levelText) { r'(?:Lv\.?|Level|等级|级)\s*([0-5])|([0-5])\s*(?:级|Level)', caseSensitive: false, ).firstMatch(text); - final fallbackMatch = RegExp(r'[0-5]').firstMatch(text); + final compactMatch = RegExp(r'^[0-5]$').firstMatch(text); final rawLevel = - levelMatch?.group(1) ?? levelMatch?.group(2) ?? fallbackMatch?.group(0); + levelMatch?.group(1) ?? levelMatch?.group(2) ?? compactMatch?.group(0); final level = int.tryParse(rawLevel ?? ''); return level?.clamp(0, 5).toInt(); } diff --git a/test/room_rocket_reward_dialog_loader_test.dart b/test/room_rocket_reward_dialog_loader_test.dart index bda3aa8..6283fb7 100644 --- a/test/room_rocket_reward_dialog_loader_test.dart +++ b/test/room_rocket_reward_dialog_loader_test.dart @@ -1,11 +1,36 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:yumi/services/audio/rtm_manager.dart'; import 'package:yumi/shared/business_logic/models/res/sc_room_rocket_api_res.dart'; import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_dialog.dart'; import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_reward_dialog.dart'; import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_reward_dialog_loader.dart'; void main() { + group('room rocket reward popup audience', () { + test('winner ids do not block empty reward users', () { + final shouldShow = roomRocketRewardPopupTargetsCurrentUser( + payload: { + 'winnerUserIds': ['user-a', 'user-b'], + }, + currentUserKeys: {'user-c'}, + ); + + expect(shouldShow, isTrue); + }); + + test('explicit popup ids still target popup recipients', () { + final shouldShow = roomRocketRewardPopupTargetsCurrentUser( + payload: { + 'popupUserIds': ['user-a', 'user-b'], + }, + currentUserKeys: {'user-c'}, + ); + + expect(shouldShow, isFalse); + }); + }); + group('room rocket reward dialog loader', () { test('deduplicates reward records by id', () { final records = debugUniqueRoomRocketRewardRecords([ diff --git a/test/room_rocket_status_test.dart b/test/room_rocket_status_test.dart index f4f3f87..77258c1 100644 --- a/test/room_rocket_status_test.dart +++ b/test/room_rocket_status_test.dart @@ -264,6 +264,17 @@ void main() { expect(status.displayPercent, 0); }); + test('parses realtime current rocket level aliases', () { + final status = SCRoomRocketStatusRes.fromJson({ + 'room_id': 'room-1', + 'current_rocket_level': 2, + 'display_percent': 0, + }); + + expect(status.currentLevel, 2); + expect(status.level, 2); + }); + test('merges realtime rocket status without dropping level assets', () { final current = SCRoomRocketStatusRes.fromJson({ 'room_id': 'room-1', diff --git a/test/sc_cp_relation_pair_utils_test.dart b/test/sc_cp_relation_pair_utils_test.dart index af74dd1..6a13671 100644 --- a/test/sc_cp_relation_pair_utils_test.dart +++ b/test/sc_cp_relation_pair_utils_test.dart @@ -95,6 +95,37 @@ void main() { expect(scCpRelationTypeBetweenProfiles(left, right), isNull); }); + + test('relation version changes when cp level or value changes', () { + final base = SocialChatUserProfile( + id: '100', + cpList: [ + CPRes( + meUserId: '100', + cpUserId: '200', + relationType: 'CP', + levelText: 'Lv.1', + cpValue: 90, + ), + ], + ); + final upgraded = base.copyWith( + cpList: [ + CPRes( + meUserId: '100', + cpUserId: '200', + relationType: 'CP', + levelText: 'Lv.2', + cpValue: 120, + ), + ], + ); + + expect( + scCpProfileRelationVersion(base), + isNot(scCpProfileRelationVersion(upgraded)), + ); + }); }); group('scCpRelationLevelTextFromCabin', () { @@ -112,6 +143,15 @@ void main() { expect(scCpRelationLevelTextFromCabin(cabin), 'Lv.2 Sweet Love'); }); + test('keeps zero value relation at level zero', () { + final cabin = SCCpCabinRes( + cpVal: 0, + currentCpCabinConfigCO: const {'level': 5, 'levelName': 'Soulmate'}, + ); + + expect(scCpRelationLevelTextFromCabin(cabin), 'Lv.0'); + }); + test( 'supports zero based CP levels and hides hardcoded simple love copy', () { @@ -140,5 +180,22 @@ void main() { expect(scCpRelationLevelTextFromCabin(levelOneCabin), 'Lv.1'); }, ); + + test('does not infer cp level from unrelated digits', () { + expect(scCpRelationLevelIndexOrNull('520 Couple'), isNull); + expect(scCpRelationLevelIndexOrNull('Lv.5 Soulmate'), 5); + expect(scCpRelationLevelIndexOrNull('5'), 5); + }); + + test('does not parse generic relation name as level text', () { + final relation = CPRes.fromJson({ + 'meUserId': '100', + 'cpUserId': '200', + 'relationType': 'CP', + 'name': '520 Couple', + }); + + expect(relation.levelText, isNull); + }); }); }