修复问题

This commit is contained in:
roxy 2026-05-25 17:05:30 +08:00
parent de4a06be77
commit 5f6bd6f5f8
14 changed files with 639 additions and 135 deletions

View File

@ -889,6 +889,9 @@ class _MessageItem extends StatelessWidget {
return null; return null;
} }
final state = _cpInviteStateForMessage(data); final state = _cpInviteStateForMessage(data);
if (_shouldRenderCpInviteCardForData(data, state)) {
return null;
}
if (state == _CpInviteMessageState.pending || !_isCpInvitePayload(data)) { if (state == _CpInviteMessageState.pending || !_isCpInvitePayload(data)) {
return null; return null;
} }
@ -1325,6 +1328,21 @@ class _MessageItem extends StatelessWidget {
return _firstNonBlankString([nickname, actualAccount, account, fallback]); return _firstNonBlankString([nickname, actualAccount, account, fallback]);
} }
bool _shouldRenderCpInviteCardForData(
Map<String, dynamic> 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( String _firstNonBlankString(
Iterable<String?> values, { Iterable<String?> values, {
String fallback = "User", String fallback = "User",
@ -1506,7 +1524,7 @@ class _MessageItem extends StatelessWidget {
}, },
context, context,
); );
} else if (type == SCSysytemMessageType.CP_BUILD.name) { } else if (_shouldRenderCpInviteCardForData(data)) {
final cpInvite = _cpInviteFromMessageData(data); final cpInvite = _cpInviteFromMessageData(data);
return _buildCpBuildMessage(data, (ct, content) { return _buildCpBuildMessage(data, (ct, content) {
_showMsgItemMenu(ct, content); _showMsgItemMenu(ct, content);

View File

@ -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/local/user_manager.dart';
import 'package:yumi/shared/data_sources/sources/remote/net/api.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_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/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_gift_relation_utils.dart';
import 'package:yumi/main.dart'; import 'package:yumi/main.dart';
@ -1519,6 +1520,8 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
'giftType=$giftType', 'giftType=$giftType',
); );
await _showCpGiftRelationCheckToastIfNeeded(request);
try { try {
final repository = SCChatRoomRepository(); final repository = SCChatRoomRepository();
_giftFxLog( _giftFxLog(
@ -1725,6 +1728,49 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
); );
} }
Future<void> _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<List<MicRes>> _giveBackpackGiftToRecipients( Future<List<MicRes>> _giveBackpackGiftToRecipients(
SCChatRoomRepository repository, SCChatRoomRepository repository,
_GiftSendRequest request, _GiftSendRequest request,
@ -1857,7 +1903,7 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
); );
} }
num coins = (gift.giftCandy ?? 0) * quantity; num coins = (gift.giftCandy ?? 0) * quantity;
if (coins > 9999) { if (coins > 9999 && !_isCpGift(gift)) {
var fMsg = SCFloatingMessage( var fMsg = SCFloatingMessage(
type: 1, type: 1,
userId: AccountStorage().getCurrentUser()?.userProfile?.id ?? "", userId: AccountStorage().getCurrentUser()?.userProfile?.id ?? "",
@ -1900,7 +1946,8 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
'animationCount=$animationCount', 'animationCount=$animationCount',
); );
SCGiftVapSvgaManager().play(gift.giftSourceUrl!); SCGiftVapSvgaManager().play(gift.giftSourceUrl!);
} else if (SCGlobalConfig.isLowPerformanceDevice && } else if (!_isCpGift(gift) &&
SCGlobalConfig.isLowPerformanceDevice &&
SCGlobalConfig.isGiftSpecialEffects && SCGlobalConfig.isGiftSpecialEffects &&
(rtcProvider?.shouldShowRoomVisualEffects ?? false)) { (rtcProvider?.shouldShowRoomVisualEffects ?? false)) {
_enqueueLowPerformanceGiftCompensation( _enqueueLowPerformanceGiftCompensation(

View File

@ -21,10 +21,12 @@ import 'package:yumi/services/music/room_music_manager.dart';
import 'package:yumi/services/audio/rtm_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_gift_vap_svga_manager.dart';
import 'package:yumi/shared/tools/sc_lucky_gift_win_sound_player.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_network_image_utils.dart';
import 'package:yumi/shared/tools/sc_path_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_effect_scheduler.dart';
import 'package:yumi/shared/tools/sc_room_top_layer_guard.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/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/l_gift_animal_view.dart';
import 'package:yumi/ui_kit/widgets/room/anim/room_gift_seat_flight_overlay.dart'; import 'package:yumi/ui_kit/widgets/room/anim/room_gift_seat_flight_overlay.dart';
@ -741,6 +743,7 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
} }
final giftPhoto = (msg.gift?.giftPhoto ?? "").trim(); final giftPhoto = (msg.gift?.giftPhoto ?? "").trim();
_enqueueCpGiftFloatingIfNeeded(msg);
if (_supportsComboMilestoneEffects(msg)) { if (_supportsComboMilestoneEffects(msg)) {
_handleComboMilestoneVisuals(msg, targetGroupKey); _handleComboMilestoneVisuals(msg, targetGroupKey);
} }
@ -762,6 +765,48 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
); );
} }
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<RtcProvider>(
context,
listen: false,
).currenRoom?.roomProfile?.roomProfile?.id ??
"";
}
void _luckyGiftRewardTickerListener(Msg msg) { void _luckyGiftRewardTickerListener(Msg msg) {
if (!Provider.of<RtcProvider>( if (!Provider.of<RtcProvider>(
context, context,

View File

@ -259,9 +259,7 @@ class _EditUserInfoPage2State extends State<EditUserInfoPage2>
sex == 1 sex == 1
? SCAppLocalizations.of(context)!.man ? SCAppLocalizations.of(context)!.man
: SCAppLocalizations.of(context)!.woman, : SCAppLocalizations.of(context)!.woman,
() { null,
_showSexDialog();
},
), ),
_buildItem( _buildItem(
@ -538,6 +536,7 @@ class _EditUserInfoPage2State extends State<EditUserInfoPage2>
} }
} }
// ignore: unused_element
void _showSexDialog() { void _showSexDialog() {
showCenterDialog( showCenterDialog(
context, context,
@ -619,7 +618,7 @@ class _EditUserInfoPage2State extends State<EditUserInfoPage2>
); );
} }
_buildItem(String title, String value, Function() func) { _buildItem(String title, String value, Function()? func) {
return SCDebounceWidget( return SCDebounceWidget(
child: Column( child: Column(
children: [ children: [
@ -638,11 +637,12 @@ class _EditUserInfoPage2State extends State<EditUserInfoPage2>
child: text(value, textColor: Colors.white, fontSize: 15.sp), child: text(value, textColor: Colors.white, fontSize: 15.sp),
), ),
), ),
Icon( if (func != null)
Icons.keyboard_arrow_right, Icon(
size: 20.w, Icons.keyboard_arrow_right,
color: Colors.white70, size: 20.w,
), color: Colors.white70,
),
SizedBox(width: 8.w), SizedBox(width: 8.w),
], ],
), ),
@ -656,7 +656,7 @@ class _EditUserInfoPage2State extends State<EditUserInfoPage2>
], ],
), ),
onTap: () { onTap: () {
func.call(); func?.call();
}, },
); );
} }

View File

@ -109,13 +109,25 @@ class _PersonDetailPageState extends State<PersonDetailPage> with RouteAware {
} }
}); });
Provider.of<SocialChatUserProfileManager>(context, listen: false) WidgetsBinding.instance.addPostFrameCallback((_) {
.userProfile = null; if (!mounted) {
Provider.of<SocialChatUserProfileManager>( return;
}
_loadInitialProfileData();
});
}
void _loadInitialProfileData() {
final profileManager = Provider.of<SocialChatUserProfileManager>(
context, context,
listen: false, listen: false,
).getUserInfoById(widget.tageId); );
profileManager.userProfile = null;
profileManager.getUserInfoById(widget.tageId);
SCAccountRepository().userIdentity(userId: widget.tageId).then((v) { SCAccountRepository().userIdentity(userId: widget.tageId).then((v) {
if (!mounted) {
return;
}
userIdentity = v; userIdentity = v;
setState(() {}); setState(() {});
}); });
@ -123,11 +135,17 @@ class _PersonDetailPageState extends State<PersonDetailPage> with RouteAware {
SCAccountRepository() SCAccountRepository()
.followCheck(widget.tageId) .followCheck(widget.tageId)
.then((v) { .then((v) {
if (!mounted) {
return;
}
isFollow = v; isFollow = v;
isLoading = false; isLoading = false;
setState(() {}); setState(() {});
}) })
.catchError((e) { .catchError((e) {
if (!mounted) {
return;
}
setState(() { setState(() {
isLoading = false; isLoading = false;
}); });
@ -135,6 +153,9 @@ class _PersonDetailPageState extends State<PersonDetailPage> with RouteAware {
SCAccountRepository() SCAccountRepository()
.blacklistCheck(widget.tageId) .blacklistCheck(widget.tageId)
.then((v) { .then((v) {
if (!mounted) {
return;
}
isBlacklist = v; isBlacklist = v;
isBlacklistLoading = false; isBlacklistLoading = false;
setState(() {}); setState(() {});
@ -145,6 +166,9 @@ class _PersonDetailPageState extends State<PersonDetailPage> with RouteAware {
} }
}) })
.catchError((e) { .catchError((e) {
if (!mounted) {
return;
}
setState(() { setState(() {
isBlacklistLoading = false; isBlacklistLoading = false;
}); });
@ -1564,7 +1588,7 @@ class _PersonDetailPageState extends State<PersonDetailPage> with RouteAware {
if (payload.rightUserId.trim().isEmpty) { if (payload.rightUserId.trim().isEmpty) {
return; return;
} }
final cancelText = SCAppLocalizations.of(context)!.cancel; const cancelText = 'unbing';
SCLoadingManager.show(context: context); SCLoadingManager.show(context: context);
late final _ProfileRelationProgressData progress; late final _ProfileRelationProgressData progress;
try { try {
@ -1668,6 +1692,15 @@ class _PersonDetailPageState extends State<PersonDetailPage> with RouteAware {
if (!mounted) { if (!mounted) {
return; return;
} }
final profileManager = Provider.of<SocialChatUserProfileManager>(
context,
listen: false,
);
profileManager.removeLoadedCpRelation(
userId: payload.leftUserId,
peerUserId: partnerUserId,
relationType: payload.relationType,
);
Provider.of<RtcProvider?>( Provider.of<RtcProvider?>(
context, context,
listen: false, listen: false,
@ -1676,10 +1709,12 @@ class _PersonDetailPageState extends State<PersonDetailPage> with RouteAware {
peerUserId: partnerUserId, peerUserId: partnerUserId,
relationType: payload.relationType, relationType: payload.relationType,
); );
await Provider.of<SocialChatUserProfileManager>( await profileManager.refreshLoadedCpProfiles();
context, profileManager.removeLoadedCpRelation(
listen: false, userId: payload.leftUserId,
).refreshLoadedCpProfiles(); peerUserId: partnerUserId,
relationType: payload.relationType,
);
await SmartDialog.dismiss(tag: RoomCpProgressDialog.dialogTag); await SmartDialog.dismiss(tag: RoomCpProgressDialog.dialogTag);
SCTts.show(l10n.operationSuccessful); SCTts.show(l10n.operationSuccessful);
} catch (_) { } catch (_) {
@ -2693,7 +2728,21 @@ class _PersonDetailPageState extends State<PersonDetailPage> with RouteAware {
ref.userProfile?.id ?? "", ref.userProfile?.id ?? "",
relationType: cpRelation?.relationType ?? "CP", 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) { if (mounted) {
Provider.of<RtcProvider?>( Provider.of<RtcProvider?>(
context, context,
@ -2708,7 +2757,12 @@ class _PersonDetailPageState extends State<PersonDetailPage> with RouteAware {
relationType: cpRelation?.relationType ?? "CP", relationType: cpRelation?.relationType ?? "CP",
); );
} }
ref.refreshLoadedCpProfiles(); await ref.refreshLoadedCpProfiles();
ref.removeLoadedCpRelation(
userId: currentUserId,
peerUserId: profileUserId,
relationType: cpRelation?.relationType ?? "CP",
);
SmartDialog.dismiss(tag: "showPartWaysDialog"); SmartDialog.dismiss(tag: "showPartWaysDialog");
SCTts.show( SCTts.show(
SCAppLocalizations.of( SCAppLocalizations.of(

View File

@ -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_permission_utils.dart';
import 'package:yumi/shared/tools/sc_room_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_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/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/room_rocket_launch_broadcast_message.dart';
import 'package:yumi/shared/data_sources/models/message/sc_floating_message.dart'; import 'package:yumi/shared/data_sources/models/message/sc_floating_message.dart';
@ -1833,31 +1832,23 @@ class RealTimeCommunicationManager extends ChangeNotifier {
final closeFriendPairs = <CPRes>[]; final closeFriendPairs = <CPRes>[];
await Future.wait( await Future.wait(
_roomMicRelationTypes.map((relationType) async { _roomMicRelationTypes.map((relationType) async {
final cabin = await _safeLoadRoomMicRelationCabin(userId, relationType); final pairs = await _safeLoadRoomMicRelationPairs(userId, relationType);
final pair = cabin?.cpPairUserProfileCO; for (final pair in pairs) {
if (!_hasRoomMicRelationPair(pair)) { if (!_hasRoomMicRelationPair(pair)) {
return; continue;
} }
final normalizedType = scNormalizeCpRelationType( final normalizedType = scNormalizeCpRelationType(
pair?.relationType ?? relationType, pair.relationType ?? relationType,
); );
final enrichedPair = pair!.copyWith( final enrichedPair = pair.copyWith(relationType: normalizedType);
relationType: normalizedType, if (_isRoomMicRelationLocallyDismissed(enrichedPair)) {
cpValue: pair.cpValue ?? cabin?.cpVal?.toDouble(), continue;
levelText: }
scCpRelationLevelTextFromCabin( if (normalizedType == "CP") {
cabin, cpPairs.add(enrichedPair);
fallbackText: pair.levelText, } else {
) ?? closeFriendPairs.add(enrichedPair);
_roomMicRelationLevelTextFromCabin(cabin), }
);
if (_isRoomMicRelationLocallyDismissed(enrichedPair)) {
return;
}
if (normalizedType == "CP") {
cpPairs.add(enrichedPair);
} else {
closeFriendPairs.add(enrichedPair);
} }
}), }),
); );
@ -1868,53 +1859,26 @@ class RealTimeCommunicationManager extends ChangeNotifier {
); );
} }
Future<SCCpCabinRes?> _safeLoadRoomMicRelationCabin( Future<List<CPRes>> _safeLoadRoomMicRelationPairs(
String userId, String userId,
String relationType, String relationType,
) async { ) async {
try { try {
return await SCAccountRepository().cpCabin( return await SCAccountRepository().cpRelationshipPairs(
userId, userId: userId,
relationType: relationType, relationType: relationType,
); );
} catch (error) { } catch (error) {
if (kDebugMode) { if (kDebugMode) {
debugPrint( debugPrint(
"[CP][MicRelation] cabin failed userId=$userId " "[CP][MicRelation] pair failed userId=$userId "
"relationType=$relationType error=$error", "relationType=$relationType error=$error",
); );
} }
return null; return const <CPRes>[];
} }
} }
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<dynamic> values) {
for (final value in values) {
final text = value?.toString().trim() ?? "";
if (text.isNotEmpty) {
return text;
}
}
return "";
}
void _applyHydratedRoomMicRelationProfile( void _applyHydratedRoomMicRelationProfile(
String roomId, String roomId,
String userId, String userId,

View File

@ -6338,7 +6338,8 @@ class RealTimeMessagingManager extends ChangeNotifier {
'giftId=${gift.id} giftName=${gift.giftName}', 'giftId=${gift.id} giftName=${gift.giftName}',
); );
SCGiftVapSvgaManager().play(giftSourceUrl); SCGiftVapSvgaManager().play(giftSourceUrl);
} else if (SCGlobalConfig.isLowPerformanceDevice && } else if (!scIsCpGift(gift) &&
SCGlobalConfig.isLowPerformanceDevice &&
SCGlobalConfig.isGiftSpecialEffects && SCGlobalConfig.isGiftSpecialEffects &&
rtcProvider.shouldShowRoomVisualEffects) { rtcProvider.shouldShowRoomVisualEffects) {
final roomId = final roomId =
@ -6414,7 +6415,7 @@ class RealTimeMessagingManager extends ChangeNotifier {
); );
} }
final coins = (msg.number ?? 0) * (gift.giftCandy ?? 0); final coins = (msg.number ?? 0) * (gift.giftCandy ?? 0);
if (coins > 9999) { if (coins > 9999 && !scIsCpGift(gift)) {
_enqueueGiftFloatingMessage( _enqueueGiftFloatingMessage(
SCFloatingMessage( SCFloatingMessage(
type: 1, type: 1,

View File

@ -210,6 +210,7 @@ class SocialChatUserProfileManager extends ChangeNotifier {
RoomUserCardRes? userCardInfo; RoomUserCardRes? userCardInfo;
Object? userCardLoadError; Object? userCardLoadError;
bool isUserCardLoading = false; bool isUserCardLoading = false;
bool isUserCardRelationLoading = false;
int _userCardRequestSerial = 0; int _userCardRequestSerial = 0;
int _userCardSessionSerial = 0; int _userCardSessionSerial = 0;
int? _activeUserCardSessionSerial; int? _activeUserCardSessionSerial;
@ -235,6 +236,7 @@ class SocialChatUserProfileManager extends ChangeNotifier {
userCardInfo = null; userCardInfo = null;
userCardLoadError = null; userCardLoadError = null;
isUserCardLoading = true; isUserCardLoading = true;
isUserCardRelationLoading = false;
notifyListeners(); notifyListeners();
if (normalizedUserId.isEmpty) { if (normalizedUserId.isEmpty) {
@ -257,6 +259,7 @@ class SocialChatUserProfileManager extends ChangeNotifier {
userCardInfo = card; userCardInfo = card;
userCardLoadError = null; userCardLoadError = null;
isUserCardLoading = false; isUserCardLoading = false;
isUserCardRelationLoading = card.userProfile != null;
notifyListeners(); notifyListeners();
unawaited( unawaited(
_hydrateRoomUserCard( _hydrateRoomUserCard(
@ -325,8 +328,14 @@ class SocialChatUserProfileManager extends ChangeNotifier {
} }
userCardInfo = hydratedCard; userCardInfo = hydratedCard;
userCardLoadError = null; userCardLoadError = null;
isUserCardRelationLoading = false;
notifyListeners(); notifyListeners();
} catch (error, stackTrace) { } catch (error, stackTrace) {
if (requestSerial == _userCardRequestSerial &&
sessionSerial == _activeUserCardSessionSerial) {
isUserCardRelationLoading = false;
notifyListeners();
}
_debugProfileLoadFailure( _debugProfileLoadFailure(
scope: 'roomCardHydrate', scope: 'roomCardHydrate',
userId: userId, userId: userId,
@ -346,6 +355,7 @@ class SocialChatUserProfileManager extends ChangeNotifier {
userCardInfo = null; userCardInfo = null;
userCardLoadError = null; userCardLoadError = null;
isUserCardLoading = false; isUserCardLoading = false;
isUserCardRelationLoading = false;
} }
Future<RoomUserCardRes?> _withCurrentVipLevelForCard( Future<RoomUserCardRes?> _withCurrentVipLevelForCard(
@ -357,7 +367,7 @@ class SocialChatUserProfileManager extends ChangeNotifier {
return card; return card;
} }
var mergedProfile = await _withCurrentVipLevel(profile, userId); var mergedProfile = await _withCurrentVipLevel(profile, userId);
mergedProfile = await _withCpRelationshipProfiles(mergedProfile, userId); mergedProfile = await _withCpCabinProfiles(mergedProfile, userId);
if (mergedProfile == profile) { if (mergedProfile == profile) {
return card; return card;
} }
@ -487,44 +497,6 @@ class SocialChatUserProfileManager extends ChangeNotifier {
); );
} }
Future<SocialChatUserProfile> _withCpRelationshipProfiles(
SocialChatUserProfile profile,
String userId,
) async {
if (userId.trim().isEmpty) {
return profile;
}
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 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({ void _addCpRelationPairs({
required List<CPRes> cpPairs, required List<CPRes> cpPairs,
required List<CPRes> closeFriendPairs, required List<CPRes> closeFriendPairs,
@ -953,6 +925,127 @@ class SocialChatUserProfileManager extends ChangeNotifier {
await _refreshLoadedCpProfilesForUserIds(normalizedUserIds); 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<CPRes>? filterRelations(List<CPRes>? 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 = <String>{
relation.meUserId?.trim() ?? "",
relation.cpUserId?.trim() ?? "",
}..remove("");
return relationUserIds.contains(userId) &&
relationUserIds.contains(peerUserId);
}
Future<void> _refreshLoadedCpProfilesForUserIds(Set<String>? userIds) async { Future<void> _refreshLoadedCpProfilesForUserIds(Set<String>? userIds) async {
String refreshedUserId(String? userId) { String refreshedUserId(String? userId) {
final normalizedUserId = userId?.trim() ?? ""; final normalizedUserId = userId?.trim() ?? "";
@ -981,7 +1074,7 @@ class SocialChatUserProfileManager extends ChangeNotifier {
final cardUserId = refreshedUserId(cardProfile?.id); final cardUserId = refreshedUserId(cardProfile?.id);
if (userCardInfo != null && cardProfile != null && cardUserId.isNotEmpty) { if (userCardInfo != null && cardProfile != null && cardUserId.isNotEmpty) {
userCardInfo = userCardInfo?.copyWith( userCardInfo = userCardInfo?.copyWith(
userProfile: await _withCpRelationshipProfiles(cardProfile, cardUserId), userProfile: await _withCpCabinProfiles(cardProfile, cardUserId),
); );
} }

View File

@ -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 <String, dynamic>{};
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<String, dynamic> 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() ?? '');
}

View File

@ -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_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_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_rights_config_res.dart';
import 'package:yumi/shared/business_logic/models/res/sc_cp_gift_relation_check_res.dart';
abstract class SocialChatUserRepository { abstract class SocialChatUserRepository {
Future<SocialChatLoginRes> getUser(String id); Future<SocialChatLoginRes> getUser(String id);
@ -329,6 +330,12 @@ abstract class SocialChatUserRepository {
/// CP /// CP
Future<SCCpCabinRes> cpCabin(String userId, {String relationType = 'CP'}); Future<SCCpCabinRes> cpCabin(String userId, {String relationType = 'CP'});
///CP
Future<SCCpGiftRelationCheckRes> cpGiftRelationCheck({
required String acceptUserId,
String relationType = 'CP',
});
/// CP /// CP
Future<SCCpCabinValueRes> cpCabinValue( Future<SCCpCabinValueRes> cpCabinValue(
String userId, { String userId, {

View File

@ -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/sc_public_message_page_res.dart';
import 'package:yumi/shared/business_logic/models/res/room_res.dart' import 'package:yumi/shared/business_logic/models/res/room_res.dart'
hide WearBadge; 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_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_task_list_res.dart';
import 'package:yumi/shared/business_logic/models/res/sc_user_counter_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<SCCpGiftRelationCheckRes> 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<SCCpGiftRelationCheckRes>(
"/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 ///user/cp/cabin
@override @override
Future<SCCpCabinRes> cpCabin( Future<SCCpCabinRes> cpCabin(

View File

@ -37,7 +37,7 @@ class RoomCpProgressDialog extends StatelessWidget {
this.targetValue = 1, this.targetValue = 1,
this.expAwayText = '0', this.expAwayText = '0',
this.nextLevelText = 'Lv. 1', this.nextLevelText = 'Lv. 1',
this.cancelText = 'Cancel', this.cancelText = 'unbing',
this.dismissTag = dialogTag, this.dismissTag = dialogTag,
this.onCancel, this.onCancel,
}); });
@ -68,7 +68,7 @@ class RoomCpProgressDialog extends StatelessWidget {
double targetValue = 1, double targetValue = 1,
String expAwayText = '0', String expAwayText = '0',
String nextLevelText = 'Lv. 1', String nextLevelText = 'Lv. 1',
String cancelText = 'Cancel', String cancelText = 'unbing',
String dismissTag = dialogTag, String dismissTag = dialogTag,
VoidCallback? onCancel, VoidCallback? onCancel,
}) async { }) async {
@ -495,7 +495,7 @@ class _CancelButton extends StatelessWidget {
fit: BoxFit.fill, fit: BoxFit.fill,
), ),
Text( Text(
text.trim().isEmpty ? 'Cancel' : text.trim(), text.trim().isEmpty ? 'unbing' : text.trim(),
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center, textAlign: TextAlign.center,

View File

@ -76,7 +76,12 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
context, context,
listen: false, listen: false,
); );
_loadRoomUserCard(); WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) {
return;
}
_loadRoomUserCard();
});
SCAccountRepository().userIdentity(userId: widget.userId).then((v) { SCAccountRepository().userIdentity(userId: widget.userId).then((v) {
if (!mounted) { if (!mounted) {
return; return;
@ -139,7 +144,11 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
final profile = ref.userCardInfo?.userProfile; final profile = ref.userCardInfo?.userProfile;
final dataCard = _activeDataCard(ref.userCardInfo); final dataCard = _activeDataCard(ref.userCardInfo);
final sheetHeight = _resolvedProfileSheetHeight(screenHeight, profile); final sheetHeight = _resolvedProfileSheetHeight(
screenHeight,
profile,
relationLoading: ref.isUserCardRelationLoading,
);
return _buildProfileSheet( return _buildProfileSheet(
context: context, context: context,
@ -231,9 +240,13 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
double _resolvedProfileSheetHeight( double _resolvedProfileSheetHeight(
double screenHeight, double screenHeight,
SocialChatUserProfile? profile, SocialChatUserProfile? profile, {
) { required bool relationLoading,
}) {
final defaultSheetHeight = screenHeight * _profileSheetHeightFactor; final defaultSheetHeight = screenHeight * _profileSheetHeightFactor;
if (relationLoading) {
return defaultSheetHeight;
}
final hasCpRelation = _hasCpRelation(profile); final hasCpRelation = _hasCpRelation(profile);
final hasCloseFriends = _closeFriendProfileData(profile).isNotEmpty; final hasCloseFriends = _closeFriendProfileData(profile).isNotEmpty;
var resolvedHeight = defaultSheetHeight; var resolvedHeight = defaultSheetHeight;
@ -356,6 +369,7 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
}) { }) {
final hasCpRelation = _hasCpRelation(profile); final hasCpRelation = _hasCpRelation(profile);
final closeFriends = _closeFriendProfileData(profile); final closeFriends = _closeFriendProfileData(profile);
final isRelationLoading = ref.isUserCardRelationLoading;
return Stack( return Stack(
children: [ children: [
@ -386,7 +400,12 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
ref, ref,
profile, profile,
), ),
if (hasCpRelation) ...[ if (isRelationLoading) ...[
SizedBox(height: 12.w),
_buildCpRelationSkeletonCard(),
SizedBox(height: 11.w),
_buildCloseFriendSkeletonStrip(),
] else if (hasCpRelation) ...[
SizedBox(height: 12.w), SizedBox(height: 12.w),
_buildCpRelationCard( _buildCpRelationCard(
context: context, context: context,
@ -400,7 +419,9 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
], ],
SizedBox( SizedBox(
height: height:
hasCpRelation || closeFriends.isNotEmpty isRelationLoading ||
hasCpRelation ||
closeFriends.isNotEmpty
? 14.w ? 14.w
: 12.w, : 12.w,
), ),
@ -691,6 +712,51 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
); );
} }
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( Widget _buildCpCardText(
String value, { String value, {
required Color color, required Color color,
@ -791,6 +857,69 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
); );
} }
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) { _RoomProfileCpData _cpProfileData(SocialChatUserProfile? profile) {
final currentProfile = AccountStorage().getCurrentUser()?.userProfile; final currentProfile = AccountStorage().getCurrentUser()?.userProfile;
final cp = _firstRelation( final cp = _firstRelation(
@ -1432,8 +1561,15 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
void _openProfile(BuildContext context) { void _openProfile(BuildContext context) {
final route = final route =
"${SCMainRoute.person}?isMe=${AccountStorage().getCurrentUser()?.userProfile?.id == widget.userId}&tageId=${widget.userId}"; "${SCMainRoute.person}?isMe=${AccountStorage().getCurrentUser()?.userProfile?.id == widget.userId}&tageId=${widget.userId}";
final rootContext = navigatorKey.currentState?.context;
Navigator.of(context).pop(); 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<void> _openChat(BuildContext context) async { Future<void> _openChat(BuildContext context) async {
@ -1497,7 +1633,7 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
SocialChatUserProfile? profile, SocialChatUserProfile? profile,
) async { ) async {
final cp = _cpProfileData(profile); final cp = _cpProfileData(profile);
final cancelText = SCAppLocalizations.of(context)!.cancel; const cancelText = 'unbing';
SCLoadingManager.show(); SCLoadingManager.show();
late final _RoomCpProgressData progress; late final _RoomCpProgressData progress;
try { try {

View File

@ -21,4 +21,24 @@ void main() {
expect(manager.userCardInfo, isNull); 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');
});
} }