Merge branch 'feature' of gitea.haiyihy.com:hy/chatapp3-flutter into feature

This commit is contained in:
zhx 2026-05-25 18:02:12 +08:00
commit 85925d3607
14 changed files with 639 additions and 135 deletions

View File

@ -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<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(
Iterable<String?> 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);

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/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<GiftPage> with TickerProviderStateMixin {
'giftType=$giftType',
);
await _showCpGiftRelationCheckToastIfNeeded(request);
try {
final repository = SCChatRoomRepository();
_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(
SCChatRoomRepository repository,
_GiftSendRequest request,
@ -1857,7 +1903,7 @@ class _GiftPageState extends State<GiftPage> 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<GiftPage> with TickerProviderStateMixin {
'animationCount=$animationCount',
);
SCGiftVapSvgaManager().play(gift.giftSourceUrl!);
} else if (SCGlobalConfig.isLowPerformanceDevice &&
} else if (!_isCpGift(gift) &&
SCGlobalConfig.isLowPerformanceDevice &&
SCGlobalConfig.isGiftSpecialEffects &&
(rtcProvider?.shouldShowRoomVisualEffects ?? false)) {
_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/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<VoiceRoomPage>
}
final giftPhoto = (msg.gift?.giftPhoto ?? "").trim();
_enqueueCpGiftFloatingIfNeeded(msg);
if (_supportsComboMilestoneEffects(msg)) {
_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) {
if (!Provider.of<RtcProvider>(
context,

View File

@ -259,9 +259,7 @@ class _EditUserInfoPage2State extends State<EditUserInfoPage2>
sex == 1
? SCAppLocalizations.of(context)!.man
: SCAppLocalizations.of(context)!.woman,
() {
_showSexDialog();
},
null,
),
_buildItem(
@ -538,6 +536,7 @@ class _EditUserInfoPage2State extends State<EditUserInfoPage2>
}
}
// ignore: unused_element
void _showSexDialog() {
showCenterDialog(
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(
child: Column(
children: [
@ -638,11 +637,12 @@ class _EditUserInfoPage2State extends State<EditUserInfoPage2>
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<EditUserInfoPage2>
],
),
onTap: () {
func.call();
func?.call();
},
);
}

View File

@ -109,13 +109,25 @@ class _PersonDetailPageState extends State<PersonDetailPage> with RouteAware {
}
});
Provider.of<SocialChatUserProfileManager>(context, listen: false)
.userProfile = null;
Provider.of<SocialChatUserProfileManager>(
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) {
return;
}
_loadInitialProfileData();
});
}
void _loadInitialProfileData() {
final profileManager = Provider.of<SocialChatUserProfileManager>(
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<PersonDetailPage> 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<PersonDetailPage> with RouteAware {
SCAccountRepository()
.blacklistCheck(widget.tageId)
.then((v) {
if (!mounted) {
return;
}
isBlacklist = v;
isBlacklistLoading = false;
setState(() {});
@ -145,6 +166,9 @@ class _PersonDetailPageState extends State<PersonDetailPage> with RouteAware {
}
})
.catchError((e) {
if (!mounted) {
return;
}
setState(() {
isBlacklistLoading = false;
});
@ -1564,7 +1588,7 @@ class _PersonDetailPageState extends State<PersonDetailPage> 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<PersonDetailPage> with RouteAware {
if (!mounted) {
return;
}
final profileManager = Provider.of<SocialChatUserProfileManager>(
context,
listen: false,
);
profileManager.removeLoadedCpRelation(
userId: payload.leftUserId,
peerUserId: partnerUserId,
relationType: payload.relationType,
);
Provider.of<RtcProvider?>(
context,
listen: false,
@ -1676,10 +1709,12 @@ class _PersonDetailPageState extends State<PersonDetailPage> with RouteAware {
peerUserId: partnerUserId,
relationType: payload.relationType,
);
await Provider.of<SocialChatUserProfileManager>(
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<PersonDetailPage> 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<RtcProvider?>(
context,
@ -2708,7 +2757,12 @@ class _PersonDetailPageState extends State<PersonDetailPage> 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(

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_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 = <CPRes>[];
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<SCCpCabinRes?> _safeLoadRoomMicRelationCabin(
Future<List<CPRes>> _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 <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(
String roomId,
String userId,

View File

@ -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,

View File

@ -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<RoomUserCardRes?> _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<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({
required List<CPRes> cpPairs,
required List<CPRes> 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<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 {
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),
);
}

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_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<SocialChatLoginRes> getUser(String id);
@ -329,6 +330,12 @@ abstract class SocialChatUserRepository {
/// CP
Future<SCCpCabinRes> cpCabin(String userId, {String relationType = 'CP'});
///CP
Future<SCCpGiftRelationCheckRes> cpGiftRelationCheck({
required String acceptUserId,
String relationType = 'CP',
});
/// CP
Future<SCCpCabinValueRes> cpCabinValue(
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/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<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
@override
Future<SCCpCabinRes> cpCabin(

View File

@ -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,

View File

@ -76,7 +76,12 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
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<RoomUserInfoCard> {
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<RoomUserInfoCard> {
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<RoomUserInfoCard> {
}) {
final hasCpRelation = _hasCpRelation(profile);
final closeFriends = _closeFriendProfileData(profile);
final isRelationLoading = ref.isUserCardRelationLoading;
return Stack(
children: [
@ -386,7 +400,12 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
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<RoomUserInfoCard> {
],
SizedBox(
height:
hasCpRelation || closeFriends.isNotEmpty
isRelationLoading ||
hasCpRelation ||
closeFriends.isNotEmpty
? 14.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(
String value, {
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) {
final currentProfile = AccountStorage().getCurrentUser()?.userProfile;
final cp = _firstRelation(
@ -1432,8 +1561,15 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
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<void> _openChat(BuildContext context) async {
@ -1497,7 +1633,7 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
SocialChatUserProfile? profile,
) async {
final cp = _cpProfileData(profile);
final cancelText = SCAppLocalizations.of(context)!.cancel;
const cancelText = 'unbing';
SCLoadingManager.show();
late final _RoomCpProgressData progress;
try {

View File

@ -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');
});
}