diff --git a/lib/services/audio/rtc_manager.dart b/lib/services/audio/rtc_manager.dart index c2a5041..6e7fec3 100644 --- a/lib/services/audio/rtc_manager.dart +++ b/lib/services/audio/rtc_manager.dart @@ -500,6 +500,12 @@ class RealTimeCommunicationManager extends ChangeNotifier { void setVoiceRoomRouteVisible(bool visible) { _voiceRoomRouteVisible = visible; + if (visible) { + final roomId = (currenRoom?.roomProfile?.roomProfile?.id ?? "").trim(); + if (roomId.isNotEmpty) { + rtmProvider?.handleVoiceRoomReadyForRocketRewards(roomId); + } + } } void _setRoomStartupLoading() { @@ -2694,6 +2700,7 @@ class RealTimeCommunicationManager extends ChangeNotifier { roomId: roomId, ); _loadRoomSecondaryData(); + rtmProvider?.handleVoiceRoomReadyForRocketRewards(roomId); } catch (error, stackTrace) { if (!_isActiveRoomStartup(entryRequestSerial, roomId)) { return; diff --git a/lib/services/audio/rtm_manager.dart b/lib/services/audio/rtm_manager.dart index 11c77f2..0a47650 100644 --- a/lib/services/audio/rtm_manager.dart +++ b/lib/services/audio/rtm_manager.dart @@ -106,6 +106,18 @@ class _LuckGiftPushQueueEntry { } } +class _PendingRoomRocketRewardPopup { + const _PendingRoomRocketRewardPopup({ + required this.roomId, + required this.receivedAt, + this.initialRoomRecords = const [], + }); + + final String roomId; + final int receivedAt; + final List initialRoomRecords; +} + class RealTimeMessagingManager extends ChangeNotifier { static const String _roomRocketRewardDialogTag = 'showRoomRocketRewardDialog'; @@ -121,12 +133,20 @@ class RealTimeMessagingManager extends ChangeNotifier { static const int _roomRocketLaunchRecentTtlMs = 90000; static const int _roomRocketLaunchLooseRecentTtlMs = 12000; static const int _roomRocketRegionBroadcastRecentTtlMs = 90000; + static const int _roomRocketRewardPendingTtlMs = 120000; + static const int _roomRocketRewardProbeTtlMs = 120000; static const Duration _createRoomGroupTimeout = Duration(seconds: 12); static const List _roomRocketRewardPopupRetryDelays = [ Duration(milliseconds: 700), Duration(seconds: 2), Duration(seconds: 5), ]; + static const List _roomRocketRoomReadyRewardRetryDelays = [ + Duration(milliseconds: 300), + Duration(milliseconds: 900), + Duration(seconds: 2), + Duration(seconds: 4), + ]; BuildContext? context; @@ -239,7 +259,11 @@ class RealTimeMessagingManager extends ChangeNotifier { final Map _recentRoomRocketLaunchLooseTimes = {}; final Map _recentRoomRocketRegionBroadcastTimes = {}; + final _pendingRoomRocketRewardPopups = + {}; + final Map _recentRoomRocketRewardProbeTimes = {}; Timer? _roomRocketRewardPopupRetryTimer; + Timer? _roomRocketRoomReadyRewardRetryTimer; int get currentLuckGiftBurstPlaybackToken => _luckGiftPushPlaybackToken; String? roomRedPacketBroadcastGroupId; String? roomRedPacketBroadcastRegionCode; @@ -1941,6 +1965,30 @@ class RealTimeMessagingManager extends ChangeNotifier { currentContext, listen: false, ); + final isCurrentVisibleRoom = + rtcProvider.shouldShowRoomVisualEffects && + rtcProvider.isVoiceRoomRouteVisible; + final launch = _buildRoomRocketLaunchFromStatusPayload( + rtcProvider: rtcProvider, + payload: data, + roomId: roomId, + ); + if (_shouldHandleRoomRocketLaunchFromStatusPayload( + rtcProvider: rtcProvider, + payload: data, + launch: launch, + )) { + if (isCurrentVisibleRoom) { + _handleCurrentVisibleRoomRocketLaunch( + rtcProvider: rtcProvider, + launch: launch, + publishRegionBroadcast: true, + ); + } else { + _markRoomRocketRewardProbe(launch.roomId); + _publishRoomRocketRegionBroadcastIfNeeded(launch); + } + } unawaited(rtcProvider.refreshRoomRocketStatus(roomId: roomId)); } @@ -1971,9 +2019,6 @@ class RealTimeMessagingManager extends ChangeNotifier { rtcProvider.shouldShowRoomVisualEffects && rtcProvider.isVoiceRoomRouteVisible; if (isCurrentVisibleRoom) { - if (!isRegionBroadcast) { - _publishRoomRocketRegionBroadcastIfNeeded(launch); - } _handleCurrentVisibleRoomRocketLaunch( rtcProvider: rtcProvider, launch: launch, @@ -1982,8 +2027,10 @@ class RealTimeMessagingManager extends ChangeNotifier { } } + if (!isCurrentVisibleRoom) { + _markRoomRocketRewardProbe(launch.roomId); + } if (isCurrentRoom && !isCurrentVisibleRoom && !isRegionBroadcast) { - _publishRoomRocketRegionBroadcastIfNeeded(launch); return; } OverlayManager().addMessage( @@ -1999,18 +2046,34 @@ class RealTimeMessagingManager extends ChangeNotifier { final roomId = _payloadRoomId(data); final currentRoomId = _currentVoiceRoomId(); final resolvedRoomId = roomId.isEmpty ? currentRoomId : roomId; - if (resolvedRoomId.isEmpty || - (currentRoomId.isNotEmpty && resolvedRoomId != currentRoomId)) { + if (resolvedRoomId.isEmpty) { + return; + } + final initialRoomRecords = _roomRocketRewardRecordsFromPayload(data); + if (currentRoomId.isNotEmpty && resolvedRoomId != currentRoomId) { + _cachePendingRoomRocketRewardPopup( + resolvedRoomId, + initialRoomRecords: initialRoomRecords, + ); return; } if (!_isCurrentVoiceRoom(resolvedRoomId)) { + _cachePendingRoomRocketRewardPopup( + resolvedRoomId, + initialRoomRecords: initialRoomRecords, + ); return; } if (!_isCurrentVisibleVoiceRoom(resolvedRoomId)) { + _cachePendingRoomRocketRewardPopup( + resolvedRoomId, + initialRoomRecords: initialRoomRecords, + ); return; } - final initialRoomRecords = _roomRocketRewardRecordsFromPayload(data); + _pendingRoomRocketRewardPopups.remove(resolvedRoomId); + _recentRoomRocketRewardProbeTimes.remove(resolvedRoomId); unawaited( _showRoomRocketRewardPopupDialog( resolvedRoomId, @@ -2019,9 +2082,110 @@ class RealTimeMessagingManager extends ChangeNotifier { ); } + void handleVoiceRoomReadyForRocketRewards(String roomId, {int attempt = 0}) { + final normalizedRoomId = roomId.trim(); + if (normalizedRoomId.isEmpty) { + return; + } + final now = DateTime.now().millisecondsSinceEpoch; + _pruneRoomRocketRewardFallbacks(now); + final pending = _pendingRoomRocketRewardPopups[normalizedRoomId]; + final hasProbe = + _recentRoomRocketRewardProbeTimes[normalizedRoomId] != null; + if (pending == null && !hasProbe) { + return; + } + if (!_isCurrentVisibleVoiceRoom(normalizedRoomId)) { + _scheduleRoomRocketReadyRewardRetry(normalizedRoomId, attempt); + return; + } + if (pending == null && attempt == 0) { + _scheduleRoomRocketReadyRewardRetry(normalizedRoomId, attempt); + return; + } + _pendingRoomRocketRewardPopups.remove(normalizedRoomId); + _recentRoomRocketRewardProbeTimes.remove(normalizedRoomId); + _roomRocketRoomReadyRewardRetryTimer?.cancel(); + _roomRocketRoomReadyRewardRetryTimer = null; + unawaited( + _showRoomRocketRewardPopupDialog( + normalizedRoomId, + initialRoomRecords: + pending?.initialRoomRecords ?? + const [], + ), + ); + } + + void _scheduleRoomRocketReadyRewardRetry(String roomId, int attempt) { + if (attempt >= _roomRocketRoomReadyRewardRetryDelays.length) { + return; + } + _roomRocketRoomReadyRewardRetryTimer?.cancel(); + _roomRocketRoomReadyRewardRetryTimer = Timer( + _roomRocketRoomReadyRewardRetryDelays[attempt], + () { + _roomRocketRoomReadyRewardRetryTimer = null; + handleVoiceRoomReadyForRocketRewards(roomId, attempt: attempt + 1); + }, + ); + } + + void _cachePendingRoomRocketRewardPopup( + String roomId, { + List initialRoomRecords = + const [], + }) { + final normalizedRoomId = roomId.trim(); + if (normalizedRoomId.isEmpty) { + return; + } + final now = DateTime.now().millisecondsSinceEpoch; + _pruneRoomRocketRewardFallbacks(now); + final existing = _pendingRoomRocketRewardPopups[normalizedRoomId]; + final records = + initialRoomRecords.isNotEmpty + ? List.unmodifiable(initialRoomRecords) + : existing?.initialRoomRecords ?? + const []; + _pendingRoomRocketRewardPopups[normalizedRoomId] = + _PendingRoomRocketRewardPopup( + roomId: normalizedRoomId, + receivedAt: now, + initialRoomRecords: records, + ); + } + + void _markRoomRocketRewardProbe(String roomId) { + final normalizedRoomId = roomId.trim(); + if (normalizedRoomId.isEmpty) { + return; + } + final now = DateTime.now().millisecondsSinceEpoch; + _pruneRoomRocketRewardFallbacks(now); + _recentRoomRocketRewardProbeTimes[normalizedRoomId] = now; + } + + void _pruneRoomRocketRewardFallbacks(int now) { + _pendingRoomRocketRewardPopups.removeWhere( + (_, pending) => now - pending.receivedAt > _roomRocketRewardPendingTtlMs, + ); + _recentRoomRocketRewardProbeTimes.removeWhere( + (_, timestamp) => now - timestamp > _roomRocketRewardProbeTtlMs, + ); + } + + void _clearRoomRocketRewardFallbackState() { + _pendingRoomRocketRewardPopups.clear(); + _recentRoomRocketRewardProbeTimes.clear(); + _roomRocketRoomReadyRewardRetryTimer?.cancel(); + _roomRocketRoomReadyRewardRetryTimer = null; + } + void _handleCurrentVisibleRoomRocketLaunch({ required RealTimeCommunicationManager rtcProvider, required RoomRocketLaunchBroadcastMessage launch, + bool publishRegionBroadcast = false, }) { if (!_isCurrentVisibleVoiceRoom(launch.roomId)) { return; @@ -2049,6 +2213,9 @@ class RealTimeMessagingManager extends ChangeNotifier { launch, ), ); + if (publishRegionBroadcast) { + _publishRoomRocketRegionBroadcastIfNeeded(launch); + } rtcProvider.handleRoomRocketLaunchBroadcast(launch); } unawaited(_showRoomRocketRewardPopupDialog(launch.roomId)); @@ -2318,6 +2485,147 @@ class RealTimeMessagingManager extends ChangeNotifier { return ['rocket_launch_loose', launch.roomId, launch.safeLevel].join('|'); } + bool _shouldHandleRoomRocketLaunchFromStatusPayload({ + required RealTimeCommunicationManager rtcProvider, + required Map payload, + required RoomRocketLaunchBroadcastMessage launch, + }) { + final currentPercent = _roomRocketPayloadPercent(payload); + if (currentPercent < 99.5) { + return false; + } + final previousPercent = _roomRocketStatusPercent( + rtcProvider.roomRocketStatus, + ); + if (previousPercent < 99.5) { + return true; + } + final launchKey = _roomRocketLaunchNoticeKey(launch); + if (launchKey.isEmpty) { + return false; + } + _pruneRecentRoomRocketLaunches(DateTime.now().millisecondsSinceEpoch); + final looseLaunchKey = _roomRocketLaunchLooseKey(launch); + return !_recentRoomRocketLaunchTimes.containsKey(launchKey) && + (looseLaunchKey.isEmpty || + !_recentRoomRocketLaunchLooseTimes.containsKey(looseLaunchKey)); + } + + RoomRocketLaunchBroadcastMessage _buildRoomRocketLaunchFromStatusPayload({ + required RealTimeCommunicationManager rtcProvider, + required Map payload, + required String roomId, + }) { + final roomProfile = rtcProvider.currenRoom?.roomProfile?.roomProfile; + final level = _roomRocketPayloadLevel( + payload, + rtcProvider.roomRocketStatus?.currentLevel ?? + rtcProvider.roomRocketStatus?.level?.toInt() ?? + 1, + ); + return RoomRocketLaunchBroadcastMessage( + roomId: roomId, + roomAccount: _firstNonBlank([ + roomProfile?.roomAccount, + _payloadText(payload['roomAccount'] ?? payload['room_account']), + ]), + roomName: _firstNonBlank([ + roomProfile?.roomName, + _payloadText(payload['roomName'] ?? payload['room_name']), + ]), + roomCoverUrl: _firstNonBlank([ + roomProfile?.roomCover, + _payloadAssetText(payload['roomCoverUrl'] ?? payload['room_cover_url']), + _payloadAssetText(payload['roomCover'] ?? payload['room_cover']), + _payloadAssetText(payload['roomAvatar'] ?? payload['room_avatar']), + _payloadAssetText(payload['roomIcon'] ?? payload['room_icon']), + _payloadAssetText(payload['coverUrl'] ?? payload['cover_url']), + _payloadAssetText(payload['cover']), + _payloadAssetText(_broadcastPayloadMap(payload['room'])['coverUrl']), + _payloadAssetText(_broadcastPayloadMap(payload['room'])['cover']), + _payloadAssetText(_broadcastPayloadMap(payload['room'])['avatar']), + _payloadAssetText( + _broadcastPayloadMap(payload['roomInfo'])['coverUrl'], + ), + _payloadAssetText(_broadcastPayloadMap(payload['roomInfo'])['cover']), + _payloadAssetText(_broadcastPayloadMap(payload['roomInfo'])['avatar']), + ]), + roundNo: + _payloadNum(payload['roundNo'] ?? payload['round_no'])?.toInt() ?? 0, + level: level, + rocketIconUrl: _firstNonBlank([ + _rocketIconFromPayload(payload), + _rocketIconUrlForLevel(level), + ]), + rocketAnimationUrl: _rocketAnimationFromPayload(payload), + durationSeconds: _roomRocketPayloadNoticeSeconds(payload), + triggerUser: const RoomRocketLaunchTriggerUser( + userId: '', + nickname: '', + avatar: '', + account: '', + countryCode: '', + countryName: '', + ), + triggerUserId: _payloadText( + payload['triggerUserId'] ?? payload['trigger_user_id'], + ), + launchNo: _payloadText(payload['launchNo'] ?? payload['launch_no']), + ); + } + + double _roomRocketStatusPercent(dynamic status) { + if (status == null) { + return 0; + } + final displayPercent = _payloadNum(status.displayPercent); + if (displayPercent != null) { + final raw = displayPercent.toDouble(); + final percent = raw > 0 && raw <= 1 ? raw * 100 : raw; + return percent.clamp(0, 100).toDouble(); + } + final energyPercent = _payloadNum(status.energyPercent); + if (energyPercent != null) { + final raw = energyPercent.toDouble(); + final percent = raw > 0 && raw <= 1 ? raw * 100 : raw; + return percent.clamp(0, 100).toDouble(); + } + final currentEnergy = _payloadNum(status.currentEnergy); + final maxEnergy = _payloadNum(status.maxEnergy ?? status.needEnergy); + if (currentEnergy == null || maxEnergy == null || maxEnergy <= 0) { + return 0; + } + return ((currentEnergy / maxEnergy) * 100).clamp(0, 100).toDouble(); + } + + double _roomRocketPayloadPercent(Map payload) { + final value = _payloadNum( + payload['displayPercent'] ?? + payload['display_percent'] ?? + payload['energyPercent'] ?? + payload['energy_percent'] ?? + payload['percent'], + ); + if (value != null) { + final raw = value.toDouble(); + final percent = raw > 0 && raw <= 1 ? raw * 100 : raw; + return percent.clamp(0, 100).toDouble(); + } + final currentEnergy = _payloadNum( + payload['currentEnergy'] ?? payload['current_energy'], + ); + final maxEnergy = _payloadNum( + payload['maxEnergy'] ?? + payload['max_energy'] ?? + payload['needEnergy'] ?? + payload['need_energy'], + ); + if (currentEnergy == null || maxEnergy == null || maxEnergy <= 0) { + return 0; + } + return ((currentEnergy / maxEnergy) * 100).clamp(0, 100).toDouble(); + } + int _roomRocketPayloadLevel(Map payload, int fallback) { final value = _payloadNum( payload['currentLevel'] ?? @@ -2331,6 +2639,25 @@ class RealTimeMessagingManager extends ChangeNotifier { return (value?.toInt() ?? fallback).clamp(1, 99).toInt(); } + int _roomRocketPayloadNoticeSeconds(Map payload) { + final seconds = + _payloadNum( + payload['countdownSeconds'] ?? + payload['countdown_seconds'] ?? + payload['launchDelaySeconds'] ?? + payload['launch_delay_seconds'] ?? + payload['delaySeconds'] ?? + payload['delay_seconds'] ?? + payload['durationSeconds'] ?? + payload['duration_seconds'], + )?.toInt() ?? + 10; + if (seconds > 0 && seconds <= 60) { + return seconds; + } + return 10; + } + String _rocketIconFromPayload(Map payload) { return _firstNonBlank([ _payloadAssetText(payload['rocketIconUrl']), @@ -2342,6 +2669,22 @@ class RealTimeMessagingManager extends ChangeNotifier { ]); } + String _rocketAnimationFromPayload(Map payload) { + return _firstNonBlank([ + _payloadAssetText(payload['rocketAnimationUrl']), + _payloadAssetText(payload['rocket_animation_url']), + _payloadAssetText(payload['rocketSvgaUrl']), + _payloadAssetText(payload['rocket_svga_url']), + _payloadAssetText(payload['animationUrl']), + _payloadAssetText(payload['animation_url']), + _payloadAssetText(payload['launchAnimationUrl']), + _payloadAssetText(payload['launch_animation_url']), + _payloadAssetText(payload['svgaUrl']), + _payloadAssetText(payload['sourceUrl']), + _payloadAssetText(payload['source_url']), + ]); + } + String _payloadRoomId(Map payload) { return _firstNonBlank([ _payloadText(payload['roomId']), @@ -2464,6 +2807,7 @@ class RealTimeMessagingManager extends ChangeNotifier { logout() async { roomRedPacketBroadcastGroupId = null; roomRedPacketBroadcastRegionCode = null; + _clearRoomRocketRewardFallbackState(); RoomRedPacketOpenDialog.clearClaimedState(); RoomRedPacketPendingCache.instance.clear(); V2TimCallback logoutRes = await TencentImSDKPlugin.v2TIMManager.logout();