This commit is contained in:
roxy 2026-05-23 19:12:58 +08:00
parent f8e13f31ba
commit 6eb7cb91df
12 changed files with 629 additions and 93 deletions

View File

@ -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<PersonDetailPage> {
class _PersonDetailPageState extends State<PersonDetailPage> 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<PersonDetailPage> {
//
final ScrollController _scrollController = ScrollController();
double _opacity = 0.0;
PageRoute<dynamic>? _routeObserverRoute;
@override
void initState() {
@ -155,6 +158,52 @@ class _PersonDetailPageState extends State<PersonDetailPage> {
_loadGiftWall();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
final route = ModalRoute.of(context);
if (route is PageRoute<dynamic> && _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<SocialChatUserProfileManager>(
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<PersonDetailPage> {
@override
void dispose() {
routeObserver.unsubscribe(this);
_routeObserverRoute = null;
_scrollController.dispose();
super.dispose();
}

View File

@ -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<String> 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 = <String>{};
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 <SCRoomRocketLevelRes>[]),
]..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 <SCRoomRocketLevelRes>[]) {
if (item.level == normalizedLevel) {
return item;
}
}
return null;
}
void handleRoomRocketLaunchBroadcast(
RoomRocketLaunchBroadcastMessage launch,
) {

View File

@ -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<SCRoomRocketRewardRecordRes> initialRoomRecords;
}
bool roomRocketRewardPopupTargetsCurrentUser({
required Map<String, dynamic> payload,
required Set<String> currentUserKeys,
}) {
final audienceUserIds = _roomRocketRewardPopupAudienceUserIds(payload);
if (audienceUserIds == null) {
return true;
}
if (audienceUserIds.isEmpty || currentUserKeys.isEmpty) {
return false;
}
return currentUserKeys.any(audienceUserIds.contains);
}
Set<String>? _roomRocketRewardPopupAudienceUserIds(
Map<String, dynamic> payload,
) {
for (final key in const [
'popupUserIds',
'popup_user_ids',
'notifyUserIds',
'notify_user_ids',
'audienceUserIds',
'audience_user_ids',
'visibleUserIds',
'visible_user_ids',
]) {
if (payload.containsKey(key)) {
return _roomRocketPayloadStringSet(payload[key]);
}
}
return null;
}
Set<String> _roomRocketPayloadStringSet(dynamic value) {
if (value == null) {
return <String>{};
}
if (value is Iterable && value is! String) {
return value
.map((item) => item?.toString().trim() ?? '')
.where((item) => item.isNotEmpty)
.toSet();
}
if (value is Map) {
return value.values
.map((item) => item?.toString().trim() ?? '')
.where((item) => item.isNotEmpty)
.toSet();
}
final raw = value.toString().trim();
if (raw.isEmpty) {
return <String>{};
}
if (raw.startsWith('[') || raw.startsWith('{')) {
try {
return _roomRocketPayloadStringSet(jsonDecode(raw));
} catch (_) {
return <String>{};
}
}
return raw
.split(',')
.map((item) => item.trim())
.where((item) => item.isNotEmpty)
.toSet();
}
class RealTimeMessagingManager extends ChangeNotifier {
static const String _roomRocketRewardDialogTag = 'showRoomRocketRewardDialog';
@ -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<Duration> _cpRelationGiftRefreshDelays = [
Duration(milliseconds: 300),
Duration(milliseconds: 1500),
Duration(milliseconds: 3500),
];
static const int _roomRocketLaunchRecentTtlMs = 90000;
static const int _roomRocketLaunchLooseRecentTtlMs = 12000;
static const int _roomRocketRegionBroadcastRecentTtlMs = 90000;
static const int _roomRocketRewardPendingTtlMs = 3600000;
static const int _roomRocketRewardProbeTtlMs = 3600000;
static const int _roomRocketEmptyRewardPopupRecentTtlMs = 120000;
static const int _roomRocketLaunchCountdownRetainMs = 10000;
static const int _roomRocketLaunchEffectConsumedTtlMs = 120000;
static const Duration _createRoomGroupTimeout = Duration(seconds: 12);
@ -296,12 +372,15 @@ class RealTimeMessagingManager extends ChangeNotifier {
final Map<String, int> _roomRocketRewardHoldUntilTimes = <String, int>{};
final Map<String, Timer> _roomRocketRewardHoldTimers = <String, Timer>{};
final Set<String> _roomRocketRewardPopupLoadingRoomIds = <String>{};
final Map<String, int> _shownRoomRocketEmptyRewardPopupTimes =
<String, int>{};
Timer? _roomRocketRewardPopupRetryTimer;
Timer? _roomRocketRoomReadyRewardRetryTimer;
final Set<String> _shownCpInviteMessageKeys = <String>{};
final Set<String> _shownCpInviteResultMessageKeys = <String>{};
final Set<String> _shownCpInviteRejectedDialogKeys = <String>{};
final Map<String, int> _recentCpRelationNoticeTimes = <String, int>{};
int _lastCpRelationGiftRefreshAt = 0;
int get currentLuckGiftBurstPlaybackToken => _luckGiftPushPlaybackToken;
String? roomRedPacketBroadcastGroupId;
String? roomRedPacketBroadcastRegionCode;
@ -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<String>.unmodifiable(_cpRelationGiftUserIds(msg));
for (final delay in _cpRelationGiftRefreshDelays) {
unawaited(
Future<void>.delayed(delay, () {
_refreshCpRelationDataAfterGift(userIds);
}),
);
}
}
bool _isCpRelationGiftMessage(Msg msg) {
if (msg.type != SCRoomMsgType.gift &&
msg.type != SCRoomMsgType.luckGiftAnimOther) {
return false;
}
return scIsCpGift(msg.gift);
}
Set<String> _cpRelationGiftUserIds(Msg msg) {
return <String>{
msg.user?.id?.trim() ?? '',
msg.toUser?.id?.trim() ?? '',
...?msg.targetUserIds?.map((userId) => userId.trim()),
}..removeWhere((userId) => userId.isEmpty);
}
void _refreshCpRelationDataAfterGift(Set<String> userIds) {
final currentContext =
context?.mounted == true ? context : navigatorKey.currentState?.context;
if (currentContext == null || !currentContext.mounted) {
return;
}
try {
unawaited(
Provider.of<SocialChatUserProfileManager>(
currentContext,
listen: false,
).refreshLoadedCpProfiles(),
);
} catch (_) {}
try {
Provider.of<RealTimeCommunicationManager>(
currentContext,
listen: false,
).refreshRoomMicRelationProfilesForUsers(userIds);
} catch (_) {}
}
void _cacheRoomRedPacketMessage(Msg msg) {
try {
final decoded = jsonDecode(msg.msg ?? '');
@ -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<String, dynamic> payload) {
final userIds = _roomRocketRewardPopupUserIds(payload);
if (userIds == null) {
return true;
}
if (userIds.isEmpty) {
return false;
}
final profile = AccountStorage().getCurrentUser()?.userProfile;
final currentKeys = <String>{
profile?.id?.trim() ?? '',
profile?.account?.trim() ?? '',
}..removeWhere((item) => item.isEmpty);
if (currentKeys.isEmpty) {
return false;
}
return currentKeys.any(userIds.contains);
}
Set<String>? _roomRocketRewardPopupUserIds(Map<String, dynamic> 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<String> _payloadStringSet(dynamic value) {
if (value == null) {
return <String>{};
}
if (value is Iterable && value is! String) {
return value
.map((item) => item?.toString().trim() ?? '')
.where((item) => item.isNotEmpty)
.toSet();
}
if (value is Map) {
return value.values
.map((item) => item?.toString().trim() ?? '')
.where((item) => item.isNotEmpty)
.toSet();
}
final raw = value.toString().trim();
if (raw.isEmpty) {
return <String>{};
}
if (raw.startsWith('[') || raw.startsWith('{')) {
try {
return _payloadStringSet(jsonDecode(raw));
} catch (_) {
return <String>{};
}
}
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 <SCRoomRocketRewardRecordRes>[],
allowEmptyPopup: allowEmptyPopup,
);
return;
}
@ -4171,6 +4251,7 @@ class RealTimeMessagingManager extends ChangeNotifier {
initialRoomRecords:
pendingAfterResume?.initialRoomRecords ??
const <SCRoomRocketRewardRecordRes>[],
allowEmptyPopup: allowEmptyPopup,
),
);
}
@ -4338,6 +4419,7 @@ class RealTimeMessagingManager extends ChangeNotifier {
String roomId, {
List<SCRoomRocketRewardRecordRes> initialRoomRecords =
const <SCRoomRocketRewardRecordRes>[],
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 <SCRoomRocketRewardRecordRes>[],
allowEmptyPopup: allowEmptyPopup,
),
);
},
@ -4381,6 +4467,55 @@ class RealTimeMessagingManager extends ChangeNotifier {
);
}
bool _markRoomRocketEmptyRewardPopupForShow(
String roomId,
List<SCRoomRocketRewardRecordRes> roomRecords,
) {
final normalizedRoomId = roomId.trim();
if (normalizedRoomId.isEmpty) {
return false;
}
final now = DateTime.now().millisecondsSinceEpoch;
_pruneRoomRocketRewardFallbacks(now);
final key = _roomRocketEmptyRewardPopupKey(normalizedRoomId, roomRecords);
if (_shownRoomRocketEmptyRewardPopupTimes.containsKey(key)) {
return false;
}
_shownRoomRocketEmptyRewardPopupTimes[key] = now;
return true;
}
String _roomRocketEmptyRewardPopupKey(
String roomId,
List<SCRoomRocketRewardRecordRes> roomRecords,
) {
final launchNos =
roomRecords
.map((record) => record.launchNo.trim())
.where((launchNo) => launchNo.isNotEmpty)
.toSet()
.toList()
..sort();
if (launchNos.isNotEmpty) {
return 'room_rocket_empty_reward|$roomId|launch:${launchNos.join(",")}';
}
final roundKeys =
roomRecords
.map((record) {
final level = record.level > 0 ? record.level.toString() : '';
final round = record.roundNo > 0 ? record.roundNo.toString() : '';
return [level, round].where((item) => item.isNotEmpty).join(':');
})
.where((key) => key.isNotEmpty)
.toSet()
.toList()
..sort();
if (roundKeys.isNotEmpty) {
return 'room_rocket_empty_reward|$roomId|round:${roundKeys.join(",")}';
}
return 'room_rocket_empty_reward|$roomId|recent';
}
void _pruneRoomRocketRewardFallbacks(int now) {
_pendingRoomRocketRewardPopups.removeWhere(
(_, pending) => now - pending.receivedAt > _roomRocketRewardPendingTtlMs,
@ -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 <SCRoomRocketRewardRecordRes>[],
allowEmptyPopup: true,
);
}
}
@ -4772,6 +4914,7 @@ class RealTimeMessagingManager extends ChangeNotifier {
int attempt = 0,
List<SCRoomRocketRewardRecordRes> initialRoomRecords =
const <SCRoomRocketRewardRecordRes>[],
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<SCRoomRocketRewardRecordRes> initialRoomRecords =
const <SCRoomRocketRewardRecordRes>[],
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<RealTimeCommunicationManager>(
currentContext,
listen: false,
).scheduleRoomRocketStatusRefreshForGift(roomId: normalizedRoomId);
} catch (_) {}
}
List<SCRoomRocketRewardRecordRes> _roomRocketRewardRecordsFromPayload(
Map<String, dynamic> 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;
}

View File

@ -132,14 +132,32 @@ class SocialChatUserProfileManager extends ChangeNotifier {
/// Me
SocialChatUserProfile? userProfile;
int _userInfoRequestSerial = 0;
void getUserInfoById(String userId) async {
userProfile = null;
Future<void> getUserInfoById(String userId) {
return _loadUserInfoById(userId, clearBeforeLoad: true);
}
Future<void> reloadUserInfoById(String userId) {
return _loadUserInfoById(userId, clearBeforeLoad: false);
}
Future<void> _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 = <CPRes>[];
final closeFriendPairs = <CPRes>[];
_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<List<CPRes>> _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 <CPRes>[];
}
}
Future<SCCpCabinRes?> _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<SCVipStatusRes?> _loadCurrentVipStatus() async {

View File

@ -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']);

View File

@ -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'] ??

View File

@ -60,7 +60,11 @@ abstract class SocialChatRoomRepository {
Future micGoDown(String roomId, num mickIndex);
///
Future<bool> kickOffMicrophone(String roomId, String kickUserId);
Future<bool> kickOffMicrophone(
String roomId,
String kickUserId, {
num? mickIndex,
});
///
Future micLock(String roomId, num mickIndex, bool lock);

View File

@ -952,6 +952,7 @@ class SCChatRoomRepository implements SocialChatRoomRepository {
final result = await http.post<bool>(
"a4465894e21be08d833ef701566ffe1b6a835338046b35fb94271445c996f6e3574aff257ce7e668d08f4caccd1c6232",
data: data,
extra: const {BaseNetworkClient.silentErrorToastKey: true},
fromJson: (json) => json as bool,
);
return result;

View File

@ -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();
}

View File

@ -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([

View File

@ -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',

View File

@ -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);
});
});
}