buf fxi
This commit is contained in:
parent
27f4e75cb8
commit
44e10028b9
@ -21,7 +21,6 @@ import 'package:yumi/shared/data_sources/sources/remote/net/api.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository_imp.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/login_res.dart';
|
||||
import 'package:yumi/shared/tools/sc_cp_gift_relation_utils.dart';
|
||||
import 'package:yumi/shared/tools/sc_cp_relation_notice_utils.dart';
|
||||
import 'package:yumi/main.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:yumi/app/constants/sc_room_msg_type.dart';
|
||||
@ -35,7 +34,6 @@ import 'package:yumi/services/audio/rtc_manager.dart';
|
||||
import 'package:yumi/services/audio/rtm_manager.dart';
|
||||
import 'package:yumi/services/auth/user_profile_manager.dart';
|
||||
import 'package:yumi/ui_kit/widgets/gift/sc_gift_combo_send_button.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/cp/room_cp_invite_dialog.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart';
|
||||
import 'package:yumi/modules/gift/gift_tab_page.dart';
|
||||
import 'package:yumi/modules/gift/cp_rights/cp_rights_guide_page.dart';
|
||||
@ -1564,7 +1562,6 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
}
|
||||
await _refreshGiftBackpack();
|
||||
_refreshCpStateAfterGift(request, profileManager);
|
||||
_showCpGiftWaitingDialog(request);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1629,7 +1626,6 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
}
|
||||
profileManager?.updateBalance(result);
|
||||
_refreshCpStateAfterGift(request, profileManager);
|
||||
_showCpGiftWaitingDialog(request);
|
||||
} catch (e) {
|
||||
final errorMessage =
|
||||
request.isBackpackGiftRequest && _isBackpackQuantityError(e)
|
||||
@ -1672,81 +1668,6 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
unawaited(profileManager?.refreshLoadedCpProfiles());
|
||||
}
|
||||
|
||||
void _showCpGiftWaitingDialog(_GiftSendRequest request) {
|
||||
if (!_isCpGift(request.gift)) {
|
||||
return;
|
||||
}
|
||||
if (request.isSelfRecipientRequest) {
|
||||
_giftFxLog(
|
||||
'skip local CP waiting dialog reason=self_recipient '
|
||||
'giftId=${request.gift.id} '
|
||||
'acceptUserIds=${request.acceptUserIds.join(",")}',
|
||||
);
|
||||
return;
|
||||
}
|
||||
final context = navigatorKey.currentState?.context;
|
||||
final currentProfile = AccountStorage().getCurrentUser()?.userProfile;
|
||||
final receiverProfile =
|
||||
request.acceptUsers.isNotEmpty ? request.acceptUsers.first.user : null;
|
||||
if (context == null || currentProfile == null || receiverProfile == null) {
|
||||
_giftFxLog(
|
||||
'skip local CP waiting dialog reason=missing_profile '
|
||||
'giftId=${request.gift.id} '
|
||||
'acceptUserIds=${request.acceptUserIds.join(",")}',
|
||||
);
|
||||
return;
|
||||
}
|
||||
final relationType = scNormalizeCpRelationType(request.cpRelationType);
|
||||
_giftFxLog(
|
||||
'show local CP waiting dialog '
|
||||
'giftId=${request.gift.id} '
|
||||
'relationType=$relationType '
|
||||
'receiverId=${receiverProfile.id}',
|
||||
);
|
||||
unawaited(
|
||||
RoomCpInviteDialog.show(
|
||||
context,
|
||||
inviter: RoomCpInviteDialogUser(
|
||||
name: _cpInviteDisplayName(currentProfile),
|
||||
avatarUrl: currentProfile.userAvatar ?? "",
|
||||
headdress: currentProfile.getHeaddress()?.sourceUrl ?? "",
|
||||
headdressCover: currentProfile.getHeaddress()?.cover ?? "",
|
||||
),
|
||||
receiver: RoomCpInviteDialogUser(
|
||||
name: _cpInviteDisplayName(receiverProfile),
|
||||
avatarUrl: receiverProfile.userAvatar ?? "",
|
||||
headdress: receiverProfile.getHeaddress()?.sourceUrl ?? "",
|
||||
headdressCover: receiverProfile.getHeaddress()?.cover ?? "",
|
||||
),
|
||||
style: RoomCpInviteDialogStyle.waiting,
|
||||
countdownSeconds: 86399,
|
||||
descriptionText: _cpInviteWaitingDescriptionText(relationType),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _cpInviteDisplayName(SocialChatUserProfile profile) {
|
||||
for (final value in [profile.userNickname, profile.account, profile.id]) {
|
||||
final text = value?.trim() ?? "";
|
||||
if (text.isNotEmpty) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
return "User";
|
||||
}
|
||||
|
||||
String _cpInviteWaitingDescriptionText(String relationType) {
|
||||
switch (scNormalizeCpRelationType(relationType)) {
|
||||
case "BROTHER":
|
||||
return "We invite you to become Brother";
|
||||
case "SISTERS":
|
||||
return "We invite you to become Sister";
|
||||
case "CP":
|
||||
default:
|
||||
return "We invite you to become Couple";
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<MicRes>> _giveBackpackGiftToRecipients(
|
||||
SCChatRoomRepository repository,
|
||||
_GiftSendRequest request,
|
||||
|
||||
@ -1383,14 +1383,20 @@ class _PersonDetailPageState extends State<PersonDetailPage> {
|
||||
}
|
||||
|
||||
List<CPRes> _profileCloseFriendRelations(SocialChatUserProfile? profile) {
|
||||
return (profile?.closeFriendList ?? const <CPRes>[])
|
||||
.where((relation) {
|
||||
final type = _normalizeProfileRelationType(relation.relationType);
|
||||
return (type == "BROTHER" || type == "SISTERS") &&
|
||||
_profileRelationHasPartner(profile, relation);
|
||||
})
|
||||
.take(6)
|
||||
.toList();
|
||||
final brothers = <CPRes>[];
|
||||
final sisters = <CPRes>[];
|
||||
for (final relation in profile?.closeFriendList ?? const <CPRes>[]) {
|
||||
if (!_profileRelationHasPartner(profile, relation)) {
|
||||
continue;
|
||||
}
|
||||
final type = _normalizeProfileRelationType(relation.relationType);
|
||||
if (type == "BROTHER" && brothers.length < 3) {
|
||||
brothers.add(relation);
|
||||
} else if (type == "SISTERS" && sisters.length < 3) {
|
||||
sisters.add(relation);
|
||||
}
|
||||
}
|
||||
return <CPRes>[...brothers, ...sisters];
|
||||
}
|
||||
|
||||
bool _profileRelationHasPartner(
|
||||
@ -1611,6 +1617,14 @@ class _PersonDetailPageState extends State<PersonDetailPage> {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
Provider.of<RtcProvider?>(
|
||||
context,
|
||||
listen: false,
|
||||
)?.invalidateRoomMicRelationBetween(
|
||||
userId: payload.leftUserId,
|
||||
peerUserId: partnerUserId,
|
||||
relationType: payload.relationType,
|
||||
);
|
||||
await Provider.of<SocialChatUserProfileManager>(
|
||||
context,
|
||||
listen: false,
|
||||
@ -2589,6 +2603,20 @@ class _PersonDetailPageState extends State<PersonDetailPage> {
|
||||
relationType: cpRelation?.relationType ?? "CP",
|
||||
)
|
||||
.then((result) {
|
||||
if (mounted) {
|
||||
Provider.of<RtcProvider?>(
|
||||
context,
|
||||
listen: false,
|
||||
)?.invalidateRoomMicRelationBetween(
|
||||
userId:
|
||||
AccountStorage()
|
||||
.getCurrentUser()
|
||||
?.userProfile
|
||||
?.id,
|
||||
peerUserId: ref.userProfile?.id,
|
||||
relationType: cpRelation?.relationType ?? "CP",
|
||||
);
|
||||
}
|
||||
ref.refreshLoadedCpProfiles();
|
||||
SmartDialog.dismiss(tag: "showPartWaysDialog");
|
||||
SCTts.show(
|
||||
|
||||
@ -20,9 +20,11 @@ class RoomRtcEnterRoomConfig {
|
||||
}
|
||||
|
||||
class RoomRtcAnchorConfig {
|
||||
const RoomRtcAnchorConfig({required this.muted});
|
||||
const RoomRtcAnchorConfig({required this.muted, bool? publishMuted})
|
||||
: publishMuted = publishMuted ?? muted;
|
||||
|
||||
final bool muted;
|
||||
final bool publishMuted;
|
||||
}
|
||||
|
||||
class RoomRtcAudioMixingConfig {
|
||||
|
||||
@ -10,7 +10,6 @@ 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_notice_utils.dart';
|
||||
import 'package:yumi/shared/tools/sc_room_rocket_reward_popup_ack.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';
|
||||
import 'package:yumi/shared/data_sources/sources/local/floating_screen_manager.dart';
|
||||
@ -63,7 +62,6 @@ import '../../ui_kit/components/sc_float_ichart.dart';
|
||||
import '../../ui_kit/widgets/room/rocket/room_rocket_asset_preloader.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);
|
||||
typedef RtcProvider = RealTimeCommunicationManager;
|
||||
@ -321,8 +319,21 @@ class _RoomMicRelationProfileCache {
|
||||
bool isExpired(DateTime now, Duration ttl) => now.difference(loadedAt) > ttl;
|
||||
}
|
||||
|
||||
class _RecentMicChangeSeatSnapshot {
|
||||
const _RecentMicChangeSeatSnapshot({
|
||||
required this.mic,
|
||||
required this.expiresAtMs,
|
||||
});
|
||||
|
||||
final MicRes mic;
|
||||
final int expiresAtMs;
|
||||
|
||||
String get userId => (mic.user?.id ?? "").trim();
|
||||
|
||||
bool isExpired(int nowMs) => expiresAtMs <= nowMs;
|
||||
}
|
||||
|
||||
class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
static const String _roomRocketRewardDialogTag = 'showRoomRocketRewardDialog';
|
||||
static const String _roomRocketPagLaunchDialogTag =
|
||||
'showRoomRocketPagLaunchDialog';
|
||||
static const List<Duration> _roomRocketGiftRefreshDelays = [
|
||||
@ -348,10 +359,12 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
'SISTERS',
|
||||
];
|
||||
static const Duration _roomMicRelationCacheTtl = Duration(minutes: 5);
|
||||
static const Duration _roomMicRelationDismissedPairTtl = Duration(minutes: 5);
|
||||
static const int _roomPasswordNotTrueErrorCode = 9005;
|
||||
|
||||
static const Duration _micListPollingInterval = Duration(seconds: 2);
|
||||
static const Duration _onlineUsersPollingInterval = Duration(seconds: 3);
|
||||
static const Duration _micChangeSnapshotGracePeriod = Duration(seconds: 4);
|
||||
static const int _exitNetworkRetryLimit = 3;
|
||||
static const Duration _exitNetworkRetryDelay = Duration(milliseconds: 500);
|
||||
static const Duration _selfMicStateGracePeriod = Duration(seconds: 4);
|
||||
@ -412,6 +425,8 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
bool _pendingMicListRefreshNotifyIfUnchanged = false;
|
||||
Timer? _deferredMicListRefreshTimer;
|
||||
int _lastMicListRefreshStartedAtMs = 0;
|
||||
final Map<num, _RecentMicChangeSeatSnapshot> _recentMicChangeSeatSnapshots =
|
||||
<num, _RecentMicChangeSeatSnapshot>{};
|
||||
num? _preferredSelfMicIndex;
|
||||
num? _pendingSelfMicSourceIndex;
|
||||
int? _pendingSelfMicSwitchGuardUntilMs;
|
||||
@ -439,6 +454,8 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
final Map<String, _RoomMicRelationProfileCache> _roomMicRelationCache =
|
||||
<String, _RoomMicRelationProfileCache>{};
|
||||
final Set<String> _roomMicRelationLoadingUserIds = <String>{};
|
||||
final Map<String, DateTime> _roomMicRelationDismissedPairUntil =
|
||||
<String, DateTime>{};
|
||||
final Set<String> _roomMusicPublishingUserIds = <String>{};
|
||||
final Map<String, Timer> _roomMusicPublishingClearTimers = <String, Timer>{};
|
||||
|
||||
@ -490,6 +507,18 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
_roomMusicPublishingUserIds.contains(normalizedUserId);
|
||||
}
|
||||
|
||||
bool get _currentUserPublishingRoomMusic {
|
||||
final currentUserId =
|
||||
(AccountStorage().getCurrentUser()?.userProfile?.id ?? "").trim();
|
||||
return isUserPublishingRoomMusic(currentUserId);
|
||||
}
|
||||
|
||||
bool _isCurrentUserId(String userId) {
|
||||
final currentUserId =
|
||||
(AccountStorage().getCurrentUser()?.userProfile?.id ?? "").trim();
|
||||
return currentUserId.isNotEmpty && currentUserId == userId.trim();
|
||||
}
|
||||
|
||||
///房间红包列表
|
||||
List<SCRoomRedPacketListRes> redPacketList = [];
|
||||
|
||||
@ -574,6 +603,9 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
final roomId = (currenRoom?.roomProfile?.roomProfile?.id ?? "").trim();
|
||||
if (roomId.isNotEmpty) {
|
||||
_scheduleRoomRocketEntryReadyFallback(roomId);
|
||||
if (_roomStartupStatus == RoomStartupStatus.ready) {
|
||||
_loadRoomRocketRewardPopups(roomId);
|
||||
}
|
||||
}
|
||||
}
|
||||
notifyListeners();
|
||||
@ -772,6 +804,16 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
normalizedMessage.contains('the path was not found');
|
||||
}
|
||||
|
||||
bool _isInvalidKickOffMicPrecheckError(Object error) {
|
||||
final normalizedMessage = _errorMessageFrom(error).toLowerCase();
|
||||
return (normalizedMessage.contains('parameter') &&
|
||||
(normalizedMessage.contains('incorrect') ||
|
||||
normalizedMessage.contains('incomplete'))) ||
|
||||
(normalizedMessage.contains('参数') &&
|
||||
(normalizedMessage.contains('错误') ||
|
||||
normalizedMessage.contains('不完整')));
|
||||
}
|
||||
|
||||
String _errorMessageFrom(Object error) {
|
||||
if (error is DioException) {
|
||||
final responseData = error.response?.data;
|
||||
@ -1037,6 +1079,144 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
return _stabilizeSelfMicSnapshot(completedMap, previousMap: previousMap);
|
||||
}
|
||||
|
||||
void _rememberMicChangeSnapshot(List<MicRes> mics) {
|
||||
if (mics.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final nowMs = DateTime.now().millisecondsSinceEpoch;
|
||||
_pruneRecentMicChangeSeatSnapshots(nowMs);
|
||||
final expiresAtMs = nowMs + _micChangeSnapshotGracePeriod.inMilliseconds;
|
||||
for (final mic in mics) {
|
||||
final micIndex = mic.micIndex;
|
||||
if (micIndex == null) {
|
||||
continue;
|
||||
}
|
||||
_recentMicChangeSeatSnapshots[micIndex] = _RecentMicChangeSeatSnapshot(
|
||||
mic: _mergeKnownVipIntoMic(mic).copyWith(micIndex: micIndex),
|
||||
expiresAtMs: expiresAtMs,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _pruneRecentMicChangeSeatSnapshots([int? nowMs]) {
|
||||
if (_recentMicChangeSeatSnapshots.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final resolvedNowMs = nowMs ?? DateTime.now().millisecondsSinceEpoch;
|
||||
_recentMicChangeSeatSnapshots.removeWhere(
|
||||
(_, snapshot) => snapshot.isExpired(resolvedNowMs),
|
||||
);
|
||||
}
|
||||
|
||||
Map<num, MicRes> _stabilizeMicListSnapshotWithRecentMicChange(
|
||||
Map<num, MicRes> nextMap,
|
||||
) {
|
||||
final nowMs = DateTime.now().millisecondsSinceEpoch;
|
||||
_pruneRecentMicChangeSeatSnapshots(nowMs);
|
||||
if (_recentMicChangeSeatSnapshots.isEmpty) {
|
||||
return nextMap;
|
||||
}
|
||||
|
||||
Map<num, MicRes>? stabilizedMap;
|
||||
final confirmedSeatIndexes = <num>[];
|
||||
for (final entry in _recentMicChangeSeatSnapshots.entries) {
|
||||
final seatIndex = entry.key;
|
||||
final recentMic = entry.value.mic;
|
||||
final recentUserId = entry.value.userId;
|
||||
final currentMap = stabilizedMap ?? nextMap;
|
||||
final incomingMic = currentMap[seatIndex];
|
||||
final incomingUserId = (incomingMic?.user?.id ?? "").trim();
|
||||
|
||||
if (recentUserId.isEmpty) {
|
||||
if (incomingUserId.isEmpty) {
|
||||
confirmedSeatIndexes.add(seatIndex);
|
||||
continue;
|
||||
}
|
||||
stabilizedMap ??= Map<num, MicRes>.from(nextMap);
|
||||
stabilizedMap[seatIndex] = _micChangeAuthoritativeSeat(
|
||||
incomingMic: incomingMic,
|
||||
recentMic: recentMic,
|
||||
seatIndex: seatIndex,
|
||||
clearUser: true,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (incomingUserId == recentUserId) {
|
||||
confirmedSeatIndexes.add(seatIndex);
|
||||
continue;
|
||||
}
|
||||
|
||||
stabilizedMap ??= Map<num, MicRes>.from(nextMap);
|
||||
for (final otherEntry in currentMap.entries) {
|
||||
if (otherEntry.key == seatIndex) {
|
||||
continue;
|
||||
}
|
||||
if ((otherEntry.value.user?.id ?? "").trim() != recentUserId) {
|
||||
continue;
|
||||
}
|
||||
stabilizedMap[otherEntry.key] = otherEntry.value.copyWith(
|
||||
clearUser: true,
|
||||
);
|
||||
}
|
||||
stabilizedMap[seatIndex] = _micChangeAuthoritativeSeat(
|
||||
incomingMic: incomingMic,
|
||||
recentMic: recentMic,
|
||||
seatIndex: seatIndex,
|
||||
);
|
||||
}
|
||||
|
||||
for (final seatIndex in confirmedSeatIndexes) {
|
||||
_recentMicChangeSeatSnapshots.remove(seatIndex);
|
||||
}
|
||||
return stabilizedMap ?? nextMap;
|
||||
}
|
||||
|
||||
MicRes _micChangeAuthoritativeSeat({
|
||||
required MicRes? incomingMic,
|
||||
required MicRes recentMic,
|
||||
required num seatIndex,
|
||||
bool clearUser = false,
|
||||
}) {
|
||||
final baseMic =
|
||||
incomingMic ??
|
||||
MicRes(
|
||||
roomId: currenRoom?.roomProfile?.roomProfile?.id,
|
||||
micIndex: seatIndex,
|
||||
);
|
||||
return baseMic.copyWith(
|
||||
roomId: recentMic.roomId,
|
||||
micIndex: recentMic.micIndex ?? seatIndex,
|
||||
micLock: recentMic.micLock,
|
||||
micMute: recentMic.micMute,
|
||||
user: recentMic.user,
|
||||
clearUser: clearUser,
|
||||
roomToken: recentMic.roomToken,
|
||||
emojiPath: recentMic.emojiPath,
|
||||
type: recentMic.type,
|
||||
number: recentMic.number,
|
||||
);
|
||||
}
|
||||
|
||||
void _applyMicListSnapshot(
|
||||
List<MicRes> roomWheatList, {
|
||||
bool notifyIfUnchanged = true,
|
||||
}) {
|
||||
final nextMap = _buildMicMap(roomWheatList);
|
||||
final stabilizedNextMap = _stabilizeMicListSnapshotWithRecentMicChange(
|
||||
nextMap,
|
||||
);
|
||||
_applyMicSnapshot(stabilizedNextMap, notifyIfUnchanged: notifyIfUnchanged);
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
void debugApplyMicListSnapshot(
|
||||
List<MicRes> roomWheatList, {
|
||||
bool notifyIfUnchanged = true,
|
||||
}) {
|
||||
_applyMicListSnapshot(roomWheatList, notifyIfUnchanged: notifyIfUnchanged);
|
||||
}
|
||||
|
||||
void _startSelfMicSwitchGuard({
|
||||
required num sourceIndex,
|
||||
required num targetIndex,
|
||||
@ -1417,16 +1597,26 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
}
|
||||
|
||||
final shouldPublishLocalAudio = role == _RoomRtcLocalAudioRole.anchor;
|
||||
final recordingSignalVolume = shouldPublishLocalAudio && !muted ? 100 : 0;
|
||||
final keepAudioPublishing =
|
||||
shouldPublishLocalAudio && muted && _currentUserPublishingRoomMusic;
|
||||
final localAudioMuted =
|
||||
!shouldPublishLocalAudio || (muted && !keepAudioPublishing);
|
||||
final recordingSignalMuted = !shouldPublishLocalAudio || muted;
|
||||
final recordingSignalVolume = recordingSignalMuted ? 0 : 100;
|
||||
if (_lastAppliedLocalAudioRole == role &&
|
||||
_lastAppliedLocalAudioMuted == !shouldPublishLocalAudio &&
|
||||
_lastAppliedRecordingSignalMuted == !shouldPublishLocalAudio &&
|
||||
_lastAppliedLocalAudioMuted == localAudioMuted &&
|
||||
_lastAppliedRecordingSignalMuted == recordingSignalMuted &&
|
||||
_lastAppliedRecordingSignalVolume == recordingSignalVolume) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (shouldPublishLocalAudio) {
|
||||
unawaited(_switchRoomRtcToBroadcaster(muted: muted));
|
||||
unawaited(
|
||||
_switchRoomRtcToBroadcaster(
|
||||
muted: muted,
|
||||
keepAudioPublishing: keepAudioPublishing,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
unawaited(_switchRoomRtcToAudience());
|
||||
}
|
||||
@ -1449,8 +1639,13 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
Map<num, MicRes> nextMap, {
|
||||
bool notifyIfUnchanged = true,
|
||||
}) {
|
||||
final changed = !_sameMicMaps(roomWheatMap, nextMap);
|
||||
roomWheatMap = nextMap;
|
||||
final sanitizedNextMap = _withoutLocallyDismissedRoomMicRelationsFromMicMap(
|
||||
nextMap,
|
||||
);
|
||||
final changed =
|
||||
!identical(sanitizedNextMap, nextMap) ||
|
||||
!_sameMicMaps(roomWheatMap, sanitizedNextMap);
|
||||
roomWheatMap = sanitizedNextMap;
|
||||
final relationCacheChanged = _mergeCachedRoomMicRelationProfiles();
|
||||
_scheduleRoomMicRelationHydration();
|
||||
_syncSelfMicRuntimeState();
|
||||
@ -1463,6 +1658,58 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
void invalidateRoomMicRelationBetween({
|
||||
required String? userId,
|
||||
required String? peerUserId,
|
||||
String? relationType,
|
||||
}) {
|
||||
final currentUserId =
|
||||
(AccountStorage().getCurrentUser()?.userProfile?.id ?? "").trim();
|
||||
final leftUserId =
|
||||
(userId ?? "").trim().isNotEmpty ? userId!.trim() : currentUserId;
|
||||
final rightUserId = (peerUserId ?? "").trim();
|
||||
if (leftUserId.isEmpty ||
|
||||
rightUserId.isEmpty ||
|
||||
leftUserId == rightUserId) {
|
||||
return;
|
||||
}
|
||||
|
||||
final now = DateTime.now();
|
||||
_pruneRoomMicRelationDismissedPairs(now);
|
||||
final normalizedType = scNormalizeCpRelationType(relationType);
|
||||
final dismissedUntil = now.add(_roomMicRelationDismissedPairTtl);
|
||||
final leftKeys = _roomMicRelationIdentityKeysForUserId(leftUserId);
|
||||
final rightKeys = _roomMicRelationIdentityKeysForUserId(rightUserId);
|
||||
for (final leftKey in leftKeys) {
|
||||
for (final rightKey in rightKeys) {
|
||||
_roomMicRelationDismissedPairUntil[_roomMicRelationDismissedPairKey(
|
||||
leftKey,
|
||||
rightKey,
|
||||
normalizedType,
|
||||
)] =
|
||||
dismissedUntil;
|
||||
}
|
||||
}
|
||||
|
||||
final dismissedUserIds = <String>{leftUserId, rightUserId};
|
||||
_roomMicRelationCache.removeWhere(
|
||||
(cacheUserId, _) => dismissedUserIds.contains(cacheUserId),
|
||||
);
|
||||
_roomMicRelationLoadingUserIds.removeWhere(dismissedUserIds.contains);
|
||||
|
||||
final nextMap = _withoutLocallyDismissedRoomMicRelationsFromMicMap(
|
||||
roomWheatMap,
|
||||
);
|
||||
if (!identical(nextMap, roomWheatMap)) {
|
||||
roomWheatMap = nextMap;
|
||||
notifyListeners();
|
||||
}
|
||||
requestMicrophoneListRefresh(
|
||||
notifyIfUnchanged: false,
|
||||
minInterval: const Duration(milliseconds: 350),
|
||||
);
|
||||
}
|
||||
|
||||
bool _mergeCachedRoomMicRelationProfiles() {
|
||||
final now = DateTime.now();
|
||||
_roomMicRelationCache.removeWhere(
|
||||
@ -1561,6 +1808,9 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
levelText:
|
||||
pair.levelText ?? _roomMicRelationLevelTextFromCabin(cabin),
|
||||
);
|
||||
if (_isRoomMicRelationLocallyDismissed(enrichedPair)) {
|
||||
return;
|
||||
}
|
||||
if (normalizedType == "CP") {
|
||||
cpPairs.add(enrichedPair);
|
||||
} else {
|
||||
@ -1711,6 +1961,126 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
return user?.id?.trim() ?? "";
|
||||
}
|
||||
|
||||
Map<num, MicRes> _withoutLocallyDismissedRoomMicRelationsFromMicMap(
|
||||
Map<num, MicRes> sourceMap,
|
||||
) {
|
||||
if (_roomMicRelationDismissedPairUntil.isEmpty || sourceMap.isEmpty) {
|
||||
return sourceMap;
|
||||
}
|
||||
_pruneRoomMicRelationDismissedPairs(DateTime.now());
|
||||
if (_roomMicRelationDismissedPairUntil.isEmpty) {
|
||||
return sourceMap;
|
||||
}
|
||||
var changed = false;
|
||||
final nextMap = sourceMap.map((index, mic) {
|
||||
final user = mic.user;
|
||||
if (user == null) {
|
||||
return MapEntry(index, mic);
|
||||
}
|
||||
final nextUser = _withoutLocallyDismissedRoomMicRelations(user);
|
||||
if (identical(nextUser, user)) {
|
||||
return MapEntry(index, mic);
|
||||
}
|
||||
changed = true;
|
||||
return MapEntry(index, mic.copyWith(user: nextUser));
|
||||
});
|
||||
return changed ? nextMap : sourceMap;
|
||||
}
|
||||
|
||||
SocialChatUserProfile _withoutLocallyDismissedRoomMicRelations(
|
||||
SocialChatUserProfile user,
|
||||
) {
|
||||
final nextCpList = _withoutLocallyDismissedRoomMicRelationList(user.cpList);
|
||||
final nextCloseFriendList = _withoutLocallyDismissedRoomMicRelationList(
|
||||
user.closeFriendList,
|
||||
);
|
||||
if (identical(nextCpList, user.cpList) &&
|
||||
identical(nextCloseFriendList, user.closeFriendList)) {
|
||||
return user;
|
||||
}
|
||||
final effectiveCpList = nextCpList ?? user.cpList ?? const <CPRes>[];
|
||||
return user.copyWith(
|
||||
cpList: nextCpList,
|
||||
closeFriendList: nextCloseFriendList,
|
||||
isCpRelation: effectiveCpList.isNotEmpty,
|
||||
);
|
||||
}
|
||||
|
||||
List<CPRes>? _withoutLocallyDismissedRoomMicRelationList(
|
||||
List<CPRes>? sourceList,
|
||||
) {
|
||||
if (sourceList == null || sourceList.isEmpty) {
|
||||
return sourceList;
|
||||
}
|
||||
final filtered = sourceList
|
||||
.where((relation) => !_isRoomMicRelationLocallyDismissed(relation))
|
||||
.toList(growable: false);
|
||||
return filtered.length == sourceList.length ? sourceList : filtered;
|
||||
}
|
||||
|
||||
bool _isRoomMicRelationLocallyDismissed(CPRes relation) {
|
||||
if (_roomMicRelationDismissedPairUntil.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
_pruneRoomMicRelationDismissedPairs(DateTime.now());
|
||||
if (_roomMicRelationDismissedPairUntil.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
final keys = _roomMicRelationDismissedPairKeysFromRelation(relation);
|
||||
return keys.any(_roomMicRelationDismissedPairUntil.containsKey);
|
||||
}
|
||||
|
||||
Set<String> _roomMicRelationDismissedPairKeysFromRelation(CPRes relation) {
|
||||
final leftKeys = <String>{
|
||||
relation.meUserId?.trim() ?? "",
|
||||
relation.meAccount?.trim() ?? "",
|
||||
}..remove("");
|
||||
final rightKeys = <String>{
|
||||
relation.cpUserId?.trim() ?? "",
|
||||
relation.cpAccount?.trim() ?? "",
|
||||
}..remove("");
|
||||
if (leftKeys.isEmpty || rightKeys.isEmpty) {
|
||||
return const <String>{};
|
||||
}
|
||||
final relationType = scNormalizeCpRelationType(relation.relationType);
|
||||
return <String>{
|
||||
for (final leftKey in leftKeys)
|
||||
for (final rightKey in rightKeys)
|
||||
_roomMicRelationDismissedPairKey(leftKey, rightKey, relationType),
|
||||
};
|
||||
}
|
||||
|
||||
String _roomMicRelationDismissedPairKey(
|
||||
String leftIdentity,
|
||||
String rightIdentity,
|
||||
String relationType,
|
||||
) {
|
||||
final identities = <String>[leftIdentity.trim(), rightIdentity.trim()]
|
||||
..sort();
|
||||
return '${scNormalizeCpRelationType(relationType)}|'
|
||||
'${identities[0]}|${identities[1]}';
|
||||
}
|
||||
|
||||
Set<String> _roomMicRelationIdentityKeysForUserId(String userId) {
|
||||
final normalizedUserId = userId.trim();
|
||||
final keys = <String>{normalizedUserId};
|
||||
for (final mic in roomWheatMap.values) {
|
||||
final user = mic.user;
|
||||
if ((user?.id ?? "").trim() == normalizedUserId) {
|
||||
keys
|
||||
..add(user?.id?.trim() ?? "")
|
||||
..add(user?.account?.trim() ?? "");
|
||||
}
|
||||
}
|
||||
return keys..remove("");
|
||||
}
|
||||
|
||||
void _pruneRoomMicRelationDismissedPairs(DateTime now) {
|
||||
_roomMicRelationDismissedPairUntil.removeWhere(
|
||||
(_, expiresAt) => !expiresAt.isAfter(now),
|
||||
);
|
||||
}
|
||||
|
||||
bool _pruneRoomMusicPublishingUsersToMicSeats() {
|
||||
if (_roomMusicPublishingUserIds.isEmpty) {
|
||||
return false;
|
||||
@ -1797,6 +2167,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
if (clearMicSeats) {
|
||||
_roomMicRelationCache.clear();
|
||||
_roomMicRelationLoadingUserIds.clear();
|
||||
_roomMicRelationDismissedPairUntil.clear();
|
||||
roomWheatMap.clear();
|
||||
notifyListeners();
|
||||
}
|
||||
@ -1935,13 +2306,17 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> _switchRoomRtcToBroadcaster({required bool muted}) async {
|
||||
Future<bool> _switchRoomRtcToBroadcaster({
|
||||
required bool muted,
|
||||
bool keepAudioPublishing = false,
|
||||
}) async {
|
||||
if (!_roomRtcJoined || _roomRtcEngineAdapter == null) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
final localAudioMuted = muted && !keepAudioPublishing;
|
||||
final switched = await _currentRoomRtcEngineAdapter.switchToAnchor(
|
||||
RoomRtcAnchorConfig(muted: muted),
|
||||
RoomRtcAnchorConfig(muted: muted, publishMuted: localAudioMuted),
|
||||
);
|
||||
if (!switched) {
|
||||
_roomRtcRoleIsBroadcaster = false;
|
||||
@ -1949,8 +2324,8 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
return false;
|
||||
}
|
||||
_lastAppliedLocalAudioRole = _RoomRtcLocalAudioRole.anchor;
|
||||
_lastAppliedLocalAudioMuted = false;
|
||||
_lastAppliedRecordingSignalMuted = false;
|
||||
_lastAppliedLocalAudioMuted = localAudioMuted;
|
||||
_lastAppliedRecordingSignalMuted = muted;
|
||||
_lastAppliedRecordingSignalVolume = muted ? 0 : 100;
|
||||
_roomRtcRoleIsBroadcaster = true;
|
||||
_resyncHeartbeatFromRoomRtcState();
|
||||
@ -1965,6 +2340,9 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
Future<void> _rejoinCurrentRoomRtcForSameRoomReuse(String roomId) async {
|
||||
if (_hasCurrentRoomRtcConnection) {
|
||||
_setRoomStartupReady();
|
||||
if (_voiceRoomRouteVisible) {
|
||||
_loadRoomRocketRewardPopups(roomId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1989,6 +2367,9 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
return;
|
||||
}
|
||||
_setRoomStartupReady();
|
||||
if (_voiceRoomRouteVisible) {
|
||||
_loadRoomRocketRewardPopups(roomId);
|
||||
}
|
||||
} catch (error) {
|
||||
if (!_isCurrentRoomId(roomId)) {
|
||||
return;
|
||||
@ -2030,7 +2411,10 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
final seatMuted = seat?.micMute ?? false;
|
||||
if (seatMuted || muted) {
|
||||
if (_roomRtcJoined && isOnMai()) {
|
||||
await _switchRoomRtcToBroadcaster(muted: true);
|
||||
await _switchRoomRtcToBroadcaster(
|
||||
muted: true,
|
||||
keepAudioPublishing: _currentUserPublishingRoomMusic,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
await _switchRoomRtcToBroadcaster(muted: false);
|
||||
@ -3516,58 +3900,13 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
}
|
||||
|
||||
void _loadRoomRocketRewardPopups(String roomId) {
|
||||
if (roomId.trim().isEmpty) {
|
||||
final normalizedRoomId = roomId.trim();
|
||||
if (normalizedRoomId.isEmpty ||
|
||||
normalizedRoomId !=
|
||||
(currenRoom?.roomProfile?.roomProfile?.id ?? "").trim()) {
|
||||
return;
|
||||
}
|
||||
SCChatRoomRepository()
|
||||
.roomRocketRewardPopups(roomId)
|
||||
.then((res) {
|
||||
final popupRecords = SCRoomRocketRewardPopupAck.filterUnshownRecords(
|
||||
res.records,
|
||||
);
|
||||
if (popupRecords.isEmpty ||
|
||||
roomId !=
|
||||
(currenRoom?.roomProfile?.roomProfile?.id ?? "").trim()) {
|
||||
return;
|
||||
}
|
||||
if (SCRoomTopLayerGuard().isActive) {
|
||||
SCRoomTopLayerGuard().runWhenInactive(() {
|
||||
_loadRoomRocketRewardPopups(roomId);
|
||||
});
|
||||
return;
|
||||
}
|
||||
SCRoomRocketRewardPopupAck.markShownAndAck(popupRecords);
|
||||
final recordIds = SCRoomRocketRewardPopupAck.recordIds(popupRecords);
|
||||
SmartDialog.dismiss(tag: _roomRocketRewardDialogTag);
|
||||
SmartDialog.show(
|
||||
tag: _roomRocketRewardDialogTag,
|
||||
alignment: Alignment.center,
|
||||
debounce: true,
|
||||
animationType: SmartAnimationType.fade,
|
||||
maskColor: Colors.transparent,
|
||||
clickMaskDismiss: false,
|
||||
builder:
|
||||
(_) => SCRoomTopLayerSuppressor(
|
||||
reason: 'room_rocket_reward_dialog',
|
||||
keepDialogTags: const <String>{_roomRocketRewardDialogTag},
|
||||
child: RoomRocketRewardDialogLoader(
|
||||
roomId: roomId,
|
||||
initialPopupRecords: popupRecords,
|
||||
userProfileResolver: roomRocketRewardUserProfileForRecord,
|
||||
onClose: () {
|
||||
SmartDialog.dismiss(tag: _roomRocketRewardDialogTag);
|
||||
scheduleRoomRocketPostLaunchStatusRefresh(roomId: roomId);
|
||||
if (recordIds.isNotEmpty) {
|
||||
SCRoomRocketRewardPopupAck.markShownAndAck(
|
||||
popupRecords,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
})
|
||||
.catchError((_) {});
|
||||
rtmProvider?.queryVoiceRoomRocketRewardPopups(normalizedRoomId);
|
||||
}
|
||||
|
||||
///更新房间火箭信息
|
||||
@ -4086,8 +4425,10 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
if (roomId != currenRoom?.roomProfile?.roomProfile?.id) {
|
||||
return;
|
||||
}
|
||||
final nextMap = _buildMicMap(roomWheatList);
|
||||
_applyMicSnapshot(nextMap, notifyIfUnchanged: notifyIfUnchanged);
|
||||
_applyMicListSnapshot(
|
||||
roomWheatList,
|
||||
notifyIfUnchanged: notifyIfUnchanged,
|
||||
);
|
||||
} catch (error) {
|
||||
if (_isMissingPathError(error)) {
|
||||
_disableMicListRefreshForCurrentSession = true;
|
||||
@ -4539,8 +4880,12 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
_isHandlingRoomRtcFailure = false;
|
||||
_pendingMicListRefresh = false;
|
||||
_pendingMicListRefreshNotifyIfUnchanged = false;
|
||||
_recentMicChangeSeatSnapshots.clear();
|
||||
_seatEmojiEventVersions.clear();
|
||||
_emojiPlaybackEventVersion = 0;
|
||||
_roomMicRelationCache.clear();
|
||||
_roomMicRelationLoadingUserIds.clear();
|
||||
_roomMicRelationDismissedPairUntil.clear();
|
||||
_clearRoomMusicPublishingStates();
|
||||
roomRocketStatus = null;
|
||||
rtmProvider
|
||||
@ -4788,6 +5133,9 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
final targetIndex = micGoUpRes.micIndex ?? index;
|
||||
final switched = await _switchRoomRtcToBroadcaster(
|
||||
muted: (micGoUpRes.micMute ?? false) || isMic,
|
||||
keepAudioPublishing:
|
||||
((micGoUpRes.micMute ?? false) || isMic) &&
|
||||
_currentUserPublishingRoomMusic,
|
||||
);
|
||||
if (!switched) {
|
||||
await _switchRoomRtcToAudience();
|
||||
@ -4912,34 +5260,52 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
}
|
||||
|
||||
///踢人下麦
|
||||
killXiaMai(String userId) {
|
||||
Future<void> killXiaMai(String userId) async {
|
||||
try {
|
||||
roomWheatMap.forEach((k, v) async {
|
||||
if (v.user?.id == userId) {
|
||||
var canKill = await SCChatRoomRepository().kickOffMicrophone(
|
||||
currenRoom?.roomProfile?.roomProfile?.id ?? "",
|
||||
userId,
|
||||
);
|
||||
if (canKill) {
|
||||
await SCChatRoomRepository().micKill(
|
||||
currenRoom?.roomProfile?.roomProfile?.id ?? "",
|
||||
k,
|
||||
);
|
||||
Provider.of<RtmProvider>(context!, listen: false).dispatchMessage(
|
||||
Msg(
|
||||
groupId: currenRoom?.roomProfile?.roomProfile?.roomAccount,
|
||||
msg: userId,
|
||||
type: SCRoomMsgType.killXiaMai,
|
||||
),
|
||||
addLocal: false,
|
||||
);
|
||||
roomWheatMap[k]?.setUser = null;
|
||||
notifyListeners();
|
||||
} else {
|
||||
SCTts.show("cant kill the user");
|
||||
}
|
||||
final targetUserId = userId.trim();
|
||||
final roomId = currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? "";
|
||||
if (targetUserId.isEmpty || roomId.isEmpty) {
|
||||
SCTts.show("kill the user fail");
|
||||
return;
|
||||
}
|
||||
num? targetMicIndex;
|
||||
for (final entry in roomWheatMap.entries) {
|
||||
if ((entry.value.user?.id ?? "").trim() == targetUserId) {
|
||||
targetMicIndex = entry.key;
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
if (targetMicIndex == null) {
|
||||
SCTts.show("cant kill the user");
|
||||
return;
|
||||
}
|
||||
var canKill = true;
|
||||
try {
|
||||
canKill = await SCChatRoomRepository().kickOffMicrophone(
|
||||
roomId,
|
||||
targetUserId,
|
||||
mickIndex: targetMicIndex,
|
||||
);
|
||||
} catch (error) {
|
||||
if (!_isInvalidKickOffMicPrecheckError(error)) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
if (canKill) {
|
||||
await SCChatRoomRepository().micKill(roomId, targetMicIndex);
|
||||
Provider.of<RtmProvider>(context!, listen: false).dispatchMessage(
|
||||
Msg(
|
||||
groupId: currenRoom?.roomProfile?.roomProfile?.roomAccount,
|
||||
msg: targetUserId,
|
||||
type: SCRoomMsgType.killXiaMai,
|
||||
),
|
||||
addLocal: false,
|
||||
);
|
||||
roomWheatMap[targetMicIndex]?.setUser = null;
|
||||
notifyListeners();
|
||||
} else {
|
||||
SCTts.show("cant kill the user");
|
||||
}
|
||||
} catch (e) {
|
||||
SCTts.show("kill the user fail");
|
||||
}
|
||||
@ -5024,7 +5390,9 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
if (mics == null || mics.isEmpty) {
|
||||
return;
|
||||
}
|
||||
_applyMicSnapshot(_buildMicMap(mics));
|
||||
final nextMap = _buildMicMap(mics);
|
||||
_rememberMicChangeSnapshot(mics);
|
||||
_applyMicSnapshot(nextMap);
|
||||
}
|
||||
|
||||
///找一个空麦位 -1表示没有空位
|
||||
@ -5142,7 +5510,10 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
true,
|
||||
);
|
||||
if (isOnMaiInIndex(index)) {
|
||||
await _switchRoomRtcToBroadcaster(muted: true);
|
||||
await _switchRoomRtcToBroadcaster(
|
||||
muted: true,
|
||||
keepAudioPublishing: _currentUserPublishingRoomMusic,
|
||||
);
|
||||
}
|
||||
var mic = roomWheatMap[index];
|
||||
if (mic != null) {
|
||||
@ -5316,6 +5687,9 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
() {
|
||||
_roomMusicPublishingClearTimers.remove(normalizedUserId);
|
||||
if (_roomMusicPublishingUserIds.remove(normalizedUserId)) {
|
||||
if (_isCurrentUserId(normalizedUserId)) {
|
||||
_syncSelfMicRuntimeState();
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
},
|
||||
@ -5324,6 +5698,9 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
_roomMusicPublishingClearTimers.remove(normalizedUserId)?.cancel();
|
||||
changed = _roomMusicPublishingUserIds.remove(normalizedUserId);
|
||||
}
|
||||
if (changed && _isCurrentUserId(normalizedUserId)) {
|
||||
_syncSelfMicRuntimeState();
|
||||
}
|
||||
if (changed && notifyIfChanged) {
|
||||
notifyListeners();
|
||||
}
|
||||
@ -5456,6 +5833,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
}
|
||||
return _switchRoomRtcToBroadcaster(
|
||||
muted: (currentUserMic.micMute ?? false) || isMic,
|
||||
keepAudioPublishing: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -148,8 +148,8 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
static const int _roomRocketLaunchRecentTtlMs = 90000;
|
||||
static const int _roomRocketLaunchLooseRecentTtlMs = 12000;
|
||||
static const int _roomRocketRegionBroadcastRecentTtlMs = 90000;
|
||||
static const int _roomRocketRewardPendingTtlMs = 120000;
|
||||
static const int _roomRocketRewardProbeTtlMs = 120000;
|
||||
static const int _roomRocketRewardPendingTtlMs = 3600000;
|
||||
static const int _roomRocketRewardProbeTtlMs = 3600000;
|
||||
static const int _roomRocketLaunchCountdownRetainMs = 10000;
|
||||
static const int _roomRocketLaunchEffectConsumedTtlMs = 120000;
|
||||
static const Duration _createRoomGroupTimeout = Duration(seconds: 12);
|
||||
@ -294,6 +294,7 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
final Set<String> _hiddenCurrentRoomRocketLaunchKeys = <String>{};
|
||||
final Map<String, int> _roomRocketRewardHoldUntilTimes = <String, int>{};
|
||||
final Map<String, Timer> _roomRocketRewardHoldTimers = <String, Timer>{};
|
||||
final Set<String> _roomRocketRewardPopupLoadingRoomIds = <String>{};
|
||||
Timer? _roomRocketRewardPopupRetryTimer;
|
||||
Timer? _roomRocketRoomReadyRewardRetryTimer;
|
||||
final Set<String> _shownCpInviteMessageKeys = <String>{};
|
||||
@ -3929,7 +3930,15 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
.toSet();
|
||||
}
|
||||
|
||||
void handleVoiceRoomReadyForRocketRewards(String roomId, {int attempt = 0}) {
|
||||
void queryVoiceRoomRocketRewardPopups(String roomId) {
|
||||
handleVoiceRoomReadyForRocketRewards(roomId, forceQuery: true);
|
||||
}
|
||||
|
||||
void handleVoiceRoomReadyForRocketRewards(
|
||||
String roomId, {
|
||||
int attempt = 0,
|
||||
bool forceQuery = false,
|
||||
}) {
|
||||
final normalizedRoomId = roomId.trim();
|
||||
if (normalizedRoomId.isEmpty) {
|
||||
return;
|
||||
@ -3941,11 +3950,15 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
_recentRoomRocketRewardProbeTimes[normalizedRoomId] != null;
|
||||
final hasLaunch =
|
||||
_pendingRoomRocketCountdownLaunches[normalizedRoomId] != null;
|
||||
if (pending == null && !hasProbe && !hasLaunch) {
|
||||
if (pending == null && !hasProbe && !hasLaunch && !forceQuery) {
|
||||
return;
|
||||
}
|
||||
if (!_isCurrentVisibleVoiceRoom(normalizedRoomId)) {
|
||||
_scheduleRoomRocketReadyRewardRetry(normalizedRoomId, attempt);
|
||||
_scheduleRoomRocketReadyRewardRetry(
|
||||
normalizedRoomId,
|
||||
attempt,
|
||||
forceQuery: forceQuery,
|
||||
);
|
||||
return;
|
||||
}
|
||||
final resumedLaunch = _resumeRoomRocketLaunchForCurrentRoom(
|
||||
@ -3954,11 +3967,18 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
final pendingAfterResume = _pendingRoomRocketRewardPopups[normalizedRoomId];
|
||||
final hasProbeAfterResume =
|
||||
_recentRoomRocketRewardProbeTimes[normalizedRoomId] != null;
|
||||
if (pendingAfterResume == null && !hasProbeAfterResume) {
|
||||
if (pendingAfterResume == null && !hasProbeAfterResume && !forceQuery) {
|
||||
return;
|
||||
}
|
||||
if (!resumedLaunch && pendingAfterResume == null && attempt == 0) {
|
||||
_scheduleRoomRocketReadyRewardRetry(normalizedRoomId, attempt);
|
||||
if (!resumedLaunch &&
|
||||
pendingAfterResume == null &&
|
||||
attempt == 0 &&
|
||||
!forceQuery) {
|
||||
_scheduleRoomRocketReadyRewardRetry(
|
||||
normalizedRoomId,
|
||||
attempt,
|
||||
forceQuery: forceQuery,
|
||||
);
|
||||
return;
|
||||
}
|
||||
_pendingRoomRocketRewardPopups.remove(normalizedRoomId);
|
||||
@ -3984,7 +4004,11 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
);
|
||||
}
|
||||
|
||||
void _scheduleRoomRocketReadyRewardRetry(String roomId, int attempt) {
|
||||
void _scheduleRoomRocketReadyRewardRetry(
|
||||
String roomId,
|
||||
int attempt, {
|
||||
bool forceQuery = false,
|
||||
}) {
|
||||
if (attempt >= _roomRocketRoomReadyRewardRetryDelays.length) {
|
||||
return;
|
||||
}
|
||||
@ -3993,7 +4017,11 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
_roomRocketRoomReadyRewardRetryDelays[attempt],
|
||||
() {
|
||||
_roomRocketRoomReadyRewardRetryTimer = null;
|
||||
handleVoiceRoomReadyForRocketRewards(roomId, attempt: attempt + 1);
|
||||
handleVoiceRoomReadyForRocketRewards(
|
||||
roomId,
|
||||
attempt: attempt + 1,
|
||||
forceQuery: forceQuery,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
@ -4010,11 +4038,12 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
final now = DateTime.now().millisecondsSinceEpoch;
|
||||
_pruneRoomRocketRewardFallbacks(now);
|
||||
final existing = _pendingRoomRocketRewardPopups[normalizedRoomId];
|
||||
final existingRecords =
|
||||
existing?.initialRoomRecords ?? const <SCRoomRocketRewardRecordRes>[];
|
||||
final records =
|
||||
initialRoomRecords.isNotEmpty
|
||||
? List<SCRoomRocketRewardRecordRes>.unmodifiable(initialRoomRecords)
|
||||
: existing?.initialRoomRecords ??
|
||||
const <SCRoomRocketRewardRecordRes>[];
|
||||
? _mergeRoomRocketRewardRecords(existingRecords, initialRoomRecords)
|
||||
: existingRecords;
|
||||
_pendingRoomRocketRewardPopups[normalizedRoomId] =
|
||||
_PendingRoomRocketRewardPopup(
|
||||
roomId: normalizedRoomId,
|
||||
@ -4023,6 +4052,47 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
);
|
||||
}
|
||||
|
||||
List<SCRoomRocketRewardRecordRes> _mergeRoomRocketRewardRecords(
|
||||
List<SCRoomRocketRewardRecordRes> existingRecords,
|
||||
List<SCRoomRocketRewardRecordRes> incomingRecords,
|
||||
) {
|
||||
if (existingRecords.isEmpty) {
|
||||
return List<SCRoomRocketRewardRecordRes>.unmodifiable(incomingRecords);
|
||||
}
|
||||
if (incomingRecords.isEmpty) {
|
||||
return existingRecords;
|
||||
}
|
||||
final merged = <SCRoomRocketRewardRecordRes>[...existingRecords];
|
||||
final seen = existingRecords.map(_roomRocketRewardRecordCacheKey).toSet();
|
||||
for (final record in incomingRecords) {
|
||||
if (seen.add(_roomRocketRewardRecordCacheKey(record))) {
|
||||
merged.add(record);
|
||||
}
|
||||
}
|
||||
return List<SCRoomRocketRewardRecordRes>.unmodifiable(merged);
|
||||
}
|
||||
|
||||
String _roomRocketRewardRecordCacheKey(SCRoomRocketRewardRecordRes record) {
|
||||
final id = record.id.trim();
|
||||
if (id.isNotEmpty) {
|
||||
return 'id:$id';
|
||||
}
|
||||
return [
|
||||
record.launchNo.trim(),
|
||||
record.roomId.trim(),
|
||||
record.roundNo.toString(),
|
||||
record.level.toString(),
|
||||
record.userId.trim(),
|
||||
record.rewardScene.trim().toUpperCase(),
|
||||
record.rewardType.trim().toUpperCase(),
|
||||
record.rewardItemId.trim(),
|
||||
record.rewardName.trim(),
|
||||
record.rewardAmount.toString(),
|
||||
record.grantTime.trim(),
|
||||
record.createTime.trim(),
|
||||
].join('|');
|
||||
}
|
||||
|
||||
void _markRoomRocketRewardProbe(String roomId) {
|
||||
final normalizedRoomId = roomId.trim();
|
||||
if (normalizedRoomId.isEmpty) {
|
||||
@ -4532,12 +4602,14 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
List<SCRoomRocketRewardRecordRes> initialRoomRecords =
|
||||
const <SCRoomRocketRewardRecordRes>[],
|
||||
}) async {
|
||||
if (!_isCurrentVisibleVoiceRoom(roomId)) {
|
||||
final normalizedRoomId = roomId.trim();
|
||||
if (normalizedRoomId.isEmpty ||
|
||||
!_isCurrentVisibleVoiceRoom(normalizedRoomId)) {
|
||||
return;
|
||||
}
|
||||
if (_isRoomRocketRewardPopupHeld(roomId)) {
|
||||
if (_isRoomRocketRewardPopupHeld(normalizedRoomId)) {
|
||||
_scheduleRoomRocketRewardPopupAfterHold(
|
||||
roomId,
|
||||
normalizedRoomId,
|
||||
initialRoomRecords: initialRoomRecords,
|
||||
);
|
||||
return;
|
||||
@ -4546,7 +4618,7 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
SCRoomTopLayerGuard().runWhenInactive(() {
|
||||
unawaited(
|
||||
_showRoomRocketRewardPopupDialog(
|
||||
roomId,
|
||||
normalizedRoomId,
|
||||
attempt: attempt,
|
||||
initialRoomRecords: initialRoomRecords,
|
||||
),
|
||||
@ -4554,16 +4626,21 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!_roomRocketRewardPopupLoadingRoomIds.add(normalizedRoomId)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
final res = await SCChatRoomRepository().roomRocketRewardPopups(roomId);
|
||||
if (!_isCurrentVisibleVoiceRoom(roomId)) {
|
||||
final res = await SCChatRoomRepository().roomRocketRewardPopups(
|
||||
normalizedRoomId,
|
||||
);
|
||||
if (!_isCurrentVisibleVoiceRoom(normalizedRoomId)) {
|
||||
return;
|
||||
}
|
||||
if (res.records.isEmpty &&
|
||||
initialRoomRecords.isEmpty &&
|
||||
attempt < _roomRocketRewardPopupRetryDelays.length) {
|
||||
_scheduleRoomRocketRewardPopupRetry(
|
||||
roomId,
|
||||
normalizedRoomId,
|
||||
attempt,
|
||||
initialRoomRecords: initialRoomRecords,
|
||||
);
|
||||
@ -4581,44 +4658,103 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
_roomRocketRewardPopupRetryTimer?.cancel();
|
||||
_roomRocketRewardPopupRetryTimer = null;
|
||||
SCRoomRocketRewardPopupAck.markShownAndAck(popupRecords);
|
||||
final recordIds = SCRoomRocketRewardPopupAck.recordIds(popupRecords);
|
||||
final userProfileResolver = _roomRocketRewardUserProfileResolver();
|
||||
SmartDialog.dismiss(tag: _roomRocketRewardDialogTag);
|
||||
SmartDialog.show(
|
||||
tag: _roomRocketRewardDialogTag,
|
||||
alignment: Alignment.center,
|
||||
debounce: true,
|
||||
animationType: SmartAnimationType.fade,
|
||||
maskColor: Colors.transparent,
|
||||
clickMaskDismiss: false,
|
||||
builder:
|
||||
(_) => SCRoomTopLayerSuppressor(
|
||||
reason: 'room_rocket_reward_dialog',
|
||||
keepDialogTags: const <String>{_roomRocketRewardDialogTag},
|
||||
child: RoomRocketRewardDialogLoader(
|
||||
roomId: roomId,
|
||||
initialPopupRecords: popupRecords,
|
||||
initialRoomRecords: initialRoomRecords,
|
||||
userProfileResolver: userProfileResolver,
|
||||
onClose: () {
|
||||
SmartDialog.dismiss(tag: _roomRocketRewardDialogTag);
|
||||
_scheduleRoomRocketPostLaunchStatusRefresh(roomId);
|
||||
if (recordIds.isNotEmpty) {
|
||||
SCRoomRocketRewardPopupAck.markShownAndAck(popupRecords);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
final popupGroups = RoomRocketApiMapper.rewardPopupRecordGroups(
|
||||
popupRecords,
|
||||
);
|
||||
final groupsToShow =
|
||||
popupGroups.isNotEmpty
|
||||
? popupGroups
|
||||
: const <List<SCRoomRocketRewardRecordRes>>[
|
||||
<SCRoomRocketRewardRecordRes>[],
|
||||
];
|
||||
_showRoomRocketRewardPopupGroups(
|
||||
roomId: normalizedRoomId,
|
||||
popupGroups: groupsToShow,
|
||||
groupIndex: 0,
|
||||
initialRoomRecords: initialRoomRecords,
|
||||
userProfileResolver: userProfileResolver,
|
||||
);
|
||||
} catch (_) {
|
||||
_scheduleRoomRocketRewardPopupRetry(
|
||||
roomId,
|
||||
normalizedRoomId,
|
||||
attempt,
|
||||
initialRoomRecords: initialRoomRecords,
|
||||
);
|
||||
} finally {
|
||||
_roomRocketRewardPopupLoadingRoomIds.remove(normalizedRoomId);
|
||||
}
|
||||
}
|
||||
|
||||
void _showRoomRocketRewardPopupGroups({
|
||||
required String roomId,
|
||||
required List<List<SCRoomRocketRewardRecordRes>> popupGroups,
|
||||
required int groupIndex,
|
||||
required List<SCRoomRocketRewardRecordRes> initialRoomRecords,
|
||||
required RoomRocketRewardUserProfileResolver? userProfileResolver,
|
||||
}) {
|
||||
if (!_isCurrentVisibleVoiceRoom(roomId) ||
|
||||
groupIndex >= popupGroups.length) {
|
||||
return;
|
||||
}
|
||||
if (SCRoomTopLayerGuard().isActive) {
|
||||
SCRoomTopLayerGuard().runWhenInactive(() {
|
||||
_showRoomRocketRewardPopupGroups(
|
||||
roomId: roomId,
|
||||
popupGroups: popupGroups,
|
||||
groupIndex: groupIndex,
|
||||
initialRoomRecords: initialRoomRecords,
|
||||
userProfileResolver: userProfileResolver,
|
||||
);
|
||||
});
|
||||
return;
|
||||
}
|
||||
final popupRecords = popupGroups[groupIndex];
|
||||
final roomRecords = RoomRocketApiMapper.rewardRoomRecordsForPopupGroup(
|
||||
initialRoomRecords,
|
||||
popupRecords,
|
||||
);
|
||||
final nextGroupIndex = groupIndex + 1;
|
||||
SmartDialog.dismiss(tag: _roomRocketRewardDialogTag);
|
||||
SmartDialog.show(
|
||||
tag: _roomRocketRewardDialogTag,
|
||||
alignment: Alignment.center,
|
||||
debounce: true,
|
||||
animationType: SmartAnimationType.fade,
|
||||
maskColor: Colors.transparent,
|
||||
clickMaskDismiss: false,
|
||||
builder:
|
||||
(_) => SCRoomTopLayerSuppressor(
|
||||
reason: 'room_rocket_reward_dialog',
|
||||
keepDialogTags: const <String>{_roomRocketRewardDialogTag},
|
||||
child: RoomRocketRewardDialogLoader(
|
||||
roomId: roomId,
|
||||
initialPopupRecords: popupRecords,
|
||||
initialRoomRecords: roomRecords,
|
||||
userProfileResolver: userProfileResolver,
|
||||
onClose: () {
|
||||
SmartDialog.dismiss(tag: _roomRocketRewardDialogTag);
|
||||
_scheduleRoomRocketPostLaunchStatusRefresh(roomId);
|
||||
SCRoomRocketRewardPopupAck.markShownAndAck(popupRecords);
|
||||
if (nextGroupIndex < popupGroups.length) {
|
||||
unawaited(
|
||||
Future<void>.delayed(const Duration(milliseconds: 160), () {
|
||||
_showRoomRocketRewardPopupGroups(
|
||||
roomId: roomId,
|
||||
popupGroups: popupGroups,
|
||||
groupIndex: nextGroupIndex,
|
||||
initialRoomRecords: initialRoomRecords,
|
||||
userProfileResolver: userProfileResolver,
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _scheduleRoomRocketRewardPopupRetry(
|
||||
String roomId,
|
||||
int attempt, {
|
||||
|
||||
@ -34,6 +34,7 @@ abstract class RoomTrtcAudioEffectManager {
|
||||
void resumePlayMusic(int id);
|
||||
void setMusicPublishVolume(int id, int volume);
|
||||
void setMusicPlayoutVolume(int id, int volume);
|
||||
void setVoiceCaptureVolume(int volume);
|
||||
int getMusicCurrentPosInMS(int id);
|
||||
int getMusicDurationInMS(String path);
|
||||
void seekMusicToPosInTime(int id, int pts);
|
||||
@ -156,6 +157,11 @@ class TencentRoomTrtcAudioEffectManager implements RoomTrtcAudioEffectManager {
|
||||
_manager.setMusicPlayoutVolume(id, volume);
|
||||
}
|
||||
|
||||
@override
|
||||
void setVoiceCaptureVolume(int volume) {
|
||||
_manager.setVoiceCaptureVolume(volume);
|
||||
}
|
||||
|
||||
@override
|
||||
int getMusicCurrentPosInMS(int id) {
|
||||
return _manager.getMusicCurrentPosInMS(id);
|
||||
@ -364,7 +370,8 @@ class TrtcRoomRtcEngineAdapter implements RoomRtcEngineAdapter {
|
||||
try {
|
||||
cloud.switchRole(TRTCRoleType.anchor);
|
||||
cloud.startLocalAudio(TRTCAudioQuality.defaultMode);
|
||||
cloud.muteLocalAudio(config.muted);
|
||||
_musicManager?.setVoiceCaptureVolume(config.muted ? 0 : 100);
|
||||
cloud.muteLocalAudio(config.publishMuted);
|
||||
_isLocalAudioStarted = true;
|
||||
return true;
|
||||
} catch (_) {
|
||||
|
||||
@ -180,6 +180,17 @@ class SocialChatUserProfileManager extends ChangeNotifier {
|
||||
}
|
||||
final cpPairs = <CPRes>[];
|
||||
final closeFriendPairs = <CPRes>[];
|
||||
_addCpRelationPairs(
|
||||
cpPairs: cpPairs,
|
||||
closeFriendPairs: closeFriendPairs,
|
||||
relations: profile.cpList,
|
||||
fallbackRelationType: 'CP',
|
||||
);
|
||||
_addCpRelationPairs(
|
||||
cpPairs: cpPairs,
|
||||
closeFriendPairs: closeFriendPairs,
|
||||
relations: profile.closeFriendList,
|
||||
);
|
||||
await Future.wait(
|
||||
_cpProfileRelationTypes.map((relationType) async {
|
||||
final cabin = await _safeLoadCpCabin(userId, relationType);
|
||||
@ -206,11 +217,12 @@ class SocialChatUserProfileManager extends ChangeNotifier {
|
||||
levelText: rightsLevelText ?? cabinLevelText,
|
||||
dismissCost: cpPair.dismissCost ?? _cpDismissCostFromCabin(cabin),
|
||||
);
|
||||
if (normalizedType == 'CP') {
|
||||
cpPairs.add(enrichedPair);
|
||||
} else {
|
||||
closeFriendPairs.add(enrichedPair);
|
||||
}
|
||||
_addCpRelationPair(
|
||||
cpPairs: cpPairs,
|
||||
closeFriendPairs: closeFriendPairs,
|
||||
relation: enrichedPair,
|
||||
fallbackRelationType: normalizedType,
|
||||
);
|
||||
}),
|
||||
);
|
||||
if (kDebugMode) {
|
||||
@ -226,6 +238,81 @@ class SocialChatUserProfileManager extends ChangeNotifier {
|
||||
);
|
||||
}
|
||||
|
||||
void _addCpRelationPairs({
|
||||
required List<CPRes> cpPairs,
|
||||
required List<CPRes> closeFriendPairs,
|
||||
required List<CPRes>? relations,
|
||||
String? fallbackRelationType,
|
||||
}) {
|
||||
for (final relation in relations ?? const <CPRes>[]) {
|
||||
_addCpRelationPair(
|
||||
cpPairs: cpPairs,
|
||||
closeFriendPairs: closeFriendPairs,
|
||||
relation: relation,
|
||||
fallbackRelationType: fallbackRelationType,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _addCpRelationPair({
|
||||
required List<CPRes> cpPairs,
|
||||
required List<CPRes> closeFriendPairs,
|
||||
required CPRes relation,
|
||||
String? fallbackRelationType,
|
||||
}) {
|
||||
if (!_hasCpPair(relation)) {
|
||||
return;
|
||||
}
|
||||
final relationTypeSource =
|
||||
(relation.relationType?.trim().isNotEmpty ?? false)
|
||||
? relation.relationType
|
||||
: fallbackRelationType;
|
||||
if ((relationTypeSource ?? "").trim().isEmpty) {
|
||||
return;
|
||||
}
|
||||
final normalizedType = _normalizeCpRelationType(relationTypeSource);
|
||||
final normalizedRelation = relation.copyWith(relationType: normalizedType);
|
||||
final target = normalizedType == 'CP' ? cpPairs : closeFriendPairs;
|
||||
final relationKey = _cpRelationPairKey(normalizedRelation);
|
||||
final existingIndex = target.indexWhere(
|
||||
(item) => _cpRelationPairKey(item) == relationKey,
|
||||
);
|
||||
if (existingIndex >= 0) {
|
||||
target[existingIndex] = normalizedRelation;
|
||||
} else {
|
||||
target.add(normalizedRelation);
|
||||
}
|
||||
}
|
||||
|
||||
String _cpRelationPairKey(CPRes relation) {
|
||||
final type = _normalizeCpRelationType(relation.relationType);
|
||||
final userIds = <String>[
|
||||
relation.meUserId?.trim() ?? "",
|
||||
relation.cpUserId?.trim() ?? "",
|
||||
]..removeWhere((value) => value.isEmpty);
|
||||
if (userIds.length >= 2) {
|
||||
userIds.sort();
|
||||
return '$type|id|${userIds.join("|")}';
|
||||
}
|
||||
final accounts = <String>[
|
||||
relation.meAccount?.trim() ?? "",
|
||||
relation.cpAccount?.trim() ?? "",
|
||||
]..removeWhere((value) => value.isEmpty);
|
||||
if (accounts.length >= 2) {
|
||||
accounts.sort();
|
||||
return '$type|account|${accounts.join("|")}';
|
||||
}
|
||||
return [
|
||||
type,
|
||||
relation.meUserId?.trim() ?? "",
|
||||
relation.meAccount?.trim() ?? "",
|
||||
relation.cpUserId?.trim() ?? "",
|
||||
relation.cpAccount?.trim() ?? "",
|
||||
relation.cpUserNickname?.trim() ?? "",
|
||||
relation.meUserNickname?.trim() ?? "",
|
||||
].join('|');
|
||||
}
|
||||
|
||||
Future<SCCpCabinRes?> _safeLoadCpCabin(
|
||||
String userId,
|
||||
String relationType,
|
||||
|
||||
@ -355,12 +355,145 @@ class SCRoomRocketRewardRes {
|
||||
|
||||
factory SCRoomRocketRewardRes.fromJson(dynamic json) {
|
||||
final data = _asMap(json);
|
||||
final reward = _asMap(
|
||||
_firstPayload([
|
||||
data['reward'],
|
||||
data['rewardInfo'],
|
||||
data['reward_info'],
|
||||
data['rewardConfig'],
|
||||
data['reward_config'],
|
||||
data['propsResources'],
|
||||
data['props_resources'],
|
||||
data['resource'],
|
||||
]),
|
||||
);
|
||||
final resource = _asMap(
|
||||
_firstPayload([
|
||||
data['propsResources'],
|
||||
data['props_resources'],
|
||||
data['resource'],
|
||||
reward['propsResources'],
|
||||
reward['props_resources'],
|
||||
reward['resource'],
|
||||
]),
|
||||
);
|
||||
final rewardType = _stringValue(
|
||||
data['rewardType'] ??
|
||||
data['reward_type'] ??
|
||||
data['detailType'] ??
|
||||
data['detail_type'] ??
|
||||
data['type'] ??
|
||||
reward['rewardType'] ??
|
||||
reward['reward_type'] ??
|
||||
reward['detailType'] ??
|
||||
reward['detail_type'] ??
|
||||
reward['type'] ??
|
||||
resource['type'],
|
||||
);
|
||||
final rewardAmountSource =
|
||||
_isGoldRewardType(rewardType)
|
||||
? _firstPayload([
|
||||
data['rewardAmount'],
|
||||
data['reward_amount'],
|
||||
data['amount'],
|
||||
data['content'],
|
||||
data['rewardQuantity'],
|
||||
data['reward_quantity'],
|
||||
data['quantity'],
|
||||
data['count'],
|
||||
data['num'],
|
||||
reward['rewardAmount'],
|
||||
reward['reward_amount'],
|
||||
reward['amount'],
|
||||
reward['content'],
|
||||
reward['rewardQuantity'],
|
||||
reward['reward_quantity'],
|
||||
reward['quantity'],
|
||||
reward['count'],
|
||||
reward['num'],
|
||||
])
|
||||
: _firstPayload([
|
||||
data['rewardAmount'],
|
||||
data['reward_amount'],
|
||||
data['rewardQuantity'],
|
||||
data['reward_quantity'],
|
||||
data['quantity'],
|
||||
data['count'],
|
||||
data['num'],
|
||||
data['amount'],
|
||||
data['content'],
|
||||
reward['rewardAmount'],
|
||||
reward['reward_amount'],
|
||||
reward['rewardQuantity'],
|
||||
reward['reward_quantity'],
|
||||
reward['quantity'],
|
||||
reward['count'],
|
||||
reward['num'],
|
||||
reward['amount'],
|
||||
reward['content'],
|
||||
]);
|
||||
return SCRoomRocketRewardRes(
|
||||
rewardScene: _stringValue(data['rewardScene']),
|
||||
rewardType: _stringValue(data['rewardType']),
|
||||
rewardItemId: _stringValue(data['rewardItemId']),
|
||||
rewardName: _stringValue(data['rewardName']),
|
||||
rewardCover: _stringValue(data['rewardCover']),
|
||||
rewardScene: _stringValue(
|
||||
data['rewardScene'] ??
|
||||
data['reward_scene'] ??
|
||||
reward['rewardScene'] ??
|
||||
reward['reward_scene'],
|
||||
),
|
||||
rewardType: rewardType,
|
||||
rewardItemId: _stringValue(
|
||||
data['rewardItemId'] ??
|
||||
data['reward_item_id'] ??
|
||||
data['itemId'] ??
|
||||
data['item_id'] ??
|
||||
data['resourceId'] ??
|
||||
data['resource_id'] ??
|
||||
reward['rewardItemId'] ??
|
||||
reward['reward_item_id'] ??
|
||||
reward['itemId'] ??
|
||||
reward['item_id'] ??
|
||||
reward['resourceId'] ??
|
||||
reward['resource_id'] ??
|
||||
reward['id'] ??
|
||||
reward['amount'] ??
|
||||
reward['content'] ??
|
||||
resource['id'],
|
||||
),
|
||||
rewardName: _stringValue(
|
||||
data['rewardName'] ??
|
||||
data['reward_name'] ??
|
||||
data['name'] ??
|
||||
data['badgeName'] ??
|
||||
data['badge_name'] ??
|
||||
reward['rewardName'] ??
|
||||
reward['reward_name'] ??
|
||||
reward['name'] ??
|
||||
reward['badgeName'] ??
|
||||
reward['badge_name'] ??
|
||||
resource['name'] ??
|
||||
resource['badgeName'] ??
|
||||
resource['badge_name'],
|
||||
),
|
||||
rewardCover: _firstAssetString([
|
||||
data['rewardCover'],
|
||||
data['reward_cover'],
|
||||
data['cover'],
|
||||
data['sourceUrl'],
|
||||
data['source_url'],
|
||||
data['iconUrl'],
|
||||
data['icon_url'],
|
||||
reward['rewardCover'],
|
||||
reward['reward_cover'],
|
||||
reward['cover'],
|
||||
reward['sourceUrl'],
|
||||
reward['source_url'],
|
||||
reward['iconUrl'],
|
||||
reward['icon_url'],
|
||||
resource['cover'],
|
||||
resource['sourceUrl'],
|
||||
resource['source_url'],
|
||||
resource['iconUrl'],
|
||||
resource['icon_url'],
|
||||
]),
|
||||
rocketIconUrl: _firstAssetString([
|
||||
data['rocket_icon_url'],
|
||||
data['rocketIconUrl'],
|
||||
@ -376,8 +509,28 @@ class SCRoomRocketRewardRes {
|
||||
_asMap(data['config'])['rocketIconUrl'],
|
||||
_asMap(data['config'])['rocketIcon'],
|
||||
]),
|
||||
rewardAmount: _rewardAmountValue(data['rewardAmount']) ?? 0,
|
||||
expireDays: _dayValue(data['expireDays']),
|
||||
rewardAmount: _rewardAmountValue(rewardAmountSource) ?? 0,
|
||||
expireDays: _dayValue(
|
||||
_firstPayload([
|
||||
data['expireDays'],
|
||||
data['expire_days'],
|
||||
data['durationDays'],
|
||||
data['duration_days'],
|
||||
data['validDays'],
|
||||
data['valid_days'],
|
||||
data['validityDays'],
|
||||
data['validity_days'],
|
||||
reward['expireDays'],
|
||||
reward['expire_days'],
|
||||
reward['durationDays'],
|
||||
reward['duration_days'],
|
||||
reward['validDays'],
|
||||
reward['valid_days'],
|
||||
reward['validityDays'],
|
||||
reward['validity_days'],
|
||||
reward['quantity'],
|
||||
]),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -578,11 +731,20 @@ class SCRoomRocketRewardPreviewRes {
|
||||
|
||||
static bool _looksLikeReward(Map<String, dynamic> data) {
|
||||
return data.containsKey('rewardScene') ||
|
||||
data.containsKey('reward_scene') ||
|
||||
data.containsKey('rewardType') ||
|
||||
data.containsKey('reward_type') ||
|
||||
data.containsKey('rewardItemId') ||
|
||||
data.containsKey('reward_item_id') ||
|
||||
data.containsKey('rewardName') ||
|
||||
data.containsKey('reward_name') ||
|
||||
data.containsKey('rewardCover') ||
|
||||
data.containsKey('rewardAmount');
|
||||
data.containsKey('reward_cover') ||
|
||||
data.containsKey('rewardAmount') ||
|
||||
data.containsKey('reward_amount') ||
|
||||
data.containsKey('reward') ||
|
||||
data.containsKey('rewardInfo') ||
|
||||
data.containsKey('reward_info');
|
||||
}
|
||||
|
||||
static String _normalizedScene(String value) {
|
||||
@ -925,6 +1087,102 @@ class SCRoomRocketRewardRecordRes {
|
||||
winner,
|
||||
]),
|
||||
);
|
||||
final reward = _asMap(
|
||||
_firstPayload([
|
||||
data['reward'],
|
||||
data['rewardInfo'],
|
||||
data['reward_info'],
|
||||
data['rewardConfig'],
|
||||
data['reward_config'],
|
||||
data['propsResources'],
|
||||
data['props_resources'],
|
||||
data['resource'],
|
||||
]),
|
||||
);
|
||||
final resource = _asMap(
|
||||
_firstPayload([
|
||||
data['propsResources'],
|
||||
data['props_resources'],
|
||||
data['resource'],
|
||||
reward['propsResources'],
|
||||
reward['props_resources'],
|
||||
reward['resource'],
|
||||
]),
|
||||
);
|
||||
final rewardType = _stringValue(
|
||||
data['rewardType'] ??
|
||||
data['reward_type'] ??
|
||||
data['detailType'] ??
|
||||
data['detail_type'] ??
|
||||
data['type'] ??
|
||||
reward['rewardType'] ??
|
||||
reward['reward_type'] ??
|
||||
reward['detailType'] ??
|
||||
reward['detail_type'] ??
|
||||
reward['type'] ??
|
||||
resource['type'],
|
||||
);
|
||||
final rewardAmountSource =
|
||||
_isGoldRewardType(rewardType)
|
||||
? _firstPayload([
|
||||
data['rewardAmount'],
|
||||
data['reward_amount'],
|
||||
data['amount'],
|
||||
data['content'],
|
||||
data['rewardQuantity'],
|
||||
data['reward_quantity'],
|
||||
data['quantity'],
|
||||
data['count'],
|
||||
data['num'],
|
||||
reward['rewardAmount'],
|
||||
reward['reward_amount'],
|
||||
reward['amount'],
|
||||
reward['content'],
|
||||
reward['rewardQuantity'],
|
||||
reward['reward_quantity'],
|
||||
reward['quantity'],
|
||||
reward['count'],
|
||||
reward['num'],
|
||||
])
|
||||
: _firstPayload([
|
||||
data['rewardAmount'],
|
||||
data['reward_amount'],
|
||||
data['rewardQuantity'],
|
||||
data['reward_quantity'],
|
||||
data['quantity'],
|
||||
data['count'],
|
||||
data['num'],
|
||||
data['amount'],
|
||||
data['content'],
|
||||
reward['rewardAmount'],
|
||||
reward['reward_amount'],
|
||||
reward['rewardQuantity'],
|
||||
reward['reward_quantity'],
|
||||
reward['quantity'],
|
||||
reward['count'],
|
||||
reward['num'],
|
||||
reward['amount'],
|
||||
reward['content'],
|
||||
]);
|
||||
final expireDaysSource = _firstPayload([
|
||||
data['expireDays'],
|
||||
data['expire_days'],
|
||||
data['durationDays'],
|
||||
data['duration_days'],
|
||||
data['validDays'],
|
||||
data['valid_days'],
|
||||
data['validityDays'],
|
||||
data['validity_days'],
|
||||
reward['expireDays'],
|
||||
reward['expire_days'],
|
||||
reward['durationDays'],
|
||||
reward['duration_days'],
|
||||
reward['validDays'],
|
||||
reward['valid_days'],
|
||||
reward['validityDays'],
|
||||
reward['validity_days'],
|
||||
reward['quantity'],
|
||||
]);
|
||||
return SCRoomRocketRewardRecordRes(
|
||||
id: _stringValue(data['id']),
|
||||
launchNo: _stringValue(data['launchNo'] ?? data['launch_no']),
|
||||
@ -1085,16 +1343,68 @@ class SCRoomRocketRewardRecordRes {
|
||||
profile['actual_account'],
|
||||
]),
|
||||
),
|
||||
rewardScene: _stringValue(data['rewardScene'] ?? data['reward_scene']),
|
||||
rewardType: _stringValue(data['rewardType'] ?? data['reward_type']),
|
||||
rewardItemId: _stringValue(
|
||||
data['rewardItemId'] ?? data['reward_item_id'],
|
||||
rewardScene: _stringValue(
|
||||
data['rewardScene'] ??
|
||||
data['reward_scene'] ??
|
||||
reward['rewardScene'] ??
|
||||
reward['reward_scene'],
|
||||
),
|
||||
rewardName: _stringValue(data['rewardName'] ?? data['reward_name']),
|
||||
rewardCover: _stringValue(data['rewardCover'] ?? data['reward_cover']),
|
||||
rewardAmount:
|
||||
_rewardAmountValue(data['rewardAmount'] ?? data['reward_amount']) ??
|
||||
0,
|
||||
rewardType: rewardType,
|
||||
rewardItemId: _stringValue(
|
||||
data['rewardItemId'] ??
|
||||
data['reward_item_id'] ??
|
||||
data['itemId'] ??
|
||||
data['item_id'] ??
|
||||
data['resourceId'] ??
|
||||
data['resource_id'] ??
|
||||
reward['rewardItemId'] ??
|
||||
reward['reward_item_id'] ??
|
||||
reward['itemId'] ??
|
||||
reward['item_id'] ??
|
||||
reward['resourceId'] ??
|
||||
reward['resource_id'] ??
|
||||
reward['id'] ??
|
||||
reward['amount'] ??
|
||||
reward['content'] ??
|
||||
resource['id'],
|
||||
),
|
||||
rewardName: _stringValue(
|
||||
data['rewardName'] ??
|
||||
data['reward_name'] ??
|
||||
data['name'] ??
|
||||
data['badgeName'] ??
|
||||
data['badge_name'] ??
|
||||
reward['rewardName'] ??
|
||||
reward['reward_name'] ??
|
||||
reward['name'] ??
|
||||
reward['badgeName'] ??
|
||||
reward['badge_name'] ??
|
||||
resource['name'] ??
|
||||
resource['badgeName'] ??
|
||||
resource['badge_name'],
|
||||
),
|
||||
rewardCover: _firstAssetString([
|
||||
data['rewardCover'],
|
||||
data['reward_cover'],
|
||||
data['cover'],
|
||||
data['sourceUrl'],
|
||||
data['source_url'],
|
||||
data['iconUrl'],
|
||||
data['icon_url'],
|
||||
reward['rewardCover'],
|
||||
reward['reward_cover'],
|
||||
reward['cover'],
|
||||
reward['sourceUrl'],
|
||||
reward['source_url'],
|
||||
reward['iconUrl'],
|
||||
reward['icon_url'],
|
||||
resource['cover'],
|
||||
resource['sourceUrl'],
|
||||
resource['source_url'],
|
||||
resource['iconUrl'],
|
||||
resource['icon_url'],
|
||||
]),
|
||||
rewardAmount: _rewardAmountValue(rewardAmountSource) ?? 0,
|
||||
rocketIconUrl: _firstAssetString([
|
||||
data['rocket_icon_url'],
|
||||
data['rocketIconUrl'],
|
||||
@ -1110,7 +1420,7 @@ class SCRoomRocketRewardRecordRes {
|
||||
_asMap(data['config'])['rocketIconUrl'],
|
||||
_asMap(data['config'])['rocketIcon'],
|
||||
]),
|
||||
expireDays: _dayValue(data['expireDays'] ?? data['expire_days']),
|
||||
expireDays: _dayValue(expireDaysSource),
|
||||
grantStatus: _stringValue(data['grantStatus'] ?? data['grant_status']),
|
||||
popupStatus: _stringValue(data['popupStatus'] ?? data['popup_status']),
|
||||
grantTime: _stringValue(data['grantTime'] ?? data['grant_time']),
|
||||
@ -1253,6 +1563,9 @@ bool _looksLikeRewardRecord(Map<String, dynamic> data) {
|
||||
'reward_cover',
|
||||
'rewardAmount',
|
||||
'reward_amount',
|
||||
'reward',
|
||||
'rewardInfo',
|
||||
'reward_info',
|
||||
'grantStatus',
|
||||
'grant_status',
|
||||
'popupStatus',
|
||||
@ -1420,6 +1733,15 @@ num? _rewardAmountValue(dynamic value) {
|
||||
return _numValue(value) ?? _dayNumberValue(value);
|
||||
}
|
||||
|
||||
bool _isGoldRewardType(String value) {
|
||||
final normalized = value.trim().toUpperCase();
|
||||
return normalized == 'GOLD' ||
|
||||
normalized == 'COIN' ||
|
||||
normalized == 'COINS' ||
|
||||
normalized == 'GOLD_COIN' ||
|
||||
normalized == 'GOLD_COINS';
|
||||
}
|
||||
|
||||
int _dayValue(dynamic value, {int fallback = 0}) {
|
||||
final numeric = _numValue(value) ?? _dayNumberValue(value);
|
||||
return numeric?.toInt() ?? fallback;
|
||||
|
||||
@ -937,10 +937,21 @@ class SCChatRoomRepository implements SocialChatRoomRepository {
|
||||
|
||||
///room/setting/kick-off-microphone
|
||||
@override
|
||||
Future<bool> kickOffMicrophone(String roomId, String kickUserId) async {
|
||||
Future<bool> kickOffMicrophone(
|
||||
String roomId,
|
||||
String kickUserId, {
|
||||
num? mickIndex,
|
||||
}) async {
|
||||
final data = <String, dynamic>{
|
||||
"roomId": roomId,
|
||||
"kickUserId": kickUserId,
|
||||
"userId": kickUserId,
|
||||
if (mickIndex != null) "mickIndex": mickIndex,
|
||||
if (mickIndex != null) "micIndex": mickIndex,
|
||||
};
|
||||
final result = await http.post<bool>(
|
||||
"a4465894e21be08d833ef701566ffe1b6a835338046b35fb94271445c996f6e3574aff257ce7e668d08f4caccd1c6232",
|
||||
data: {"roomId": roomId, "kickUserId": kickUserId},
|
||||
data: data,
|
||||
fromJson: (json) => json as bool,
|
||||
);
|
||||
return result;
|
||||
|
||||
@ -75,6 +75,9 @@ SCCpRelationPairInfo? _relationInfoIfContainsBoth(
|
||||
SocialChatUserProfile left,
|
||||
SocialChatUserProfile right,
|
||||
) {
|
||||
if (_isInactiveRelationStatus(relation.status)) {
|
||||
return null;
|
||||
}
|
||||
final leftKeys = _profileKeys(left);
|
||||
final rightKeys = _profileKeys(right);
|
||||
if (leftKeys.isEmpty || rightKeys.isEmpty) {
|
||||
@ -92,6 +95,21 @@ SCCpRelationPairInfo? _relationInfoIfContainsBoth(
|
||||
);
|
||||
}
|
||||
|
||||
bool _isInactiveRelationStatus(String? status) {
|
||||
final normalizedStatus = status?.trim().toUpperCase() ?? "";
|
||||
return const <String>{
|
||||
"NONE",
|
||||
"INACTIVE",
|
||||
"DISMISSED",
|
||||
"DISSOLVED",
|
||||
"CANCELLED",
|
||||
"CANCELED",
|
||||
"EXPIRED",
|
||||
"ENDED",
|
||||
"REJECTED",
|
||||
}.contains(normalizedStatus);
|
||||
}
|
||||
|
||||
bool _hasRelationIdentity(SocialChatUserProfile? profile) =>
|
||||
_profileKeys(profile).isNotEmpty;
|
||||
|
||||
|
||||
@ -29,6 +29,58 @@ class RoomRocketRewardUserProfile {
|
||||
class RoomRocketApiMapper {
|
||||
const RoomRocketApiMapper._();
|
||||
|
||||
static List<List<SCRoomRocketRewardRecordRes>> rewardPopupRecordGroups(
|
||||
List<SCRoomRocketRewardRecordRes> records,
|
||||
) {
|
||||
if (records.isEmpty) {
|
||||
return const <List<SCRoomRocketRewardRecordRes>>[];
|
||||
}
|
||||
final order = <String>[];
|
||||
final groups = <String, List<SCRoomRocketRewardRecordRes>>{};
|
||||
for (var index = 0; index < records.length; index += 1) {
|
||||
final record = records[index];
|
||||
final launchNo = record.launchNo.trim();
|
||||
final id = record.id.trim();
|
||||
final key =
|
||||
launchNo.isNotEmpty
|
||||
? 'launch:$launchNo'
|
||||
: id.isNotEmpty
|
||||
? 'record:$id'
|
||||
: 'index:$index';
|
||||
if (!groups.containsKey(key)) {
|
||||
order.add(key);
|
||||
groups[key] = <SCRoomRocketRewardRecordRes>[];
|
||||
}
|
||||
groups[key]!.add(record);
|
||||
}
|
||||
return order
|
||||
.map(
|
||||
(key) => List<SCRoomRocketRewardRecordRes>.unmodifiable(groups[key]!),
|
||||
)
|
||||
.toList(growable: false);
|
||||
}
|
||||
|
||||
static List<SCRoomRocketRewardRecordRes> rewardRoomRecordsForPopupGroup(
|
||||
List<SCRoomRocketRewardRecordRes> roomRecords,
|
||||
List<SCRoomRocketRewardRecordRes> popupRecords,
|
||||
) {
|
||||
if (roomRecords.isEmpty || popupRecords.isEmpty) {
|
||||
return roomRecords;
|
||||
}
|
||||
final launchNos =
|
||||
popupRecords
|
||||
.map((record) => record.launchNo.trim())
|
||||
.where((item) => item.isNotEmpty)
|
||||
.toSet();
|
||||
if (launchNos.isEmpty) {
|
||||
return roomRecords;
|
||||
}
|
||||
final matched = roomRecords
|
||||
.where((record) => launchNos.contains(record.launchNo.trim()))
|
||||
.toList(growable: false);
|
||||
return matched.isEmpty ? roomRecords : matched;
|
||||
}
|
||||
|
||||
static List<RoomRocketLevelItem>? levelsFromStatus(
|
||||
SCRoomRocketStatusRes? status,
|
||||
) {
|
||||
|
||||
@ -88,12 +88,12 @@ class _RoomRocketRewardDialogLoaderState
|
||||
if (useInitialPopupRecords) {
|
||||
_usedInitialPopupRecords = true;
|
||||
}
|
||||
final launchNo = _launchNoFromRecords(
|
||||
final launchNos = _launchNosFromRecords(
|
||||
nextPopupRecords.isNotEmpty ? nextPopupRecords : _popupRecords,
|
||||
);
|
||||
final nextRoomRecords = await _loadRoomRewardRecords(
|
||||
final nextRoomRecordPage = await _loadRoomRewardRecordPage(
|
||||
requestCursor,
|
||||
launchNo: launchNo,
|
||||
launchNos: launchNos,
|
||||
);
|
||||
if (!mounted) {
|
||||
return;
|
||||
@ -103,17 +103,17 @@ class _RoomRocketRewardDialogLoaderState
|
||||
_replaceUniqueRewardRecords(_popupRecords, nextPopupRecords);
|
||||
_replaceUniqueRewardRecords(_roomRecords, [
|
||||
...widget.initialRoomRecords,
|
||||
...nextRoomRecords,
|
||||
...nextRoomRecordPage.records,
|
||||
]);
|
||||
} else {
|
||||
_appendUniqueRewardRecords(_roomRecords, nextRoomRecords);
|
||||
_appendUniqueRewardRecords(_roomRecords, nextRoomRecordPage.records);
|
||||
}
|
||||
_rewards = RoomRocketApiMapper.rewardItemsFromRecords(_popupRecords);
|
||||
_winners = RoomRocketApiMapper.winnersFromRecords(
|
||||
_visibleRoomRewardRecords(),
|
||||
userProfileResolver: widget.userProfileResolver,
|
||||
);
|
||||
_hasMore = nextRoomRecords.length >= _rewardDialogPageSize;
|
||||
_hasMore = nextRoomRecordPage.hasMore;
|
||||
if (_hasMore) {
|
||||
_cursor = requestCursor + 1;
|
||||
}
|
||||
@ -125,7 +125,8 @@ class _RoomRocketRewardDialogLoaderState
|
||||
'popupCount=${nextPopupRecords.length} '
|
||||
'popupTotal=${_popupRecords.length} '
|
||||
'initialRoomCount=${widget.initialRoomRecords.length} '
|
||||
'roomCount=${nextRoomRecords.length} '
|
||||
'launchNos=${launchNos.join(",")} '
|
||||
'roomCount=${nextRoomRecordPage.records.length} '
|
||||
'roomTotal=${_roomRecords.length} '
|
||||
'visibleRoomTotal=${_visibleRoomRewardRecords().length} '
|
||||
'hasMore=$_hasMore nextCursor=$_cursor',
|
||||
@ -167,7 +168,7 @@ class _RoomRocketRewardDialogLoaderState
|
||||
try {
|
||||
_rewardDialogDebug(
|
||||
'request reward-room-records roomId=${widget.roomId} '
|
||||
'cursor=$cursor limit=$_rewardDialogPageSize',
|
||||
'cursor=$cursor limit=$_rewardDialogPageSize launchNo=$launchNo',
|
||||
);
|
||||
final res = await SCChatRoomRepository().roomRocketRewardRecords(
|
||||
widget.roomId,
|
||||
@ -183,6 +184,39 @@ class _RoomRocketRewardDialogLoaderState
|
||||
}
|
||||
}
|
||||
|
||||
Future<_RoomRocketRewardRecordPage> _loadRoomRewardRecordPage(
|
||||
int cursor, {
|
||||
required Set<String> launchNos,
|
||||
}) async {
|
||||
final normalizedLaunchNos =
|
||||
launchNos.where((item) => item.trim().isNotEmpty).toSet();
|
||||
if (normalizedLaunchNos.isEmpty) {
|
||||
final records = await _loadRoomRewardRecords(cursor);
|
||||
return _RoomRocketRewardRecordPage(
|
||||
records: records,
|
||||
hasMore: records.length >= _rewardDialogPageSize,
|
||||
);
|
||||
}
|
||||
final pages = await Future.wait(
|
||||
normalizedLaunchNos.map((launchNo) async {
|
||||
final records = await _loadRoomRewardRecords(
|
||||
cursor,
|
||||
launchNo: launchNo,
|
||||
);
|
||||
return _RoomRocketRewardRecordPage(
|
||||
records: records,
|
||||
hasMore: records.length >= _rewardDialogPageSize,
|
||||
);
|
||||
}),
|
||||
);
|
||||
return _RoomRocketRewardRecordPage(
|
||||
records: debugUniqueRoomRocketRewardRecords(
|
||||
pages.expand((page) => page.records).toList(growable: false),
|
||||
),
|
||||
hasMore: pages.any((page) => page.hasMore),
|
||||
);
|
||||
}
|
||||
|
||||
List<SCRoomRocketRewardRecordRes> _visibleRoomRewardRecords() {
|
||||
if (_roomRecords.isEmpty) {
|
||||
return _popupRecords;
|
||||
@ -217,14 +251,21 @@ class _RoomRocketRewardDialogLoaderState
|
||||
}
|
||||
}
|
||||
|
||||
String _launchNoFromRecords(List<SCRoomRocketRewardRecordRes> records) {
|
||||
for (final record in records) {
|
||||
final launchNo = record.launchNo.trim();
|
||||
if (launchNo.isNotEmpty) {
|
||||
return launchNo;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
class _RoomRocketRewardRecordPage {
|
||||
const _RoomRocketRewardRecordPage({
|
||||
required this.records,
|
||||
required this.hasMore,
|
||||
});
|
||||
|
||||
final List<SCRoomRocketRewardRecordRes> records;
|
||||
final bool hasMore;
|
||||
}
|
||||
|
||||
Set<String> _launchNosFromRecords(List<SCRoomRocketRewardRecordRes> records) {
|
||||
return records
|
||||
.map((record) => record.launchNo.trim())
|
||||
.where((launchNo) => launchNo.isNotEmpty)
|
||||
.toSet();
|
||||
}
|
||||
|
||||
void _replaceUniqueRewardRecords(
|
||||
|
||||
@ -290,7 +290,7 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
|
||||
required bool canShowSetting,
|
||||
}) {
|
||||
final hasCpRelation = _hasCpRelation(profile);
|
||||
final closeFriends = _closeFriendProfileData(profile).take(5).toList();
|
||||
final closeFriends = _closeFriendProfileData(profile);
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
@ -790,7 +790,8 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
|
||||
}
|
||||
|
||||
List<String> _closeFriendProfileData(SocialChatUserProfile? profile) {
|
||||
final avatars = <String>[];
|
||||
final brotherAvatars = <String>[];
|
||||
final sisterAvatars = <String>[];
|
||||
for (final relation in profile?.closeFriendList ?? const <CPRes>[]) {
|
||||
final type = _normalizeCpRelationType(relation.relationType);
|
||||
if (type != "BROTHER" && type != "SISTERS") {
|
||||
@ -802,10 +803,14 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
|
||||
final friend = _relationPartner(profile, relation);
|
||||
final avatar = friend.avatarUrl?.trim() ?? "";
|
||||
if (avatar.isNotEmpty) {
|
||||
avatars.add(avatar);
|
||||
if (type == "BROTHER" && brotherAvatars.length < 3) {
|
||||
brotherAvatars.add(avatar);
|
||||
} else if (type == "SISTERS" && sisterAvatars.length < 3) {
|
||||
sisterAvatars.add(avatar);
|
||||
}
|
||||
}
|
||||
}
|
||||
return avatars;
|
||||
return <String>[...brotherAvatars, ...sisterAvatars];
|
||||
}
|
||||
|
||||
Widget _buildCloseFriendAvatar(String avatarUrl) {
|
||||
@ -1525,6 +1530,16 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
|
||||
partnerUserId,
|
||||
relationType: cp.relationType,
|
||||
);
|
||||
if (context.mounted) {
|
||||
Provider.of<RtcProvider?>(
|
||||
context,
|
||||
listen: false,
|
||||
)?.invalidateRoomMicRelationBetween(
|
||||
userId: cp.leftUserId,
|
||||
peerUserId: partnerUserId,
|
||||
relationType: cp.relationType,
|
||||
);
|
||||
}
|
||||
await userProvider?.refreshLoadedCpProfiles();
|
||||
await SmartDialog.dismiss(tag: RoomCpProgressDialog.dialogTag);
|
||||
SCTts.show(l10n.operationSuccessful);
|
||||
|
||||
@ -78,6 +78,34 @@ void main() {
|
||||
expect(text, isNot(contains('4. Leaderboard')));
|
||||
});
|
||||
|
||||
test('groups unread reward popups by launch number', () {
|
||||
final groups = RoomRocketApiMapper.rewardPopupRecordGroups([
|
||||
_rewardRecord(id: 'r1', launchNo: 'launch-a'),
|
||||
_rewardRecord(id: 'r2', launchNo: 'launch-a'),
|
||||
_rewardRecord(id: 'r3', launchNo: 'launch-b'),
|
||||
_rewardRecord(id: 'r4', launchNo: ''),
|
||||
_rewardRecord(id: '', launchNo: ''),
|
||||
]);
|
||||
|
||||
expect(groups.map((group) => group.length), [2, 1, 1, 1]);
|
||||
expect(groups.first.map((record) => record.id), ['r1', 'r2']);
|
||||
expect(groups[1].single.launchNo, 'launch-b');
|
||||
});
|
||||
|
||||
test('matches room winner records to the current popup batch', () {
|
||||
final roomRecords = [
|
||||
_rewardRecord(id: 'room-a', launchNo: 'launch-a'),
|
||||
_rewardRecord(id: 'room-b', launchNo: 'launch-b'),
|
||||
];
|
||||
final matched = RoomRocketApiMapper.rewardRoomRecordsForPopupGroup(
|
||||
roomRecords,
|
||||
[_rewardRecord(id: 'popup-b', launchNo: 'launch-b')],
|
||||
);
|
||||
|
||||
expect(matched, hasLength(1));
|
||||
expect(matched.single.id, 'room-b');
|
||||
});
|
||||
|
||||
test(
|
||||
'parses level-specific reward previews and rocket kings from levels',
|
||||
() {
|
||||
@ -494,6 +522,30 @@ void main() {
|
||||
expect(winners.last.rewards.single.amount, '200');
|
||||
});
|
||||
|
||||
test('parses nested rocket reward record resource payloads', () {
|
||||
final record = SCRoomRocketRewardRecordRes.fromJson({
|
||||
'id': 'r1',
|
||||
'launchNo': 'launch-1',
|
||||
'roomId': 'room-1',
|
||||
'userInfo': {'id': 'u1', 'nickname': 'winner'},
|
||||
'reward': {
|
||||
'detailType': 'AVATAR_FRAME',
|
||||
'amount': '1969014438069481474',
|
||||
'quantity': 7,
|
||||
'name': 'yumi-KingTOP1 frame',
|
||||
'cover': {'sourceUrl': 'https://example.com/frame.png'},
|
||||
},
|
||||
});
|
||||
final reward = RoomRocketApiMapper.rewardItemsFromRecords([record]).single;
|
||||
|
||||
expect(record.rewardType, 'AVATAR_FRAME');
|
||||
expect(record.rewardItemId, isNotEmpty);
|
||||
expect(record.rewardName, 'yumi-KingTOP1 frame');
|
||||
expect(record.rewardCover, 'https://example.com/frame.png');
|
||||
expect(record.expireDays, 7);
|
||||
expect(reward.displayText, '7d');
|
||||
});
|
||||
|
||||
test('parses rocket level contents response from go api', () {
|
||||
final contents = SCRoomRocketLevelContentsRes.fromJson({
|
||||
'configured': true,
|
||||
@ -565,3 +617,26 @@ void main() {
|
||||
expect(groups?.setOff.single.amount, '200');
|
||||
});
|
||||
}
|
||||
|
||||
SCRoomRocketRewardRecordRes _rewardRecord({
|
||||
required String id,
|
||||
required String launchNo,
|
||||
}) {
|
||||
return SCRoomRocketRewardRecordRes.fromJson({
|
||||
'id': id,
|
||||
'launchNo': launchNo,
|
||||
'roomId': 'room-1',
|
||||
'dayKey': '2026-05-15',
|
||||
'roundNo': 1,
|
||||
'level': 3,
|
||||
'userId': 'user-1',
|
||||
'userNickname': 'User 1',
|
||||
'rewardScene': 'IN_ROOM',
|
||||
'rewardType': 'GOLD',
|
||||
'rewardItemId': 'gold',
|
||||
'rewardName': 'Yumi',
|
||||
'rewardAmount': 100,
|
||||
'grantStatus': 'SUCCESS',
|
||||
'popupStatus': 'UNREAD',
|
||||
});
|
||||
}
|
||||
|
||||
@ -134,6 +134,49 @@ void main() {
|
||||
},
|
||||
);
|
||||
|
||||
test('micChange keeps a recent up-mic snapshot over stale mic list data', () {
|
||||
final rtcProvider = RealTimeCommunicationManager();
|
||||
final user = SocialChatUserProfile(id: 'user-1', userNickname: 'Alice');
|
||||
|
||||
rtcProvider.micChange([MicRes(micIndex: 0, user: user)]);
|
||||
expect(rtcProvider.roomWheatMap[0]?.user?.id, 'user-1');
|
||||
|
||||
rtcProvider.debugApplyMicListSnapshot([
|
||||
MicRes(micIndex: 0),
|
||||
], notifyIfUnchanged: false);
|
||||
|
||||
expect(rtcProvider.roomWheatMap[0]?.user?.id, 'user-1');
|
||||
});
|
||||
|
||||
test(
|
||||
'micChange keeps a recent down-mic snapshot over stale mic list data',
|
||||
() {
|
||||
final rtcProvider =
|
||||
RealTimeCommunicationManager()
|
||||
..roomWheatMap = {
|
||||
0: MicRes(
|
||||
micIndex: 0,
|
||||
user: SocialChatUserProfile(
|
||||
id: 'user-1',
|
||||
userNickname: 'Alice',
|
||||
),
|
||||
),
|
||||
};
|
||||
|
||||
rtcProvider.micChange([MicRes(micIndex: 0)]);
|
||||
expect(rtcProvider.roomWheatMap[0]?.user, isNull);
|
||||
|
||||
rtcProvider.debugApplyMicListSnapshot([
|
||||
MicRes(
|
||||
micIndex: 0,
|
||||
user: SocialChatUserProfile(id: 'user-1', userNickname: 'Alice'),
|
||||
),
|
||||
], notifyIfUnchanged: false);
|
||||
|
||||
expect(rtcProvider.roomWheatMap[0]?.user, isNull);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('RoomSeatWidget derives seat count from highest mic index', (
|
||||
tester,
|
||||
) async {
|
||||
|
||||
@ -78,6 +78,23 @@ void main() {
|
||||
|
||||
expect(scCpRelationTypeBetweenProfiles(left, right), isNull);
|
||||
});
|
||||
|
||||
test('ignores inactive relation payloads', () {
|
||||
final left = SocialChatUserProfile(
|
||||
id: '100',
|
||||
cpList: [
|
||||
CPRes(
|
||||
meUserId: '100',
|
||||
cpUserId: '200',
|
||||
relationType: 'CP',
|
||||
status: 'DISMISSED',
|
||||
),
|
||||
],
|
||||
);
|
||||
final right = SocialChatUserProfile(id: '200');
|
||||
|
||||
expect(scCpRelationTypeBetweenProfiles(left, right), isNull);
|
||||
});
|
||||
});
|
||||
|
||||
group('scCpRelationLevelTextFromCabin', () {
|
||||
|
||||
@ -55,6 +55,24 @@ void main() {
|
||||
expect(cloud.localAudioMuted, isTrue);
|
||||
});
|
||||
|
||||
test('keeps music publishing while microphone voice is muted', () async {
|
||||
final cloud = _FakeTrtcCloud();
|
||||
final adapter = TrtcRoomRtcEngineAdapter(cloudFactory: () async => cloud);
|
||||
|
||||
await _enter(adapter);
|
||||
expect(
|
||||
await adapter.switchToAnchor(
|
||||
const RoomRtcAnchorConfig(muted: true, publishMuted: false),
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
|
||||
expect(cloud.lastRole, TRTCRoleType.anchor);
|
||||
expect(cloud.localAudioStarted, isTrue);
|
||||
expect(cloud.localAudioMuted, isFalse);
|
||||
expect(cloud.audioEffectManager.voiceCaptureVolume, 0);
|
||||
});
|
||||
|
||||
test('maps TRTC music calls to AudioEffectManager', () async {
|
||||
final cloud = _FakeTrtcCloud();
|
||||
final adapter = TrtcRoomRtcEngineAdapter(cloudFactory: () async => cloud);
|
||||
@ -197,6 +215,7 @@ class _FakeAudioEffectManager implements RoomTrtcAudioEffectManager {
|
||||
final resumedIds = <int>[];
|
||||
int? playoutVolume;
|
||||
int? publishVolume;
|
||||
int? voiceCaptureVolume;
|
||||
int position = 0;
|
||||
|
||||
@override
|
||||
@ -233,6 +252,11 @@ class _FakeAudioEffectManager implements RoomTrtcAudioEffectManager {
|
||||
playoutVolume = volume;
|
||||
}
|
||||
|
||||
@override
|
||||
void setVoiceCaptureVolume(int volume) {
|
||||
voiceCaptureVolume = volume;
|
||||
}
|
||||
|
||||
@override
|
||||
int getMusicCurrentPosInMS(int id) {
|
||||
return position;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user