又修复了一版
This commit is contained in:
parent
2999de23d1
commit
0e2f4cf4bf
@ -18,7 +18,6 @@ import 'package:yumi/app/config/business_logic_strategy.dart';
|
||||
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_config_repository_imp.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository_imp.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/login_res.dart';
|
||||
import 'package:yumi/shared/tools/sc_cp_gift_relation_utils.dart';
|
||||
@ -83,7 +82,6 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
TabController? _tabController;
|
||||
List<String> _tabTypes = <String>[];
|
||||
bool _didApplyInitialTab = false;
|
||||
bool _showFirstRechargeGiftEntry = false;
|
||||
|
||||
/// 业务逻辑策略访问器
|
||||
BusinessLogicStrategy get _strategy => SCGlobalConfig.businessLogicStrategy;
|
||||
@ -240,7 +238,6 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
listen: false,
|
||||
).giftBackpack(forceRefresh: true),
|
||||
);
|
||||
unawaited(_loadFirstRechargeGiftEntryState());
|
||||
Provider.of<SocialChatUserProfileManager>(context, listen: false).balance();
|
||||
rtcProvider?.roomWheatMap.forEach((k, v) {
|
||||
if (v.user != null) {
|
||||
@ -276,35 +273,6 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
_normalizeCpRecipientSelection(notify: true);
|
||||
}
|
||||
|
||||
Future<void> _loadFirstRechargeGiftEntryState() async {
|
||||
if (SCGlobalConfig.isReview || AccountStorage().getToken().isEmpty) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
final home = await SCConfigRepositoryImp().firstRechargeRewardHome();
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
final shouldShow = home.shouldShowRewardDialog;
|
||||
if (_showFirstRechargeGiftEntry == shouldShow) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_showFirstRechargeGiftEntry = shouldShow;
|
||||
});
|
||||
} catch (error) {
|
||||
if (kDebugMode) {
|
||||
debugPrint('[FirstRecharge][gift] load entry failed: $error');
|
||||
}
|
||||
if (!mounted || !_showFirstRechargeGiftEntry) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_showFirstRechargeGiftEntry = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
bool get _isCurrentCpGiftTab {
|
||||
final controller = _tabController;
|
||||
if (controller == null || controller.index >= _tabTypes.length) {
|
||||
@ -730,7 +698,9 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
final currentTabType = giftTabs[tabController.index].type;
|
||||
final isCpTab = currentTabType == "CP";
|
||||
final showFirstRechargeEntry =
|
||||
!SCGlobalConfig.isReview && !isCpTab && _showFirstRechargeGiftEntry;
|
||||
!SCGlobalConfig.isReview &&
|
||||
!isCpTab &&
|
||||
ref.shouldShowFirstRechargeReward;
|
||||
_ensureCurrentTabSelection(ref, giftTabs[tabController.index].type);
|
||||
final isCpRecipientSingleMode = isCpTab || _isCpGift(checkedGift);
|
||||
return SafeArea(
|
||||
@ -1599,6 +1569,7 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
animationCount: request.clickCount,
|
||||
);
|
||||
}
|
||||
_syncLocalCpInviteAfterGift(request);
|
||||
await _refreshGiftBackpack();
|
||||
_refreshCpStateAfterGift(request, profileManager);
|
||||
return;
|
||||
@ -1612,6 +1583,7 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
request.quantity,
|
||||
false,
|
||||
roomId: request.roomId,
|
||||
relationType: request.cpRelationType,
|
||||
)
|
||||
: await repository.giveGift(
|
||||
request.acceptUserIds,
|
||||
@ -1619,6 +1591,7 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
request.quantity,
|
||||
false,
|
||||
roomId: request.roomId,
|
||||
relationType: request.cpRelationType,
|
||||
);
|
||||
_giftFxLog(
|
||||
'$requestName success trigger=$trigger '
|
||||
@ -1662,6 +1635,7 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
);
|
||||
}
|
||||
profileManager?.updateBalance(result);
|
||||
_syncLocalCpInviteAfterGift(request);
|
||||
_refreshCpStateAfterGift(request, profileManager);
|
||||
} catch (e) {
|
||||
final errorMessage =
|
||||
@ -1705,6 +1679,41 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
unawaited(profileManager?.refreshLoadedCpProfiles());
|
||||
}
|
||||
|
||||
void _syncLocalCpInviteAfterGift(_GiftSendRequest request) {
|
||||
final relationType = request.cpRelationType;
|
||||
if (relationType == null || request.acceptUserIds.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final context = navigatorKey.currentState?.context;
|
||||
if (context == null) {
|
||||
return;
|
||||
}
|
||||
final receiverId = request.acceptUserIds.first.trim();
|
||||
if (receiverId.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final receiverProfile =
|
||||
request.acceptUsers.isNotEmpty
|
||||
? request.acceptUsers.first.user
|
||||
: SocialChatUserProfile(id: receiverId, account: receiverId);
|
||||
_giftFxLog(
|
||||
'sync local CP invite after gift '
|
||||
'receiver=$receiverId relationType=$relationType giftId=${request.gift.id}',
|
||||
);
|
||||
unawaited(
|
||||
Provider.of<RtmProvider>(
|
||||
context,
|
||||
listen: false,
|
||||
).insertLocalOutgoingCpGiftInvite(
|
||||
receiverId: receiverId,
|
||||
receiverProfile: receiverProfile,
|
||||
relationType: relationType,
|
||||
gift: request.gift,
|
||||
giftCount: request.quantity,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<MicRes>> _giveBackpackGiftToRecipients(
|
||||
SCChatRoomRepository repository,
|
||||
_GiftSendRequest request,
|
||||
@ -1726,6 +1735,7 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
roomId: request.roomId,
|
||||
seatIndex:
|
||||
acceptUser?.micIndex ?? _resolveSeatIndexForUserId(acceptUserId),
|
||||
relationType: request.cpRelationType,
|
||||
);
|
||||
if (acceptUser != null) {
|
||||
sentUsers.add(acceptUser);
|
||||
@ -2309,6 +2319,13 @@ class _GiftSendRequest {
|
||||
return isLuckyGiftRequest ? 'giveLuckyGift' : 'giveGift';
|
||||
}
|
||||
|
||||
String? get cpRelationType {
|
||||
if (!scIsCpGift(gift)) {
|
||||
return null;
|
||||
}
|
||||
return scCpRelationTypeForGift(gift);
|
||||
}
|
||||
|
||||
String get batchKey {
|
||||
final sortedAcceptUserIds = List<String>.from(acceptUserIds)..sort();
|
||||
final type =
|
||||
|
||||
@ -498,7 +498,7 @@ class _SCIndexPageState extends State<SCIndexPage>
|
||||
return;
|
||||
}
|
||||
_hasRequestedFirstRechargeDialog = true;
|
||||
await SCDialogUtils.showFirstRechargeDialog(context);
|
||||
await SCDialogUtils.showFirstRechargeDialog(context, forceRefresh: true);
|
||||
}
|
||||
|
||||
Future<void> _waitForRegisterRewardSocketIfNeeded() async {
|
||||
|
||||
@ -20,9 +20,11 @@ import 'package:yumi/ui_kit/components/sc_tts.dart';
|
||||
import 'package:yumi/app/routes/sc_routes.dart';
|
||||
import 'package:yumi/app/routes/sc_fluro_navigator.dart';
|
||||
import 'package:yumi/shared/tools/sc_cp_relation_notice_utils.dart';
|
||||
import 'package:yumi/shared/tools/sc_cp_relation_level_utils.dart';
|
||||
import 'package:yumi/shared/tools/sc_loading_manager.dart';
|
||||
import 'package:yumi/shared/tools/sc_lk_event_bus.dart';
|
||||
import 'package:yumi/shared/tools/sc_pick_utils.dart';
|
||||
import 'package:yumi/shared/tools/sc_string_utils.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/gift_res.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
|
||||
import 'package:yumi/shared/data_sources/models/enum/sc_props_type.dart';
|
||||
@ -33,12 +35,14 @@ import 'package:yumi/shared/business_logic/models/res/login_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/my_room_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/room_res.dart'
|
||||
hide WearBadge, UseProps, PropsResources;
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_cp_rights_config_res.dart';
|
||||
import 'package:yumi/services/audio/rtc_manager.dart';
|
||||
import 'package:yumi/services/audio/rtm_manager.dart';
|
||||
import 'package:yumi/services/auth/user_profile_manager.dart';
|
||||
import 'package:yumi/services/general/sc_app_general_manager.dart';
|
||||
import 'package:yumi/ui_kit/widgets/badge/sc_user_badge_strip.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/cp/sc_cp_corner_label.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/cp/room_cp_progress_dialog.dart';
|
||||
|
||||
import '../../../app/constants/sc_screen.dart';
|
||||
import '../../../shared/business_logic/models/res/sc_user_counter_res.dart';
|
||||
@ -969,7 +973,7 @@ class _PersonDetailPageState extends State<PersonDetailPage> {
|
||||
) {
|
||||
final owner = _profileRelationOwner(profile, relation);
|
||||
final partner = _profileRelationPartner(profile, relation);
|
||||
return SizedBox(
|
||||
final card = SizedBox(
|
||||
height: 114.w,
|
||||
width: double.infinity,
|
||||
child: Stack(
|
||||
@ -1037,6 +1041,11 @@ class _PersonDetailPageState extends State<PersonDetailPage> {
|
||||
],
|
||||
),
|
||||
);
|
||||
return _buildProfileRelationProgressTap(
|
||||
profile: profile,
|
||||
relation: relation,
|
||||
child: card,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProfileCpCardText(
|
||||
@ -1093,7 +1102,7 @@ class _PersonDetailPageState extends State<PersonDetailPage> {
|
||||
final type = _normalizeProfileRelationType(relation.relationType);
|
||||
final friend = _profileRelationPartner(profile, relation);
|
||||
final isSister = type == "SISTERS";
|
||||
return SizedBox(
|
||||
final card = SizedBox(
|
||||
width: 113.w,
|
||||
height: 100.w,
|
||||
child: Stack(
|
||||
@ -1180,6 +1189,26 @@ class _PersonDetailPageState extends State<PersonDetailPage> {
|
||||
],
|
||||
),
|
||||
);
|
||||
return _buildProfileRelationProgressTap(
|
||||
profile: profile,
|
||||
relation: relation,
|
||||
child: card,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProfileRelationProgressTap({
|
||||
required SocialChatUserProfile? profile,
|
||||
required CPRes relation,
|
||||
required Widget child,
|
||||
}) {
|
||||
if (!_isOwnProfile(profile) ||
|
||||
!_profileRelationHasPartner(profile, relation)) {
|
||||
return child;
|
||||
}
|
||||
return SCDebounceWidget(
|
||||
onTap: () => _openProfileRelationProgress(profile, relation),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProfileRelationAvatar(String avatarUrl, double size) {
|
||||
@ -1448,21 +1477,186 @@ class _PersonDetailPageState extends State<PersonDetailPage> {
|
||||
}
|
||||
}
|
||||
|
||||
bool _isOwnProfile(SocialChatUserProfile? profile) {
|
||||
if (widget.isMe == "true") {
|
||||
return true;
|
||||
}
|
||||
final currentUserId =
|
||||
AccountStorage().getCurrentUser()?.userProfile?.id?.trim() ?? "";
|
||||
final profileId = (profile?.id ?? widget.tageId).trim();
|
||||
return currentUserId.isNotEmpty && currentUserId == profileId;
|
||||
}
|
||||
|
||||
_ProfileRelationProgressPayload _profileRelationProgressPayload(
|
||||
SocialChatUserProfile? profile,
|
||||
CPRes relation,
|
||||
) {
|
||||
final currentProfile = AccountStorage().getCurrentUser()?.userProfile;
|
||||
final owner = _profileRelationOwner(profile, relation);
|
||||
final partner = _profileRelationPartner(profile, relation);
|
||||
return _ProfileRelationProgressPayload(
|
||||
leftAvatarUrl: _firstNonBlankProfileValue([
|
||||
owner.avatarUrl,
|
||||
profile?.userAvatar,
|
||||
currentProfile?.userAvatar,
|
||||
]),
|
||||
rightAvatarUrl: partner.avatarUrl?.trim() ?? "",
|
||||
leftName: _firstNonBlankProfileValue([
|
||||
owner.nickname,
|
||||
profile?.userNickname,
|
||||
currentProfile?.userNickname,
|
||||
]),
|
||||
rightName: _firstNonBlankProfileValue([
|
||||
partner.nickname,
|
||||
partner.account,
|
||||
]),
|
||||
leftUserId: _firstNonBlankProfileValue([
|
||||
owner.userId,
|
||||
profile?.id,
|
||||
currentProfile?.id,
|
||||
]),
|
||||
rightUserId: _firstNonBlankProfileValue([partner.userId]),
|
||||
days: _profileRelationDays(relation),
|
||||
levelText: _profileRelationLevelText(relation),
|
||||
relationType: _normalizeProfileRelationType(relation.relationType),
|
||||
cpValue: relation.cpValue ?? 0,
|
||||
dismissCost: relation.dismissCost,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _openProfileRelationProgress(
|
||||
SocialChatUserProfile? profile,
|
||||
CPRes relation,
|
||||
) async {
|
||||
final payload = _profileRelationProgressPayload(profile, relation);
|
||||
if (payload.rightUserId.trim().isEmpty) {
|
||||
return;
|
||||
}
|
||||
final cancelText = SCAppLocalizations.of(context)!.cancel;
|
||||
SCLoadingManager.show(context: context);
|
||||
late final _ProfileRelationProgressData progress;
|
||||
try {
|
||||
progress = await _loadProfileRelationProgressData(payload);
|
||||
} finally {
|
||||
SCLoadingManager.hide();
|
||||
}
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
RoomCpProgressDialog.show(
|
||||
context,
|
||||
leftUser: RoomCpProgressDialogUser(
|
||||
nickname: payload.leftName,
|
||||
userId: payload.leftUserId,
|
||||
avatarUrl: payload.leftAvatarUrl,
|
||||
),
|
||||
rightUser: RoomCpProgressDialogUser(
|
||||
nickname: payload.rightName,
|
||||
userId: payload.rightUserId,
|
||||
avatarUrl: payload.rightAvatarUrl,
|
||||
),
|
||||
daysValue: payload.days,
|
||||
levelText: progress.levelText,
|
||||
currentValue: progress.currentValue,
|
||||
targetValue: progress.targetValue,
|
||||
expAwayText: progress.expAwayText,
|
||||
nextLevelText: progress.nextLevelText,
|
||||
cancelText: cancelText,
|
||||
onCancel:
|
||||
() => _showProfileRelationDismissConfirm(
|
||||
payload,
|
||||
dismissCost: progress.dismissCost,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<_ProfileRelationProgressData> _loadProfileRelationProgressData(
|
||||
_ProfileRelationProgressPayload payload,
|
||||
) async {
|
||||
final partnerUserId = payload.rightUserId.trim();
|
||||
final fallback = _ProfileRelationProgressData.fallback(payload);
|
||||
if (partnerUserId.isEmpty) {
|
||||
return fallback;
|
||||
}
|
||||
var latestFallback = fallback;
|
||||
try {
|
||||
final config = await SCAccountRepository().cpRightsConfig(
|
||||
relationType: payload.relationType,
|
||||
cpUserId: partnerUserId,
|
||||
);
|
||||
final progress = _ProfileRelationProgressData.fromRightsConfig(
|
||||
config,
|
||||
payload,
|
||||
);
|
||||
latestFallback = progress;
|
||||
if (config.levels.isNotEmpty) {
|
||||
return progress;
|
||||
}
|
||||
} catch (_) {
|
||||
// Fall back to the cabin endpoint below.
|
||||
}
|
||||
try {
|
||||
final cabin = await SCAccountRepository().cpCabin(
|
||||
payload.leftUserId,
|
||||
relationType: payload.relationType,
|
||||
);
|
||||
return _ProfileRelationProgressData.fromCabin(
|
||||
cabin,
|
||||
payload,
|
||||
fallbackDismissCost: latestFallback.dismissCost,
|
||||
);
|
||||
} catch (_) {
|
||||
return latestFallback;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _showProfileRelationDismissConfirm(
|
||||
_ProfileRelationProgressPayload payload, {
|
||||
int? dismissCost,
|
||||
}) async {
|
||||
final partnerUserId = payload.rightUserId.trim();
|
||||
if (partnerUserId.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final l10n = SCAppLocalizations.of(context)!;
|
||||
await showRoomCpDismissConfirmDialog(
|
||||
context,
|
||||
message: l10n.cpDismissConfirmMessage(
|
||||
dismissCost == null ? "--" : "$dismissCost",
|
||||
),
|
||||
cancelText: l10n.cancel,
|
||||
confirmText: l10n.confirm,
|
||||
onConfirm: () async {
|
||||
SCLoadingManager.show(context: context);
|
||||
try {
|
||||
await SCAccountRepository().cpRelationshipDismissApply(
|
||||
partnerUserId,
|
||||
relationType: payload.relationType,
|
||||
);
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
await Provider.of<SocialChatUserProfileManager>(
|
||||
context,
|
||||
listen: false,
|
||||
).refreshLoadedCpProfiles();
|
||||
await SmartDialog.dismiss(tag: RoomCpProgressDialog.dialogTag);
|
||||
SCTts.show(l10n.operationSuccessful);
|
||||
} catch (_) {
|
||||
// Repository/network layer surfaces backend errors.
|
||||
} finally {
|
||||
SCLoadingManager.hide();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
String _profileCpLevelAsset(CPRes relation) {
|
||||
return "$_cpProfileAssetBase/sc_cp_profile_cp_level_${_profileRelationLevel(relation)}.png";
|
||||
}
|
||||
|
||||
int _profileRelationLevel(CPRes relation) {
|
||||
final levelText = relation.levelText ?? "";
|
||||
final levelMatch = RegExp(
|
||||
r'(?:Lv\.?|Level)\s*(\d+)',
|
||||
caseSensitive: false,
|
||||
).firstMatch(levelText);
|
||||
final fallbackMatch = RegExp(r'\d+').firstMatch(levelText);
|
||||
final rawLevel =
|
||||
int.tryParse(levelMatch?.group(1) ?? fallbackMatch?.group(0) ?? "") ??
|
||||
1;
|
||||
return rawLevel.clamp(1, 5).toInt();
|
||||
return scCpRelationLevelIndex(relation.levelText);
|
||||
}
|
||||
|
||||
String _profileRelationLevelText(CPRes relation) {
|
||||
@ -1483,19 +1677,7 @@ class _PersonDetailPageState extends State<PersonDetailPage> {
|
||||
}
|
||||
|
||||
String _profileRelationLevelDisplayText(String rawText, CPRes relation) {
|
||||
final text = rawText.trim();
|
||||
final level = _profileRelationLevel(relation);
|
||||
if (text.isEmpty) {
|
||||
return "Lv.1 Simple Love";
|
||||
}
|
||||
final compactLevelPattern = RegExp(
|
||||
r'^(?:Lv\.?|Level)?\s*\d+$',
|
||||
caseSensitive: false,
|
||||
);
|
||||
if (compactLevelPattern.hasMatch(text) && level == 1) {
|
||||
return "Lv.1 Simple Love";
|
||||
}
|
||||
return text;
|
||||
return scCpRelationLevelDisplayText(rawText);
|
||||
}
|
||||
|
||||
String _firstNonBlankProfileValue(List<String?> values) {
|
||||
@ -2455,6 +2637,278 @@ class _PersonDetailPageState extends State<PersonDetailPage> {
|
||||
}
|
||||
}
|
||||
|
||||
class _ProfileRelationProgressData {
|
||||
const _ProfileRelationProgressData({
|
||||
required this.levelText,
|
||||
required this.currentValue,
|
||||
required this.targetValue,
|
||||
required this.expAwayText,
|
||||
required this.nextLevelText,
|
||||
this.dismissCost,
|
||||
});
|
||||
|
||||
factory _ProfileRelationProgressData.fallback(
|
||||
_ProfileRelationProgressPayload payload,
|
||||
) {
|
||||
final currentValue = payload.cpValue;
|
||||
return _ProfileRelationProgressData(
|
||||
levelText:
|
||||
payload.levelText.trim().isEmpty
|
||||
? "Lv.1 Simple Love"
|
||||
: payload.levelText,
|
||||
currentValue: currentValue,
|
||||
targetValue: currentValue > 0 ? currentValue : 1,
|
||||
expAwayText: SCStringUtils.formatCompactNumber(0),
|
||||
nextLevelText: _levelShortText(
|
||||
_nextLevelIndex(_levelIndex(payload.levelText)),
|
||||
),
|
||||
dismissCost: payload.dismissCost,
|
||||
);
|
||||
}
|
||||
|
||||
factory _ProfileRelationProgressData.fromRightsConfig(
|
||||
SCCpRightsConfigRes config,
|
||||
_ProfileRelationProgressPayload payload,
|
||||
) {
|
||||
final currentValue = (config.intimacyValue ?? payload.cpValue).toDouble();
|
||||
final levels =
|
||||
config.levels
|
||||
.map(_ProfileRelationProgressLevel.fromRightsLevel)
|
||||
.toList();
|
||||
return _ProfileRelationProgressData.fromLevels(
|
||||
currentValue: currentValue,
|
||||
levels: levels,
|
||||
payload: payload,
|
||||
dismissCost: config.dismissConsumeGold ?? payload.dismissCost,
|
||||
);
|
||||
}
|
||||
|
||||
factory _ProfileRelationProgressData.fromCabin(
|
||||
SCCpCabinRes cabin,
|
||||
_ProfileRelationProgressPayload payload, {
|
||||
int? fallbackDismissCost,
|
||||
}) {
|
||||
final currentValue =
|
||||
cabin.cpVal?.toDouble() ??
|
||||
cabin.cpPairUserProfileCO?.cpValue ??
|
||||
payload.cpValue;
|
||||
final levels =
|
||||
(cabin.cpCabinConfigCO ?? const <dynamic>[])
|
||||
.map(_ProfileRelationProgressLevel.fromDynamic)
|
||||
.toList();
|
||||
return _ProfileRelationProgressData.fromLevels(
|
||||
currentValue: currentValue,
|
||||
levels: levels,
|
||||
payload: payload,
|
||||
dismissCost:
|
||||
cabin.dismissCost ?? fallbackDismissCost ?? payload.dismissCost,
|
||||
);
|
||||
}
|
||||
|
||||
factory _ProfileRelationProgressData.fromLevels({
|
||||
required double currentValue,
|
||||
required List<_ProfileRelationProgressLevel> levels,
|
||||
required _ProfileRelationProgressPayload payload,
|
||||
int? dismissCost,
|
||||
}) {
|
||||
final sortedLevels =
|
||||
levels.where((level) => level.requiredValue != null).toList()..sort(
|
||||
(left, right) => left.requiredValue!.compareTo(right.requiredValue!),
|
||||
);
|
||||
_ProfileRelationProgressLevel? nextLevel;
|
||||
for (final level in sortedLevels) {
|
||||
if ((level.requiredValue ?? 0) > currentValue) {
|
||||
nextLevel = level;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
final targetValue =
|
||||
nextLevel?.requiredValue?.toDouble() ??
|
||||
(currentValue > 0 ? currentValue : 1);
|
||||
final expAway =
|
||||
nextLevel == null
|
||||
? 0
|
||||
: (targetValue - currentValue).ceil().clamp(0, 1 << 62).toInt();
|
||||
final currentLevel = _currentLevelFromConfig(sortedLevels, currentValue);
|
||||
|
||||
return _ProfileRelationProgressData(
|
||||
levelText: _levelDisplayText(currentLevel, payload.levelText),
|
||||
currentValue: currentValue,
|
||||
targetValue: targetValue > 0 ? targetValue : 1,
|
||||
expAwayText: SCStringUtils.formatCompactNumber(expAway),
|
||||
nextLevelText:
|
||||
nextLevel == null
|
||||
? _levelShortText(
|
||||
_nextLevelIndex(
|
||||
currentLevel?.level ?? _levelIndex(payload.levelText),
|
||||
),
|
||||
)
|
||||
: _levelShortText(nextLevel.level, fallback: nextLevel.levelName),
|
||||
dismissCost: dismissCost,
|
||||
);
|
||||
}
|
||||
|
||||
final String levelText;
|
||||
final double currentValue;
|
||||
final double targetValue;
|
||||
final String expAwayText;
|
||||
final String nextLevelText;
|
||||
final int? dismissCost;
|
||||
|
||||
static _ProfileRelationProgressLevel? _currentLevelFromConfig(
|
||||
List<_ProfileRelationProgressLevel> levels,
|
||||
double currentValue,
|
||||
) {
|
||||
_ProfileRelationProgressLevel? currentLevel;
|
||||
for (final level in levels) {
|
||||
if ((level.requiredValue ?? 0) <= currentValue) {
|
||||
currentLevel = level;
|
||||
}
|
||||
}
|
||||
return currentLevel;
|
||||
}
|
||||
|
||||
static String _levelDisplayText(
|
||||
_ProfileRelationProgressLevel? level,
|
||||
String fallback,
|
||||
) {
|
||||
final fallbackText = fallback.trim();
|
||||
if (level == null) {
|
||||
return fallbackText.isEmpty ? "Lv.1 Simple Love" : fallbackText;
|
||||
}
|
||||
final levelName = level.levelName?.trim();
|
||||
if (levelName != null && levelName.isNotEmpty) {
|
||||
return levelName;
|
||||
}
|
||||
if (level.level != null) {
|
||||
return "Lv.${level.level}";
|
||||
}
|
||||
return fallbackText.isEmpty ? "Lv.1 Simple Love" : fallbackText;
|
||||
}
|
||||
|
||||
static String _levelShortText(int? level, {String? fallback}) {
|
||||
if (level != null) {
|
||||
return "Lv. $level";
|
||||
}
|
||||
final fallbackText = fallback?.trim() ?? "";
|
||||
if (fallbackText.isNotEmpty) {
|
||||
return fallbackText;
|
||||
}
|
||||
return "Lv. 1";
|
||||
}
|
||||
|
||||
static int _levelIndex(String levelText) {
|
||||
return scCpRelationLevelIndex(levelText);
|
||||
}
|
||||
|
||||
static int _nextLevelIndex(int level) {
|
||||
return (level + 1).clamp(1, 5).toInt();
|
||||
}
|
||||
}
|
||||
|
||||
class _ProfileRelationProgressLevel {
|
||||
const _ProfileRelationProgressLevel({
|
||||
this.level,
|
||||
this.levelName,
|
||||
this.requiredValue,
|
||||
});
|
||||
|
||||
factory _ProfileRelationProgressLevel.fromRightsLevel(
|
||||
SCCpRightsLevelRes level,
|
||||
) {
|
||||
return _ProfileRelationProgressLevel(
|
||||
level: level.level,
|
||||
levelName: level.levelName,
|
||||
requiredValue: level.requiredIntimacyValue,
|
||||
);
|
||||
}
|
||||
|
||||
factory _ProfileRelationProgressLevel.fromDynamic(dynamic value) {
|
||||
final source = value is Map ? value : const <String, dynamic>{};
|
||||
return _ProfileRelationProgressLevel(
|
||||
level: _asInt(
|
||||
_firstValue(source, const ["level", "cpLevel", "cabinLevel", "lv"]),
|
||||
),
|
||||
levelName: _asString(
|
||||
_firstValue(source, const [
|
||||
"levelName",
|
||||
"name",
|
||||
"title",
|
||||
"cabinName",
|
||||
"cpName",
|
||||
]),
|
||||
),
|
||||
requiredValue: _asInt(
|
||||
_firstValue(source, const [
|
||||
"requiredIntimacyValue",
|
||||
"requiredCpValue",
|
||||
"requiredCpVal",
|
||||
"cpValue",
|
||||
"cpVal",
|
||||
"intimacyValue",
|
||||
]),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final int? level;
|
||||
final String? levelName;
|
||||
final int? requiredValue;
|
||||
}
|
||||
|
||||
dynamic _firstValue(Map source, List<String> keys) {
|
||||
for (final key in keys) {
|
||||
if (source.containsKey(key) && source[key] != null) {
|
||||
return source[key];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String? _asString(dynamic value) {
|
||||
final text = value?.toString().trim();
|
||||
return text == null || text.isEmpty ? null : text;
|
||||
}
|
||||
|
||||
int? _asInt(dynamic value) {
|
||||
if (value is int) {
|
||||
return value;
|
||||
}
|
||||
if (value is num) {
|
||||
return value.toInt();
|
||||
}
|
||||
return int.tryParse(value?.toString() ?? "");
|
||||
}
|
||||
|
||||
class _ProfileRelationProgressPayload {
|
||||
const _ProfileRelationProgressPayload({
|
||||
required this.leftAvatarUrl,
|
||||
required this.rightAvatarUrl,
|
||||
required this.leftName,
|
||||
required this.rightName,
|
||||
required this.leftUserId,
|
||||
required this.rightUserId,
|
||||
required this.days,
|
||||
required this.levelText,
|
||||
required this.relationType,
|
||||
required this.cpValue,
|
||||
this.dismissCost,
|
||||
});
|
||||
|
||||
final String leftAvatarUrl;
|
||||
final String rightAvatarUrl;
|
||||
final String leftName;
|
||||
final String rightName;
|
||||
final String leftUserId;
|
||||
final String rightUserId;
|
||||
final String days;
|
||||
final String levelText;
|
||||
final String relationType;
|
||||
final double cpValue;
|
||||
final int? dismissCost;
|
||||
}
|
||||
|
||||
class _ProfileRelationUser {
|
||||
const _ProfileRelationUser({
|
||||
this.userId,
|
||||
|
||||
@ -28,11 +28,13 @@ import 'package:tencent_cloud_chat_sdk/enum/V2TimSDKListener.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/enum/conversation_type.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/enum/group_type.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/enum/log_level_enum.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/enum/message_elem_type.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/enum/message_status.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/manager/v2_tim_group_manager.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_callback.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation_result.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_custom_elem.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_member_info.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_image_elem.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart';
|
||||
@ -295,6 +297,7 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
Timer? _roomRocketRoomReadyRewardRetryTimer;
|
||||
final Set<String> _shownCpInviteMessageKeys = <String>{};
|
||||
final Set<String> _forwardedCpInviteC2CMessageKeys = <String>{};
|
||||
final Set<String> _echoedCpInviteC2CMessageKeys = <String>{};
|
||||
int get currentLuckGiftBurstPlaybackToken => _luckGiftPushPlaybackToken;
|
||||
String? roomRedPacketBroadcastGroupId;
|
||||
String? roomRedPacketBroadcastRegionCode;
|
||||
@ -683,10 +686,13 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
String? msgId,
|
||||
String? fallbackUserId,
|
||||
}) async {
|
||||
final cpData = _decodeCpSystemMessageData(message);
|
||||
unawaited(_handleIncomingCpBuildMessage(message));
|
||||
final cpPeerUserId = _cpInviteConversationPeerIdFromData(cpData);
|
||||
final didSyncConversation = _upsertC2CConversationFromMessage(
|
||||
message,
|
||||
fallbackUserId: fallbackUserId,
|
||||
fallbackUserId: fallbackUserId ?? cpPeerUserId,
|
||||
preferFallbackUserId: cpPeerUserId != null,
|
||||
);
|
||||
if (!didSyncConversation) {
|
||||
systemUnReadCount = _systemUnreadCount();
|
||||
@ -700,11 +706,16 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
bool _upsertC2CConversationFromMessage(
|
||||
V2TimMessage? message, {
|
||||
String? fallbackUserId,
|
||||
bool preferFallbackUserId = false,
|
||||
}) {
|
||||
if (message == null || message.groupID != null) {
|
||||
return false;
|
||||
}
|
||||
final peerId = _messagePeerId(message, fallbackUserId);
|
||||
final peerId = _messagePeerId(
|
||||
message,
|
||||
fallbackUserId,
|
||||
preferFallbackUserId: preferFallbackUserId,
|
||||
);
|
||||
if (peerId.isEmpty) {
|
||||
final cpData = _decodeCpSystemMessageData(message);
|
||||
if (cpData != null) {
|
||||
@ -766,12 +777,21 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
return true;
|
||||
}
|
||||
|
||||
String _messagePeerId(V2TimMessage message, String? fallbackUserId) {
|
||||
String _messagePeerId(
|
||||
V2TimMessage message,
|
||||
String? fallbackUserId, {
|
||||
bool preferFallbackUserId = false,
|
||||
}) {
|
||||
final currentUserId =
|
||||
AccountStorage().getCurrentUser()?.userProfile?.id?.trim() ?? "";
|
||||
final userId = message.userID?.trim() ?? "";
|
||||
final sender = message.sender?.trim() ?? "";
|
||||
final fallback = fallbackUserId?.trim() ?? "";
|
||||
if (preferFallbackUserId &&
|
||||
fallback.isNotEmpty &&
|
||||
fallback != currentUserId) {
|
||||
return fallback;
|
||||
}
|
||||
if (message.isSelf == true) {
|
||||
if (userId.isNotEmpty) {
|
||||
return userId;
|
||||
@ -799,6 +819,21 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
return "";
|
||||
}
|
||||
|
||||
String? _cpInviteConversationPeerIdFromData(Map<String, dynamic>? data) {
|
||||
if (data == null) {
|
||||
return null;
|
||||
}
|
||||
final invite = _cpInviteFromMessageData(data);
|
||||
final currentProfile = AccountStorage().getCurrentUser()?.userProfile;
|
||||
if (invite == null || currentProfile == null) {
|
||||
return null;
|
||||
}
|
||||
if (_cpInviteSenderMatchesCurrent(invite, currentProfile)) {
|
||||
return _cpInviteReceiverIdFromMessageData(data);
|
||||
}
|
||||
return _cpInviteInviterIdFromMessageData(data, invite);
|
||||
}
|
||||
|
||||
String _c2cConversationDisplayName(
|
||||
V2TimConversation conversation,
|
||||
V2TimMessage message,
|
||||
@ -850,7 +885,7 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
if (noticeType != SCSysytemMessageType.CP_BUILD.name) {
|
||||
return;
|
||||
}
|
||||
final applyId = data["expand"]?.toString().trim() ?? "";
|
||||
final applyId = _cpInviteApplyIdFromData(data);
|
||||
final invite = _cpInviteFromMessageData(data);
|
||||
final messageKey = applyId.isNotEmpty ? applyId : message?.msgID ?? "";
|
||||
if (messageKey.isNotEmpty && !_shownCpInviteMessageKeys.add(messageKey)) {
|
||||
@ -888,6 +923,14 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
fallback: currentProfile.id,
|
||||
);
|
||||
final relationType = scNormalizeCpRelationType(invite.relationType);
|
||||
_persistBackendCpInviteToC2CConversationIfNeeded(
|
||||
sourceMessage: message,
|
||||
data: data,
|
||||
applyId: applyId,
|
||||
invite: invite,
|
||||
currentProfile: currentProfile,
|
||||
isSender: isSender,
|
||||
);
|
||||
_forwardCpInviteC2CMessageIfNeeded(
|
||||
sourceMessage: message,
|
||||
data: data,
|
||||
@ -895,6 +938,14 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
invite: invite,
|
||||
isSender: isSender,
|
||||
);
|
||||
_echoCpInviteC2CMessageToSenderIfNeeded(
|
||||
sourceMessage: message,
|
||||
data: data,
|
||||
applyId: applyId,
|
||||
invite: invite,
|
||||
currentProfile: currentProfile,
|
||||
isSender: isSender,
|
||||
);
|
||||
if (isSender) {
|
||||
final receiver = _cpInviteReceiverFromMessageData(data);
|
||||
_cpInviteLog(
|
||||
@ -984,6 +1035,21 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
return null;
|
||||
}
|
||||
|
||||
String _cpInviteApplyIdFromData(Map<String, dynamic> data) {
|
||||
final content = _cpInviteContentMap(data);
|
||||
return _firstCpInviteValue([
|
||||
data["expand"]?.toString(),
|
||||
data["applyId"]?.toString(),
|
||||
data["apply_id"]?.toString(),
|
||||
data["id"]?.toString(),
|
||||
content["expand"]?.toString(),
|
||||
content["applyId"]?.toString(),
|
||||
content["apply_id"]?.toString(),
|
||||
content["id"]?.toString(),
|
||||
]) ??
|
||||
"";
|
||||
}
|
||||
|
||||
bool _handleCpInviteResultMessage(Map<String, dynamic> data) {
|
||||
final result = _cpInviteResultFromMessageData(data);
|
||||
if (result == null) {
|
||||
@ -1363,6 +1429,10 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
if (!isSender || sourceMessage?.isSelf == true) {
|
||||
return;
|
||||
}
|
||||
final noticeSource = data["noticeSource"]?.toString() ?? "";
|
||||
if (noticeSource == "RECEIVER_ECHO" || noticeSource == "LOCAL_OUTGOING") {
|
||||
return;
|
||||
}
|
||||
final normalizedApplyId = applyId.trim();
|
||||
if (normalizedApplyId.isEmpty ||
|
||||
!_forwardedCpInviteC2CMessageKeys.add(normalizedApplyId)) {
|
||||
@ -1391,6 +1461,98 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
);
|
||||
}
|
||||
|
||||
void _echoCpInviteC2CMessageToSenderIfNeeded({
|
||||
required V2TimMessage? sourceMessage,
|
||||
required Map<String, dynamic> data,
|
||||
required String applyId,
|
||||
required SCSystemInvitMessageRes invite,
|
||||
required SocialChatUserProfile currentProfile,
|
||||
required bool isSender,
|
||||
}) {
|
||||
if (isSender || sourceMessage?.isSelf == true) {
|
||||
return;
|
||||
}
|
||||
final normalizedApplyId = applyId.trim();
|
||||
if (normalizedApplyId.isEmpty ||
|
||||
!_echoedCpInviteC2CMessageKeys.add(normalizedApplyId)) {
|
||||
return;
|
||||
}
|
||||
final inviterId = _cpInviteInviterIdFromMessageData(data, invite);
|
||||
if (inviterId.isEmpty) {
|
||||
_echoedCpInviteC2CMessageKeys.remove(normalizedApplyId);
|
||||
_cpInviteLog(
|
||||
'skip echoing CP_BUILD C2C reason=no_inviter '
|
||||
'applyId=$normalizedApplyId',
|
||||
);
|
||||
return;
|
||||
}
|
||||
final relationType = scNormalizeCpRelationType(invite.relationType);
|
||||
final echoData = <String, dynamic>{...data};
|
||||
echoData["noticeType"] = SCSysytemMessageType.CP_BUILD.name;
|
||||
echoData["noticeSource"] = "RECEIVER_ECHO";
|
||||
echoData["expand"] = normalizedApplyId;
|
||||
echoData["applyId"] = normalizedApplyId;
|
||||
echoData["relationType"] = relationType;
|
||||
echoData["acceptUserId"] ??= currentProfile.id;
|
||||
echoData["acceptAccount"] ??= currentProfile.account;
|
||||
echoData["acceptActualAccount"] ??= currentProfile.id;
|
||||
echoData["acceptNickname"] ??= _cpInviteDisplayName(
|
||||
nickname: currentProfile.userNickname,
|
||||
account: currentProfile.account,
|
||||
fallback: currentProfile.id,
|
||||
);
|
||||
echoData["acceptUserNickname"] ??= currentProfile.userNickname;
|
||||
echoData["acceptUserAvatar"] ??= currentProfile.userAvatar;
|
||||
echoData["acceptHeaddress"] ??= currentProfile.getHeaddress()?.sourceUrl;
|
||||
echoData["acceptHeaddressCover"] ??= currentProfile.getHeaddress()?.cover;
|
||||
_cpInviteLog(
|
||||
'echo backend CP_BUILD to sender C2C '
|
||||
'sender=$inviterId applyId=$normalizedApplyId relationType=$relationType',
|
||||
);
|
||||
unawaited(
|
||||
_sendCpBuildCustomMessage(
|
||||
receiverId: inviterId,
|
||||
data: echoData,
|
||||
debugName: 'invite-echo',
|
||||
applyId: normalizedApplyId,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _cpInviteInviterIdFromMessageData(
|
||||
Map<String, dynamic> data,
|
||||
SCSystemInvitMessageRes invite,
|
||||
) {
|
||||
final content = _cpInviteContentMap(data);
|
||||
return _firstCpInviteValue([
|
||||
data["actualAccount"]?.toString(),
|
||||
data["applyActualAccount"]?.toString(),
|
||||
data["applyUserActualAccount"]?.toString(),
|
||||
data["inviterActualAccount"]?.toString(),
|
||||
data["inviterUserId"]?.toString(),
|
||||
data["inviterId"]?.toString(),
|
||||
data["senderUserId"]?.toString(),
|
||||
data["senderId"]?.toString(),
|
||||
data["fromUserId"]?.toString(),
|
||||
data["userId"]?.toString(),
|
||||
content["actualAccount"]?.toString(),
|
||||
content["applyActualAccount"]?.toString(),
|
||||
content["applyUserActualAccount"]?.toString(),
|
||||
content["inviterActualAccount"]?.toString(),
|
||||
content["inviterUserId"]?.toString(),
|
||||
content["inviterId"]?.toString(),
|
||||
content["senderUserId"]?.toString(),
|
||||
content["senderId"]?.toString(),
|
||||
content["fromUserId"]?.toString(),
|
||||
content["userId"]?.toString(),
|
||||
invite.actualAccount,
|
||||
data["applyUserId"]?.toString(),
|
||||
content["applyUserId"]?.toString(),
|
||||
invite.account,
|
||||
]) ??
|
||||
"";
|
||||
}
|
||||
|
||||
String _cpInviteReceiverIdFromMessageData(Map<String, dynamic> data) {
|
||||
final content = _cpInviteContentMap(data);
|
||||
return _firstCpInviteValue([
|
||||
@ -1512,6 +1674,158 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> insertLocalOutgoingCpGiftInvite({
|
||||
required String receiverId,
|
||||
required String relationType,
|
||||
required SocialChatGiftRes gift,
|
||||
required int giftCount,
|
||||
SocialChatUserProfile? receiverProfile,
|
||||
}) async {
|
||||
final targetId = receiverId.trim();
|
||||
final currentProfile = AccountStorage().getCurrentUser()?.userProfile;
|
||||
final senderId = currentProfile?.id?.trim() ?? "";
|
||||
if (targetId.isEmpty || senderId.isEmpty) {
|
||||
_cpInviteLog(
|
||||
'skip local outgoing invite reason=missing_user '
|
||||
'sender=$senderId receiver=$targetId',
|
||||
);
|
||||
return;
|
||||
}
|
||||
final normalizedRelationType = scNormalizeCpRelationType(relationType);
|
||||
final applyId =
|
||||
'local_${normalizedRelationType}_${senderId}_${targetId}_'
|
||||
'${DateTime.now().microsecondsSinceEpoch}';
|
||||
final receiverName = _cpInviteDisplayName(
|
||||
nickname: receiverProfile?.userNickname,
|
||||
account: receiverProfile?.account,
|
||||
fallback: receiverProfile?.id ?? targetId,
|
||||
);
|
||||
final invite = SCSystemInvitMessageRes(
|
||||
account: currentProfile?.account,
|
||||
actualAccount: senderId,
|
||||
userNickname: currentProfile?.userNickname,
|
||||
userAvatar: currentProfile?.userAvatar,
|
||||
userHeaddress: currentProfile?.getHeaddress()?.sourceUrl,
|
||||
userHeaddressCover: currentProfile?.getHeaddress()?.cover,
|
||||
relationType: normalizedRelationType,
|
||||
dismissEndTime: DateTime.now().millisecondsSinceEpoch + 86400000,
|
||||
giftCover: gift.giftPhoto,
|
||||
giftCount: giftCount,
|
||||
);
|
||||
final inviteJson = invite.toJson();
|
||||
inviteJson["gift"] = gift.toJson();
|
||||
final data = <String, dynamic>{
|
||||
"noticeType": SCSysytemMessageType.CP_BUILD.name,
|
||||
"noticeAction": "INVITE_PENDING",
|
||||
"noticeSource": "LOCAL_OUTGOING",
|
||||
"action": "INVITE_PENDING",
|
||||
"status": "PENDING",
|
||||
"result": "PENDING",
|
||||
"state": "PENDING",
|
||||
"expand": applyId,
|
||||
"applyId": applyId,
|
||||
"relationType": normalizedRelationType,
|
||||
"account": currentProfile?.account,
|
||||
"actualAccount": senderId,
|
||||
"applyUserId": senderId,
|
||||
"applyActualAccount": senderId,
|
||||
"userNickname": currentProfile?.userNickname,
|
||||
"userAvatar": currentProfile?.userAvatar,
|
||||
"userHeaddress": currentProfile?.getHeaddress()?.sourceUrl,
|
||||
"userHeaddressCover": currentProfile?.getHeaddress()?.cover,
|
||||
"acceptUserId": receiverProfile?.id ?? targetId,
|
||||
"acceptAccount": receiverProfile?.account ?? targetId,
|
||||
"acceptActualAccount": receiverProfile?.id ?? targetId,
|
||||
"acceptNickname": receiverName,
|
||||
"acceptUserNickname": receiverProfile?.userNickname,
|
||||
"acceptUserAvatar": receiverProfile?.userAvatar,
|
||||
"acceptHeaddress": receiverProfile?.getHeaddress()?.sourceUrl,
|
||||
"acceptHeaddressCover": receiverProfile?.getHeaddress()?.cover,
|
||||
"gift": gift.toJson(),
|
||||
"giftCover": gift.giftPhoto,
|
||||
"giftCount": giftCount,
|
||||
"content": jsonEncode(inviteJson),
|
||||
};
|
||||
_cpInviteLog(
|
||||
'insert local outgoing CP_BUILD '
|
||||
'receiver=$targetId applyId=$applyId relationType=$normalizedRelationType',
|
||||
);
|
||||
if (_shownCpInviteMessageKeys.add(applyId)) {
|
||||
unawaited(
|
||||
_showOutgoingCpInviteWaitingDialog(
|
||||
applyId: applyId,
|
||||
invite: invite,
|
||||
currentProfile: currentProfile,
|
||||
receiverProfile: receiverProfile,
|
||||
receiverId: targetId,
|
||||
relationType: normalizedRelationType,
|
||||
),
|
||||
);
|
||||
}
|
||||
await _insertCpBuildC2CMessageToLocalStorage(
|
||||
peerId: targetId,
|
||||
senderId: senderId,
|
||||
data: data,
|
||||
debugName: 'local-outgoing-invite',
|
||||
applyId: applyId,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showOutgoingCpInviteWaitingDialog({
|
||||
required String applyId,
|
||||
required SCSystemInvitMessageRes invite,
|
||||
required SocialChatUserProfile? currentProfile,
|
||||
required SocialChatUserProfile? receiverProfile,
|
||||
required String receiverId,
|
||||
required String relationType,
|
||||
}) async {
|
||||
final context = navigatorKey.currentState?.context;
|
||||
if (context == null || currentProfile == null) {
|
||||
_cpInviteLog(
|
||||
'skip local waiting dialog reason=no_context applyId=$applyId',
|
||||
);
|
||||
return;
|
||||
}
|
||||
final currentName = _cpInviteDisplayName(
|
||||
nickname: currentProfile.userNickname,
|
||||
account: currentProfile.account,
|
||||
fallback: currentProfile.id,
|
||||
);
|
||||
final receiverName = _cpInviteDisplayName(
|
||||
nickname: receiverProfile?.userNickname,
|
||||
account: receiverProfile?.account,
|
||||
fallback: receiverProfile?.id ?? receiverId,
|
||||
);
|
||||
_cpInviteLog(
|
||||
'show local waiting CP_BUILD applyId=$applyId '
|
||||
'relationType=$relationType receiver=$receiverId',
|
||||
);
|
||||
await RoomCpInviteDialog.show(
|
||||
context,
|
||||
inviter: RoomCpInviteDialogUser(
|
||||
name: currentName,
|
||||
avatarUrl: currentProfile.userAvatar ?? "",
|
||||
headdress: currentProfile.getHeaddress()?.sourceUrl ?? "",
|
||||
headdressCover: currentProfile.getHeaddress()?.cover ?? "",
|
||||
),
|
||||
receiver: RoomCpInviteDialogUser(
|
||||
name: receiverName,
|
||||
avatarUrl: receiverProfile?.userAvatar ?? "",
|
||||
headdress: receiverProfile?.getHeaddress()?.sourceUrl ?? "",
|
||||
headdressCover: receiverProfile?.getHeaddress()?.cover ?? "",
|
||||
),
|
||||
style: RoomCpInviteDialogStyle.waiting,
|
||||
countdownSeconds: _cpInviteCountdownSeconds(invite),
|
||||
descriptionText: _cpInviteWaitingDescriptionText(relationType),
|
||||
onClose: () {
|
||||
_cpInviteLog('close local waiting CP_BUILD applyId=$applyId');
|
||||
},
|
||||
onTimeout: () {
|
||||
_cpInviteLog('timeout local waiting CP_BUILD applyId=$applyId');
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _sendCpBuildCustomMessage({
|
||||
required String receiverId,
|
||||
required Map<String, dynamic> data,
|
||||
@ -1572,6 +1886,228 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
void _persistBackendCpInviteToC2CConversationIfNeeded({
|
||||
required V2TimMessage? sourceMessage,
|
||||
required Map<String, dynamic> data,
|
||||
required String applyId,
|
||||
required SCSystemInvitMessageRes invite,
|
||||
required SocialChatUserProfile currentProfile,
|
||||
required bool isSender,
|
||||
}) {
|
||||
final noticeSource = data["noticeSource"]?.toString() ?? "";
|
||||
if (noticeSource == "LOCAL_OUTGOING") {
|
||||
return;
|
||||
}
|
||||
final peerId =
|
||||
isSender
|
||||
? _cpInviteReceiverIdFromMessageData(data)
|
||||
: _cpInviteInviterIdFromMessageData(data, invite);
|
||||
if (peerId.isEmpty ||
|
||||
_isCpBuildMessageAlreadyInPeerConversation(sourceMessage, peerId)) {
|
||||
return;
|
||||
}
|
||||
final senderId = isSender ? (currentProfile.id?.trim() ?? "") : peerId;
|
||||
if (senderId.isEmpty) {
|
||||
return;
|
||||
}
|
||||
_cpInviteLog(
|
||||
'persist backend CP_BUILD to local C2C '
|
||||
'peer=$peerId sender=$senderId applyId=$applyId relationType=${invite.relationType}',
|
||||
);
|
||||
unawaited(
|
||||
_insertCpBuildC2CMessageToLocalStorage(
|
||||
peerId: peerId,
|
||||
senderId: senderId,
|
||||
data: data,
|
||||
debugName: 'backend-invite',
|
||||
applyId: applyId,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
bool _isCpBuildMessageAlreadyInPeerConversation(
|
||||
V2TimMessage? message,
|
||||
String peerId,
|
||||
) {
|
||||
if (message == null || message.groupID != null) {
|
||||
return false;
|
||||
}
|
||||
final normalizedPeerId = peerId.trim();
|
||||
if (normalizedPeerId.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
return (message.userID?.trim() == normalizedPeerId ||
|
||||
message.sender?.trim() == normalizedPeerId) &&
|
||||
!SCGlobalConfig.isSystemUserId(message.userID) &&
|
||||
!SCGlobalConfig.isSystemUserId(message.sender);
|
||||
}
|
||||
|
||||
Future<V2TimMessage?> _insertCpBuildC2CMessageToLocalStorage({
|
||||
required String peerId,
|
||||
required String senderId,
|
||||
required Map<String, dynamic> data,
|
||||
required String debugName,
|
||||
required String applyId,
|
||||
}) async {
|
||||
final normalizedPeerId = peerId.trim();
|
||||
final normalizedSenderId = senderId.trim();
|
||||
if (normalizedPeerId.isEmpty || normalizedSenderId.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
final messageManager =
|
||||
TencentImSDKPlugin.v2TIMManager.getMessageManager();
|
||||
final createCustomMessageRes = await messageManager.createCustomMessage(
|
||||
data: jsonEncode(data),
|
||||
extension: SCSysytemMessageType.CP_BUILD.name,
|
||||
desc: SCSysytemMessageType.CP_BUILD.name,
|
||||
);
|
||||
if (createCustomMessageRes.code != 0) {
|
||||
_cpInviteLog(
|
||||
'create $debugName local C2C failed '
|
||||
'code=${createCustomMessageRes.code} '
|
||||
'desc=${createCustomMessageRes.desc}',
|
||||
);
|
||||
return _upsertSyntheticCpBuildC2CMessage(
|
||||
peerId: normalizedPeerId,
|
||||
senderId: normalizedSenderId,
|
||||
data: data,
|
||||
debugName: debugName,
|
||||
applyId: applyId,
|
||||
);
|
||||
}
|
||||
final createdMsgId = createCustomMessageRes.data?.id ?? "";
|
||||
if (createdMsgId.isEmpty) {
|
||||
_cpInviteLog('create $debugName local C2C failed reason=no_id');
|
||||
return _upsertSyntheticCpBuildC2CMessage(
|
||||
peerId: normalizedPeerId,
|
||||
senderId: normalizedSenderId,
|
||||
data: data,
|
||||
debugName: debugName,
|
||||
applyId: applyId,
|
||||
);
|
||||
}
|
||||
var insertMessageRes = await messageManager
|
||||
.insertC2CMessageToLocalStorageV2(
|
||||
userID: normalizedPeerId,
|
||||
senderID: normalizedSenderId,
|
||||
createdMsgID: createdMsgId,
|
||||
);
|
||||
if (insertMessageRes.code != 0) {
|
||||
_cpInviteLog(
|
||||
'insert $debugName local C2C v2 failed '
|
||||
'peer=$normalizedPeerId sender=$normalizedSenderId '
|
||||
'applyId=$applyId code=${insertMessageRes.code} '
|
||||
'desc=${insertMessageRes.desc}',
|
||||
);
|
||||
// ignore: deprecated_member_use
|
||||
insertMessageRes = await messageManager.insertC2CMessageToLocalStorage(
|
||||
data: jsonEncode(data),
|
||||
userID: normalizedPeerId,
|
||||
sender: normalizedSenderId,
|
||||
isExcludedFromLastMessage: false,
|
||||
);
|
||||
}
|
||||
if (insertMessageRes.code != 0) {
|
||||
_cpInviteLog(
|
||||
'insert $debugName local C2C failed '
|
||||
'peer=$normalizedPeerId sender=$normalizedSenderId '
|
||||
'applyId=$applyId code=${insertMessageRes.code} '
|
||||
'desc=${insertMessageRes.desc}',
|
||||
);
|
||||
return _upsertSyntheticCpBuildC2CMessage(
|
||||
peerId: normalizedPeerId,
|
||||
senderId: normalizedSenderId,
|
||||
data: data,
|
||||
debugName: debugName,
|
||||
applyId: applyId,
|
||||
);
|
||||
}
|
||||
final message =
|
||||
insertMessageRes.data ?? createCustomMessageRes.data?.messageInfo;
|
||||
if (message == null) {
|
||||
_cpInviteLog('insert $debugName local C2C failed reason=no_message');
|
||||
return _upsertSyntheticCpBuildC2CMessage(
|
||||
peerId: normalizedPeerId,
|
||||
senderId: normalizedSenderId,
|
||||
data: data,
|
||||
debugName: debugName,
|
||||
applyId: applyId,
|
||||
);
|
||||
}
|
||||
message.userID = normalizedPeerId;
|
||||
message.sender ??= normalizedSenderId;
|
||||
message.groupID = null;
|
||||
message.timestamp ??= DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
||||
message.customElem?.data ??= jsonEncode(data);
|
||||
_upsertC2CConversationFromMessage(
|
||||
message,
|
||||
fallbackUserId: normalizedPeerId,
|
||||
preferFallbackUserId: true,
|
||||
);
|
||||
onNewMessageCurrentConversationListener?.call(message);
|
||||
_cpInviteLog(
|
||||
'insert $debugName local C2C success '
|
||||
'peer=$normalizedPeerId sender=$normalizedSenderId '
|
||||
'applyId=$applyId msgId=${message.msgID} self=${message.isSelf}',
|
||||
);
|
||||
return message;
|
||||
} catch (error) {
|
||||
_cpInviteLog(
|
||||
'insert $debugName local C2C error '
|
||||
'peer=$normalizedPeerId sender=$normalizedSenderId '
|
||||
'applyId=$applyId error=$error',
|
||||
);
|
||||
return _upsertSyntheticCpBuildC2CMessage(
|
||||
peerId: normalizedPeerId,
|
||||
senderId: normalizedSenderId,
|
||||
data: data,
|
||||
debugName: debugName,
|
||||
applyId: applyId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
V2TimMessage _upsertSyntheticCpBuildC2CMessage({
|
||||
required String peerId,
|
||||
required String senderId,
|
||||
required Map<String, dynamic> data,
|
||||
required String debugName,
|
||||
required String applyId,
|
||||
}) {
|
||||
final currentUserId =
|
||||
AccountStorage().getCurrentUser()?.userProfile?.id?.trim() ?? "";
|
||||
final encodedData = jsonEncode(data);
|
||||
final message = V2TimMessage(
|
||||
msgID: 'synthetic_${debugName}_$applyId',
|
||||
timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||
sender: senderId,
|
||||
userID: peerId,
|
||||
status: MessageStatus.V2TIM_MSG_STATUS_SEND_SUCC,
|
||||
elemType: MessageElemType.V2TIM_ELEM_TYPE_CUSTOM,
|
||||
customElem: V2TimCustomElem(
|
||||
data: encodedData,
|
||||
desc: SCSysytemMessageType.CP_BUILD.name,
|
||||
extension: SCSysytemMessageType.CP_BUILD.name,
|
||||
),
|
||||
isSelf: currentUserId.isNotEmpty && senderId == currentUserId,
|
||||
isExcludedFromUnreadCount: senderId == currentUserId,
|
||||
isExcludedFromLastMessage: false,
|
||||
isSupportMessageExtension: true,
|
||||
);
|
||||
_upsertC2CConversationFromMessage(
|
||||
message,
|
||||
fallbackUserId: peerId,
|
||||
preferFallbackUserId: true,
|
||||
);
|
||||
onNewMessageCurrentConversationListener?.call(message);
|
||||
_cpInviteLog(
|
||||
'upsert $debugName synthetic C2C '
|
||||
'peer=$peerId sender=$senderId applyId=$applyId',
|
||||
);
|
||||
return message;
|
||||
}
|
||||
|
||||
void _sendIncomingCpInviteRejectedNotice(
|
||||
SCSystemInvitMessageRes invite,
|
||||
RtcProvider rtcProvider,
|
||||
|
||||
@ -12,6 +12,7 @@ import 'package:yumi/shared/business_logic/models/res/login_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/room_user_card_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_user_identity_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_vip_res.dart';
|
||||
import 'package:yumi/shared/tools/sc_cp_relation_level_utils.dart';
|
||||
|
||||
class SocialChatUserProfileManager extends ChangeNotifier {
|
||||
static const List<String> _cpProfileRelationTypes = [
|
||||
@ -188,10 +189,20 @@ class SocialChatUserProfileManager extends ChangeNotifier {
|
||||
final normalizedType = _normalizeCpRelationType(
|
||||
cpPair.relationType ?? relationType,
|
||||
);
|
||||
final cabinLevelText = scCpRelationLevelTextFromCabin(
|
||||
cabin,
|
||||
fallbackText: cpPair.levelText,
|
||||
);
|
||||
final rightsLevelText = await _safeLoadCurrentUserCpLevelText(
|
||||
userId: userId,
|
||||
cpPair: cpPair,
|
||||
relationType: normalizedType,
|
||||
fallbackText: cabinLevelText,
|
||||
);
|
||||
final enrichedPair = cpPair.copyWith(
|
||||
relationType: normalizedType,
|
||||
cpValue: cpPair.cpValue ?? cabin?.cpVal?.toDouble(),
|
||||
levelText: _cpLevelTextFromCabin(cabin),
|
||||
levelText: rightsLevelText ?? cabinLevelText,
|
||||
dismissCost: cpPair.dismissCost ?? _cpDismissCostFromCabin(cabin),
|
||||
);
|
||||
if (normalizedType == 'CP') {
|
||||
@ -241,6 +252,50 @@ class SocialChatUserProfileManager extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> _safeLoadCurrentUserCpLevelText({
|
||||
required String userId,
|
||||
required CPRes cpPair,
|
||||
required String relationType,
|
||||
String? fallbackText,
|
||||
}) async {
|
||||
if (!_isCurrentUserId(userId)) {
|
||||
return null;
|
||||
}
|
||||
final partnerUserId = _cpPartnerUserId(userId, cpPair);
|
||||
if (partnerUserId.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
final config = await SCAccountRepository().cpRightsConfig(
|
||||
relationType: relationType,
|
||||
cpUserId: partnerUserId,
|
||||
);
|
||||
return scCpRelationLevelTextFromRightsConfig(
|
||||
config,
|
||||
fallbackText: fallbackText,
|
||||
);
|
||||
} catch (error) {
|
||||
if (kDebugMode) {
|
||||
debugPrint(
|
||||
'[CP][Profile] rightsConfig failed userId=$userId '
|
||||
'partnerUserId=$partnerUserId relationType=$relationType '
|
||||
'error=$error',
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
String _cpPartnerUserId(String userId, CPRes cpPair) {
|
||||
final currentId = userId.trim();
|
||||
final meUserId = cpPair.meUserId?.trim() ?? "";
|
||||
final cpUserId = cpPair.cpUserId?.trim() ?? "";
|
||||
if (currentId.isNotEmpty && currentId == cpUserId) {
|
||||
return meUserId;
|
||||
}
|
||||
return cpUserId;
|
||||
}
|
||||
|
||||
bool _hasCpPair(CPRes? pair) {
|
||||
return (pair?.cpUserId?.trim().isNotEmpty ?? false) ||
|
||||
(pair?.cpUserAvatar?.trim().isNotEmpty ?? false) ||
|
||||
@ -266,33 +321,6 @@ class SocialChatUserProfileManager extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
String? _cpLevelTextFromCabin(SCCpCabinRes? cabin) {
|
||||
final config = cabin?.currentCpCabinConfigCO;
|
||||
if (config is! Map) {
|
||||
return null;
|
||||
}
|
||||
final level = _firstCpConfigText([
|
||||
config["level"],
|
||||
config["cpLevel"],
|
||||
config["cabinLevel"],
|
||||
config["lv"],
|
||||
]);
|
||||
final name = _firstCpConfigText([
|
||||
config["levelName"],
|
||||
config["name"],
|
||||
config["title"],
|
||||
config["cabinName"],
|
||||
config["cpName"],
|
||||
]);
|
||||
if (level.isNotEmpty && name.isNotEmpty) {
|
||||
return "Lv.$level $name";
|
||||
}
|
||||
if (level.isNotEmpty) {
|
||||
return "Lv.$level";
|
||||
}
|
||||
return name.isNotEmpty ? name : null;
|
||||
}
|
||||
|
||||
int? _cpDismissCostFromCabin(SCCpCabinRes? cabin) {
|
||||
final directCost = cabin?.dismissCost;
|
||||
if (directCost != null) {
|
||||
@ -340,16 +368,6 @@ class SocialChatUserProfileManager extends ChangeNotifier {
|
||||
return int.tryParse(value?.toString() ?? "");
|
||||
}
|
||||
|
||||
String _firstCpConfigText(Iterable<dynamic> values) {
|
||||
for (final value in values) {
|
||||
final text = value?.toString().trim() ?? "";
|
||||
if (text.isNotEmpty && text.toLowerCase() != "null") {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
Future<SocialChatUserProfile> _withCurrentVipLevel(
|
||||
SocialChatUserProfile profile,
|
||||
String userId,
|
||||
|
||||
@ -11,6 +11,7 @@ import 'package:yumi/shared/business_logic/models/res/sc_banner_leaderboard_res.
|
||||
import 'package:yumi/shared/business_logic/models/res/country_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/gift_backpack_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/gift_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_first_recharge_reward_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_index_banner_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_room_emoji_res.dart';
|
||||
|
||||
@ -36,6 +37,9 @@ class SCAppGeneralManager extends ChangeNotifier {
|
||||
bool _hasGiftListLoaded = false;
|
||||
bool _isFetchingGiftBackpack = false;
|
||||
bool _hasGiftBackpackLoaded = false;
|
||||
bool _isFetchingFirstRechargeRewardHome = false;
|
||||
bool _hasFirstRechargeRewardHomeLoaded = false;
|
||||
SCFirstRechargeRewardHomeRes? _firstRechargeRewardHome;
|
||||
|
||||
Map<String, List<SocialChatGiftRes>> giftByTab = {};
|
||||
Map<String, List<SocialChatGiftRes>> activityGiftByTab = {};
|
||||
@ -59,10 +63,40 @@ class SCAppGeneralManager extends ChangeNotifier {
|
||||
bool get isGiftBackpackLoading =>
|
||||
_isFetchingGiftBackpack && !_hasGiftBackpackLoaded;
|
||||
|
||||
bool get shouldShowFirstRechargeReward =>
|
||||
_firstRechargeRewardHome?.shouldShowRewardDialog ?? false;
|
||||
|
||||
SCFirstRechargeRewardHomeRes? get firstRechargeRewardHome =>
|
||||
_firstRechargeRewardHome;
|
||||
|
||||
bool isGiftTabLoading(String type) {
|
||||
return type == backpackGiftTab ? isGiftBackpackLoading : isGiftListLoading;
|
||||
}
|
||||
|
||||
Future<SCFirstRechargeRewardHomeRes?> refreshFirstRechargeRewardHome({
|
||||
bool forceRefresh = false,
|
||||
}) async {
|
||||
if (_isFetchingFirstRechargeRewardHome) {
|
||||
return _firstRechargeRewardHome;
|
||||
}
|
||||
if (_hasFirstRechargeRewardHomeLoaded && !forceRefresh) {
|
||||
return _firstRechargeRewardHome;
|
||||
}
|
||||
|
||||
_isFetchingFirstRechargeRewardHome = true;
|
||||
try {
|
||||
final home = await SCConfigRepositoryImp().firstRechargeRewardHome();
|
||||
_firstRechargeRewardHome = home;
|
||||
_hasFirstRechargeRewardHomeLoaded = true;
|
||||
return home;
|
||||
} catch (_) {
|
||||
return _firstRechargeRewardHome;
|
||||
} finally {
|
||||
_isFetchingFirstRechargeRewardHome = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
///加载国家列表
|
||||
Future<void> fetchCountryList() async {
|
||||
if (_isFetchingCountryList) {
|
||||
|
||||
@ -4,6 +4,7 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:in_app_purchase/in_app_purchase.dart';
|
||||
import 'package:yumi/services/auth/user_profile_manager.dart';
|
||||
import 'package:yumi/services/general/sc_app_general_manager.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:yumi/shared/tools/sc_loading_manager.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
@ -23,7 +24,7 @@ class IOSPaymentProcessor extends ChangeNotifier {
|
||||
// 只复制必要的状态变量
|
||||
bool _isAvailable = false;
|
||||
String _errorMessage = '';
|
||||
List<SelecteProductConfig> _products = [];
|
||||
final List<SelecteProductConfig> _products = [];
|
||||
|
||||
late BuildContext context;
|
||||
Map<String, SCProductConfigRes> productMap = {};
|
||||
@ -125,11 +126,11 @@ class IOSPaymentProcessor extends ChangeNotifier {
|
||||
// 发起iOS购买
|
||||
Future<void> processPurchase() async {
|
||||
ProductDetails? product;
|
||||
_products.forEach((d) {
|
||||
for (final d in _products) {
|
||||
if (d.isSelecte) {
|
||||
product = d.produc;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (product == null) {
|
||||
SCTts.show(SCAppLocalizations.of(context)!.pleaseSelectaItem);
|
||||
@ -226,6 +227,14 @@ class IOSPaymentProcessor extends ChangeNotifier {
|
||||
|
||||
// 验证iOS支付
|
||||
Future<void> _verifyIOSPayment(PurchaseDetails purchase) async {
|
||||
final profileManager = Provider.of<SocialChatUserProfileManager>(
|
||||
context,
|
||||
listen: false,
|
||||
);
|
||||
final firstRechargeManager = Provider.of<SCAppGeneralManager>(
|
||||
context,
|
||||
listen: false,
|
||||
);
|
||||
SCLoadingManager.show();
|
||||
try {
|
||||
String receiptData = purchase.verificationData.serverVerificationData;
|
||||
@ -242,14 +251,9 @@ class IOSPaymentProcessor extends ChangeNotifier {
|
||||
// 🔥 iOS关键:必须完成交易
|
||||
await iap.completePurchase(purchase);
|
||||
// 更新用户信息
|
||||
Provider.of<SocialChatUserProfileManager>(
|
||||
context,
|
||||
listen: false,
|
||||
).fetchUserProfileData();
|
||||
Provider.of<SocialChatUserProfileManager>(
|
||||
context,
|
||||
listen: false,
|
||||
).balance();
|
||||
profileManager.fetchUserProfileData();
|
||||
profileManager.balance();
|
||||
_refreshFirstRechargeRewardState(firstRechargeManager);
|
||||
SCTts.show('Purchase is successful!');
|
||||
} catch (e) {
|
||||
if (e.toString().endsWith("${SCErroCode.orderExistsCreated.code}")) {
|
||||
@ -270,9 +274,14 @@ class IOSPaymentProcessor extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
// 交付商品(与安卓相同)
|
||||
Future<void> _deliverProduct(String productId) async {
|
||||
// 使用相同的业务逻辑
|
||||
void _refreshFirstRechargeRewardState(SCAppGeneralManager manager) {
|
||||
try {
|
||||
unawaited(manager.refreshFirstRechargeRewardHome(forceRefresh: true));
|
||||
} catch (error) {
|
||||
if (kDebugMode) {
|
||||
debugPrint('[FirstRecharge][ios_payment] refresh state failed: $error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// iOS错误处理
|
||||
|
||||
@ -6,6 +6,7 @@ import 'package:in_app_purchase/in_app_purchase.dart';
|
||||
import 'package:in_app_purchase_android/in_app_purchase_android.dart';
|
||||
import 'package:yumi/app_localizations.dart';
|
||||
import 'package:yumi/services/auth/user_profile_manager.dart';
|
||||
import 'package:yumi/services/general/sc_app_general_manager.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:yumi/shared/tools/sc_string_utils.dart';
|
||||
import 'package:yumi/shared/tools/sc_loading_manager.dart';
|
||||
@ -298,12 +299,17 @@ class AndroidPaymentProcessor extends ChangeNotifier {
|
||||
context,
|
||||
listen: false,
|
||||
);
|
||||
final firstRechargeManager = Provider.of<SCAppGeneralManager>(
|
||||
context,
|
||||
listen: false,
|
||||
);
|
||||
try {
|
||||
// 如果是消耗型商品,尝试消耗
|
||||
if (_getProductType(purchase.productID) == 'consumable') {
|
||||
await consumePurchase(purchase);
|
||||
// 消耗后重新获取余额
|
||||
profileManager.balance();
|
||||
_refreshFirstRechargeRewardState(firstRechargeManager);
|
||||
SCTts.show('outstanding purchases have been reinstated');
|
||||
}
|
||||
} catch (e) {
|
||||
@ -317,6 +323,10 @@ class AndroidPaymentProcessor extends ChangeNotifier {
|
||||
context,
|
||||
listen: false,
|
||||
);
|
||||
final firstRechargeManager = Provider.of<SCAppGeneralManager>(
|
||||
context,
|
||||
listen: false,
|
||||
);
|
||||
try {
|
||||
// 1. 基本验证
|
||||
if (purchase.verificationData.serverVerificationData.isEmpty) {
|
||||
@ -359,6 +369,7 @@ class AndroidPaymentProcessor extends ChangeNotifier {
|
||||
// 8. 更新用户余额
|
||||
profileManager.fetchUserProfileData();
|
||||
profileManager.balance();
|
||||
_refreshFirstRechargeRewardState(firstRechargeManager);
|
||||
|
||||
SCTts.show('purchase successful');
|
||||
} catch (e) {
|
||||
@ -376,6 +387,16 @@ class AndroidPaymentProcessor extends ChangeNotifier {
|
||||
// 例如:增加用户余额、解锁功能等
|
||||
}
|
||||
|
||||
void _refreshFirstRechargeRewardState(SCAppGeneralManager manager) {
|
||||
try {
|
||||
unawaited(manager.refreshFirstRechargeRewardHome(forceRefresh: true));
|
||||
} catch (error) {
|
||||
if (kDebugMode) {
|
||||
debugPrint('[FirstRecharge][payment] refresh state failed: $error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 新增:确保购买被消耗
|
||||
Future<void> _ensurePurchaseConsumed(PurchaseDetails purchase) async {
|
||||
try {
|
||||
|
||||
@ -1162,6 +1162,10 @@ class CPRes {
|
||||
_levelText = _asString(
|
||||
source['levelText'] ??
|
||||
source['levelName'] ??
|
||||
source['level'] ??
|
||||
source['cpLevel'] ??
|
||||
source['cabinLevel'] ??
|
||||
source['lv'] ??
|
||||
source['name'] ??
|
||||
source['title'],
|
||||
);
|
||||
|
||||
@ -4,6 +4,7 @@ class SCCpRightsConfigRes {
|
||||
this.cpUserId,
|
||||
this.relationshipStatus,
|
||||
this.intimacyValue,
|
||||
this.dismissConsumeGold,
|
||||
List<SCCpRightsLevelRes>? levels,
|
||||
}) : levels = levels ?? const [];
|
||||
|
||||
@ -18,6 +19,11 @@ class SCCpRightsConfigRes {
|
||||
cpUserId: _asString(source['cpUserId']),
|
||||
relationshipStatus: _asString(source['relationshipStatus']),
|
||||
intimacyValue: _asInt(source['intimacyValue']),
|
||||
dismissConsumeGold: _asInt(
|
||||
source['dismissConsumeGold'] ??
|
||||
source['dismissCost'] ??
|
||||
source['dismissGold'],
|
||||
),
|
||||
levels:
|
||||
rawLevels is List
|
||||
? rawLevels
|
||||
@ -31,6 +37,7 @@ class SCCpRightsConfigRes {
|
||||
final String? cpUserId;
|
||||
final String? relationshipStatus;
|
||||
final int? intimacyValue;
|
||||
final int? dismissConsumeGold;
|
||||
final List<SCCpRightsLevelRes> levels;
|
||||
|
||||
static String? _asString(dynamic value) {
|
||||
|
||||
@ -41,7 +41,7 @@ class SCSystemInvitMessageRes {
|
||||
}
|
||||
|
||||
SCSystemInvitMessageRes.fromJson(dynamic json) {
|
||||
_account = json['account'];
|
||||
_account = _firstNonBlank(json, const ['account', 'applyUserAccount']);
|
||||
_countryCode = json['countryCode'];
|
||||
_countryName = json['countryName'];
|
||||
_userAvatar = json['userAvatar'];
|
||||
@ -61,9 +61,20 @@ class SCSystemInvitMessageRes {
|
||||
'avatarFrameCover',
|
||||
]);
|
||||
_userNickname = json['userNickname'];
|
||||
_actualAccount = json['actualAccount'];
|
||||
_actualAccount = _firstNonBlank(json, const [
|
||||
'actualAccount',
|
||||
'applyActualAccount',
|
||||
'applyUserActualAccount',
|
||||
'inviterActualAccount',
|
||||
'inviterUserId',
|
||||
'inviterId',
|
||||
'senderUserId',
|
||||
'senderId',
|
||||
'fromUserId',
|
||||
'userId',
|
||||
]);
|
||||
_applyType = json['applyType'];
|
||||
_content = json['content'];
|
||||
_content = _contentString(json['content']);
|
||||
_relationType = _firstNonBlank(json, _relationTypeKeys);
|
||||
_dismissEndTime = _asInt(json['dismissEndTime']);
|
||||
_giftCover = json['giftCover'];
|
||||
@ -80,6 +91,20 @@ class SCSystemInvitMessageRes {
|
||||
return int.tryParse(value?.toString() ?? '');
|
||||
}
|
||||
|
||||
static String? _contentString(dynamic value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (value is String) {
|
||||
return value;
|
||||
}
|
||||
try {
|
||||
return jsonEncode(value);
|
||||
} catch (_) {
|
||||
return value.toString();
|
||||
}
|
||||
}
|
||||
|
||||
String? _account;
|
||||
String? _countryCode;
|
||||
String? _countryName;
|
||||
@ -140,7 +165,10 @@ class SCSystemInvitMessageRes {
|
||||
!_hasInviteIdentity(root)) {
|
||||
return null;
|
||||
}
|
||||
final source = <String, dynamic>{...(content.isNotEmpty ? content : root)};
|
||||
final source = <String, dynamic>{
|
||||
...root,
|
||||
if (content.isNotEmpty) ...content,
|
||||
};
|
||||
final relationType =
|
||||
_firstNonBlank(source, _relationTypeKeys) ??
|
||||
_firstNonBlank(root, _relationTypeKeys) ??
|
||||
|
||||
@ -94,6 +94,7 @@ abstract class SocialChatRoomRepository {
|
||||
String? roomId,
|
||||
SCGiveAwayGiftRoomAcceptsCmd? accepts,
|
||||
String? dynamicContentId,
|
||||
String? relationType,
|
||||
});
|
||||
|
||||
///举报
|
||||
@ -182,6 +183,7 @@ abstract class SocialChatRoomRepository {
|
||||
Object? gameId,
|
||||
String? dynamicContentId,
|
||||
String? songId,
|
||||
String? relationType,
|
||||
});
|
||||
|
||||
///查询榜单前三名
|
||||
@ -199,6 +201,7 @@ abstract class SocialChatRoomRepository {
|
||||
String? roomId,
|
||||
num? seatIndex,
|
||||
String? songId,
|
||||
String? relationType,
|
||||
});
|
||||
|
||||
///查询房间火箭状态
|
||||
|
||||
@ -610,6 +610,7 @@ class SCChatRoomRepository implements SocialChatRoomRepository {
|
||||
String? roomId,
|
||||
SCGiveAwayGiftRoomAcceptsCmd? accepts,
|
||||
String? dynamicContentId,
|
||||
String? relationType,
|
||||
}) async {
|
||||
Map<String, dynamic> params = {};
|
||||
if (roomId != null) {
|
||||
@ -625,6 +626,9 @@ class SCChatRoomRepository implements SocialChatRoomRepository {
|
||||
if (dynamicContentId != null) {
|
||||
params["dynamicContentId"] = dynamicContentId;
|
||||
}
|
||||
if (relationType != null && relationType.trim().isNotEmpty) {
|
||||
params["relationType"] = relationType.trim();
|
||||
}
|
||||
|
||||
_giftRepoLog(
|
||||
'request giveGift endpoint=/gift/batch '
|
||||
@ -632,6 +636,7 @@ class SCChatRoomRepository implements SocialChatRoomRepository {
|
||||
'roomId=$roomId '
|
||||
'quantity=$quantity '
|
||||
'checkCombo=$checkCombo '
|
||||
'relationType=$relationType '
|
||||
'dynamicContentId=$dynamicContentId '
|
||||
'acceptUserIds=${acceptUserIds.join(",")} '
|
||||
'payload=${_stringifyGiftDebug(params)}',
|
||||
@ -953,6 +958,7 @@ class SCChatRoomRepository implements SocialChatRoomRepository {
|
||||
Object? gameId,
|
||||
String? dynamicContentId,
|
||||
String? songId,
|
||||
String? relationType,
|
||||
}) async {
|
||||
Map<String, dynamic> params = {};
|
||||
if (roomId != null) {
|
||||
@ -969,6 +975,9 @@ class SCChatRoomRepository implements SocialChatRoomRepository {
|
||||
.toList();
|
||||
params["dynamicContentId"] = dynamicContentId;
|
||||
params["songId"] = songId;
|
||||
if (relationType != null && relationType.trim().isNotEmpty) {
|
||||
params["relationType"] = relationType.trim();
|
||||
}
|
||||
|
||||
_giftRepoLog(
|
||||
'request giveLuckyGift endpoint=/gift/give/lucky-gift '
|
||||
@ -976,6 +985,7 @@ class SCChatRoomRepository implements SocialChatRoomRepository {
|
||||
'roomId=$roomId '
|
||||
'quantity=$quantity '
|
||||
'checkCombo=$checkCombo '
|
||||
'relationType=$relationType '
|
||||
'gameId=$gameId '
|
||||
'dynamicContentId=$dynamicContentId '
|
||||
'songId=$songId '
|
||||
@ -1042,6 +1052,7 @@ class SCChatRoomRepository implements SocialChatRoomRepository {
|
||||
String? roomId,
|
||||
num? seatIndex,
|
||||
String? songId,
|
||||
String? relationType,
|
||||
}) async {
|
||||
final params = <String, dynamic>{
|
||||
"id": id,
|
||||
@ -1058,6 +1069,18 @@ class SCChatRoomRepository implements SocialChatRoomRepository {
|
||||
if (songId != null && songId.trim().isNotEmpty) {
|
||||
params["songId"] = songId;
|
||||
}
|
||||
if (relationType != null && relationType.trim().isNotEmpty) {
|
||||
params["relationType"] = relationType.trim();
|
||||
}
|
||||
_giftRepoLog(
|
||||
'request giveBackpackGift endpoint=/gift/backpack/give-away '
|
||||
'giftId=$giftId '
|
||||
'roomId=$roomId '
|
||||
'quantity=$quantity '
|
||||
'relationType=$relationType '
|
||||
'acceptUserId=$acceptUserId '
|
||||
'payload=${_stringifyGiftDebug(params)}',
|
||||
);
|
||||
final result = await http.post<Object?>(
|
||||
"/gift/backpack/give-away",
|
||||
data: params,
|
||||
|
||||
@ -2,10 +2,22 @@ import 'package:yumi/shared/business_logic/models/res/gift_res.dart';
|
||||
import 'package:yumi/shared/data_sources/models/enum/sc_gift_type.dart';
|
||||
import 'package:yumi/shared/tools/sc_cp_relation_notice_utils.dart';
|
||||
|
||||
bool scIsCpGiftTab(String? tabType) =>
|
||||
tabType?.trim().toUpperCase() == SCGiftType.CP.name;
|
||||
bool scIsCpGiftTab(String? tabType) {
|
||||
final text = tabType?.trim();
|
||||
if (text == null || text.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
return text.toUpperCase() == SCGiftType.CP.name ||
|
||||
_inferCpRelationTypeFromGiftText(text) != null;
|
||||
}
|
||||
|
||||
bool scIsCpGift(SocialChatGiftRes? gift) => scIsCpGiftTab(gift?.giftTab);
|
||||
bool scIsCpGift(SocialChatGiftRes? gift) {
|
||||
if (gift == null) {
|
||||
return false;
|
||||
}
|
||||
return scIsCpGiftTab(gift.giftTab) ||
|
||||
_normalizeExplicitGiftRelationType(gift.relationType) != null;
|
||||
}
|
||||
|
||||
String scCpRelationTypeForGift(SocialChatGiftRes gift) {
|
||||
final explicit = _normalizeExplicitGiftRelationType(gift.relationType);
|
||||
@ -18,6 +30,7 @@ String scCpRelationTypeForGift(SocialChatGiftRes gift) {
|
||||
gift.giftName,
|
||||
gift.special,
|
||||
gift.standardId,
|
||||
gift.giftTab,
|
||||
]) {
|
||||
final inferred = _inferCpRelationTypeFromGiftText(value);
|
||||
if (inferred != null) {
|
||||
@ -33,10 +46,13 @@ String? _normalizeExplicitGiftRelationType(String? relationType) {
|
||||
return null;
|
||||
}
|
||||
final normalized = scNormalizeCpRelationType(raw);
|
||||
if (normalized != "CP" || _isCpRelationText(raw)) {
|
||||
if (normalized != "CP") {
|
||||
return normalized;
|
||||
}
|
||||
return _inferCpRelationTypeFromGiftText(raw) ?? normalized;
|
||||
if (_isCpRelationText(raw)) {
|
||||
return normalized;
|
||||
}
|
||||
return _inferCpRelationTypeFromGiftText(raw);
|
||||
}
|
||||
|
||||
bool _isCpRelationText(String value) {
|
||||
@ -49,7 +65,14 @@ String? _inferCpRelationTypeFromGiftText(String? value) {
|
||||
if (upper == null || upper.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
if (_hasRelationToken(upper, const ["SIS", "SISTER", "SISTERS"])) {
|
||||
if (_hasRelationToken(upper, const [
|
||||
"SIS",
|
||||
"SISTER",
|
||||
"SISTERS",
|
||||
"姐妹",
|
||||
"姊妹",
|
||||
"闺蜜",
|
||||
])) {
|
||||
return "SISTERS";
|
||||
}
|
||||
if (_hasRelationToken(upper, const [
|
||||
@ -61,10 +84,13 @@ String? _inferCpRelationTypeFromGiftText(String? value) {
|
||||
"SWORN_BROS",
|
||||
"SWORN_BROTHER",
|
||||
"SWORN_BROTHERS",
|
||||
"兄弟",
|
||||
"哥们",
|
||||
"兄妹",
|
||||
])) {
|
||||
return "BROTHER";
|
||||
}
|
||||
if (_hasRelationToken(upper, const ["CP", "COUPLE"])) {
|
||||
if (_hasRelationToken(upper, const ["CP", "COUPLE", "情侣", "恋人", "伴侣"])) {
|
||||
return "CP";
|
||||
}
|
||||
return null;
|
||||
@ -73,6 +99,9 @@ String? _inferCpRelationTypeFromGiftText(String? value) {
|
||||
bool _hasRelationToken(String upperText, List<String> tokens) {
|
||||
for (final token in tokens) {
|
||||
final escaped = RegExp.escape(token);
|
||||
if (RegExp(r'[^\x00-\x7F]').hasMatch(token) && upperText.contains(token)) {
|
||||
return true;
|
||||
}
|
||||
if (RegExp('(^|[^A-Z0-9])$escaped([^A-Z0-9]|\$)').hasMatch(upperText)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
227
lib/shared/tools/sc_cp_relation_level_utils.dart
Normal file
227
lib/shared/tools/sc_cp_relation_level_utils.dart
Normal file
@ -0,0 +1,227 @@
|
||||
import 'package:yumi/shared/business_logic/models/res/login_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_cp_rights_config_res.dart';
|
||||
|
||||
String? scCpRelationLevelTextFromCabin(
|
||||
SCCpCabinRes? cabin, {
|
||||
String? fallbackText,
|
||||
}) {
|
||||
if (cabin == null) {
|
||||
return _blankAsNull(fallbackText);
|
||||
}
|
||||
|
||||
final currentValue =
|
||||
cabin.cpVal?.toDouble() ?? cabin.cpPairUserProfileCO?.cpValue;
|
||||
final computedLevel = _currentLevelFromDynamicConfigs(
|
||||
cabin.cpCabinConfigCO ?? const <dynamic>[],
|
||||
currentValue,
|
||||
);
|
||||
final computedText = _formattedLevelText(
|
||||
level: computedLevel?.level,
|
||||
name: computedLevel?.levelName,
|
||||
);
|
||||
if (computedText != null) {
|
||||
return computedText;
|
||||
}
|
||||
|
||||
final currentConfig = _CpRelationLevelSource.fromDynamic(
|
||||
cabin.currentCpCabinConfigCO,
|
||||
);
|
||||
final currentText = _formattedLevelText(
|
||||
level: currentConfig.level,
|
||||
name: currentConfig.levelName,
|
||||
);
|
||||
if (currentText != null) {
|
||||
return currentText;
|
||||
}
|
||||
|
||||
return _blankAsNull(cabin.cpPairUserProfileCO?.levelText) ??
|
||||
_blankAsNull(fallbackText);
|
||||
}
|
||||
|
||||
String? scCpRelationLevelTextFromRightsConfig(
|
||||
SCCpRightsConfigRes config, {
|
||||
String? fallbackText,
|
||||
}) {
|
||||
final currentValue = config.intimacyValue?.toDouble();
|
||||
final computedLevel = _currentLevelFromRightsLevels(
|
||||
config.levels,
|
||||
currentValue,
|
||||
);
|
||||
final computedText = _formattedLevelText(
|
||||
level: computedLevel?.level,
|
||||
name: computedLevel?.levelName,
|
||||
);
|
||||
return computedText ?? _blankAsNull(fallbackText);
|
||||
}
|
||||
|
||||
String scCpRelationLevelDisplayText(String? rawText) {
|
||||
final text = rawText?.trim() ?? '';
|
||||
final level = scCpRelationLevelIndexOrNull(text);
|
||||
if (text.isEmpty) {
|
||||
return 'Lv.1 Simple Love';
|
||||
}
|
||||
final compactLevelPattern = RegExp(
|
||||
r'^(?:Lv\.?|Level)?\s*\d+$',
|
||||
caseSensitive: false,
|
||||
);
|
||||
if (compactLevelPattern.hasMatch(text) && level == 1) {
|
||||
return 'Lv.1 Simple Love';
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
int scCpRelationLevelIndex(String? levelText) {
|
||||
return scCpRelationLevelIndexOrNull(levelText) ?? 1;
|
||||
}
|
||||
|
||||
int? scCpRelationLevelIndexOrNull(String? levelText) {
|
||||
final text = levelText?.trim() ?? '';
|
||||
if (text.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
final levelMatch = RegExp(
|
||||
r'(?:Lv\.?|Level|等级|级)\s*([1-5])|([1-5])\s*(?:级|Level)',
|
||||
caseSensitive: false,
|
||||
).firstMatch(text);
|
||||
final fallbackMatch = RegExp(r'[1-5]').firstMatch(text);
|
||||
final rawLevel =
|
||||
levelMatch?.group(1) ?? levelMatch?.group(2) ?? fallbackMatch?.group(0);
|
||||
final level = int.tryParse(rawLevel ?? '');
|
||||
return level?.clamp(1, 5).toInt();
|
||||
}
|
||||
|
||||
_CpRelationLevelSource? _currentLevelFromDynamicConfigs(
|
||||
List<dynamic> configs,
|
||||
double? currentValue,
|
||||
) {
|
||||
final levels =
|
||||
configs.map(_CpRelationLevelSource.fromDynamic).where((level) {
|
||||
return level.requiredValue != null;
|
||||
}).toList()
|
||||
..sort(
|
||||
(left, right) => left.requiredValue!.compareTo(right.requiredValue!),
|
||||
);
|
||||
return _currentLevelFromSources(levels, currentValue);
|
||||
}
|
||||
|
||||
_CpRelationLevelSource? _currentLevelFromRightsLevels(
|
||||
List<SCCpRightsLevelRes> levels,
|
||||
double? currentValue,
|
||||
) {
|
||||
final sources =
|
||||
levels.map(_CpRelationLevelSource.fromRightsLevel).where((level) {
|
||||
return level.requiredValue != null;
|
||||
}).toList()
|
||||
..sort(
|
||||
(left, right) => left.requiredValue!.compareTo(right.requiredValue!),
|
||||
);
|
||||
return _currentLevelFromSources(sources, currentValue);
|
||||
}
|
||||
|
||||
_CpRelationLevelSource? _currentLevelFromSources(
|
||||
List<_CpRelationLevelSource> levels,
|
||||
double? currentValue,
|
||||
) {
|
||||
if (levels.isEmpty || currentValue == null) {
|
||||
return null;
|
||||
}
|
||||
_CpRelationLevelSource? currentLevel;
|
||||
for (final level in levels) {
|
||||
if ((level.requiredValue ?? 0) <= currentValue) {
|
||||
currentLevel = level;
|
||||
}
|
||||
}
|
||||
return currentLevel;
|
||||
}
|
||||
|
||||
String? _formattedLevelText({int? level, String? name}) {
|
||||
final levelName = name?.trim() ?? '';
|
||||
if (level != null && levelName.isNotEmpty) {
|
||||
final nameLevel = scCpRelationLevelIndexOrNull(levelName);
|
||||
if (nameLevel == level) {
|
||||
return levelName;
|
||||
}
|
||||
return 'Lv.$level $levelName';
|
||||
}
|
||||
if (level != null) {
|
||||
return 'Lv.$level';
|
||||
}
|
||||
return _blankAsNull(levelName);
|
||||
}
|
||||
|
||||
String? _blankAsNull(String? value) {
|
||||
final text = value?.trim() ?? '';
|
||||
return text.isEmpty ? null : text;
|
||||
}
|
||||
|
||||
class _CpRelationLevelSource {
|
||||
const _CpRelationLevelSource({
|
||||
this.level,
|
||||
this.levelName,
|
||||
this.requiredValue,
|
||||
});
|
||||
|
||||
factory _CpRelationLevelSource.fromRightsLevel(SCCpRightsLevelRes level) {
|
||||
return _CpRelationLevelSource(
|
||||
level: level.level,
|
||||
levelName: level.levelName,
|
||||
requiredValue: level.requiredIntimacyValue,
|
||||
);
|
||||
}
|
||||
|
||||
factory _CpRelationLevelSource.fromDynamic(dynamic value) {
|
||||
final source = value is Map ? value : const <String, dynamic>{};
|
||||
return _CpRelationLevelSource(
|
||||
level: _asInt(
|
||||
_firstValue(source, const ['level', 'cpLevel', 'cabinLevel', 'lv']),
|
||||
),
|
||||
levelName: _asString(
|
||||
_firstValue(source, const [
|
||||
'levelName',
|
||||
'name',
|
||||
'title',
|
||||
'cabinName',
|
||||
'cpName',
|
||||
]),
|
||||
),
|
||||
requiredValue: _asInt(
|
||||
_firstValue(source, const [
|
||||
'requiredIntimacyValue',
|
||||
'requiredCpValue',
|
||||
'requiredCpVal',
|
||||
'cpValue',
|
||||
'cpVal',
|
||||
'intimacyValue',
|
||||
]),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final int? level;
|
||||
final String? levelName;
|
||||
final int? requiredValue;
|
||||
}
|
||||
|
||||
dynamic _firstValue(Map source, List<String> keys) {
|
||||
for (final key in keys) {
|
||||
if (source.containsKey(key) && source[key] != null) {
|
||||
return source[key];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String? _asString(dynamic value) {
|
||||
final text = value?.toString().trim();
|
||||
return text == null || text.isEmpty ? null : text;
|
||||
}
|
||||
|
||||
int? _asInt(dynamic value) {
|
||||
if (value is int) {
|
||||
return value;
|
||||
}
|
||||
if (value is num) {
|
||||
return value.toInt();
|
||||
}
|
||||
return int.tryParse(value?.toString() ?? '');
|
||||
}
|
||||
@ -1,4 +1,5 @@
|
||||
import 'package:yumi/shared/business_logic/models/res/login_res.dart';
|
||||
import 'package:yumi/shared/tools/sc_cp_relation_level_utils.dart';
|
||||
import 'package:yumi/shared/tools/sc_cp_relation_notice_utils.dart';
|
||||
|
||||
class SCCpRelationPairInfo {
|
||||
@ -113,18 +114,5 @@ Set<String> _relationKeys(CPRes relation) {
|
||||
String _normalizedRelationKey(String? value) => value?.trim() ?? "";
|
||||
|
||||
int? _cpLevelFromRelation(CPRes relation) {
|
||||
final levelText = relation.levelText?.trim() ?? "";
|
||||
if (levelText.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
final levelMatch = RegExp(
|
||||
r'(?:lv\.?|level|等级|级)\s*([1-5])|([1-5])\s*(?:级|level)',
|
||||
caseSensitive: false,
|
||||
).firstMatch(levelText);
|
||||
final rawLevel = levelMatch?.group(1) ?? levelMatch?.group(2);
|
||||
final level = int.tryParse(rawLevel ?? "");
|
||||
if (level == null) {
|
||||
return null;
|
||||
}
|
||||
return level.clamp(1, 5);
|
||||
return scCpRelationLevelIndexOrNull(relation.levelText);
|
||||
}
|
||||
|
||||
@ -13,8 +13,14 @@ import 'package:yumi/ui_kit/widgets/first_recharge/first_recharge_dialog.dart';
|
||||
import 'package:yumi/ui_kit/components/text/sc_text.dart';
|
||||
|
||||
class SCDialogUtils {
|
||||
static Future<void> showFirstRechargeDialog(BuildContext context) {
|
||||
return FirstRechargeDialog.showFromRemote(context);
|
||||
static Future<void> showFirstRechargeDialog(
|
||||
BuildContext context, {
|
||||
bool forceRefresh = false,
|
||||
}) {
|
||||
return FirstRechargeDialog.showFromRemote(
|
||||
context,
|
||||
forceRefresh: forceRefresh,
|
||||
);
|
||||
}
|
||||
|
||||
static void showDynamicCommentOptDialog(
|
||||
|
||||
@ -6,9 +6,9 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:yumi/services/general/sc_app_general_manager.dart';
|
||||
import 'package:yumi/services/payment/google_payment_manager.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_first_recharge_reward_res.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_config_repository_imp.dart';
|
||||
import 'package:yumi/shared/tools/sc_loading_manager.dart';
|
||||
import 'package:yumi/ui_kit/components/sc_debounce_widget.dart';
|
||||
import 'package:yumi/ui_kit/components/sc_tts.dart';
|
||||
@ -247,14 +247,20 @@ class FirstRechargeDialog extends StatefulWidget {
|
||||
);
|
||||
}
|
||||
|
||||
static Future<void> showFromRemote(BuildContext context) async {
|
||||
static Future<void> showFromRemote(
|
||||
BuildContext context, {
|
||||
bool forceRefresh = false,
|
||||
}) async {
|
||||
try {
|
||||
SCLoadingManager.show(context: context);
|
||||
final home = await SCConfigRepositoryImp().firstRechargeRewardHome();
|
||||
final home = await Provider.of<SCAppGeneralManager>(
|
||||
context,
|
||||
listen: false,
|
||||
).refreshFirstRechargeRewardHome(forceRefresh: forceRefresh);
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
if (!home.shouldShowRewardDialog) {
|
||||
if (home == null || !home.shouldShowRewardDialog) {
|
||||
SmartDialog.dismiss(tag: dialogTag);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -10,6 +10,7 @@ import 'package:tencent_cloud_chat_sdk/enum/message_elem_type.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/enum/message_status.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_custom_elem.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_text_elem.dart';
|
||||
import 'package:yumi/app_localizations.dart';
|
||||
import 'package:yumi/ui_kit/components/dialog/dialog_base.dart';
|
||||
@ -17,6 +18,7 @@ import 'package:yumi/ui_kit/components/sc_compontent.dart';
|
||||
import 'package:yumi/ui_kit/components/text/sc_text.dart';
|
||||
import 'package:yumi/app/constants/sc_global_config.dart';
|
||||
import 'package:yumi/app/routes/sc_fluro_navigator.dart';
|
||||
import 'package:yumi/shared/data_sources/models/enum/sc_sysytem_message_type.dart';
|
||||
import 'package:yumi/shared/tools/sc_date_utils.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';
|
||||
@ -406,7 +408,15 @@ class _ConversationItemState extends State<ConversationItem> {
|
||||
!_isSystemConversation &&
|
||||
conversation.conversationID != "customer" &&
|
||||
conversation.conversationID != "article" &&
|
||||
(conversation.userID?.isNotEmpty ?? false);
|
||||
_effectiveUserId.isNotEmpty;
|
||||
|
||||
String get _effectiveUserId {
|
||||
final cpInvitePeerId = _cpInvitePeerUserIdFromLastMessage();
|
||||
if (cpInvitePeerId.isNotEmpty) {
|
||||
return cpInvitePeerId;
|
||||
}
|
||||
return conversation.userID?.trim() ?? "";
|
||||
}
|
||||
|
||||
String get _displayAvatar {
|
||||
final userAvatar = user?.userAvatar ?? "";
|
||||
@ -490,20 +500,21 @@ class _ConversationItemState extends State<ConversationItem> {
|
||||
}*/
|
||||
return GestureDetector(
|
||||
onTap: () async {
|
||||
if (_isC2CConversation && (conversation.userID?.isEmpty ?? true)) {
|
||||
final routeConversation = _routeConversation();
|
||||
if (_isC2CConversation && (routeConversation.userID?.isEmpty ?? true)) {
|
||||
return;
|
||||
}
|
||||
if (conversation.type == ConversationType.V2TIM_GROUP &&
|
||||
(conversation.groupID?.isEmpty ?? true)) {
|
||||
if (routeConversation.type == ConversationType.V2TIM_GROUP &&
|
||||
(routeConversation.groupID?.isEmpty ?? true)) {
|
||||
return;
|
||||
}
|
||||
conversation.unreadCount = 0;
|
||||
var bool = await Provider.of<RtmProvider>(
|
||||
context,
|
||||
listen: false,
|
||||
).startConversation(conversation);
|
||||
).startConversation(routeConversation);
|
||||
if (!bool) return;
|
||||
var json = jsonEncode(widget.conversation.toJson());
|
||||
var json = jsonEncode(routeConversation.toJson());
|
||||
final route =
|
||||
_isSystemConversation ? SCChatRouter.systemChat : SCChatRouter.chat;
|
||||
SCNavigatorUtils.push(
|
||||
@ -662,10 +673,146 @@ class _ConversationItemState extends State<ConversationItem> {
|
||||
|
||||
void loadUserInfo() {
|
||||
if (!_shouldLoadUserInfo || user != null) return;
|
||||
SCAccountRepository().loadUserInfo(conversation.userID!).then((value) {
|
||||
final userId = _effectiveUserId;
|
||||
if (userId.isEmpty) return;
|
||||
SCAccountRepository().loadUserInfo(userId).then((value) {
|
||||
if (!mounted) return;
|
||||
user = value;
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
|
||||
V2TimConversation _routeConversation() {
|
||||
final peerId = _effectiveUserId;
|
||||
if (!_isC2CConversation ||
|
||||
peerId.isEmpty ||
|
||||
peerId == (conversation.userID?.trim() ?? "")) {
|
||||
return conversation;
|
||||
}
|
||||
final json = conversation.toJson();
|
||||
json["userID"] = peerId;
|
||||
json["conversationID"] = "c2c_$peerId";
|
||||
return V2TimConversation.fromJson(json);
|
||||
}
|
||||
|
||||
String _cpInvitePeerUserIdFromLastMessage() {
|
||||
if (!_isC2CConversation) {
|
||||
return "";
|
||||
}
|
||||
final data = _cpInviteCustomData(conversation.lastMessage);
|
||||
if (data == null) {
|
||||
return "";
|
||||
}
|
||||
final content = _cpInviteContentMap(data);
|
||||
final noticeType =
|
||||
_firstCpInviteValue([
|
||||
data["noticeType"]?.toString(),
|
||||
data["type"]?.toString(),
|
||||
content["noticeType"]?.toString(),
|
||||
content["type"]?.toString(),
|
||||
])?.toUpperCase() ??
|
||||
"";
|
||||
if (!noticeType.contains(SCSysytemMessageType.CP_BUILD.name)) {
|
||||
return "";
|
||||
}
|
||||
final currentProfile = AccountStorage().getCurrentUser()?.userProfile;
|
||||
final currentIds =
|
||||
<String?>[currentProfile?.id, currentProfile?.account]
|
||||
.map((value) => value?.trim() ?? "")
|
||||
.where((value) => value.isNotEmpty)
|
||||
.toSet();
|
||||
final inviterIds = _compactCpInviteValues([
|
||||
data["actualAccount"]?.toString(),
|
||||
data["applyActualAccount"]?.toString(),
|
||||
data["applyUserActualAccount"]?.toString(),
|
||||
data["inviterActualAccount"]?.toString(),
|
||||
data["inviterUserId"]?.toString(),
|
||||
data["inviterId"]?.toString(),
|
||||
data["senderUserId"]?.toString(),
|
||||
data["senderId"]?.toString(),
|
||||
data["fromUserId"]?.toString(),
|
||||
data["userId"]?.toString(),
|
||||
content["actualAccount"]?.toString(),
|
||||
content["applyActualAccount"]?.toString(),
|
||||
content["applyUserActualAccount"]?.toString(),
|
||||
content["inviterActualAccount"]?.toString(),
|
||||
content["inviterUserId"]?.toString(),
|
||||
content["inviterId"]?.toString(),
|
||||
content["senderUserId"]?.toString(),
|
||||
content["senderId"]?.toString(),
|
||||
content["fromUserId"]?.toString(),
|
||||
content["userId"]?.toString(),
|
||||
data["applyUserId"]?.toString(),
|
||||
content["applyUserId"]?.toString(),
|
||||
data["account"]?.toString(),
|
||||
content["account"]?.toString(),
|
||||
]);
|
||||
final isCurrentInviter = currentIds.any(inviterIds.contains);
|
||||
final values =
|
||||
isCurrentInviter
|
||||
? [
|
||||
data["acceptActualAccount"]?.toString(),
|
||||
data["acceptUserId"]?.toString(),
|
||||
data["receiverActualAccount"]?.toString(),
|
||||
data["receiverId"]?.toString(),
|
||||
data["receiverUserId"]?.toString(),
|
||||
content["acceptActualAccount"]?.toString(),
|
||||
content["acceptUserId"]?.toString(),
|
||||
content["receiverActualAccount"]?.toString(),
|
||||
content["receiverId"]?.toString(),
|
||||
content["receiverUserId"]?.toString(),
|
||||
]
|
||||
: inviterIds;
|
||||
return _firstCpInviteValue(values) ?? "";
|
||||
}
|
||||
|
||||
Map<String, dynamic>? _cpInviteCustomData(V2TimMessage? message) {
|
||||
if (message?.elemType != MessageElemType.V2TIM_ELEM_TYPE_CUSTOM) {
|
||||
return null;
|
||||
}
|
||||
final raw = message?.customElem?.data;
|
||||
if (raw == null || raw.trim().isEmpty) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
final decoded = jsonDecode(raw);
|
||||
if (decoded is Map) {
|
||||
return decoded.map((key, value) => MapEntry(key.toString(), value));
|
||||
}
|
||||
} catch (_) {}
|
||||
return null;
|
||||
}
|
||||
|
||||
Map<String, dynamic> _cpInviteContentMap(Map<String, dynamic> data) {
|
||||
final content = data["content"];
|
||||
if (content is String && content.trim().isNotEmpty) {
|
||||
try {
|
||||
final decoded = jsonDecode(content);
|
||||
if (decoded is Map) {
|
||||
return decoded.map((key, value) => MapEntry(key.toString(), value));
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
if (content is Map) {
|
||||
return content.map((key, value) => MapEntry(key.toString(), value));
|
||||
}
|
||||
return const <String, dynamic>{};
|
||||
}
|
||||
|
||||
List<String> _compactCpInviteValues(Iterable<String?> values) {
|
||||
return values
|
||||
.map((value) => value?.trim() ?? "")
|
||||
.where((value) => value.isNotEmpty)
|
||||
.toList();
|
||||
}
|
||||
|
||||
String? _firstCpInviteValue(Iterable<String?> values) {
|
||||
for (final value in values) {
|
||||
final text = value?.trim() ?? "";
|
||||
if (text.isNotEmpty) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@ -24,6 +24,7 @@ import 'package:yumi/shared/tools/sc_loading_manager.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_string_utils.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/enum/sc_props_type.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart';
|
||||
@ -838,31 +839,11 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
|
||||
}
|
||||
|
||||
int _cpLevelIndex(String levelText) {
|
||||
final levelMatch = RegExp(
|
||||
r'(?:Lv\.?|Level)\s*(\d+)',
|
||||
caseSensitive: false,
|
||||
).firstMatch(levelText);
|
||||
final fallbackMatch = RegExp(r'\d+').firstMatch(levelText);
|
||||
final rawLevel =
|
||||
int.tryParse(levelMatch?.group(1) ?? fallbackMatch?.group(0) ?? "") ??
|
||||
1;
|
||||
return rawLevel.clamp(1, 5).toInt();
|
||||
return scCpRelationLevelIndex(levelText);
|
||||
}
|
||||
|
||||
String _cpLevelDisplayText(String rawText) {
|
||||
final text = rawText.trim();
|
||||
final level = _cpLevelIndex(text);
|
||||
if (text.isEmpty) {
|
||||
return "Lv.1 Simple Love";
|
||||
}
|
||||
final compactLevelPattern = RegExp(
|
||||
r'^(?:Lv\.?|Level)?\s*\d+$',
|
||||
caseSensitive: false,
|
||||
);
|
||||
if (compactLevelPattern.hasMatch(text) && level == 1) {
|
||||
return "Lv.1 Simple Love";
|
||||
}
|
||||
return text;
|
||||
return scCpRelationLevelDisplayText(rawText);
|
||||
}
|
||||
|
||||
bool _relationHasPartner(SocialChatUserProfile? profile, CPRes relation) {
|
||||
@ -1485,7 +1466,12 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
|
||||
expAwayText: progress.expAwayText,
|
||||
nextLevelText: progress.nextLevelText,
|
||||
cancelText: cancelText,
|
||||
onCancel: () => _showCpDismissConfirm(context, cp),
|
||||
onCancel:
|
||||
() => _showCpDismissConfirm(
|
||||
context,
|
||||
cp,
|
||||
dismissCost: progress.dismissCost,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -1514,7 +1500,11 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
|
||||
cp.leftUserId,
|
||||
relationType: cp.relationType,
|
||||
);
|
||||
return _RoomCpProgressData.fromCabin(cabin, cp);
|
||||
return _RoomCpProgressData.fromCabin(
|
||||
cabin,
|
||||
cp,
|
||||
fallbackDismissCost: latestFallback.dismissCost,
|
||||
);
|
||||
} catch (_) {
|
||||
return latestFallback;
|
||||
}
|
||||
@ -1522,8 +1512,9 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
|
||||
|
||||
Future<void> _showCpDismissConfirm(
|
||||
BuildContext context,
|
||||
_RoomProfileCpData cp,
|
||||
) async {
|
||||
_RoomProfileCpData cp, {
|
||||
int? dismissCost,
|
||||
}) async {
|
||||
final l10n = SCAppLocalizations.of(context)!;
|
||||
final partnerUserId = cp.rightUserId.trim();
|
||||
if (partnerUserId.isEmpty) {
|
||||
@ -1532,7 +1523,7 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
|
||||
await showRoomCpDismissConfirmDialog(
|
||||
context,
|
||||
message: l10n.cpDismissConfirmMessage(
|
||||
cp.dismissCost == null ? "--" : "${cp.dismissCost}",
|
||||
dismissCost == null ? "--" : "$dismissCost",
|
||||
),
|
||||
cancelText: l10n.cancel,
|
||||
confirmText: l10n.confirm,
|
||||
@ -1821,6 +1812,7 @@ class _RoomCpProgressData {
|
||||
required this.targetValue,
|
||||
required this.expAwayText,
|
||||
required this.nextLevelText,
|
||||
this.dismissCost,
|
||||
});
|
||||
|
||||
factory _RoomCpProgressData.fallback(_RoomProfileCpData cp) {
|
||||
@ -1834,6 +1826,7 @@ class _RoomCpProgressData {
|
||||
nextLevelText: _levelShortText(
|
||||
_nextLevelIndex(_levelIndex(cp.levelText)),
|
||||
),
|
||||
dismissCost: cp.dismissCost,
|
||||
);
|
||||
}
|
||||
|
||||
@ -1847,13 +1840,15 @@ class _RoomCpProgressData {
|
||||
currentValue: currentValue,
|
||||
levels: levels,
|
||||
cp: cp,
|
||||
dismissCost: config.dismissConsumeGold ?? cp.dismissCost,
|
||||
);
|
||||
}
|
||||
|
||||
factory _RoomCpProgressData.fromCabin(
|
||||
SCCpCabinRes cabin,
|
||||
_RoomProfileCpData cp,
|
||||
) {
|
||||
_RoomProfileCpData cp, {
|
||||
int? fallbackDismissCost,
|
||||
}) {
|
||||
final currentValue =
|
||||
(cabin.cpVal?.toDouble() ??
|
||||
cabin.cpPairUserProfileCO?.cpValue ??
|
||||
@ -1866,6 +1861,7 @@ class _RoomCpProgressData {
|
||||
currentValue: currentValue,
|
||||
levels: levels,
|
||||
cp: cp,
|
||||
dismissCost: cabin.dismissCost ?? fallbackDismissCost ?? cp.dismissCost,
|
||||
);
|
||||
}
|
||||
|
||||
@ -1873,6 +1869,7 @@ class _RoomCpProgressData {
|
||||
required double currentValue,
|
||||
required List<_CpProgressLevel> levels,
|
||||
required _RoomProfileCpData cp,
|
||||
int? dismissCost,
|
||||
}) {
|
||||
final sortedLevels =
|
||||
levels.where((level) => level.requiredValue != null).toList()..sort(
|
||||
@ -1908,6 +1905,7 @@ class _RoomCpProgressData {
|
||||
),
|
||||
)
|
||||
: _levelShortText(nextLevel.level, fallback: nextLevel.levelName),
|
||||
dismissCost: dismissCost,
|
||||
);
|
||||
}
|
||||
|
||||
@ -1916,6 +1914,7 @@ class _RoomCpProgressData {
|
||||
final double targetValue;
|
||||
final String expAwayText;
|
||||
final String nextLevelText;
|
||||
final int? dismissCost;
|
||||
|
||||
static _CpProgressLevel? _currentLevelFromConfig(
|
||||
List<_CpProgressLevel> levels,
|
||||
@ -1957,15 +1956,7 @@ class _RoomCpProgressData {
|
||||
}
|
||||
|
||||
static int _levelIndex(String levelText) {
|
||||
final levelMatch = RegExp(
|
||||
r'(?:Lv\.?|Level)\s*(\d+)',
|
||||
caseSensitive: false,
|
||||
).firstMatch(levelText);
|
||||
final fallbackMatch = RegExp(r'\d+').firstMatch(levelText);
|
||||
return int.tryParse(
|
||||
levelMatch?.group(1) ?? fallbackMatch?.group(0) ?? "",
|
||||
) ??
|
||||
1;
|
||||
return scCpRelationLevelIndex(levelText);
|
||||
}
|
||||
|
||||
static int _nextLevelIndex(int level) {
|
||||
|
||||
@ -61,6 +61,15 @@ void main() {
|
||||
expect(scCpRelationTypeForGift(cpGift), 'CP');
|
||||
expect(scIsCpGift(sisterGift), isTrue);
|
||||
});
|
||||
|
||||
test('infers brother and sister relation from tab and localized names', () {
|
||||
final brotherGift = SocialChatGiftRes(giftTab: 'BROTHER');
|
||||
final sisterGift = SocialChatGiftRes(giftTab: 'CP', giftName: '闺蜜皇冠');
|
||||
|
||||
expect(scIsCpGift(brotherGift), isTrue);
|
||||
expect(scCpRelationTypeForGift(brotherGift), 'BROTHER');
|
||||
expect(scCpRelationTypeForGift(sisterGift), 'SISTERS');
|
||||
});
|
||||
});
|
||||
|
||||
group('SCSystemInvitMessageRes avatar frame', () {
|
||||
@ -87,6 +96,18 @@ void main() {
|
||||
expect(invite?.relationType, 'BROTHER');
|
||||
});
|
||||
|
||||
test('keeps long actual account when content only has short account', () {
|
||||
final invite = SCSystemInvitMessageRes.fromMessageData({
|
||||
'account': '99004',
|
||||
'actualAccount': '2057731345099841537',
|
||||
'relationType': 'BROTHER',
|
||||
'content': jsonEncode({'account': '99004', 'userNickname': 'Alex'}),
|
||||
});
|
||||
|
||||
expect(invite?.account, '99004');
|
||||
expect(invite?.actualAccount, '2057731345099841537');
|
||||
});
|
||||
|
||||
test('infers relation type from gift payload when needed', () {
|
||||
final invite = SCSystemInvitMessageRes.fromMessageData({
|
||||
'content': {
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/login_res.dart';
|
||||
import 'package:yumi/shared/tools/sc_cp_relation_level_utils.dart';
|
||||
import 'package:yumi/shared/tools/sc_cp_relation_pair_utils.dart';
|
||||
|
||||
void main() {
|
||||
@ -62,4 +63,20 @@ void main() {
|
||||
expect(scCpRelationTypeBetweenProfiles(left, right), isNull);
|
||||
});
|
||||
});
|
||||
|
||||
group('scCpRelationLevelTextFromCabin', () {
|
||||
test('uses cp value and config thresholds for current level text', () {
|
||||
final cabin = SCCpCabinRes(
|
||||
cpVal: 1200,
|
||||
cpCabinConfigCO: const [
|
||||
{'level': 1, 'levelName': 'Simple Love', 'requiredCpValue': 0},
|
||||
{'level': 2, 'levelName': 'Sweet Love', 'requiredCpValue': 1000},
|
||||
{'level': 3, 'levelName': 'Deep Love', 'requiredCpValue': 3000},
|
||||
],
|
||||
currentCpCabinConfigCO: const {'level': 1, 'levelName': 'Simple Love'},
|
||||
);
|
||||
|
||||
expect(scCpRelationLevelTextFromCabin(cabin), 'Lv.2 Sweet Love');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -9,6 +9,7 @@ void main() {
|
||||
'cpUserId': null,
|
||||
'relationshipStatus': 'NONE',
|
||||
'intimacyValue': 0,
|
||||
'dismissConsumeGold': 88,
|
||||
'levels': [
|
||||
{
|
||||
'level': 1,
|
||||
@ -32,6 +33,7 @@ void main() {
|
||||
expect(config.cpUserId, isNull);
|
||||
expect(config.relationshipStatus, 'NONE');
|
||||
expect(config.intimacyValue, 0);
|
||||
expect(config.dismissConsumeGold, 88);
|
||||
expect(config.levels, hasLength(1));
|
||||
expect(config.levels.first.levelName, 'CP 1级');
|
||||
expect(config.levels.first.requiredIntimacyValue, 0);
|
||||
@ -49,6 +51,7 @@ void main() {
|
||||
'cpUserId': 123,
|
||||
'relationshipStatus': 'ACTIVE',
|
||||
'intimacyValue': 1200,
|
||||
'dismissConsumeGold': 120,
|
||||
'levels': [
|
||||
{
|
||||
'level': 2,
|
||||
@ -65,6 +68,7 @@ void main() {
|
||||
expect(config.cpUserId, '123');
|
||||
expect(config.relationshipStatus, 'ACTIVE');
|
||||
expect(config.intimacyValue, 1200);
|
||||
expect(config.dismissConsumeGold, 120);
|
||||
expect(config.levels.first.level, 2);
|
||||
expect(config.levels.first.unlocked, true);
|
||||
});
|
||||
|
||||
34
需求进度.md
34
需求进度.md
@ -980,3 +980,37 @@
|
||||
- [ ] 2026-05-22 17:26:58 准备热加载进场弹窗候选兜底逻辑,并通过前后台切换触发一次 entry popup 验证日志。
|
||||
- [ ] 2026-05-22 17:31:21 当前 iPhone 17 Pro flutter run 会话已停止,避免后台继续占用;候选兜底逻辑已通过 flutter analyze,未再做前后台日志复验。
|
||||
- [x] 2026-05-22 17:32:46 本轮完成:首充奖励按有效接口 item 动态渲染;启动弹窗顺序调整为注册奖励 > 每日签到 > 进场弹窗 > 首充弹窗;进场弹窗接口已接入并支持路径降级与候选图片兜底。
|
||||
- [ ] 2026-05-22 17:52:18 新增性能问题:礼物弹窗每次打开都会请求首充 home 接口导致慢和卡;需要改为进房预请求缓存,房内充值成功后再刷新状态。
|
||||
- [ ] 2026-05-22 17:52:43 开始检查礼物面板首充请求、房间进入生命周期和支付成功刷新入口。
|
||||
- [ ] 2026-05-22 17:55:06 新增方案确认:启动时首充弹窗请求结果缓存到全局状态;礼物弹窗只读缓存不再请求;用户充值成功后刷新该状态。
|
||||
- [ ] 2026-05-22 17:55:29 开始读取首充远程弹窗、礼物弹窗缓存读取点和支付成功刷新点。
|
||||
- [ ] 2026-05-22 17:56:21 准备在 SCAppGeneralManager 增加首充 home 全局缓存,礼物面板改为只读缓存,支付成功后强制刷新。
|
||||
- [ ] 2026-05-22 17:57:23 开始编码:新增 SCAppGeneralManager 首充缓存,移除 GiftPage 首充接口请求,支付成功后刷新缓存。
|
||||
|
||||
- [ ] 18:00:55 继续处理首充状态缓存:复核当前改动,并补充支付成功后刷新全局首充状态。
|
||||
|
||||
- [x] 18:01:20 已确认 GiftPage 打开时的首充接口请求已移除,当前只需要补齐支付成功后的全局状态刷新。
|
||||
|
||||
- [ ] 18:01:37 开始修改支付成功回调:Google/iOS 验证成功后强制刷新全局首充弹窗状态。
|
||||
|
||||
- [x] 18:01:59 已补充 Google/iOS 支付成功后的首充全局状态强制刷新。
|
||||
|
||||
- [ ] 18:02:37 调整首充弹窗入口:App 进场时强制请求并写入缓存,礼物悬浮板打开时复用缓存。
|
||||
|
||||
- [x] 18:02:59 已区分首充弹窗请求来源:首页进场强刷缓存,礼物入口打开复用缓存。
|
||||
|
||||
- [ ] 18:03:29 开始格式化本次涉及的首充缓存、弹窗入口、礼物面板与支付文件。
|
||||
|
||||
- [x] 18:03:41 已完成 Dart 格式化。
|
||||
|
||||
- [ ] 18:03:50 开始静态检查本次变更文件,确认首充缓存和支付刷新逻辑无编译错误。
|
||||
|
||||
- [ ] 18:04:31 静态检查发现支付文件 lint,开始收紧 iOS/Google 支付刷新实现并移除 iOS 未使用空方法。
|
||||
|
||||
- [x] 18:05:31 已收紧支付刷新实现:异步前缓存 Provider 引用,iOS 支付文件移除未使用空方法并修复 forEach lint。
|
||||
|
||||
- [x] 18:06:11 静态检查通过:本次涉及的 7 个 Dart 文件 flutter analyze 无问题。
|
||||
|
||||
- [ ] 18:06:26 检查 iOS 模拟器设备,准备进行运行验证。
|
||||
|
||||
- [x] 18:06:55 已确认本机可用 iPhone 17 Pro 与 iPhone 17 Pro Max 模拟器,开始用 iPhone 17 Pro 编译运行验证。
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user