又一版

This commit is contained in:
roxy 2026-05-15 23:35:35 +08:00
parent 51083ef57f
commit be6b2f8f88
14 changed files with 1093 additions and 162 deletions

View File

@ -58,7 +58,6 @@
android:exported="true"
android:hardwareAccelerated="true"
android:launchMode="singleTask"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as

View File

@ -1,5 +1,6 @@
package com.org.yumiparty
import android.content.Intent
import com.android.installreferrer.api.InstallReferrerClient
import com.android.installreferrer.api.InstallReferrerStateListener
import io.flutter.embedding.android.FlutterActivity
@ -59,6 +60,11 @@ class MainActivity : FlutterActivity() {
}
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
}
override fun onDestroy() {
installReferrerClient?.endConnection()
installReferrerClient = null

View File

@ -54,7 +54,9 @@ class VoiceRoomForegroundService : Service() {
packageManager.getLaunchIntentForPackage(packageName)
?: Intent(this, MainActivity::class.java)
).apply {
addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP)
flags = Intent.FLAG_ACTIVITY_NEW_TASK or
Intent.FLAG_ACTIVITY_CLEAR_TOP or
Intent.FLAG_ACTIVITY_SINGLE_TOP
}
val pendingIntent = PendingIntent.getActivity(
this,

View File

@ -559,10 +559,9 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
///
_floatingGiftListener(Msg msg) {
if (!Provider.of<RtcProvider>(
context,
listen: false,
).shouldShowRoomVisualEffects) {
final rtcProvider = Provider.of<RtcProvider>(context, listen: false);
rtcProvider.scheduleRoomRocketStatusRefreshForGift();
if (!rtcProvider.shouldShowRoomVisualEffects) {
return;
}
if (Provider.of<GiftProvider>(context, listen: false).hideLGiftAnimal) {

View File

@ -48,6 +48,7 @@ import '../../shared/data_sources/models/enum/sc_heartbeat_status.dart';
import '../../shared/data_sources/models/enum/sc_room_info_event_type.dart';
import '../../shared/data_sources/models/enum/sc_room_roles_type.dart';
import '../../shared/business_logic/models/res/sc_is_follow_room_res.dart';
import '../../shared/business_logic/models/res/sc_room_rocket_api_res.dart';
import '../../shared/business_logic/models/res/sc_room_red_packet_config_res.dart';
import '../../shared/business_logic/models/res/sc_room_red_packet_list_res.dart';
import '../../shared/business_logic/models/res/sc_room_rocket_status_res.dart';
@ -56,6 +57,7 @@ import '../../shared/business_logic/models/res/sc_vip_res.dart';
import '../../shared/tools/sc_room_profile_cache.dart';
import '../../ui_kit/components/sc_float_ichart.dart';
import '../../ui_kit/widgets/room/rocket/room_rocket_pag_effect_overlay.dart';
import '../../ui_kit/widgets/room/rocket/room_rocket_api_mapper.dart';
import '../../ui_kit/widgets/room/rocket/room_rocket_reward_dialog_loader.dart';
typedef OnSoundVoiceChange = Function(num index, int volum);
@ -292,6 +294,9 @@ class RealTimeCommunicationManager extends ChangeNotifier {
static const String _roomRocketRewardDialogTag = 'showRoomRocketRewardDialog';
static const String _roomRocketPagLaunchDialogTag =
'showRoomRocketPagLaunchDialog';
static const Duration _roomRocketGiftRefreshDelay = Duration(
milliseconds: 700,
);
static const int _roomPasswordNotTrueErrorCode = 9005;
static const Duration _micListPollingInterval = Duration(seconds: 2);
@ -331,6 +336,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
Timer? _onlineUsersPollingTimer;
Timer? _roomEntryEffectTimer;
Timer? _roomRocketLaunchAnimationTimer;
Timer? _roomRocketGiftRefreshTimer;
String? _lastRoomRocketLaunchNoticeKey;
Timer? _roomRedPacketPresenceTimer;
Future<void>? _rtcEnginePrewarmTask;
@ -477,6 +483,8 @@ class RealTimeCommunicationManager extends ChangeNotifier {
_roomEntryEffectTimer = null;
_roomRocketLaunchAnimationTimer?.cancel();
_roomRocketLaunchAnimationTimer = null;
_roomRocketGiftRefreshTimer?.cancel();
_roomRocketGiftRefreshTimer = null;
}
_roomVisualEffectsEnabled = enabled;
notifyListeners();
@ -2753,6 +2761,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
(_) => RoomRocketRewardDialogLoader(
roomId: roomId,
initialPopupRecords: res.records,
userProfileResolver: roomRocketRewardUserProfileForRecord,
onClose: () {
SmartDialog.dismiss(tag: _roomRocketRewardDialogTag);
if (recordIds.isNotEmpty) {
@ -2795,6 +2804,19 @@ class RealTimeCommunicationManager extends ChangeNotifier {
_publishRoomRocketLaunchNoticeIfNeeded(previousStatus, res);
}
void scheduleRoomRocketStatusRefreshForGift({String? roomId}) {
final resolvedRoomId =
(roomId ?? currenRoom?.roomProfile?.roomProfile?.id ?? "").trim();
if (resolvedRoomId.isEmpty) {
return;
}
_roomRocketGiftRefreshTimer?.cancel();
_roomRocketGiftRefreshTimer = Timer(_roomRocketGiftRefreshDelay, () {
_roomRocketGiftRefreshTimer = null;
unawaited(refreshRoomRocketStatus(roomId: resolvedRoomId));
});
}
void handleRoomRocketLaunchBroadcast(
RoomRocketLaunchBroadcastMessage launch,
) {
@ -3000,6 +3022,63 @@ class RealTimeCommunicationManager extends ChangeNotifier {
return null;
}
RoomRocketRewardUserProfile? roomRocketRewardUserProfileForRecord(
SCRoomRocketRewardRecordRes record,
) {
final keys =
<String>{
record.userId.trim(),
record.account.trim(),
}.where((item) => item.isNotEmpty).toSet();
if (keys.isEmpty) {
return null;
}
for (final profile in _roomRocketKnownUserProfiles()) {
if (_roomRocketProfileMatches(profile, keys)) {
return RoomRocketRewardUserProfile(
userId: profile.id?.trim() ?? '',
account: profile.account?.trim() ?? '',
nickname: profile.userNickname?.trim() ?? '',
avatarUrl: profile.userAvatar?.trim() ?? '',
);
}
}
return null;
}
Iterable<SocialChatUserProfile> _roomRocketKnownUserProfiles() sync* {
final currentUser = AccountStorage().getCurrentUser()?.userProfile;
if (currentUser != null) {
yield currentUser;
}
final ownerProfile = currenRoom?.roomProfile?.userProfile;
if (ownerProfile != null) {
yield ownerProfile;
}
for (final user in onlineUsers) {
yield user;
}
for (final seat in roomWheatMap.values) {
final user = seat.user;
if (user != null) {
yield user;
}
}
for (final user in SCRoomUtils.roomUsersMap.values) {
yield user;
}
}
bool _roomRocketProfileMatches(
SocialChatUserProfile profile,
Set<String> keys,
) {
return [profile.id, profile.account, profile.inRoomId].any((value) {
final text = value?.trim() ?? '';
return text.isNotEmpty && keys.contains(text);
});
}
///线
Future<void> fetchOnlineUsersList({bool notifyIfUnchanged = true}) async {
final roomId = currenRoom?.roomProfile?.roomProfile?.id ?? "";
@ -3491,6 +3570,8 @@ class RealTimeCommunicationManager extends ChangeNotifier {
_roomEntryEffectTimer = null;
_roomRocketLaunchAnimationTimer?.cancel();
_roomRocketLaunchAnimationTimer = null;
_roomRocketGiftRefreshTimer?.cancel();
_roomRocketGiftRefreshTimer = null;
_lastRoomRocketLaunchNoticeKey = null;
_stopRoomRedPacketPresenceHeartbeat();
_previewRoomSeatCount = null;

View File

@ -50,6 +50,7 @@ import 'package:yumi/shared/data_sources/models/message/big_broadcast_group_mess
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';
import 'package:yumi/shared/business_logic/models/res/sc_broad_cast_luck_gift_push.dart';
import 'package:yumi/shared/business_logic/models/res/sc_room_rocket_api_res.dart';
import 'package:yumi/shared/business_logic/models/res/broad_cast_mic_change_push.dart'
hide Data;
import 'package:yumi/shared/business_logic/models/res/gift_res.dart';
@ -60,6 +61,7 @@ import 'package:yumi/ui_kit/widgets/room/invite/invite_room_dialog.dart';
import 'package:yumi/ui_kit/widgets/room/red_packet/room_red_packet_models.dart';
import 'package:yumi/ui_kit/widgets/room/red_packet/room_red_packet_open_dialog.dart';
import 'package:yumi/ui_kit/widgets/room/red_packet/room_red_packet_pending_cache.dart';
import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_api_mapper.dart';
import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_reward_dialog_loader.dart';
import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart';
import 'package:yumi/shared/business_logic/models/res/login_res.dart';
@ -116,6 +118,13 @@ class RealTimeMessagingManager extends ChangeNotifier {
static const int _luckyGiftBurstMergeWindowMs = 1800;
static const int _luckyGiftBurstRecentEventTtlMs = 5000;
static const int _luckyGiftBurstPlaybackWatchdogMs = 8000;
static const int _roomRocketLaunchRecentTtlMs = 90000;
static const int _roomRocketLaunchLooseRecentTtlMs = 12000;
static const List<Duration> _roomRocketRewardPopupRetryDelays = [
Duration(milliseconds: 700),
Duration(seconds: 2),
Duration(seconds: 5),
];
BuildContext? context;
@ -224,6 +233,9 @@ class RealTimeMessagingManager extends ChangeNotifier {
Timer? _luckGiftPushPlayTimer;
int _luckGiftPushPlaybackToken = 0;
final Map<String, int> _recentLuckGiftPushEventTimes = <String, int>{};
final Map<String, int> _recentRoomRocketLaunchTimes = <String, int>{};
final Map<String, int> _recentRoomRocketLaunchLooseTimes = <String, int>{};
Timer? _roomRocketRewardPopupRetryTimer;
int get currentLuckGiftBurstPlaybackToken => _luckGiftPushPlaybackToken;
String? roomRedPacketBroadcastGroupId;
String? roomRedPacketBroadcastRegionCode;
@ -1904,7 +1916,7 @@ class RealTimeMessagingManager extends ChangeNotifier {
void _handleVoiceRoomRocketStatusUpdate(dynamic payload) {
final data = _broadcastPayloadMap(payload);
final roomId = _payloadText(data['roomId']);
final roomId = _payloadRoomId(data);
if (!_isCurrentVoiceRoom(roomId)) {
return;
}
@ -1916,11 +1928,21 @@ class RealTimeMessagingManager extends ChangeNotifier {
currentContext,
listen: false,
);
_publishRoomRocketLaunchNoticeFromStatusPayload(
final launch = _buildRoomRocketLaunchFromStatusPayload(
rtcProvider: rtcProvider,
payload: data,
roomId: roomId,
);
if (_shouldHandleRoomRocketLaunchFromStatusPayload(
rtcProvider: rtcProvider,
payload: data,
launch: launch,
)) {
_handleCurrentVisibleRoomRocketLaunch(
rtcProvider: rtcProvider,
launch: launch,
);
}
unawaited(rtcProvider.refreshRoomRocketStatus(roomId: roomId));
}
@ -1948,16 +1970,10 @@ class RealTimeMessagingManager extends ChangeNotifier {
rtcProvider.shouldShowRoomVisualEffects &&
rtcProvider.isVoiceRoomRouteVisible;
if (isCurrentVisibleRoom) {
addMsg(
_buildRoomRocketLaunchNoticeMsg(
_firstNonBlank([
rtcProvider.currenRoom?.roomProfile?.roomProfile?.roomAccount,
launch.roomAccount,
]),
launch,
),
_handleCurrentVisibleRoomRocketLaunch(
rtcProvider: rtcProvider,
launch: launch,
);
rtcProvider.handleRoomRocketLaunchBroadcast(launch);
}
}
}
@ -1970,7 +1986,7 @@ class RealTimeMessagingManager extends ChangeNotifier {
void _handleVoiceRoomRocketRewardPopup(dynamic payload) {
final data = _broadcastPayloadMap(payload);
final roomId = _payloadText(data['roomId']);
final roomId = _payloadRoomId(data);
final currentRoomId = _currentVoiceRoomId();
final resolvedRoomId = roomId.isEmpty ? currentRoomId : roomId;
if (resolvedRoomId.isEmpty ||
@ -1981,13 +1997,45 @@ class RealTimeMessagingManager extends ChangeNotifier {
return;
}
final userIds = _payloadStringList(data['userIds']);
final currentUserId =
AccountStorage().getCurrentUser()?.userProfile?.id?.trim() ?? '';
if (currentUserId.isEmpty || !userIds.contains(currentUserId)) {
return;
final initialRoomRecords = _roomRocketRewardRecordsFromPayload(data);
unawaited(
_showRoomRocketRewardPopupDialog(
resolvedRoomId,
initialRoomRecords: initialRoomRecords,
),
);
}
void _handleCurrentVisibleRoomRocketLaunch({
required RealTimeCommunicationManager rtcProvider,
required RoomRocketLaunchBroadcastMessage launch,
}) {
final launchKey = _roomRocketLaunchNoticeKey(launch);
final looseLaunchKey = _roomRocketLaunchLooseKey(launch);
final now = DateTime.now().millisecondsSinceEpoch;
_pruneRecentRoomRocketLaunches(now);
final alreadyHandled =
(launchKey.isNotEmpty &&
_recentRoomRocketLaunchTimes.containsKey(launchKey)) ||
(looseLaunchKey.isNotEmpty &&
_recentRoomRocketLaunchLooseTimes.containsKey(looseLaunchKey));
if (!alreadyHandled && launchKey.isNotEmpty) {
_recentRoomRocketLaunchTimes[launchKey] = now;
if (looseLaunchKey.isNotEmpty) {
_recentRoomRocketLaunchLooseTimes[looseLaunchKey] = now;
}
addMsg(
_buildRoomRocketLaunchNoticeMsg(
_firstNonBlank([
rtcProvider.currenRoom?.roomProfile?.roomProfile?.roomAccount,
launch.roomAccount,
]),
launch,
),
);
rtcProvider.handleRoomRocketLaunchBroadcast(launch);
}
unawaited(_showRoomRocketRewardPopupDialog(resolvedRoomId));
unawaited(_showRoomRocketRewardPopupDialog(launch.roomId));
}
String _rocketIconUrlForLevel(int level) {
@ -2008,16 +2056,29 @@ class RealTimeMessagingManager extends ChangeNotifier {
return '';
}
Future<void> _showRoomRocketRewardPopupDialog(String roomId) async {
Future<void> _showRoomRocketRewardPopupDialog(
String roomId, {
int attempt = 0,
List<SCRoomRocketRewardRecordRes> initialRoomRecords =
const <SCRoomRocketRewardRecordRes>[],
}) async {
try {
final res = await SCChatRoomRepository().roomRocketRewardPopups(roomId);
if (res.records.isEmpty) {
if (res.records.isEmpty && initialRoomRecords.isEmpty) {
_scheduleRoomRocketRewardPopupRetry(
roomId,
attempt,
initialRoomRecords: initialRoomRecords,
);
return;
}
_roomRocketRewardPopupRetryTimer?.cancel();
_roomRocketRewardPopupRetryTimer = null;
final recordIds = res.records
.map((item) => item.id)
.where((item) => item.isNotEmpty)
.toList(growable: false);
final userProfileResolver = _roomRocketRewardUserProfileResolver();
SmartDialog.dismiss(tag: _roomRocketRewardDialogTag);
SmartDialog.show(
tag: _roomRocketRewardDialogTag,
@ -2030,6 +2091,8 @@ class RealTimeMessagingManager extends ChangeNotifier {
(_) => RoomRocketRewardDialogLoader(
roomId: roomId,
initialPopupRecords: res.records,
initialRoomRecords: initialRoomRecords,
userProfileResolver: userProfileResolver,
onClose: () {
SmartDialog.dismiss(tag: _roomRocketRewardDialogTag);
if (recordIds.isNotEmpty) {
@ -2040,7 +2103,83 @@ class RealTimeMessagingManager extends ChangeNotifier {
},
),
);
} catch (_) {}
} catch (_) {
_scheduleRoomRocketRewardPopupRetry(
roomId,
attempt,
initialRoomRecords: initialRoomRecords,
);
}
}
void _scheduleRoomRocketRewardPopupRetry(
String roomId,
int attempt, {
List<SCRoomRocketRewardRecordRes> initialRoomRecords =
const <SCRoomRocketRewardRecordRes>[],
}) {
if (attempt >= _roomRocketRewardPopupRetryDelays.length ||
!_isCurrentVoiceRoom(roomId)) {
return;
}
_roomRocketRewardPopupRetryTimer?.cancel();
_roomRocketRewardPopupRetryTimer = Timer(
_roomRocketRewardPopupRetryDelays[attempt],
() {
_roomRocketRewardPopupRetryTimer = null;
unawaited(
_showRoomRocketRewardPopupDialog(
roomId,
attempt: attempt + 1,
initialRoomRecords: initialRoomRecords,
),
);
},
);
}
RoomRocketRewardUserProfileResolver? _roomRocketRewardUserProfileResolver() {
final currentContext = context;
if (currentContext == null || !currentContext.mounted) {
return null;
}
try {
final rtcProvider = Provider.of<RealTimeCommunicationManager>(
currentContext,
listen: false,
);
return rtcProvider.roomRocketRewardUserProfileForRecord;
} catch (_) {
return null;
}
}
List<SCRoomRocketRewardRecordRes> _roomRocketRewardRecordsFromPayload(
Map<String, dynamic> payload,
) {
final candidates = [
payload['roomRecords'],
payload['roomRewardRecords'],
payload['room_reward_records'],
payload['allRecords'],
payload['allRewardRecords'],
payload['all_reward_records'],
payload['rewardRecords'],
payload['reward_records'],
payload['rewardRecordList'],
payload['recordList'],
payload['records'],
payload['list'],
payload['items'],
payload['rows'],
];
for (final candidate in candidates) {
final records = SCRoomRocketRewardRecordRes.listFromJson(candidate);
if (records.isNotEmpty) {
return records;
}
}
return const <SCRoomRocketRewardRecordRes>[];
}
SCFloatingMessage _buildRoomRocketLaunchFloatingMessage(
@ -2095,47 +2234,87 @@ class RealTimeMessagingManager extends ChangeNotifier {
);
}
void _publishRoomRocketLaunchNoticeFromStatusPayload({
String _roomRocketLaunchLooseKey(RoomRocketLaunchBroadcastMessage launch) {
if (launch.roomId.isEmpty) {
return '';
}
return ['rocket_launch_loose', launch.roomId, launch.safeLevel].join('|');
}
bool _shouldHandleRoomRocketLaunchFromStatusPayload({
required RealTimeCommunicationManager rtcProvider,
required Map<String, dynamic> 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<String, dynamic> payload,
required String roomId,
}) {
final previousPercent = _roomRocketStatusPercent(
rtcProvider.roomRocketStatus,
);
final currentPercent = _roomRocketPayloadPercent(payload);
if (previousPercent >= 99.5 || currentPercent < 99.5) {
return;
}
final groupId =
(rtcProvider.currenRoom?.roomProfile?.roomProfile?.roomAccount ?? '')
.trim();
if (groupId.isEmpty) {
return;
}
final roomProfile = rtcProvider.currenRoom?.roomProfile?.roomProfile;
final level = _roomRocketPayloadLevel(
payload,
rtcProvider.roomRocketStatus?.currentLevel ??
rtcProvider.roomRocketStatus?.level?.toInt() ??
1,
);
addMsg(
Msg(
groupId: groupId,
msg: _roomRocketPayloadNoticeSeconds(payload).toString(),
type: SCRoomMsgType.rocketLaunchNotice,
number: _roomRocketPayloadNoticeSeconds(payload),
rocketIconUrl: _firstNonBlank([
_rocketIconFromPayload(payload),
_rocketIconUrlForLevel(level),
]),
giftBatchId: _roomRocketLaunchNoticeKeyForValues(
roomId: roomId,
level: level,
roundNo: _payloadNum(payload['roundNo'])?.toInt() ?? 0,
launchNo: _payloadText(payload['launchNo']),
),
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']),
]),
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']),
);
}
@ -2160,7 +2339,9 @@ class RealTimeMessagingManager extends ChangeNotifier {
double _roomRocketPayloadPercent(Map<String, dynamic> payload) {
final value = _payloadNum(
payload['displayPercent'] ??
payload['display_percent'] ??
payload['energyPercent'] ??
payload['energy_percent'] ??
payload['percent'],
);
if (value != null) {
@ -2168,9 +2349,14 @@ class RealTimeMessagingManager extends ChangeNotifier {
final percent = raw > 0 && raw <= 1 ? raw * 100 : raw;
return percent.clamp(0, 100).toDouble();
}
final currentEnergy = _payloadNum(payload['currentEnergy']);
final currentEnergy = _payloadNum(
payload['currentEnergy'] ?? payload['current_energy'],
);
final maxEnergy = _payloadNum(
payload['maxEnergy'] ?? payload['needEnergy'],
payload['maxEnergy'] ??
payload['max_energy'] ??
payload['needEnergy'] ??
payload['need_energy'],
);
if (currentEnergy == null || maxEnergy == null || maxEnergy <= 0) {
return 0;
@ -2181,8 +2367,11 @@ class RealTimeMessagingManager extends ChangeNotifier {
int _roomRocketPayloadLevel(Map<String, dynamic> payload, int fallback) {
final value = _payloadNum(
payload['currentLevel'] ??
payload['current_level'] ??
payload['level'] ??
payload['rocketLevel'] ??
payload['rocket_level'] ??
payload['from_level'] ??
payload['fromLevel'],
);
return (value?.toInt() ?? fallback).clamp(1, 99).toInt();
@ -2192,8 +2381,12 @@ class RealTimeMessagingManager extends ChangeNotifier {
final seconds =
_payloadNum(
payload['countdownSeconds'] ??
payload['countdown_seconds'] ??
payload['launchDelaySeconds'] ??
payload['launch_delay_seconds'] ??
payload['delaySeconds'] ??
payload['delay_seconds'] ??
payload['duration_seconds'] ??
payload['durationSeconds'],
)?.toInt() ??
10;
@ -2206,6 +2399,7 @@ class RealTimeMessagingManager extends ChangeNotifier {
String _rocketIconFromPayload(Map<String, dynamic> payload) {
return _firstNonBlank([
_payloadAssetText(payload['rocketIconUrl']),
_payloadAssetText(payload['rocket_icon_url']),
_payloadAssetText(payload['rocketIcon']),
_payloadAssetText(payload['rocketUrl']),
_payloadAssetText(payload['rocketImageUrl']),
@ -2213,6 +2407,49 @@ class RealTimeMessagingManager extends ChangeNotifier {
]);
}
String _rocketAnimationFromPayload(Map<String, dynamic> 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<String, dynamic> payload) {
return _firstNonBlank([
_payloadText(payload['roomId']),
_payloadText(payload['room_id']),
_payloadText(payload['roomID']),
_payloadText(payload['voiceRoomId']),
_payloadText(payload['voice_room_id']),
_payloadText(payload['chatRoomId']),
_payloadText(payload['chat_room_id']),
_payloadText(_broadcastPayloadMap(payload['room'])['id']),
_payloadText(_broadcastPayloadMap(payload['room'])['roomId']),
_payloadText(_broadcastPayloadMap(payload['room'])['room_id']),
_payloadText(_broadcastPayloadMap(payload['roomInfo'])['id']),
_payloadText(_broadcastPayloadMap(payload['roomInfo'])['roomId']),
_payloadText(_broadcastPayloadMap(payload['roomInfo'])['room_id']),
]);
}
void _pruneRecentRoomRocketLaunches(int now) {
_recentRoomRocketLaunchTimes.removeWhere(
(_, timestamp) => now - timestamp > _roomRocketLaunchRecentTtlMs,
);
_recentRoomRocketLaunchLooseTimes.removeWhere(
(_, timestamp) => now - timestamp > _roomRocketLaunchLooseRecentTtlMs,
);
}
String _roomRocketLaunchNoticeKeyForValues({
required String roomId,
required int level,
@ -2398,37 +2635,35 @@ class RealTimeMessagingManager extends ChangeNotifier {
} else if (type == "ROCKET_ENERGY_LAUNCH") {
///
var fdata = data["data"];
final launchRoomId = _payloadText(fdata["roomId"]);
final payload = _broadcastPayloadMap(fdata);
final launchRoomId = _payloadRoomId(payload);
if (_isCurrentHiddenVoiceRoom(launchRoomId)) {
return;
}
final rocketIconUrl = _firstNonBlank([
_payloadText(fdata["rocketIconUrl"]),
_payloadText(fdata["rocketIcon"]),
_payloadText(fdata["rocketUrl"]),
_payloadText(fdata["rocketImageUrl"]),
_payloadText(fdata["giftUrl"]),
_payloadText(fdata["giftIcon"]),
_rocketIconFromPayload(payload),
_payloadAssetText(payload["giftUrl"]),
_payloadAssetText(payload["giftIcon"]),
]);
SCFloatingMessage msg = SCFloatingMessage(
type: 3,
roomId: launchRoomId,
rocketLevel: fdata["fromLevel"],
rocketLevel: _roomRocketPayloadLevel(payload, 1),
userAvatarUrl: _firstNonBlank([
_payloadText(fdata["roomCoverUrl"]),
_payloadText(fdata["roomCover"]),
_payloadText(fdata["roomAvatar"]),
_payloadText(fdata["roomIcon"]),
_payloadText(fdata["coverUrl"]),
_payloadText(fdata["cover"]),
_payloadText(fdata["userAvatar"]),
_payloadAssetText(payload["roomCoverUrl"]),
_payloadAssetText(payload["roomCover"]),
_payloadAssetText(payload["roomAvatar"]),
_payloadAssetText(payload["roomIcon"]),
_payloadAssetText(payload["coverUrl"]),
_payloadAssetText(payload["cover"]),
_payloadAssetText(payload["userAvatar"]),
]),
userName: _firstNonBlank([
_payloadText(fdata["roomName"]),
_payloadText(fdata["roomAccount"]),
_payloadText(fdata["nickname"]),
_payloadText(payload["roomName"]),
_payloadText(payload["roomAccount"]),
_payloadText(payload["nickname"]),
]),
userId: fdata["actualAccount"],
userId: _payloadText(payload["actualAccount"]),
giftUrl: rocketIconUrl,
fallbackUrl: rocketIconUrl,
priority: 1000,
@ -3167,6 +3402,10 @@ class RealTimeMessagingManager extends ChangeNotifier {
roomGiftMsgList.clear();
roomChatMsgList.clear();
_resetLuckGiftBurstQueue();
_recentRoomRocketLaunchTimes.clear();
_recentRoomRocketLaunchLooseTimes.clear();
_roomRocketRewardPopupRetryTimer?.cancel();
_roomRocketRewardPopupRetryTimer = null;
onNewMessageListenerGroupMap.forEach((k, v) {
v = null;
});

View File

@ -604,17 +604,41 @@ class SCRoomRocketKingUserRes {
factory SCRoomRocketKingUserRes.fromJson(dynamic json) {
final data = _asMap(json);
final profile = _asMap(data['userProfile'] ?? data['user']);
final profile = _asMap(
data['userProfile'] ??
data['user'] ??
data['userInfo'] ??
data['profile'],
);
return SCRoomRocketKingUserRes(
rank: _intValue(data['rank'] ?? data['ranking']),
userId: _stringValue(data['userId'] ?? profile['id']),
userAvatar: _stringValue(
data['userAvatar'] ??
data['avatar'] ??
data['avatarUrl'] ??
profile['userAvatar'] ??
profile['avatar'],
),
userAvatar: _firstAssetString([
data['userAvatar'],
data['userAvatarUrl'],
data['avatar'],
data['avatarUrl'],
data['headImg'],
data['headUrl'],
data['photo'],
data['portrait'],
data['profileImageUrl'],
data['profilePicture'],
data['avatarResource'],
data['userAvatarResource'],
profile['userAvatar'],
profile['userAvatarUrl'],
profile['avatar'],
profile['avatarUrl'],
profile['headImg'],
profile['headUrl'],
profile['photo'],
profile['portrait'],
profile['profileImageUrl'],
profile['profilePicture'],
profile['avatarResource'],
profile['userAvatarResource'],
]),
userNickname: _stringValue(
data['userNickname'] ??
data['nickname'] ??
@ -771,7 +795,7 @@ class SCRoomRocketRewardPopupRes {
factory SCRoomRocketRewardPopupRes.fromJson(dynamic json) {
final data = _asMap(json);
return SCRoomRocketRewardPopupRes(
records: SCRoomRocketRewardRecordRes.listFromJson(data['records']),
records: _rewardRecordList(data),
cursor: _intValue(data['cursor'], fallback: 1),
limit: _intValue(data['limit'], fallback: 10),
emptyReason: _stringValue(data['emptyReason']),
@ -804,7 +828,7 @@ class SCRoomRocketRewardRecordsRes {
factory SCRoomRocketRewardRecordsRes.fromJson(dynamic json) {
final data = _asMap(json);
return SCRoomRocketRewardRecordsRes(
records: SCRoomRocketRewardRecordRes.listFromJson(data['records']),
records: _rewardRecordList(data),
cursor: _intValue(data['cursor'], fallback: 1),
limit: _intValue(data['limit'], fallback: 10),
emptyReason: _stringValue(data['emptyReason']),
@ -855,39 +879,222 @@ class SCRoomRocketRewardRecordRes {
factory SCRoomRocketRewardRecordRes.fromJson(dynamic json) {
final data = _asMap(json);
final user = _asMap(data['user']);
final userInfo = _asMap(data['userInfo'] ?? data['user_info']);
final receiver = _asMap(data['receiver']);
final receiveUser = _asMap(data['receiveUser'] ?? data['receive_user']);
final acceptUser = _asMap(data['acceptUser'] ?? data['accept_user']);
final recipient = _asMap(
data['recipient'] ?? data['targetUser'] ?? data['target_user'],
);
final winner = _asMap(
data['winner'] ?? data['winnerUser'] ?? data['winner_user'],
);
final profile = _asMap(
_firstPayload([
data['userProfile'],
data['user_profile'],
user['userProfile'],
user['user_profile'],
user['profile'],
userInfo['userProfile'],
userInfo['user_profile'],
userInfo['profile'],
receiver['userProfile'],
receiver['user_profile'],
receiver['profile'],
receiveUser['userProfile'],
receiveUser['user_profile'],
receiveUser['profile'],
acceptUser['userProfile'],
acceptUser['user_profile'],
acceptUser['profile'],
recipient['userProfile'],
recipient['user_profile'],
recipient['profile'],
winner['userProfile'],
winner['user_profile'],
winner['profile'],
userInfo,
data['profile'],
user,
receiver,
receiveUser,
acceptUser,
recipient,
winner,
]),
);
return SCRoomRocketRewardRecordRes(
id: _stringValue(data['id']),
launchNo: _stringValue(data['launchNo']),
roomId: _stringValue(data['roomId']),
dayKey: _stringValue(data['dayKey']),
roundNo: _intValue(data['roundNo']),
launchNo: _stringValue(data['launchNo'] ?? data['launch_no']),
roomId: _stringValue(data['roomId'] ?? data['room_id']),
dayKey: _stringValue(data['dayKey'] ?? data['day_key']),
roundNo: _intValue(data['roundNo'] ?? data['round_no']),
level: _intValue(data['level']),
userId: _stringValue(data['userId']),
userAvatar: _stringValue(
data['userAvatar'] ??
data['avatar'] ??
data['avatarUrl'] ??
_asMap(data['userProfile'] ?? data['user'])['userAvatar'] ??
_asMap(data['userProfile'] ?? data['user'])['avatar'],
userId: _stringValue(
_firstPayload([
data['userId'],
data['user_id'],
data['uid'],
data['receiverUserId'],
data['receiveUserId'],
data['acceptUserId'],
data['recipientUserId'],
data['targetUserId'],
data['winnerUserId'],
data['receiver_user_id'],
data['receive_user_id'],
data['accept_user_id'],
data['recipient_user_id'],
data['target_user_id'],
data['winner_user_id'],
profile['id'],
profile['userId'],
profile['user_id'],
profile['uid'],
]),
),
userAvatar: _firstAssetString([
data['userAvatar'],
data['user_avatar'],
data['userAvatarUrl'],
data['user_avatar_url'],
data['receiverAvatar'],
data['receiver_avatar'],
data['receiverUserAvatar'],
data['receiver_user_avatar'],
data['receiveUserAvatar'],
data['receive_user_avatar'],
data['acceptUserAvatar'],
data['accept_user_avatar'],
data['recipientAvatar'],
data['recipient_avatar'],
data['targetUserAvatar'],
data['target_user_avatar'],
data['winnerAvatar'],
data['winner_avatar'],
data['avatar'],
data['avatarUrl'],
data['avatar_url'],
data['headImg'],
data['head_img'],
data['headUrl'],
data['head_url'],
data['headIcon'],
data['head_icon'],
data['photo'],
data['photoUrl'],
data['photo_url'],
data['portrait'],
data['profileImageUrl'],
data['profile_image_url'],
data['profilePicture'],
data['profile_picture'],
data['avatarResource'],
data['avatar_resource'],
data['userAvatarResource'],
data['user_avatar_resource'],
profile['userAvatar'],
profile['user_avatar'],
profile['userAvatarUrl'],
profile['user_avatar_url'],
profile['avatar'],
profile['avatarUrl'],
profile['avatar_url'],
profile['headImg'],
profile['head_img'],
profile['headUrl'],
profile['head_url'],
profile['headIcon'],
profile['head_icon'],
profile['photo'],
profile['photoUrl'],
profile['photo_url'],
profile['portrait'],
profile['profileImageUrl'],
profile['profile_image_url'],
profile['profilePicture'],
profile['profile_picture'],
profile['avatarResource'],
profile['avatar_resource'],
profile['userAvatarResource'],
profile['user_avatar_resource'],
profile['imageUrl'],
profile['image_url'],
profile['image'],
profile['iconUrl'],
profile['icon_url'],
profile['icon'],
]),
userNickname: _stringValue(
data['userNickname'] ??
data['user_nickname'] ??
data['userName'] ??
data['user_name'] ??
data['receiverNickname'] ??
data['receiver_nickname'] ??
data['receiveNickname'] ??
data['receive_nickname'] ??
data['acceptNickname'] ??
data['accept_nickname'] ??
data['recipientNickname'] ??
data['recipient_nickname'] ??
data['targetNickname'] ??
data['target_nickname'] ??
data['winnerNickname'] ??
data['winner_nickname'] ??
data['nickname'] ??
data['nickName'] ??
_asMap(data['userProfile'] ?? data['user'])['userNickname'] ??
_asMap(data['userProfile'] ?? data['user'])['nickname'] ??
_asMap(data['userProfile'] ?? data['user'])['nickName'],
data['nick_name'] ??
data['nick'] ??
data['displayName'] ??
data['display_name'] ??
data['name'] ??
profile['userNickname'] ??
profile['user_nickname'] ??
profile['userName'] ??
profile['user_name'] ??
profile['nickname'] ??
profile['nickName'] ??
profile['nick_name'] ??
profile['nick'] ??
profile['displayName'] ??
profile['display_name'] ??
profile['name'],
),
account: _stringValue(
data['account'] ??
_asMap(data['userProfile'] ?? data['user'])['account'],
_firstPayload([
data['account'],
data['actualAccount'],
data['receiverAccount'],
data['receiveAccount'],
data['acceptAccount'],
data['recipientAccount'],
data['targetAccount'],
data['winnerAccount'],
data['actual_account'],
data['receiver_account'],
data['receive_account'],
data['accept_account'],
data['recipient_account'],
data['target_account'],
data['winner_account'],
profile['account'],
profile['actualAccount'],
profile['actual_account'],
]),
),
rewardScene: _stringValue(data['rewardScene']),
rewardType: _stringValue(data['rewardType']),
rewardItemId: _stringValue(data['rewardItemId']),
rewardName: _stringValue(data['rewardName']),
rewardCover: _stringValue(data['rewardCover']),
rewardAmount: _rewardAmountValue(data['rewardAmount']) ?? 0,
rewardScene: _stringValue(data['rewardScene'] ?? data['reward_scene']),
rewardType: _stringValue(data['rewardType'] ?? data['reward_type']),
rewardItemId: _stringValue(
data['rewardItemId'] ?? data['reward_item_id'],
),
rewardName: _stringValue(data['rewardName'] ?? data['reward_name']),
rewardCover: _stringValue(data['rewardCover'] ?? data['reward_cover']),
rewardAmount:
_rewardAmountValue(data['rewardAmount'] ?? data['reward_amount']) ??
0,
rocketIconUrl: _firstAssetString([
data['rocket_icon_url'],
data['rocketIconUrl'],
@ -903,12 +1110,12 @@ class SCRoomRocketRewardRecordRes {
_asMap(data['config'])['rocketIconUrl'],
_asMap(data['config'])['rocketIcon'],
]),
expireDays: _dayValue(data['expireDays']),
grantStatus: _stringValue(data['grantStatus']),
popupStatus: _stringValue(data['popupStatus']),
grantTime: _stringValue(data['grantTime']),
createTime: _stringValue(data['createTime']),
errorMessage: _stringValue(data['errorMessage']),
expireDays: _dayValue(data['expireDays'] ?? data['expire_days']),
grantStatus: _stringValue(data['grantStatus'] ?? data['grant_status']),
popupStatus: _stringValue(data['popupStatus'] ?? data['popup_status']),
grantTime: _stringValue(data['grantTime'] ?? data['grant_time']),
createTime: _stringValue(data['createTime'] ?? data['create_time']),
errorMessage: _stringValue(data['errorMessage'] ?? data['error_message']),
);
}
@ -943,10 +1150,7 @@ class SCRoomRocketRewardRecordRes {
if (account.trim().isNotEmpty) {
return account;
}
if (userId.trim().isNotEmpty) {
return 'ID:$userId';
}
return 'You';
return 'User';
}
String get displayAmount {
@ -988,13 +1192,73 @@ class SCRoomRocketRewardRecordRes {
}
static List<SCRoomRocketRewardRecordRes> listFromJson(dynamic value) {
if (value is! Iterable) {
return const <SCRoomRocketRewardRecordRes>[];
}
return _rewardRecordList(value);
}
}
List<SCRoomRocketRewardRecordRes> _rewardRecordList(dynamic value) {
if (value is Iterable && value is! String) {
return value
.map(SCRoomRocketRewardRecordRes.fromJson)
.toList(growable: false);
}
final data = _asMap(value);
if (data.isEmpty) {
return const <SCRoomRocketRewardRecordRes>[];
}
if (_looksLikeRewardRecord(data)) {
return [SCRoomRocketRewardRecordRes.fromJson(data)];
}
final candidates = [
data['roomRecords'],
data['roomRewardRecords'],
data['room_reward_records'],
data['allRecords'],
data['allRewardRecords'],
data['all_reward_records'],
data['rewardRecords'],
data['reward_records'],
data['rewardRecordList'],
data['recordList'],
data['records'],
data['list'],
data['items'],
data['rows'],
data['content'],
data['data'],
];
for (final candidate in candidates) {
if (identical(candidate, value)) {
continue;
}
final records = _rewardRecordList(candidate);
if (records.isNotEmpty) {
return records;
}
}
return const <SCRoomRocketRewardRecordRes>[];
}
bool _looksLikeRewardRecord(Map<String, dynamic> data) {
const rewardKeys = [
'rewardScene',
'reward_scene',
'rewardType',
'reward_type',
'rewardItemId',
'reward_item_id',
'rewardName',
'reward_name',
'rewardCover',
'reward_cover',
'rewardAmount',
'reward_amount',
'grantStatus',
'grant_status',
'popupStatus',
'popup_status',
];
return rewardKeys.any(data.containsKey);
}
Map<String, dynamic> _asMap(dynamic value) {
@ -1032,14 +1296,32 @@ String _assetString(dynamic value) {
final map = _asMap(value);
return _firstAssetString([
map['sourceUrl'],
map['source_url'],
map['url'],
map['cdnUrl'],
map['cdn_url'],
map['ossUrl'],
map['oss_url'],
map['resourceUrl'],
map['resource_url'],
map['fileUrl'],
map['file_url'],
map['thumbUrl'],
map['thumb_url'],
map['thumbnail'],
map['small'],
map['medium'],
map['large'],
map['originUrl'],
map['origin_url'],
map['src'],
map['path'],
map['cover'],
map['imageUrl'],
map['image_url'],
map['image'],
map['svgUrl'],
map['svg_url'],
map['svg'],
map['value'],
]);
@ -1048,14 +1330,32 @@ String _assetString(dynamic value) {
if (map.isNotEmpty) {
return _firstAssetString([
map['sourceUrl'],
map['source_url'],
map['url'],
map['cdnUrl'],
map['cdn_url'],
map['ossUrl'],
map['oss_url'],
map['resourceUrl'],
map['resource_url'],
map['fileUrl'],
map['file_url'],
map['thumbUrl'],
map['thumb_url'],
map['thumbnail'],
map['small'],
map['medium'],
map['large'],
map['originUrl'],
map['origin_url'],
map['src'],
map['path'],
map['cover'],
map['imageUrl'],
map['image_url'],
map['image'],
map['svgUrl'],
map['svg_url'],
map['svg'],
map['value'],
]);

View File

@ -17,12 +17,15 @@ class RoomRocketLaunchBroadcastMessage {
factory RoomRocketLaunchBroadcastMessage.fromJson(dynamic json) {
final data = _asMap(json);
return RoomRocketLaunchBroadcastMessage(
roomId: _stringValue(data['roomId']),
roomAccount: _stringValue(data['roomAccount']),
roomName: _stringValue(data['roomName']),
roomId: _roomId(data),
roomAccount: _stringValue(data['roomAccount'] ?? data['room_account']),
roomName: _stringValue(data['roomName'] ?? data['room_name']),
roomCoverUrl: _roomCoverUrl(data),
roundNo: _intValue(data['roundNo']),
level: _intValue(data['level'], fallback: 1),
roundNo: _intValue(data['roundNo'] ?? data['round_no']),
level: _intValue(
data['level'] ?? data['rocketLevel'] ?? data['rocket_level'],
fallback: 1,
),
rocketIconUrl: _rocketIconUrl(data),
rocketAnimationUrl: _rocketAnimationUrl(data),
durationSeconds: _firstIntValue([
@ -36,9 +39,13 @@ class RoomRocketLaunchBroadcastMessage {
_asMap(data['config'])['countdownSeconds'],
_asMap(data['config'])['launchDelaySeconds'],
], fallback: 10),
triggerUser: RoomRocketLaunchTriggerUser.fromJson(data['triggerUser']),
triggerUserId: _stringValue(data['triggerUserId']),
launchNo: _stringValue(data['launchNo']),
triggerUser: RoomRocketLaunchTriggerUser.fromJson(
data['triggerUser'] ?? data['trigger_user'],
),
triggerUserId: _stringValue(
data['triggerUserId'] ?? data['trigger_user_id'],
),
launchNo: _stringValue(data['launchNo'] ?? data['launch_no']),
);
}
@ -88,9 +95,13 @@ class RoomRocketLaunchTriggerUser {
factory RoomRocketLaunchTriggerUser.fromJson(dynamic json) {
final data = _asMap(json);
return RoomRocketLaunchTriggerUser(
userId: _stringValue(data['userId']),
nickname: _stringValue(data['nickname']),
avatar: _stringValue(data['avatar']),
userId: _stringValue(data['userId'] ?? data['user_id']),
nickname: _stringValue(data['nickname'] ?? data['userNickname']),
avatar: _firstAssetString([
data['avatar'],
data['avatarUrl'],
data['userAvatar'],
]),
account: _stringValue(data['account']),
countryCode: _stringValue(data['countryCode']),
countryName: _stringValue(data['countryName']),
@ -122,13 +133,36 @@ String _stringValue(dynamic value) {
return value.toString().trim();
}
String _roomId(Map<String, dynamic> data) {
return _firstAssetString([
data['roomId'],
data['room_id'],
data['roomID'],
data['voiceRoomId'],
data['voice_room_id'],
data['chatRoomId'],
data['chat_room_id'],
_asMap(data['room'])['id'],
_asMap(data['room'])['roomId'],
_asMap(data['room'])['room_id'],
_asMap(data['roomInfo'])['id'],
_asMap(data['roomInfo'])['roomId'],
_asMap(data['roomInfo'])['room_id'],
]);
}
String _roomCoverUrl(Map<String, dynamic> data) {
return _firstAssetString([
data['roomCoverUrl'],
data['room_cover_url'],
data['roomCover'],
data['room_cover'],
data['roomAvatar'],
data['room_avatar'],
data['roomIcon'],
data['room_icon'],
data['coverUrl'],
data['cover_url'],
data['cover'],
_asMap(data['room'])['coverUrl'],
_asMap(data['room'])['cover'],
@ -162,11 +196,17 @@ String _rocketAnimationUrl(Map<String, dynamic> data) {
return _firstAssetString([
data['rocket_animation_url'],
data['rocketAnimationUrl'],
data['rocket_animation'],
data['rocketSvgaUrl'],
data['rocket_svga_url'],
data['animationUrl'],
data['animation_url'],
data['svgaUrl'],
data['svga_url'],
data['rocketLaunchSvgaUrl'],
data['rocket_launch_svga_url'],
data['launchAnimationUrl'],
data['launch_animation_url'],
data['launchAnimation'],
data['launchSvgaUrl'],
data['rocketAnimation'],

View File

@ -1,5 +1,6 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:yumi/shared/tools/sc_network_image_utils.dart';
import 'package:yumi/ui_kit/components/sc_rotating_dots_loading.dart';
int? _resolveMemCacheDimension(double? logicalSize) {
@ -39,10 +40,12 @@ class CustomCachedImage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final normalizedUrl = normalizeImageResourceUrl(imageUrl);
return ClipRRect(
borderRadius: BorderRadius.circular(borderRadius),
child: CachedNetworkImage(
imageUrl: imageUrl,
imageUrl: normalizedUrl,
httpHeaders: buildNetworkImageHeaders(normalizedUrl),
width: width,
height: height,
memCacheWidth: _resolveMemCacheDimension(width),

View File

@ -151,16 +151,12 @@ class _FloatingRoomRocketScreenWidgetState
duration: Duration(milliseconds: 350),
onDebounce: () {
final roomId = widget.message.roomId?.trim() ?? '';
final currentContext = navigatorKey.currentState?.context;
if (roomId.isEmpty ||
currentContext == null ||
!currentContext.mounted ||
SCRoomUtils.isCurrentRoom(roomId, currentContext)) {
final currentContext =
navigatorKey.currentState?.context ?? context;
if (roomId.isEmpty || !currentContext.mounted) {
return;
}
if (roomId.isNotEmpty) {
SCRoomUtils.goRoom(roomId, currentContext, fromFloting: true);
}
SCRoomUtils.goRoom(roomId, currentContext, fromFloting: true);
},
);
},
@ -209,7 +205,7 @@ class _FloatingRoomRocketScreenWidgetState
Positioned(
left: 65.w,
top: 9.w,
width: 126.w,
width: 112.w,
child: Text(
roomName,
maxLines: 1,
@ -227,7 +223,7 @@ class _FloatingRoomRocketScreenWidgetState
Positioned(
left: 65.w,
top: 31.w,
width: 126.w,
width: 112.w,
child: Text(
SCAppLocalizations.of(context)?.launchRocket ??
'Launch Rocket',
@ -244,14 +240,14 @@ class _FloatingRoomRocketScreenWidgetState
),
),
Positioned(
left: 201.w,
top: 8.w,
width: 40.w,
height: 40.w,
left: 191.w,
top: -2.w,
width: 60.w,
height: 60.w,
child: _RocketIcon(url: rocketIconUrl),
),
Positioned(
left: 249.w,
left: 262.w,
top: 14.w,
width: 42.w,
height: 28.w,
@ -315,7 +311,7 @@ class _RocketIcon extends StatelessWidget {
],
),
child: Padding(
padding: EdgeInsets.all(5.w),
padding: EdgeInsets.all(7.5.w),
child:
url.trim().isEmpty
? Image.asset(
@ -324,8 +320,8 @@ class _RocketIcon extends StatelessWidget {
)
: netImage(
url: url,
width: 30.w,
height: 30.w,
width: 45.w,
height: 45.w,
fit: BoxFit.contain,
noDefaultImg: true,
errorWidget: Image.asset(

View File

@ -7,6 +7,23 @@ import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_note_dialog.dart';
import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_record_dialog.dart';
import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_reward_dialog.dart';
typedef RoomRocketRewardUserProfileResolver =
RoomRocketRewardUserProfile? Function(SCRoomRocketRewardRecordRes record);
class RoomRocketRewardUserProfile {
const RoomRocketRewardUserProfile({
required this.userId,
required this.account,
required this.nickname,
required this.avatarUrl,
});
final String userId;
final String account;
final String nickname;
final String avatarUrl;
}
class RoomRocketApiMapper {
const RoomRocketApiMapper._();
@ -257,25 +274,93 @@ class RoomRocketApiMapper {
}
static List<RoomRocketRewardWinner> winnersFromRecords(
List<SCRoomRocketRewardRecordRes> records,
) {
List<SCRoomRocketRewardRecordRes> records, {
RoomRocketRewardUserProfileResolver? userProfileResolver,
}) {
final grouped = <String, List<SCRoomRocketRewardRecordRes>>{};
for (final record in records) {
final key = record.userId.isNotEmpty ? record.userId : record.launchNo;
final key = _rewardWinnerKey(record);
grouped.putIfAbsent(key, () => []).add(record);
}
return grouped.entries
.map((entry) {
final first = entry.value.first;
final profile = userProfileResolver?.call(first);
return RoomRocketRewardWinner(
nickname: first.displayName,
avatarUrl: first.userAvatar.isEmpty ? null : first.userAvatar,
nickname: _rewardWinnerName(entry.value, profile),
avatarUrl: _rewardWinnerAvatar(entry.value, profile),
rewards: rewardItemsFromRecords(entry.value),
);
})
.toList(growable: false);
}
static String _rewardWinnerName(
List<SCRoomRocketRewardRecordRes> records,
RoomRocketRewardUserProfile? profile,
) {
final profileName = profile?.nickname.trim() ?? '';
if (profileName.isNotEmpty) {
return profileName;
}
for (final record in records) {
final nickname = record.userNickname.trim();
if (nickname.isNotEmpty) {
return nickname;
}
}
for (final record in records) {
final account = record.account.trim();
if (account.isNotEmpty) {
return account;
}
}
return records.isEmpty ? 'User' : records.first.displayName;
}
static String? _rewardWinnerAvatar(
List<SCRoomRocketRewardRecordRes> records,
RoomRocketRewardUserProfile? profile,
) {
final profileAvatar = profile?.avatarUrl.trim() ?? '';
if (profileAvatar.isNotEmpty) {
return profileAvatar;
}
for (final record in records) {
final avatar = record.userAvatar.trim();
if (avatar.isNotEmpty) {
return avatar;
}
}
return null;
}
static String _rewardWinnerKey(SCRoomRocketRewardRecordRes record) {
final userId = record.userId.trim();
if (userId.isNotEmpty) {
return 'user:$userId';
}
final account = record.account.trim();
if (account.isNotEmpty) {
return 'account:$account';
}
final profileKey = [
record.userNickname.trim(),
record.userAvatar.trim(),
].where((item) => item.isNotEmpty).join('|');
if (profileKey.isNotEmpty) {
return 'profile:$profileKey';
}
final recordKey = [
record.launchNo.trim(),
record.id.trim(),
].where((item) => item.isNotEmpty).join('|');
if (recordKey.isNotEmpty) {
return 'record:$recordKey';
}
return 'record:${identityHashCode(record)}';
}
static List<RoomRocketRecordItem> recordItemsFromRecords(
List<SCRoomRocketRewardRecordRes> records,
) {

View File

@ -16,12 +16,16 @@ class RoomRocketRewardDialogLoader extends StatefulWidget {
required this.roomId,
this.selectedReward,
this.initialPopupRecords = const <SCRoomRocketRewardRecordRes>[],
this.initialRoomRecords = const <SCRoomRocketRewardRecordRes>[],
this.userProfileResolver,
this.onClose,
});
final String roomId;
final RoomRocketRewardItem? selectedReward;
final List<SCRoomRocketRewardRecordRes> initialPopupRecords;
final List<SCRoomRocketRewardRecordRes> initialRoomRecords;
final RoomRocketRewardUserProfileResolver? userProfileResolver;
final VoidCallback? onClose;
@override
@ -47,8 +51,14 @@ class _RoomRocketRewardDialogLoaderState
if (widget.initialPopupRecords.isNotEmpty) {
_popupRecords.addAll(widget.initialPopupRecords);
_rewards = RoomRocketApiMapper.rewardItemsFromRecords(_popupRecords);
_winners = RoomRocketApiMapper.winnersFromRecords(_popupRecords);
}
if (widget.initialRoomRecords.isNotEmpty) {
_roomRecords.addAll(widget.initialRoomRecords);
}
_winners = RoomRocketApiMapper.winnersFromRecords(
_visibleRoomRewardRecords(),
userProfileResolver: widget.userProfileResolver,
);
unawaited(_load(reset: true));
}
@ -89,13 +99,25 @@ class _RoomRocketRewardDialogLoaderState
..addAll(nextPopupRecords);
_roomRecords
..clear()
..addAll(nextRoomRecords);
..addAll(
_mergeRewardRecords([
...widget.initialRoomRecords,
...nextRoomRecords,
]),
);
} else {
_roomRecords.addAll(nextRoomRecords);
final mergedRoomRecords = _mergeRewardRecords([
..._roomRecords,
...nextRoomRecords,
]);
_roomRecords
..clear()
..addAll(mergedRoomRecords);
}
_rewards = RoomRocketApiMapper.rewardItemsFromRecords(_popupRecords);
_winners = RoomRocketApiMapper.winnersFromRecords(
_visibleRoomRewardRecords(),
userProfileResolver: widget.userProfileResolver,
);
_hasMore = nextRoomRecords.length >= _rewardDialogPageSize;
if (_hasMore) {
@ -108,6 +130,7 @@ class _RoomRocketRewardDialogLoaderState
'loaded cursor=$requestCursor '
'popupCount=${nextPopupRecords.length} '
'popupTotal=${_popupRecords.length} '
'initialRoomCount=${widget.initialRoomRecords.length} '
'roomCount=${nextRoomRecords.length} '
'roomTotal=${_roomRecords.length} '
'visibleRoomTotal=${_visibleRoomRewardRecords().length} '
@ -181,6 +204,20 @@ class _RoomRocketRewardDialogLoaderState
return matched.isEmpty ? _roomRecords : matched;
}
List<SCRoomRocketRewardRecordRes> _mergeRewardRecords(
List<SCRoomRocketRewardRecordRes> records,
) {
final result = <SCRoomRocketRewardRecordRes>[];
final seen = <String>{};
for (final record in records) {
final id = record.id.trim();
if (id.isEmpty || seen.add(id)) {
result.add(record);
}
}
return result;
}
@override
Widget build(BuildContext context) {
return RoomRocketRewardDialog(

View File

@ -62,16 +62,17 @@ void main() {
test('extracts launch pag urls from nested resource payloads', () {
final launch = RoomRocketLaunchBroadcastMessage.fromJson({
'roomId': 'room-1',
'rocketAnimationResource': {
'sourceUrl': 'https://cdn.example.com/rocket/launch.pag',
},
'rocketIcon': {'url': 'https://cdn.example.com/rocket/icon.png'},
'room': {
'id': 'room-1',
'cover': {'url': 'https://cdn.example.com/room/cover.png'},
},
});
expect(launch.roomId, 'room-1');
expect(
launch.rocketAnimationUrl,
'https://cdn.example.com/rocket/launch.pag',
@ -79,4 +80,36 @@ void main() {
expect(launch.rocketIconUrl, 'https://cdn.example.com/rocket/icon.png');
expect(launch.roomCoverUrl, 'https://cdn.example.com/room/cover.png');
});
test('extracts launch data from snake case broadcast payloads', () {
final launch = RoomRocketLaunchBroadcastMessage.fromJson({
'room_id': 'room-1',
'room_account': '10001',
'room_name': 'Rocket Room',
'round_no': 3,
'rocket_level': 2,
'launch_no': 'launch-3',
'rocket_animation_url': 'https://cdn.example.com/rocket/launch.pag',
'rocket_icon_url': 'https://cdn.example.com/rocket/icon.png',
'trigger_user': {
'user_id': 'u1',
'userAvatar': {'sourceUrl': 'https://cdn.example.com/avatar.png'},
},
});
expect(launch.isValid, isTrue);
expect(launch.roomId, 'room-1');
expect(launch.roomAccount, '10001');
expect(launch.roomName, 'Rocket Room');
expect(launch.roundNo, 3);
expect(launch.safeLevel, 2);
expect(launch.launchNo, 'launch-3');
expect(
launch.rocketAnimationUrl,
'https://cdn.example.com/rocket/launch.pag',
);
expect(launch.rocketIconUrl, 'https://cdn.example.com/rocket/icon.png');
expect(launch.triggerUser.userId, 'u1');
expect(launch.triggerUser.avatar, 'https://cdn.example.com/avatar.png');
});
}

View File

@ -175,6 +175,117 @@ void main() {
expect(recordRewards.single.displayText, '7d');
});
test('parses rocket reward winner avatars from nested resource payloads', () {
final winners = RoomRocketApiMapper.winnersFromRecords([
SCRoomRocketRewardRecordRes.fromJson({
'id': 'r1',
'userId': 'u1',
'userInfo': {
'nickname': 'winner',
'avatar': {'sourceUrl': 'https://example.com/avatar.png'},
},
'rewardType': 'GOLD',
'rewardAmount': 100,
}),
]);
expect(winners.single.nickname, 'winner');
expect(winners.single.avatarUrl, 'https://example.com/avatar.png');
});
test('parses rocket reward winner profile from receiver aliases', () {
final record = SCRoomRocketRewardRecordRes.fromJson({
'id': 'r1',
'acceptUserId': 'u1',
'acceptNickname': 'winner',
'acceptUserAvatar': {'cdn_url': 'https://example.com/avatar.png'},
'rewardType': 'GOLD',
'rewardAmount': 100,
});
final winners = RoomRocketApiMapper.winnersFromRecords([record]);
expect(record.userId, 'u1');
expect(record.displayName, 'winner');
expect(winners.single.nickname, 'winner');
expect(winners.single.avatarUrl, 'https://example.com/avatar.png');
});
test('hydrates rocket reward winners from room user profiles', () {
final records = [
SCRoomRocketRewardRecordRes.fromJson({
'id': 'r1',
'userId': '2042274349',
'rewardType': 'GOLD',
'rewardAmount': 100,
}),
];
final winners = RoomRocketApiMapper.winnersFromRecords(
records,
userProfileResolver:
(_) => const RoomRocketRewardUserProfile(
userId: 'u1',
account: '2042274349',
nickname: 'winner',
avatarUrl: 'https://example.com/avatar.png',
),
);
expect(records.single.displayName, 'User');
expect(winners.single.nickname, 'winner');
expect(winners.single.avatarUrl, 'https://example.com/avatar.png');
});
test('keeps nested rocket reward users separated by profile id', () {
final winners = RoomRocketApiMapper.winnersFromRecords([
SCRoomRocketRewardRecordRes.fromJson({
'id': 'r1',
'launchNo': 'launch-1',
'userInfo': {'id': 'u1', 'nickname': 'winner 1'},
'rewardType': 'GOLD',
'rewardAmount': 100,
}),
SCRoomRocketRewardRecordRes.fromJson({
'id': 'r2',
'launchNo': 'launch-1',
'userInfo': {'id': 'u2', 'nickname': 'winner 2'},
'rewardType': 'GOLD',
'rewardAmount': 100,
}),
]);
expect(winners, hasLength(2));
expect(winners.map((item) => item.nickname), ['winner 1', 'winner 2']);
});
test('parses room reward records from alternate response keys', () {
final records =
SCRoomRocketRewardRecordsRes.fromJson({
'room_reward_records': [
{
'id': 'r1',
'launch_no': 'launch-1',
'room_id': 'room-1',
'user_info': {'id': 'u1', 'user_nickname': 'winner 1'},
'reward_type': 'GOLD',
'reward_amount': 100,
},
{
'id': 'r2',
'launch_no': 'launch-1',
'room_id': 'room-1',
'user_info': {'id': 'u2', 'user_nickname': 'winner 2'},
'reward_type': 'GOLD',
'reward_amount': 200,
},
],
}).records;
final winners = RoomRocketApiMapper.winnersFromRecords(records);
expect(records.map((item) => item.userId), ['u1', 'u2']);
expect(winners, hasLength(2));
expect(winners.last.rewards.single.amount, '200');
});
test('parses rocket level contents response from go api', () {
final contents = SCRoomRocketLevelContentsRes.fromJson({
'configured': true,