This commit is contained in:
roxy 2026-05-16 00:59:32 +08:00
parent 95dcd7e625
commit ccb5579a85
6 changed files with 258 additions and 35 deletions

View File

@ -120,6 +120,7 @@ class RealTimeMessagingManager extends ChangeNotifier {
static const int _luckyGiftBurstPlaybackWatchdogMs = 8000;
static const int _roomRocketLaunchRecentTtlMs = 90000;
static const int _roomRocketLaunchLooseRecentTtlMs = 12000;
static const int _roomRocketRegionBroadcastRecentTtlMs = 90000;
static const List<Duration> _roomRocketRewardPopupRetryDelays = [
Duration(milliseconds: 700),
Duration(seconds: 2),
@ -235,6 +236,8 @@ class RealTimeMessagingManager extends ChangeNotifier {
final Map<String, int> _recentLuckGiftPushEventTimes = <String, int>{};
final Map<String, int> _recentRoomRocketLaunchTimes = <String, int>{};
final Map<String, int> _recentRoomRocketLaunchLooseTimes = <String, int>{};
final Map<String, int> _recentRoomRocketRegionBroadcastTimes =
<String, int>{};
Timer? _roomRocketRewardPopupRetryTimer;
int get currentLuckGiftBurstPlaybackToken => _luckGiftPushPlaybackToken;
String? roomRedPacketBroadcastGroupId;
@ -1936,16 +1939,19 @@ class RealTimeMessagingManager extends ChangeNotifier {
payload: data,
roomId: roomId,
);
if (isCurrentVisibleRoom &&
_shouldHandleRoomRocketLaunchFromStatusPayload(
final shouldHandleLaunch = _shouldHandleRoomRocketLaunchFromStatusPayload(
rtcProvider: rtcProvider,
payload: data,
launch: launch,
);
if (shouldHandleLaunch) {
_publishRoomRocketRegionBroadcastIfNeeded(launch);
if (isCurrentVisibleRoom) {
_handleCurrentVisibleRoomRocketLaunch(
rtcProvider: rtcProvider,
payload: data,
launch: launch,
)) {
_handleCurrentVisibleRoomRocketLaunch(
rtcProvider: rtcProvider,
launch: launch,
);
);
}
}
unawaited(rtcProvider.refreshRoomRocketStatus(roomId: roomId));
}
@ -1977,6 +1983,9 @@ class RealTimeMessagingManager extends ChangeNotifier {
rtcProvider.shouldShowRoomVisualEffects &&
rtcProvider.isVoiceRoomRouteVisible;
if (isCurrentVisibleRoom) {
if (!isRegionBroadcast) {
_publishRoomRocketRegionBroadcastIfNeeded(launch);
}
_handleCurrentVisibleRoomRocketLaunch(
rtcProvider: rtcProvider,
launch: launch,
@ -1986,6 +1995,7 @@ class RealTimeMessagingManager extends ChangeNotifier {
}
if (isCurrentRoom && !isCurrentVisibleRoom && !isRegionBroadcast) {
_publishRoomRocketRegionBroadcastIfNeeded(launch);
return;
}
OverlayManager().addMessage(
@ -2056,6 +2066,47 @@ class RealTimeMessagingManager extends ChangeNotifier {
unawaited(_showRoomRocketRewardPopupDialog(launch.roomId));
}
void _publishRoomRocketRegionBroadcastIfNeeded(
RoomRocketLaunchBroadcastMessage launch,
) {
if (!launch.isValid ||
!_shouldCurrentUserPublishRoomRocketRegionBroadcast(launch)) {
return;
}
final key = _roomRocketLaunchNoticeKey(launch);
if (key.isEmpty) {
return;
}
final now = DateTime.now().millisecondsSinceEpoch;
_recentRoomRocketRegionBroadcastTimes.removeWhere(
(_, timestamp) => now - timestamp > _roomRocketRegionBroadcastRecentTtlMs,
);
if (_recentRoomRocketRegionBroadcastTimes.containsKey(key)) {
return;
}
_recentRoomRocketRegionBroadcastTimes[key] = now;
unawaited(sendRoomRocketLaunchBroadcast(launch));
}
bool _shouldCurrentUserPublishRoomRocketRegionBroadcast(
RoomRocketLaunchBroadcastMessage launch,
) {
final currentUser = AccountStorage().getCurrentUser()?.userProfile;
final currentKeys = <String>{
currentUser?.id?.trim() ?? '',
currentUser?.account?.trim() ?? '',
}..removeWhere((item) => item.isEmpty);
final triggerKeys = <String>{
launch.triggerUserId.trim(),
launch.triggerUser.userId.trim(),
launch.triggerUser.account.trim(),
}..removeWhere((item) => item.isEmpty);
if (triggerKeys.isNotEmpty) {
return currentKeys.any(triggerKeys.contains);
}
return true;
}
String _rocketIconUrlForLevel(int level) {
final currentContext = context;
if (currentContext == null || !currentContext.mounted) {
@ -2228,10 +2279,7 @@ class RealTimeMessagingManager extends ChangeNotifier {
type: 3,
roomId: launch.roomId,
rocketLevel: launch.safeLevel,
userAvatarUrl: _firstNonBlank([
launch.roomCoverUrl,
launch.triggerUser.avatar,
]),
userAvatarUrl: launch.roomCoverUrl,
userName: _firstNonBlank([launch.roomName, launch.roomAccount]),
userId: _firstNonBlank([
launch.triggerUser.account,
@ -2334,6 +2382,18 @@ class RealTimeMessagingManager extends ChangeNotifier {
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,
@ -3377,6 +3437,81 @@ class RealTimeMessagingManager extends ChangeNotifier {
}
}
Future<void> sendRoomRocketLaunchBroadcast(
RoomRocketLaunchBroadcastMessage launch,
) async {
if (!launch.isValid) {
return;
}
var regionGroupId = roomRedPacketBroadcastGroupId?.trim() ?? '';
if (regionGroupId.isEmpty) {
await syncRoomRedPacketBroadcastGroup();
regionGroupId = roomRedPacketBroadcastGroupId?.trim() ?? '';
}
if (regionGroupId.isEmpty) {
return;
}
final message = <String, dynamic>{
'type': SCRoomMsgType.voiceRoomRocketLaunchBroadcast,
'data': _roomRocketLaunchBroadcastPayload(launch),
};
await _sendRoomRocketLaunchCustomMessage(
regionGroupId,
jsonEncode(message),
);
}
Map<String, dynamic> _roomRocketLaunchBroadcastPayload(
RoomRocketLaunchBroadcastMessage launch,
) {
final launchNo = _firstNonBlank([
launch.launchNo,
_roomRocketLaunchNoticeKey(launch),
]);
return <String, dynamic>{
'roomId': launch.roomId,
'roomAccount': launch.roomAccount,
'roomName': launch.roomName,
'roomCoverUrl': launch.roomCoverUrl,
'roundNo': launch.roundNo,
'level': launch.safeLevel,
'rocketLevel': launch.safeLevel,
'rocketIconUrl': launch.rocketIconUrl,
'rocketAnimationUrl': launch.rocketAnimationUrl,
'durationSeconds': launch.durationSeconds,
'countdownSeconds': launch.durationSeconds,
'triggerUserId': launch.triggerUserId,
'triggerUser': <String, dynamic>{
'userId': launch.triggerUser.userId,
'nickname': launch.triggerUser.nickname,
'avatar': launch.triggerUser.avatar,
'account': launch.triggerUser.account,
'countryCode': launch.triggerUser.countryCode,
'countryName': launch.triggerUser.countryName,
},
'launchNo': launchNo,
};
}
Future<void> _sendRoomRocketLaunchCustomMessage(
String groupId,
String data,
) async {
try {
final textMsg = await TencentImSDKPlugin.v2TIMManager
.getMessageManager()
.createCustomMessage(data: data);
if (textMsg.code != 0 || textMsg.data?.id == null) {
return;
}
await TencentImSDKPlugin.v2TIMManager.getMessageManager().sendMessage(
id: textMsg.data!.id!,
groupID: groupId,
receiver: '',
);
} catch (_) {}
}
Future<void> _sendRoomRedPacketCustomMessage(
String groupId,
String data,
@ -3453,6 +3588,7 @@ class RealTimeMessagingManager extends ChangeNotifier {
_resetLuckGiftBurstQueue();
_recentRoomRocketLaunchTimes.clear();
_recentRoomRocketLaunchLooseTimes.clear();
_recentRoomRocketRegionBroadcastTimes.clear();
_roomRocketRewardPopupRetryTimer?.cancel();
_roomRocketRewardPopupRetryTimer = null;
onNewMessageListenerGroupMap.forEach((k, v) {

View File

@ -167,6 +167,9 @@ String _roomCoverUrl(Map<String, dynamic> data) {
_asMap(data['room'])['coverUrl'],
_asMap(data['room'])['cover'],
_asMap(data['room'])['avatar'],
_asMap(data['roomInfo'])['coverUrl'],
_asMap(data['roomInfo'])['cover'],
_asMap(data['roomInfo'])['avatar'],
]);
}

View File

@ -492,12 +492,29 @@ class OverlayManager {
if (previousMessage != null) {
visualTop +=
_stackedVisualHeightFor(previousMessage) + _stackedFloatingGap;
} else if (_shouldReserveStackedSlot(message, key)) {
visualTop += _stackedReservedVisualHeightFor(key) + _stackedFloatingGap;
}
}
final wrapperTop = visualTop - _stackedInternalTopFor(message);
return wrapperTop < 0 ? 0 : wrapperTop;
}
bool _shouldReserveStackedSlot(SCFloatingMessage message, int slotKey) {
return _homeRootTabsVisible &&
_isRegionRoomRocketMessage(message) &&
slotKey == 4;
}
double _stackedReservedVisualHeightFor(int slotKey) {
switch (slotKey) {
case 4:
return 66;
default:
return 0;
}
}
double _stackedVisualHeightFor(SCFloatingMessage message) {
switch (message.type) {
case 0:

View File

@ -289,6 +289,8 @@ class RoomRocketApiMapper {
return RoomRocketRewardWinner(
nickname: _rewardWinnerName(entry.value, profile),
avatarUrl: _rewardWinnerAvatar(entry.value, profile),
isTop1: _hasTop1Reward(entry.value),
isIgniter: _hasIgniteReward(entry.value),
rewards: rewardItemsFromRecords(entry.value),
);
})
@ -335,6 +337,33 @@ class RoomRocketApiMapper {
return null;
}
static bool _hasTop1Reward(List<SCRoomRocketRewardRecordRes> records) {
return records.any((record) {
final scene = _normalizedRewardScene(record.rewardScene);
return scene.contains('TOP1') ||
scene.contains('TOPONE') ||
scene.contains('RANK1') ||
scene.contains('RANKONE') ||
scene.contains('FIRST') ||
scene.contains('KING');
});
}
static bool _hasIgniteReward(List<SCRoomRocketRewardRecordRes> records) {
return records.any((record) {
final scene = _normalizedRewardScene(record.rewardScene);
return scene.contains('IGNITE') ||
scene.contains('SETOFF') ||
scene.contains('LAUNCH') ||
scene.contains('TRIGGER') ||
scene.contains('LIGHT');
});
}
static String _normalizedRewardScene(String scene) {
return scene.trim().toUpperCase().replaceAll(RegExp(r'[\s_\-]+'), '');
}
static String _rewardWinnerKey(SCRoomRocketRewardRecordRes record) {
final userId = record.userId.trim();
if (userId.isNotEmpty) {

View File

@ -192,6 +192,8 @@ class RoomRocketRewardWinner {
this.avatarAsset,
this.avatarUrl,
this.frameAsset,
this.isTop1 = false,
this.isIgniter = false,
this.rewards = const <RoomRocketRewardItem>[],
});
@ -199,6 +201,8 @@ class RoomRocketRewardWinner {
final String? avatarAsset;
final String? avatarUrl;
final String? frameAsset;
final bool isTop1;
final bool isIgniter;
final List<RoomRocketRewardItem> rewards;
RoomRocketRewardWinner withRewards(List<RoomRocketRewardItem> nextRewards) {
@ -207,6 +211,8 @@ class RoomRocketRewardWinner {
avatarAsset: avatarAsset,
avatarUrl: avatarUrl,
frameAsset: frameAsset,
isTop1: isTop1,
isIgniter: isIgniter,
rewards: nextRewards,
);
}
@ -442,11 +448,7 @@ class _RewardDialogCanvas extends StatelessWidget {
if (index >= winners.length) {
return _LoadMoreRow(scale: scale);
}
return _WinnerRow(
scale: scale,
winner: winners[index],
showRocketBadge: index == 0,
);
return _WinnerRow(scale: scale, winner: winners[index]);
},
),
),
@ -595,15 +597,10 @@ class _RewardDetailCard extends StatelessWidget {
}
class _WinnerRow extends StatelessWidget {
const _WinnerRow({
required this.scale,
required this.winner,
required this.showRocketBadge,
});
const _WinnerRow({required this.scale, required this.winner});
final double scale;
final RoomRocketRewardWinner winner;
final bool showRocketBadge;
@override
Widget build(BuildContext context) {
@ -626,21 +623,36 @@ class _WinnerRow extends StatelessWidget {
height: 39 * scale,
child: _WinnerAvatar(scale: scale, winner: winner),
),
if (showRocketBadge)
if (winner.isTop1)
Positioned(
left: 40 * scale,
top: 26 * scale,
left: 17 * scale,
top: 39 * scale,
width: 39 * scale,
height: 9 * scale,
child: Text(
'top1',
textScaler: TextScaler.noScaling,
maxLines: 1,
overflow: TextOverflow.visible,
textAlign: TextAlign.center,
style: TextStyle(
color: const Color(0xFF75F3CB),
fontSize: 7 * scale,
height: 1,
fontWeight: FontWeight.w600,
decoration: TextDecoration.none,
),
),
),
if (winner.isIgniter)
Positioned(
left: 45 * scale,
top: -1 * scale,
width: 14 * scale,
height: 14 * scale,
child: ClipRRect(
borderRadius: BorderRadius.circular(3 * scale),
child: Transform.rotate(
angle: math.pi / 4,
child: Image.asset(
_RewardDialogAssets.rocketBody,
fit: BoxFit.cover,
),
),
child: Image.asset(
_RewardDialogAssets.rocketBody,
fit: BoxFit.contain,
),
),
Positioned(

View File

@ -267,6 +267,32 @@ void main() {
expect(winners.single.avatarUrl, 'https://example.com/avatar.png');
});
test('marks rocket reward winners by reward scene', () {
final winners = RoomRocketApiMapper.winnersFromRecords([
SCRoomRocketRewardRecordRes.fromJson({
'id': 'r1',
'userId': 'top-user',
'userNickname': 'top winner',
'rewardScene': 'TOP1',
'rewardType': 'GOLD',
'rewardAmount': 100,
}),
SCRoomRocketRewardRecordRes.fromJson({
'id': 'r2',
'userId': 'ignite-user',
'userNickname': 'igniter',
'rewardScene': 'SET_OFF',
'rewardType': 'PROPS',
'rewardAmount': 1,
}),
]);
expect(winners.first.isTop1, isTrue);
expect(winners.first.isIgniter, isFalse);
expect(winners.last.isTop1, isFalse);
expect(winners.last.isIgniter, isTrue);
});
test('keeps nested rocket reward users separated by profile id', () {
final winners = RoomRocketApiMapper.winnersFromRecords([
SCRoomRocketRewardRecordRes.fromJson({