diff --git a/lib/modules/chat/system/message_system_page.dart b/lib/modules/chat/system/message_system_page.dart index 245170b..fe4bd59 100644 --- a/lib/modules/chat/system/message_system_page.dart +++ b/lib/modules/chat/system/message_system_page.dart @@ -889,6 +889,9 @@ class _MessageItem extends StatelessWidget { return null; } final state = _cpInviteStateForMessage(data); + if (_shouldRenderCpInviteCardForData(data, state)) { + return null; + } if (state == _CpInviteMessageState.pending || !_isCpInvitePayload(data)) { return null; } @@ -1325,6 +1328,21 @@ class _MessageItem extends StatelessWidget { return _firstNonBlankString([nickname, actualAccount, account, fallback]); } + bool _shouldRenderCpInviteCardForData( + Map data, [ + _CpInviteMessageState? state, + ]) { + if (!_isCpInvitePayload(data)) { + return false; + } + final applyId = _cpApplyIdFromData(data); + if (applyId.isEmpty || _cpInviteFromMessageData(data) == null) { + return false; + } + final effectiveState = state ?? _cpInviteStateForMessage(data); + return effectiveState != _CpInviteMessageState.accepted; + } + String _firstNonBlankString( Iterable values, { String fallback = "User", @@ -1506,7 +1524,7 @@ class _MessageItem extends StatelessWidget { }, context, ); - } else if (type == SCSysytemMessageType.CP_BUILD.name) { + } else if (_shouldRenderCpInviteCardForData(data)) { final cpInvite = _cpInviteFromMessageData(data); return _buildCpBuildMessage(data, (ct, content) { _showMsgItemMenu(ct, content); diff --git a/lib/modules/gift/gift_page.dart b/lib/modules/gift/gift_page.dart index 36fb537..ccd86bf 100644 --- a/lib/modules/gift/gift_page.dart +++ b/lib/modules/gift/gift_page.dart @@ -19,6 +19,7 @@ import 'package:yumi/shared/tools/sc_gift_vap_svga_manager.dart'; import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; 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/data_sources/sources/repositories/sc_user_repository_impl.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/main.dart'; @@ -1519,6 +1520,8 @@ class _GiftPageState extends State with TickerProviderStateMixin { 'giftType=$giftType', ); + await _showCpGiftRelationCheckToastIfNeeded(request); + try { final repository = SCChatRoomRepository(); _giftFxLog( @@ -1725,6 +1728,49 @@ class _GiftPageState extends State with TickerProviderStateMixin { ); } + Future _showCpGiftRelationCheckToastIfNeeded( + _GiftSendRequest request, + ) async { + final relationType = request.cpRelationType; + if (relationType == null || relationType.trim().isEmpty) { + return; + } + final acceptUserId = request.acceptUserIds + .map((userId) => userId.trim()) + .firstWhere((userId) => userId.isNotEmpty, orElse: () => ''); + if (acceptUserId.isEmpty) { + return; + } + try { + final result = await SCAccountRepository().cpGiftRelationCheck( + acceptUserId: acceptUserId, + relationType: relationType, + ); + final message = result.msg.trim(); + _giftFxLog( + 'cp gift relation check ' + 'acceptUserId=$acceptUserId ' + 'relationType=$relationType ' + 'status=${result.status} ' + 'msg=$message ' + 'userCount=${result.userRelationCount}/${result.maxRelationCount} ' + 'acceptCount=${result.acceptUserRelationCount}/${result.maxRelationCount}', + ); + if (!result.status && + message.isNotEmpty && + message.toUpperCase() != 'OK') { + SCTts.show(message); + } + } catch (error) { + _giftFxLog( + 'cp gift relation check skipped ' + 'acceptUserId=$acceptUserId ' + 'relationType=$relationType ' + 'error=$error', + ); + } + } + Future> _giveBackpackGiftToRecipients( SCChatRoomRepository repository, _GiftSendRequest request, @@ -1857,7 +1903,7 @@ class _GiftPageState extends State with TickerProviderStateMixin { ); } num coins = (gift.giftCandy ?? 0) * quantity; - if (coins > 9999) { + if (coins > 9999 && !_isCpGift(gift)) { var fMsg = SCFloatingMessage( type: 1, userId: AccountStorage().getCurrentUser()?.userProfile?.id ?? "", @@ -1900,7 +1946,8 @@ class _GiftPageState extends State with TickerProviderStateMixin { 'animationCount=$animationCount', ); SCGiftVapSvgaManager().play(gift.giftSourceUrl!); - } else if (SCGlobalConfig.isLowPerformanceDevice && + } else if (!_isCpGift(gift) && + SCGlobalConfig.isLowPerformanceDevice && SCGlobalConfig.isGiftSpecialEffects && (rtcProvider?.shouldShowRoomVisualEffects ?? false)) { _enqueueLowPerformanceGiftCompensation( diff --git a/lib/modules/room/voice_room_page.dart b/lib/modules/room/voice_room_page.dart index c456fe0..95a6390 100644 --- a/lib/modules/room/voice_room_page.dart +++ b/lib/modules/room/voice_room_page.dart @@ -21,10 +21,12 @@ import 'package:yumi/services/music/room_music_manager.dart'; import 'package:yumi/services/audio/rtm_manager.dart'; import 'package:yumi/shared/tools/sc_gift_vap_svga_manager.dart'; import 'package:yumi/shared/tools/sc_lucky_gift_win_sound_player.dart'; +import 'package:yumi/shared/tools/sc_cp_gift_relation_utils.dart'; import 'package:yumi/shared/tools/sc_network_image_utils.dart'; import 'package:yumi/shared/tools/sc_path_utils.dart'; import 'package:yumi/shared/tools/sc_room_effect_scheduler.dart'; import 'package:yumi/shared/tools/sc_room_top_layer_guard.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'; import 'package:yumi/ui_kit/widgets/room/anim/l_gift_animal_view.dart'; import 'package:yumi/ui_kit/widgets/room/anim/room_gift_seat_flight_overlay.dart'; @@ -741,6 +743,7 @@ class _VoiceRoomPageState extends State } final giftPhoto = (msg.gift?.giftPhoto ?? "").trim(); + _enqueueCpGiftFloatingIfNeeded(msg); if (_supportsComboMilestoneEffects(msg)) { _handleComboMilestoneVisuals(msg, targetGroupKey); } @@ -762,6 +765,48 @@ class _VoiceRoomPageState extends State ); } + void _enqueueCpGiftFloatingIfNeeded(Msg msg) { + final gift = msg.gift; + if (!scIsCpGift(gift) || gift == null) { + return; + } + final quantity = msg.number ?? 0; + if (quantity <= 0) { + return; + } + final roomId = _resolveGiftFloatingRoomId(msg); + OverlayManager().addMessage( + SCFloatingMessage( + type: 1, + priority: 900, + userId: msg.user?.id ?? "", + toUserId: msg.toUser?.id ?? "", + userAvatarUrl: msg.user?.userAvatar ?? "", + toUserAvatarUrl: msg.toUser?.userAvatar ?? "", + userName: msg.user?.userNickname ?? "", + toUserName: msg.toUser?.userNickname ?? "", + giftUrl: gift.giftPhoto ?? "", + giftId: gift.id, + giftTab: scCpRelationTypeForGift(gift), + number: quantity, + coins: quantity * (gift.giftCandy ?? 0), + roomId: roomId, + ), + ); + } + + String _resolveGiftFloatingRoomId(Msg msg) { + final messageRoomId = (msg.msg ?? "").trim(); + if (messageRoomId.isNotEmpty) { + return messageRoomId; + } + return Provider.of( + context, + listen: false, + ).currenRoom?.roomProfile?.roomProfile?.id ?? + ""; + } + void _luckyGiftRewardTickerListener(Msg msg) { if (!Provider.of( context, diff --git a/lib/modules/user/edit/edit_user_info_page2.dart b/lib/modules/user/edit/edit_user_info_page2.dart index 615772e..de8adae 100644 --- a/lib/modules/user/edit/edit_user_info_page2.dart +++ b/lib/modules/user/edit/edit_user_info_page2.dart @@ -259,9 +259,7 @@ class _EditUserInfoPage2State extends State sex == 1 ? SCAppLocalizations.of(context)!.man : SCAppLocalizations.of(context)!.woman, - () { - _showSexDialog(); - }, + null, ), _buildItem( @@ -538,6 +536,7 @@ class _EditUserInfoPage2State extends State } } + // ignore: unused_element void _showSexDialog() { showCenterDialog( context, @@ -619,7 +618,7 @@ class _EditUserInfoPage2State extends State ); } - _buildItem(String title, String value, Function() func) { + _buildItem(String title, String value, Function()? func) { return SCDebounceWidget( child: Column( children: [ @@ -638,11 +637,12 @@ class _EditUserInfoPage2State extends State child: text(value, textColor: Colors.white, fontSize: 15.sp), ), ), - Icon( - Icons.keyboard_arrow_right, - size: 20.w, - color: Colors.white70, - ), + if (func != null) + Icon( + Icons.keyboard_arrow_right, + size: 20.w, + color: Colors.white70, + ), SizedBox(width: 8.w), ], ), @@ -656,7 +656,7 @@ class _EditUserInfoPage2State extends State ], ), onTap: () { - func.call(); + func?.call(); }, ); } diff --git a/lib/modules/user/profile/person_detail_page.dart b/lib/modules/user/profile/person_detail_page.dart index 7127033..afd92bd 100644 --- a/lib/modules/user/profile/person_detail_page.dart +++ b/lib/modules/user/profile/person_detail_page.dart @@ -109,13 +109,25 @@ class _PersonDetailPageState extends State with RouteAware { } }); - Provider.of(context, listen: false) - .userProfile = null; - Provider.of( + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) { + return; + } + _loadInitialProfileData(); + }); + } + + void _loadInitialProfileData() { + final profileManager = Provider.of( context, listen: false, - ).getUserInfoById(widget.tageId); + ); + profileManager.userProfile = null; + profileManager.getUserInfoById(widget.tageId); SCAccountRepository().userIdentity(userId: widget.tageId).then((v) { + if (!mounted) { + return; + } userIdentity = v; setState(() {}); }); @@ -123,11 +135,17 @@ class _PersonDetailPageState extends State with RouteAware { SCAccountRepository() .followCheck(widget.tageId) .then((v) { + if (!mounted) { + return; + } isFollow = v; isLoading = false; setState(() {}); }) .catchError((e) { + if (!mounted) { + return; + } setState(() { isLoading = false; }); @@ -135,6 +153,9 @@ class _PersonDetailPageState extends State with RouteAware { SCAccountRepository() .blacklistCheck(widget.tageId) .then((v) { + if (!mounted) { + return; + } isBlacklist = v; isBlacklistLoading = false; setState(() {}); @@ -145,6 +166,9 @@ class _PersonDetailPageState extends State with RouteAware { } }) .catchError((e) { + if (!mounted) { + return; + } setState(() { isBlacklistLoading = false; }); @@ -1564,7 +1588,7 @@ class _PersonDetailPageState extends State with RouteAware { if (payload.rightUserId.trim().isEmpty) { return; } - final cancelText = SCAppLocalizations.of(context)!.cancel; + const cancelText = 'unbing'; SCLoadingManager.show(context: context); late final _ProfileRelationProgressData progress; try { @@ -1668,6 +1692,15 @@ class _PersonDetailPageState extends State with RouteAware { if (!mounted) { return; } + final profileManager = Provider.of( + context, + listen: false, + ); + profileManager.removeLoadedCpRelation( + userId: payload.leftUserId, + peerUserId: partnerUserId, + relationType: payload.relationType, + ); Provider.of( context, listen: false, @@ -1676,10 +1709,12 @@ class _PersonDetailPageState extends State with RouteAware { peerUserId: partnerUserId, relationType: payload.relationType, ); - await Provider.of( - context, - listen: false, - ).refreshLoadedCpProfiles(); + await profileManager.refreshLoadedCpProfiles(); + profileManager.removeLoadedCpRelation( + userId: payload.leftUserId, + peerUserId: partnerUserId, + relationType: payload.relationType, + ); await SmartDialog.dismiss(tag: RoomCpProgressDialog.dialogTag); SCTts.show(l10n.operationSuccessful); } catch (_) { @@ -2693,7 +2728,21 @@ class _PersonDetailPageState extends State with RouteAware { ref.userProfile?.id ?? "", relationType: cpRelation?.relationType ?? "CP", ) - .then((result) { + .then((result) async { + final currentUserId = + AccountStorage() + .getCurrentUser() + ?.userProfile + ?.id + ?.trim() ?? + ""; + final profileUserId = + ref.userProfile?.id?.trim() ?? ""; + ref.removeLoadedCpRelation( + userId: currentUserId, + peerUserId: profileUserId, + relationType: cpRelation?.relationType ?? "CP", + ); if (mounted) { Provider.of( context, @@ -2708,7 +2757,12 @@ class _PersonDetailPageState extends State with RouteAware { relationType: cpRelation?.relationType ?? "CP", ); } - ref.refreshLoadedCpProfiles(); + await ref.refreshLoadedCpProfiles(); + ref.removeLoadedCpRelation( + userId: currentUserId, + peerUserId: profileUserId, + relationType: cpRelation?.relationType ?? "CP", + ); SmartDialog.dismiss(tag: "showPartWaysDialog"); SCTts.show( SCAppLocalizations.of( diff --git a/lib/services/audio/rtc_manager.dart b/lib/services/audio/rtc_manager.dart index 4a19454..c5495a4 100644 --- a/lib/services/audio/rtc_manager.dart +++ b/lib/services/audio/rtc_manager.dart @@ -9,7 +9,6 @@ import 'package:yumi/app/constants/sc_room_msg_type.dart'; import 'package:yumi/shared/tools/sc_permission_utils.dart'; import 'package:yumi/shared/tools/sc_room_utils.dart'; import 'package:yumi/shared/tools/sc_room_top_layer_guard.dart'; -import 'package:yumi/shared/tools/sc_cp_relation_level_utils.dart'; import 'package:yumi/shared/tools/sc_cp_relation_notice_utils.dart'; import 'package:yumi/shared/data_sources/models/message/room_rocket_launch_broadcast_message.dart'; import 'package:yumi/shared/data_sources/models/message/sc_floating_message.dart'; @@ -1833,31 +1832,23 @@ class RealTimeCommunicationManager extends ChangeNotifier { final closeFriendPairs = []; await Future.wait( _roomMicRelationTypes.map((relationType) async { - final cabin = await _safeLoadRoomMicRelationCabin(userId, relationType); - final pair = cabin?.cpPairUserProfileCO; - if (!_hasRoomMicRelationPair(pair)) { - return; - } - final normalizedType = scNormalizeCpRelationType( - pair?.relationType ?? relationType, - ); - final enrichedPair = pair!.copyWith( - relationType: normalizedType, - cpValue: pair.cpValue ?? cabin?.cpVal?.toDouble(), - levelText: - scCpRelationLevelTextFromCabin( - cabin, - fallbackText: pair.levelText, - ) ?? - _roomMicRelationLevelTextFromCabin(cabin), - ); - if (_isRoomMicRelationLocallyDismissed(enrichedPair)) { - return; - } - if (normalizedType == "CP") { - cpPairs.add(enrichedPair); - } else { - closeFriendPairs.add(enrichedPair); + final pairs = await _safeLoadRoomMicRelationPairs(userId, relationType); + for (final pair in pairs) { + if (!_hasRoomMicRelationPair(pair)) { + continue; + } + final normalizedType = scNormalizeCpRelationType( + pair.relationType ?? relationType, + ); + final enrichedPair = pair.copyWith(relationType: normalizedType); + if (_isRoomMicRelationLocallyDismissed(enrichedPair)) { + continue; + } + if (normalizedType == "CP") { + cpPairs.add(enrichedPair); + } else { + closeFriendPairs.add(enrichedPair); + } } }), ); @@ -1868,53 +1859,26 @@ class RealTimeCommunicationManager extends ChangeNotifier { ); } - Future _safeLoadRoomMicRelationCabin( + Future> _safeLoadRoomMicRelationPairs( String userId, String relationType, ) async { try { - return await SCAccountRepository().cpCabin( - userId, + return await SCAccountRepository().cpRelationshipPairs( + userId: userId, relationType: relationType, ); } catch (error) { if (kDebugMode) { debugPrint( - "[CP][MicRelation] cabin failed userId=$userId " + "[CP][MicRelation] pair failed userId=$userId " "relationType=$relationType error=$error", ); } - return null; + return const []; } } - String? _roomMicRelationLevelTextFromCabin(SCCpCabinRes? cabin) { - final config = cabin?.currentCpCabinConfigCO; - if (config is! Map) { - return null; - } - final level = _firstRoomMicRelationConfigText([ - config["level"], - config["cpLevel"], - config["cabinLevel"], - config["lv"], - ]); - if (level.isEmpty) { - return null; - } - return "Lv.$level"; - } - - String _firstRoomMicRelationConfigText(Iterable values) { - for (final value in values) { - final text = value?.toString().trim() ?? ""; - if (text.isNotEmpty) { - return text; - } - } - return ""; - } - void _applyHydratedRoomMicRelationProfile( String roomId, String userId, diff --git a/lib/services/audio/rtm_manager.dart b/lib/services/audio/rtm_manager.dart index ca9ca3b..d7b1b44 100644 --- a/lib/services/audio/rtm_manager.dart +++ b/lib/services/audio/rtm_manager.dart @@ -6375,7 +6375,8 @@ class RealTimeMessagingManager extends ChangeNotifier { 'giftId=${gift.id} giftName=${gift.giftName}', ); SCGiftVapSvgaManager().play(giftSourceUrl); - } else if (SCGlobalConfig.isLowPerformanceDevice && + } else if (!scIsCpGift(gift) && + SCGlobalConfig.isLowPerformanceDevice && SCGlobalConfig.isGiftSpecialEffects && rtcProvider.shouldShowRoomVisualEffects) { final roomId = @@ -6451,7 +6452,7 @@ class RealTimeMessagingManager extends ChangeNotifier { ); } final coins = (msg.number ?? 0) * (gift.giftCandy ?? 0); - if (coins > 9999) { + if (coins > 9999 && !scIsCpGift(gift)) { _enqueueGiftFloatingMessage( SCFloatingMessage( type: 1, diff --git a/lib/services/auth/user_profile_manager.dart b/lib/services/auth/user_profile_manager.dart index 33aaf5d..25ca1dd 100644 --- a/lib/services/auth/user_profile_manager.dart +++ b/lib/services/auth/user_profile_manager.dart @@ -210,6 +210,7 @@ class SocialChatUserProfileManager extends ChangeNotifier { RoomUserCardRes? userCardInfo; Object? userCardLoadError; bool isUserCardLoading = false; + bool isUserCardRelationLoading = false; int _userCardRequestSerial = 0; int _userCardSessionSerial = 0; int? _activeUserCardSessionSerial; @@ -235,6 +236,7 @@ class SocialChatUserProfileManager extends ChangeNotifier { userCardInfo = null; userCardLoadError = null; isUserCardLoading = true; + isUserCardRelationLoading = false; notifyListeners(); if (normalizedUserId.isEmpty) { @@ -257,6 +259,7 @@ class SocialChatUserProfileManager extends ChangeNotifier { userCardInfo = card; userCardLoadError = null; isUserCardLoading = false; + isUserCardRelationLoading = card.userProfile != null; notifyListeners(); unawaited( _hydrateRoomUserCard( @@ -325,8 +328,14 @@ class SocialChatUserProfileManager extends ChangeNotifier { } userCardInfo = hydratedCard; userCardLoadError = null; + isUserCardRelationLoading = false; notifyListeners(); } catch (error, stackTrace) { + if (requestSerial == _userCardRequestSerial && + sessionSerial == _activeUserCardSessionSerial) { + isUserCardRelationLoading = false; + notifyListeners(); + } _debugProfileLoadFailure( scope: 'roomCardHydrate', userId: userId, @@ -346,6 +355,7 @@ class SocialChatUserProfileManager extends ChangeNotifier { userCardInfo = null; userCardLoadError = null; isUserCardLoading = false; + isUserCardRelationLoading = false; } Future _withCurrentVipLevelForCard( @@ -357,7 +367,7 @@ class SocialChatUserProfileManager extends ChangeNotifier { return card; } var mergedProfile = await _withCurrentVipLevel(profile, userId); - mergedProfile = await _withCpRelationshipProfiles(mergedProfile, userId); + mergedProfile = await _withCpCabinProfiles(mergedProfile, userId); if (mergedProfile == profile) { return card; } @@ -487,44 +497,6 @@ class SocialChatUserProfileManager extends ChangeNotifier { ); } - Future _withCpRelationshipProfiles( - SocialChatUserProfile profile, - String userId, - ) async { - if (userId.trim().isEmpty) { - return profile; - } - final cpPairs = []; - final closeFriendPairs = []; - _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 pairs = await _safeLoadCpRelationshipPairs(userId, relationType); - _addCpRelationPairs( - cpPairs: cpPairs, - closeFriendPairs: closeFriendPairs, - relations: pairs, - fallbackRelationType: relationType, - ); - }), - ); - return profile.copyWith( - cpList: cpPairs, - closeFriendList: closeFriendPairs, - isCpRelation: cpPairs.isNotEmpty, - ); - } - void _addCpRelationPairs({ required List cpPairs, required List closeFriendPairs, @@ -953,6 +925,127 @@ class SocialChatUserProfileManager extends ChangeNotifier { await _refreshLoadedCpProfilesForUserIds(normalizedUserIds); } + void removeLoadedCpRelation({ + required String userId, + required String peerUserId, + required String relationType, + }) { + final normalizedUserId = userId.trim(); + final normalizedPeerUserId = peerUserId.trim(); + final normalizedRelationType = _normalizeCpRelationType(relationType); + if (normalizedUserId.isEmpty || + normalizedPeerUserId.isEmpty || + normalizedRelationType.isEmpty) { + return; + } + + var changed = false; + final currentProfile = currentUserProfile; + if (currentProfile != null) { + final nextCurrentProfile = _profileWithoutCpRelation( + currentProfile, + userId: normalizedUserId, + peerUserId: normalizedPeerUserId, + relationType: normalizedRelationType, + ); + if (!identical(nextCurrentProfile, currentProfile)) { + final currentUser = AccountStorage().getCurrentUser(); + currentUser?.setUserProfile(nextCurrentProfile); + if (currentUser != null) { + AccountStorage().setCurrentUser(currentUser); + } + changed = true; + } + } + + final detailProfile = userProfile; + if (detailProfile != null) { + final nextDetailProfile = _profileWithoutCpRelation( + detailProfile, + userId: normalizedUserId, + peerUserId: normalizedPeerUserId, + relationType: normalizedRelationType, + ); + if (!identical(nextDetailProfile, detailProfile)) { + userProfile = nextDetailProfile; + changed = true; + } + } + + final cardProfile = userCardInfo?.userProfile; + if (cardProfile != null) { + final nextCardProfile = _profileWithoutCpRelation( + cardProfile, + userId: normalizedUserId, + peerUserId: normalizedPeerUserId, + relationType: normalizedRelationType, + ); + if (!identical(nextCardProfile, cardProfile)) { + userCardInfo = userCardInfo?.copyWith(userProfile: nextCardProfile); + changed = true; + } + } + + if (changed) { + notifyListeners(); + } + } + + SocialChatUserProfile _profileWithoutCpRelation( + SocialChatUserProfile profile, { + required String userId, + required String peerUserId, + required String relationType, + }) { + var changed = false; + List? filterRelations(List? relations) { + if (relations == null) { + return null; + } + final filtered = + relations.where((relation) { + return !_matchesCpRelation( + relation, + userId: userId, + peerUserId: peerUserId, + relationType: relationType, + ); + }).toList(); + if (filtered.length != relations.length) { + changed = true; + } + return filtered; + } + + final nextCpList = filterRelations(profile.cpList); + final nextCloseFriendList = filterRelations(profile.closeFriendList); + if (!changed) { + return profile; + } + return profile.copyWith( + cpList: nextCpList, + closeFriendList: nextCloseFriendList, + isCpRelation: nextCpList?.isNotEmpty == true, + ); + } + + bool _matchesCpRelation( + CPRes relation, { + required String userId, + required String peerUserId, + required String relationType, + }) { + if (_normalizeCpRelationType(relation.relationType) != relationType) { + return false; + } + final relationUserIds = { + relation.meUserId?.trim() ?? "", + relation.cpUserId?.trim() ?? "", + }..remove(""); + return relationUserIds.contains(userId) && + relationUserIds.contains(peerUserId); + } + Future _refreshLoadedCpProfilesForUserIds(Set? userIds) async { String refreshedUserId(String? userId) { final normalizedUserId = userId?.trim() ?? ""; @@ -981,7 +1074,7 @@ class SocialChatUserProfileManager extends ChangeNotifier { final cardUserId = refreshedUserId(cardProfile?.id); if (userCardInfo != null && cardProfile != null && cardUserId.isNotEmpty) { userCardInfo = userCardInfo?.copyWith( - userProfile: await _withCpRelationshipProfiles(cardProfile, cardUserId), + userProfile: await _withCpCabinProfiles(cardProfile, cardUserId), ); } diff --git a/lib/shared/business_logic/models/res/sc_cp_gift_relation_check_res.dart b/lib/shared/business_logic/models/res/sc_cp_gift_relation_check_res.dart new file mode 100644 index 0000000..6bc4651 --- /dev/null +++ b/lib/shared/business_logic/models/res/sc_cp_gift_relation_check_res.dart @@ -0,0 +1,90 @@ +class SCCpGiftRelationCheckRes { + const SCCpGiftRelationCheckRes({ + this.status = true, + this.msg = '', + this.relationType = '', + this.userId = '', + this.acceptUserId = '', + this.userRelationCount = 0, + this.acceptUserRelationCount = 0, + this.maxRelationCount = 0, + this.userRelationFull = false, + this.acceptUserRelationFull = false, + }); + + factory SCCpGiftRelationCheckRes.fromJson(dynamic json) { + final source = + json is Map + ? json.map((key, value) => MapEntry(key.toString(), value)) + : const {}; + return SCCpGiftRelationCheckRes( + status: _asBool(source['status']) ?? true, + msg: source['msg']?.toString() ?? '', + relationType: source['relationType']?.toString() ?? '', + userId: source['userId']?.toString() ?? '', + acceptUserId: source['acceptUserId']?.toString() ?? '', + userRelationCount: _asInt(source['userRelationCount']) ?? 0, + acceptUserRelationCount: _asInt(source['acceptUserRelationCount']) ?? 0, + maxRelationCount: _asInt(source['maxRelationCount']) ?? 0, + userRelationFull: _asBool(source['userRelationFull']) ?? false, + acceptUserRelationFull: + _asBool(source['acceptUserRelationFull']) ?? false, + ); + } + + final bool status; + final String msg; + final String relationType; + final String userId; + final String acceptUserId; + final int userRelationCount; + final int acceptUserRelationCount; + final int maxRelationCount; + final bool userRelationFull; + final bool acceptUserRelationFull; + + Map toJson() { + return { + 'status': status, + 'msg': msg, + 'relationType': relationType, + 'userId': userId, + 'acceptUserId': acceptUserId, + 'userRelationCount': userRelationCount, + 'acceptUserRelationCount': acceptUserRelationCount, + 'maxRelationCount': maxRelationCount, + 'userRelationFull': userRelationFull, + 'acceptUserRelationFull': acceptUserRelationFull, + }; + } +} + +bool? _asBool(dynamic value) { + if (value is bool) { + return value; + } + if (value is num) { + return value != 0; + } + final text = value?.toString().trim().toLowerCase() ?? ''; + if (text.isEmpty) { + return null; + } + if (text == 'true' || text == '1' || text == 'yes') { + return true; + } + if (text == 'false' || text == '0' || text == 'no') { + return false; + } + return null; +} + +int? _asInt(dynamic value) { + if (value is int) { + return value; + } + if (value is num) { + return value.toInt(); + } + return int.tryParse(value?.toString() ?? ''); +} diff --git a/lib/shared/business_logic/repositories/user_repository.dart b/lib/shared/business_logic/repositories/user_repository.dart index e9ef588..c3d0692 100644 --- a/lib/shared/business_logic/repositories/user_repository.dart +++ b/lib/shared/business_logic/repositories/user_repository.dart @@ -29,6 +29,7 @@ import 'package:yumi/shared/business_logic/models/res/sc_user_red_packet_grab_re import 'package:yumi/shared/business_logic/models/res/sc_user_red_packet_send_res.dart'; import 'package:yumi/shared/business_logic/models/res/sc_violation_handle_res.dart'; import 'package:yumi/shared/business_logic/models/res/sc_cp_rights_config_res.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_cp_gift_relation_check_res.dart'; abstract class SocialChatUserRepository { Future getUser(String id); @@ -329,6 +330,12 @@ abstract class SocialChatUserRepository { ///查询 CP 小屋 Future cpCabin(String userId, {String relationType = 'CP'}); + ///CP 礼物关系名额校验 + Future cpGiftRelationCheck({ + required String acceptUserId, + String relationType = 'CP', + }); + ///查询 CP 值 Future cpCabinValue( String userId, { diff --git a/lib/shared/data_sources/sources/repositories/sc_user_repository_impl.dart b/lib/shared/data_sources/sources/repositories/sc_user_repository_impl.dart index 379c8f4..5186700 100644 --- a/lib/shared/data_sources/sources/repositories/sc_user_repository_impl.dart +++ b/lib/shared/data_sources/sources/repositories/sc_user_repository_impl.dart @@ -9,6 +9,7 @@ import 'package:yumi/shared/business_logic/models/res/sc_prop_coupon_record_list import 'package:yumi/shared/business_logic/models/res/sc_public_message_page_res.dart'; import 'package:yumi/shared/business_logic/models/res/room_res.dart' hide WearBadge; +import 'package:yumi/shared/business_logic/models/res/sc_cp_gift_relation_check_res.dart'; import 'package:yumi/shared/business_logic/models/res/sc_cp_rights_config_res.dart'; import 'package:yumi/shared/business_logic/models/res/sc_task_list_res.dart'; import 'package:yumi/shared/business_logic/models/res/sc_user_counter_res.dart'; @@ -1516,6 +1517,34 @@ class SCAccountRepository implements SocialChatUserRepository { } } + ///user/cp-relationship/gift-relation/check + @override + Future cpGiftRelationCheck({ + required String acceptUserId, + String relationType = 'CP', + }) async { + final query = {"acceptUserId": acceptUserId, "relationType": relationType}; + _cpDebugLog('giftRelationCheck request query=$query'); + try { + final result = await http.get( + "/user/cp-relationship/gift-relation/check", + queryParams: query, + extra: const {BaseNetworkClient.silentErrorToastKey: true}, + fromJson: SCCpGiftRelationCheckRes.fromJson, + ); + _cpDebugLog( + 'giftRelationCheck response query=$query ' + 'status=${result.status} msg=${result.msg} ' + 'userFull=${result.userRelationFull} ' + 'acceptFull=${result.acceptUserRelationFull}', + ); + return result; + } catch (error) { + _cpDebugLog('giftRelationCheck failed query=$query error=$error'); + rethrow; + } + } + ///user/cp/cabin @override Future cpCabin( diff --git a/lib/ui_kit/widgets/room/cp/room_cp_progress_dialog.dart b/lib/ui_kit/widgets/room/cp/room_cp_progress_dialog.dart index 6067730..a8934a3 100644 --- a/lib/ui_kit/widgets/room/cp/room_cp_progress_dialog.dart +++ b/lib/ui_kit/widgets/room/cp/room_cp_progress_dialog.dart @@ -37,7 +37,7 @@ class RoomCpProgressDialog extends StatelessWidget { this.targetValue = 1, this.expAwayText = '0', this.nextLevelText = 'Lv. 1', - this.cancelText = 'Cancel', + this.cancelText = 'unbing', this.dismissTag = dialogTag, this.onCancel, }); @@ -68,7 +68,7 @@ class RoomCpProgressDialog extends StatelessWidget { double targetValue = 1, String expAwayText = '0', String nextLevelText = 'Lv. 1', - String cancelText = 'Cancel', + String cancelText = 'unbing', String dismissTag = dialogTag, VoidCallback? onCancel, }) async { @@ -495,7 +495,7 @@ class _CancelButton extends StatelessWidget { fit: BoxFit.fill, ), Text( - text.trim().isEmpty ? 'Cancel' : text.trim(), + text.trim().isEmpty ? 'unbing' : text.trim(), maxLines: 1, overflow: TextOverflow.ellipsis, textAlign: TextAlign.center, diff --git a/lib/ui_kit/widgets/room/room_user_info_card.dart b/lib/ui_kit/widgets/room/room_user_info_card.dart index ae335a4..687d202 100644 --- a/lib/ui_kit/widgets/room/room_user_info_card.dart +++ b/lib/ui_kit/widgets/room/room_user_info_card.dart @@ -76,7 +76,12 @@ class _RoomUserInfoCardState extends State { context, listen: false, ); - _loadRoomUserCard(); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) { + return; + } + _loadRoomUserCard(); + }); SCAccountRepository().userIdentity(userId: widget.userId).then((v) { if (!mounted) { return; @@ -139,7 +144,11 @@ class _RoomUserInfoCardState extends State { final profile = ref.userCardInfo?.userProfile; final dataCard = _activeDataCard(ref.userCardInfo); - final sheetHeight = _resolvedProfileSheetHeight(screenHeight, profile); + final sheetHeight = _resolvedProfileSheetHeight( + screenHeight, + profile, + relationLoading: ref.isUserCardRelationLoading, + ); return _buildProfileSheet( context: context, @@ -231,9 +240,13 @@ class _RoomUserInfoCardState extends State { double _resolvedProfileSheetHeight( double screenHeight, - SocialChatUserProfile? profile, - ) { + SocialChatUserProfile? profile, { + required bool relationLoading, + }) { final defaultSheetHeight = screenHeight * _profileSheetHeightFactor; + if (relationLoading) { + return defaultSheetHeight; + } final hasCpRelation = _hasCpRelation(profile); final hasCloseFriends = _closeFriendProfileData(profile).isNotEmpty; var resolvedHeight = defaultSheetHeight; @@ -356,6 +369,7 @@ class _RoomUserInfoCardState extends State { }) { final hasCpRelation = _hasCpRelation(profile); final closeFriends = _closeFriendProfileData(profile); + final isRelationLoading = ref.isUserCardRelationLoading; return Stack( children: [ @@ -386,7 +400,12 @@ class _RoomUserInfoCardState extends State { ref, profile, ), - if (hasCpRelation) ...[ + if (isRelationLoading) ...[ + SizedBox(height: 12.w), + _buildCpRelationSkeletonCard(), + SizedBox(height: 11.w), + _buildCloseFriendSkeletonStrip(), + ] else if (hasCpRelation) ...[ SizedBox(height: 12.w), _buildCpRelationCard( context: context, @@ -400,7 +419,9 @@ class _RoomUserInfoCardState extends State { ], SizedBox( height: - hasCpRelation || closeFriends.isNotEmpty + isRelationLoading || + hasCpRelation || + closeFriends.isNotEmpty ? 14.w : 12.w, ), @@ -691,6 +712,51 @@ class _RoomUserInfoCardState extends State { ); } + Widget _buildCpRelationSkeletonCard() { + return SizedBox( + height: 114.w, + child: DecoratedBox( + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.10), + borderRadius: BorderRadius.circular(12.w), + border: Border.all(color: Colors.white.withValues(alpha: 0.12)), + ), + child: Stack( + children: [ + Positioned( + left: 39.w, + top: 26.w, + child: _buildSkeletonCircle(60.w), + ), + Positioned( + right: 38.w, + top: 26.w, + child: _buildSkeletonCircle(60.w), + ), + Positioned( + left: 142.w, + right: 142.w, + top: 28.w, + child: _buildSkeletonBlock(height: 18.w, radius: 9.w), + ), + Positioned( + left: 156.w, + right: 156.w, + top: 54.w, + child: _buildSkeletonBlock(height: 10.w, radius: 5.w), + ), + Positioned( + left: 118.w, + right: 118.w, + top: 80.w, + child: _buildSkeletonBlock(height: 14.w, radius: 7.w), + ), + ], + ), + ), + ); + } + Widget _buildCpCardText( String value, { required Color color, @@ -791,6 +857,69 @@ class _RoomUserInfoCardState extends State { ); } + Widget _buildCloseFriendSkeletonStrip() { + return SizedBox( + height: 41.w, + width: double.infinity, + child: DecoratedBox( + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.10), + borderRadius: BorderRadius.circular(12.w), + border: Border.all(color: Colors.white.withValues(alpha: 0.12)), + ), + child: Stack( + children: [ + PositionedDirectional( + top: 13.w, + start: 16.w, + width: 118.w, + child: _buildSkeletonBlock(height: 14.w, radius: 7.w), + ), + PositionedDirectional( + top: 8.w, + end: 13.w, + child: SizedBox( + height: 25.w, + width: 112.w, + child: Stack( + alignment: AlignmentDirectional.centerEnd, + children: [ + for (var index = 0; index < 4; index++) + PositionedDirectional( + end: (3 - index) * 17.w, + child: _buildSkeletonCircle(25.w), + ), + ], + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildSkeletonBlock({required double height, required double radius}) { + return Container( + height: height, + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.22), + borderRadius: BorderRadius.circular(radius), + ), + ); + } + + Widget _buildSkeletonCircle(double size) { + return Container( + width: size, + height: size, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.white.withValues(alpha: 0.22), + ), + ); + } + _RoomProfileCpData _cpProfileData(SocialChatUserProfile? profile) { final currentProfile = AccountStorage().getCurrentUser()?.userProfile; final cp = _firstRelation( @@ -1432,8 +1561,15 @@ class _RoomUserInfoCardState extends State { void _openProfile(BuildContext context) { final route = "${SCMainRoute.person}?isMe=${AccountStorage().getCurrentUser()?.userProfile?.id == widget.userId}&tageId=${widget.userId}"; + final rootContext = navigatorKey.currentState?.context; Navigator.of(context).pop(); - SCNavigatorUtils.push(navigatorKey.currentState?.context ?? context, route); + WidgetsBinding.instance.addPostFrameCallback((_) { + final targetContext = navigatorKey.currentState?.context ?? rootContext; + if (targetContext == null) { + return; + } + SCNavigatorUtils.push(targetContext, route); + }); } Future _openChat(BuildContext context) async { @@ -1497,7 +1633,7 @@ class _RoomUserInfoCardState extends State { SocialChatUserProfile? profile, ) async { final cp = _cpProfileData(profile); - final cancelText = SCAppLocalizations.of(context)!.cancel; + const cancelText = 'unbing'; SCLoadingManager.show(); late final _RoomCpProgressData progress; try { diff --git a/test/user_profile_manager_room_card_session_test.dart b/test/user_profile_manager_room_card_session_test.dart index 068c9c6..111ddbf 100644 --- a/test/user_profile_manager_room_card_session_test.dart +++ b/test/user_profile_manager_room_card_session_test.dart @@ -21,4 +21,24 @@ void main() { expect(manager.userCardInfo, isNull); }); + + test('removeLoadedCpRelation removes matching close friend relation', () { + final manager = SocialChatUserProfileManager(); + manager.userProfile = SocialChatUserProfile( + id: '100', + closeFriendList: [ + CPRes(meUserId: '100', cpUserId: '200', relationType: 'BROTHER'), + CPRes(meUserId: '100', cpUserId: '300', relationType: 'SISTERS'), + ], + ); + + manager.removeLoadedCpRelation( + userId: '100', + peerUserId: '200', + relationType: 'BROTHER', + ); + + expect(manager.userProfile?.closeFriendList, hasLength(1)); + expect(manager.userProfile?.closeFriendList?.single.cpUserId, '300'); + }); }