又修复了一版
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/tools/sc_gift_vap_svga_manager.dart';
|
||||||
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
|
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
|
||||||
import 'package:yumi/shared/data_sources/sources/remote/net/api.dart';
|
import 'package:yumi/shared/data_sources/sources/remote/net/api.dart';
|
||||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_config_repository_imp.dart';
|
|
||||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository_imp.dart';
|
import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository_imp.dart';
|
||||||
import 'package:yumi/shared/business_logic/models/res/login_res.dart';
|
import 'package:yumi/shared/business_logic/models/res/login_res.dart';
|
||||||
import 'package:yumi/shared/tools/sc_cp_gift_relation_utils.dart';
|
import 'package:yumi/shared/tools/sc_cp_gift_relation_utils.dart';
|
||||||
@ -83,7 +82,6 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
|||||||
TabController? _tabController;
|
TabController? _tabController;
|
||||||
List<String> _tabTypes = <String>[];
|
List<String> _tabTypes = <String>[];
|
||||||
bool _didApplyInitialTab = false;
|
bool _didApplyInitialTab = false;
|
||||||
bool _showFirstRechargeGiftEntry = false;
|
|
||||||
|
|
||||||
/// 业务逻辑策略访问器
|
/// 业务逻辑策略访问器
|
||||||
BusinessLogicStrategy get _strategy => SCGlobalConfig.businessLogicStrategy;
|
BusinessLogicStrategy get _strategy => SCGlobalConfig.businessLogicStrategy;
|
||||||
@ -240,7 +238,6 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
|||||||
listen: false,
|
listen: false,
|
||||||
).giftBackpack(forceRefresh: true),
|
).giftBackpack(forceRefresh: true),
|
||||||
);
|
);
|
||||||
unawaited(_loadFirstRechargeGiftEntryState());
|
|
||||||
Provider.of<SocialChatUserProfileManager>(context, listen: false).balance();
|
Provider.of<SocialChatUserProfileManager>(context, listen: false).balance();
|
||||||
rtcProvider?.roomWheatMap.forEach((k, v) {
|
rtcProvider?.roomWheatMap.forEach((k, v) {
|
||||||
if (v.user != null) {
|
if (v.user != null) {
|
||||||
@ -276,35 +273,6 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
|||||||
_normalizeCpRecipientSelection(notify: true);
|
_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 {
|
bool get _isCurrentCpGiftTab {
|
||||||
final controller = _tabController;
|
final controller = _tabController;
|
||||||
if (controller == null || controller.index >= _tabTypes.length) {
|
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 currentTabType = giftTabs[tabController.index].type;
|
||||||
final isCpTab = currentTabType == "CP";
|
final isCpTab = currentTabType == "CP";
|
||||||
final showFirstRechargeEntry =
|
final showFirstRechargeEntry =
|
||||||
!SCGlobalConfig.isReview && !isCpTab && _showFirstRechargeGiftEntry;
|
!SCGlobalConfig.isReview &&
|
||||||
|
!isCpTab &&
|
||||||
|
ref.shouldShowFirstRechargeReward;
|
||||||
_ensureCurrentTabSelection(ref, giftTabs[tabController.index].type);
|
_ensureCurrentTabSelection(ref, giftTabs[tabController.index].type);
|
||||||
final isCpRecipientSingleMode = isCpTab || _isCpGift(checkedGift);
|
final isCpRecipientSingleMode = isCpTab || _isCpGift(checkedGift);
|
||||||
return SafeArea(
|
return SafeArea(
|
||||||
@ -1599,6 +1569,7 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
|||||||
animationCount: request.clickCount,
|
animationCount: request.clickCount,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
_syncLocalCpInviteAfterGift(request);
|
||||||
await _refreshGiftBackpack();
|
await _refreshGiftBackpack();
|
||||||
_refreshCpStateAfterGift(request, profileManager);
|
_refreshCpStateAfterGift(request, profileManager);
|
||||||
return;
|
return;
|
||||||
@ -1612,6 +1583,7 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
|||||||
request.quantity,
|
request.quantity,
|
||||||
false,
|
false,
|
||||||
roomId: request.roomId,
|
roomId: request.roomId,
|
||||||
|
relationType: request.cpRelationType,
|
||||||
)
|
)
|
||||||
: await repository.giveGift(
|
: await repository.giveGift(
|
||||||
request.acceptUserIds,
|
request.acceptUserIds,
|
||||||
@ -1619,6 +1591,7 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
|||||||
request.quantity,
|
request.quantity,
|
||||||
false,
|
false,
|
||||||
roomId: request.roomId,
|
roomId: request.roomId,
|
||||||
|
relationType: request.cpRelationType,
|
||||||
);
|
);
|
||||||
_giftFxLog(
|
_giftFxLog(
|
||||||
'$requestName success trigger=$trigger '
|
'$requestName success trigger=$trigger '
|
||||||
@ -1662,6 +1635,7 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
profileManager?.updateBalance(result);
|
profileManager?.updateBalance(result);
|
||||||
|
_syncLocalCpInviteAfterGift(request);
|
||||||
_refreshCpStateAfterGift(request, profileManager);
|
_refreshCpStateAfterGift(request, profileManager);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
final errorMessage =
|
final errorMessage =
|
||||||
@ -1705,6 +1679,41 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
|||||||
unawaited(profileManager?.refreshLoadedCpProfiles());
|
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(
|
Future<List<MicRes>> _giveBackpackGiftToRecipients(
|
||||||
SCChatRoomRepository repository,
|
SCChatRoomRepository repository,
|
||||||
_GiftSendRequest request,
|
_GiftSendRequest request,
|
||||||
@ -1726,6 +1735,7 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
|||||||
roomId: request.roomId,
|
roomId: request.roomId,
|
||||||
seatIndex:
|
seatIndex:
|
||||||
acceptUser?.micIndex ?? _resolveSeatIndexForUserId(acceptUserId),
|
acceptUser?.micIndex ?? _resolveSeatIndexForUserId(acceptUserId),
|
||||||
|
relationType: request.cpRelationType,
|
||||||
);
|
);
|
||||||
if (acceptUser != null) {
|
if (acceptUser != null) {
|
||||||
sentUsers.add(acceptUser);
|
sentUsers.add(acceptUser);
|
||||||
@ -2309,6 +2319,13 @@ class _GiftSendRequest {
|
|||||||
return isLuckyGiftRequest ? 'giveLuckyGift' : 'giveGift';
|
return isLuckyGiftRequest ? 'giveLuckyGift' : 'giveGift';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String? get cpRelationType {
|
||||||
|
if (!scIsCpGift(gift)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return scCpRelationTypeForGift(gift);
|
||||||
|
}
|
||||||
|
|
||||||
String get batchKey {
|
String get batchKey {
|
||||||
final sortedAcceptUserIds = List<String>.from(acceptUserIds)..sort();
|
final sortedAcceptUserIds = List<String>.from(acceptUserIds)..sort();
|
||||||
final type =
|
final type =
|
||||||
|
|||||||
@ -498,7 +498,7 @@ class _SCIndexPageState extends State<SCIndexPage>
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_hasRequestedFirstRechargeDialog = true;
|
_hasRequestedFirstRechargeDialog = true;
|
||||||
await SCDialogUtils.showFirstRechargeDialog(context);
|
await SCDialogUtils.showFirstRechargeDialog(context, forceRefresh: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _waitForRegisterRewardSocketIfNeeded() async {
|
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_routes.dart';
|
||||||
import 'package:yumi/app/routes/sc_fluro_navigator.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_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_loading_manager.dart';
|
||||||
import 'package:yumi/shared/tools/sc_lk_event_bus.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_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/business_logic/models/res/gift_res.dart';
|
||||||
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
|
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
|
||||||
import 'package:yumi/shared/data_sources/models/enum/sc_props_type.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/my_room_res.dart';
|
||||||
import 'package:yumi/shared/business_logic/models/res/room_res.dart'
|
import 'package:yumi/shared/business_logic/models/res/room_res.dart'
|
||||||
hide WearBadge, UseProps, PropsResources;
|
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/rtc_manager.dart';
|
||||||
import 'package:yumi/services/audio/rtm_manager.dart';
|
import 'package:yumi/services/audio/rtm_manager.dart';
|
||||||
import 'package:yumi/services/auth/user_profile_manager.dart';
|
import 'package:yumi/services/auth/user_profile_manager.dart';
|
||||||
import 'package:yumi/services/general/sc_app_general_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/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/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 '../../../app/constants/sc_screen.dart';
|
||||||
import '../../../shared/business_logic/models/res/sc_user_counter_res.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 owner = _profileRelationOwner(profile, relation);
|
||||||
final partner = _profileRelationPartner(profile, relation);
|
final partner = _profileRelationPartner(profile, relation);
|
||||||
return SizedBox(
|
final card = SizedBox(
|
||||||
height: 114.w,
|
height: 114.w,
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
@ -1037,6 +1041,11 @@ class _PersonDetailPageState extends State<PersonDetailPage> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
return _buildProfileRelationProgressTap(
|
||||||
|
profile: profile,
|
||||||
|
relation: relation,
|
||||||
|
child: card,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildProfileCpCardText(
|
Widget _buildProfileCpCardText(
|
||||||
@ -1093,7 +1102,7 @@ class _PersonDetailPageState extends State<PersonDetailPage> {
|
|||||||
final type = _normalizeProfileRelationType(relation.relationType);
|
final type = _normalizeProfileRelationType(relation.relationType);
|
||||||
final friend = _profileRelationPartner(profile, relation);
|
final friend = _profileRelationPartner(profile, relation);
|
||||||
final isSister = type == "SISTERS";
|
final isSister = type == "SISTERS";
|
||||||
return SizedBox(
|
final card = SizedBox(
|
||||||
width: 113.w,
|
width: 113.w,
|
||||||
height: 100.w,
|
height: 100.w,
|
||||||
child: Stack(
|
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) {
|
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) {
|
String _profileCpLevelAsset(CPRes relation) {
|
||||||
return "$_cpProfileAssetBase/sc_cp_profile_cp_level_${_profileRelationLevel(relation)}.png";
|
return "$_cpProfileAssetBase/sc_cp_profile_cp_level_${_profileRelationLevel(relation)}.png";
|
||||||
}
|
}
|
||||||
|
|
||||||
int _profileRelationLevel(CPRes relation) {
|
int _profileRelationLevel(CPRes relation) {
|
||||||
final levelText = relation.levelText ?? "";
|
return scCpRelationLevelIndex(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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String _profileRelationLevelText(CPRes relation) {
|
String _profileRelationLevelText(CPRes relation) {
|
||||||
@ -1483,19 +1677,7 @@ class _PersonDetailPageState extends State<PersonDetailPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _profileRelationLevelDisplayText(String rawText, CPRes relation) {
|
String _profileRelationLevelDisplayText(String rawText, CPRes relation) {
|
||||||
final text = rawText.trim();
|
return scCpRelationLevelDisplayText(rawText);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String _firstNonBlankProfileValue(List<String?> values) {
|
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 {
|
class _ProfileRelationUser {
|
||||||
const _ProfileRelationUser({
|
const _ProfileRelationUser({
|
||||||
this.userId,
|
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/conversation_type.dart';
|
||||||
import 'package:tencent_cloud_chat_sdk/enum/group_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/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/enum/message_status.dart';
|
||||||
import 'package:tencent_cloud_chat_sdk/manager/v2_tim_group_manager.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_callback.dart';
|
||||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.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_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_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_image_elem.dart';
|
||||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart';
|
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart';
|
||||||
@ -295,6 +297,7 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
|||||||
Timer? _roomRocketRoomReadyRewardRetryTimer;
|
Timer? _roomRocketRoomReadyRewardRetryTimer;
|
||||||
final Set<String> _shownCpInviteMessageKeys = <String>{};
|
final Set<String> _shownCpInviteMessageKeys = <String>{};
|
||||||
final Set<String> _forwardedCpInviteC2CMessageKeys = <String>{};
|
final Set<String> _forwardedCpInviteC2CMessageKeys = <String>{};
|
||||||
|
final Set<String> _echoedCpInviteC2CMessageKeys = <String>{};
|
||||||
int get currentLuckGiftBurstPlaybackToken => _luckGiftPushPlaybackToken;
|
int get currentLuckGiftBurstPlaybackToken => _luckGiftPushPlaybackToken;
|
||||||
String? roomRedPacketBroadcastGroupId;
|
String? roomRedPacketBroadcastGroupId;
|
||||||
String? roomRedPacketBroadcastRegionCode;
|
String? roomRedPacketBroadcastRegionCode;
|
||||||
@ -683,10 +686,13 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
|||||||
String? msgId,
|
String? msgId,
|
||||||
String? fallbackUserId,
|
String? fallbackUserId,
|
||||||
}) async {
|
}) async {
|
||||||
|
final cpData = _decodeCpSystemMessageData(message);
|
||||||
unawaited(_handleIncomingCpBuildMessage(message));
|
unawaited(_handleIncomingCpBuildMessage(message));
|
||||||
|
final cpPeerUserId = _cpInviteConversationPeerIdFromData(cpData);
|
||||||
final didSyncConversation = _upsertC2CConversationFromMessage(
|
final didSyncConversation = _upsertC2CConversationFromMessage(
|
||||||
message,
|
message,
|
||||||
fallbackUserId: fallbackUserId,
|
fallbackUserId: fallbackUserId ?? cpPeerUserId,
|
||||||
|
preferFallbackUserId: cpPeerUserId != null,
|
||||||
);
|
);
|
||||||
if (!didSyncConversation) {
|
if (!didSyncConversation) {
|
||||||
systemUnReadCount = _systemUnreadCount();
|
systemUnReadCount = _systemUnreadCount();
|
||||||
@ -700,11 +706,16 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
|||||||
bool _upsertC2CConversationFromMessage(
|
bool _upsertC2CConversationFromMessage(
|
||||||
V2TimMessage? message, {
|
V2TimMessage? message, {
|
||||||
String? fallbackUserId,
|
String? fallbackUserId,
|
||||||
|
bool preferFallbackUserId = false,
|
||||||
}) {
|
}) {
|
||||||
if (message == null || message.groupID != null) {
|
if (message == null || message.groupID != null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
final peerId = _messagePeerId(message, fallbackUserId);
|
final peerId = _messagePeerId(
|
||||||
|
message,
|
||||||
|
fallbackUserId,
|
||||||
|
preferFallbackUserId: preferFallbackUserId,
|
||||||
|
);
|
||||||
if (peerId.isEmpty) {
|
if (peerId.isEmpty) {
|
||||||
final cpData = _decodeCpSystemMessageData(message);
|
final cpData = _decodeCpSystemMessageData(message);
|
||||||
if (cpData != null) {
|
if (cpData != null) {
|
||||||
@ -766,12 +777,21 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
String _messagePeerId(V2TimMessage message, String? fallbackUserId) {
|
String _messagePeerId(
|
||||||
|
V2TimMessage message,
|
||||||
|
String? fallbackUserId, {
|
||||||
|
bool preferFallbackUserId = false,
|
||||||
|
}) {
|
||||||
final currentUserId =
|
final currentUserId =
|
||||||
AccountStorage().getCurrentUser()?.userProfile?.id?.trim() ?? "";
|
AccountStorage().getCurrentUser()?.userProfile?.id?.trim() ?? "";
|
||||||
final userId = message.userID?.trim() ?? "";
|
final userId = message.userID?.trim() ?? "";
|
||||||
final sender = message.sender?.trim() ?? "";
|
final sender = message.sender?.trim() ?? "";
|
||||||
final fallback = fallbackUserId?.trim() ?? "";
|
final fallback = fallbackUserId?.trim() ?? "";
|
||||||
|
if (preferFallbackUserId &&
|
||||||
|
fallback.isNotEmpty &&
|
||||||
|
fallback != currentUserId) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
if (message.isSelf == true) {
|
if (message.isSelf == true) {
|
||||||
if (userId.isNotEmpty) {
|
if (userId.isNotEmpty) {
|
||||||
return userId;
|
return userId;
|
||||||
@ -799,6 +819,21 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
|||||||
return "";
|
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(
|
String _c2cConversationDisplayName(
|
||||||
V2TimConversation conversation,
|
V2TimConversation conversation,
|
||||||
V2TimMessage message,
|
V2TimMessage message,
|
||||||
@ -850,7 +885,7 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
|||||||
if (noticeType != SCSysytemMessageType.CP_BUILD.name) {
|
if (noticeType != SCSysytemMessageType.CP_BUILD.name) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final applyId = data["expand"]?.toString().trim() ?? "";
|
final applyId = _cpInviteApplyIdFromData(data);
|
||||||
final invite = _cpInviteFromMessageData(data);
|
final invite = _cpInviteFromMessageData(data);
|
||||||
final messageKey = applyId.isNotEmpty ? applyId : message?.msgID ?? "";
|
final messageKey = applyId.isNotEmpty ? applyId : message?.msgID ?? "";
|
||||||
if (messageKey.isNotEmpty && !_shownCpInviteMessageKeys.add(messageKey)) {
|
if (messageKey.isNotEmpty && !_shownCpInviteMessageKeys.add(messageKey)) {
|
||||||
@ -888,6 +923,14 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
|||||||
fallback: currentProfile.id,
|
fallback: currentProfile.id,
|
||||||
);
|
);
|
||||||
final relationType = scNormalizeCpRelationType(invite.relationType);
|
final relationType = scNormalizeCpRelationType(invite.relationType);
|
||||||
|
_persistBackendCpInviteToC2CConversationIfNeeded(
|
||||||
|
sourceMessage: message,
|
||||||
|
data: data,
|
||||||
|
applyId: applyId,
|
||||||
|
invite: invite,
|
||||||
|
currentProfile: currentProfile,
|
||||||
|
isSender: isSender,
|
||||||
|
);
|
||||||
_forwardCpInviteC2CMessageIfNeeded(
|
_forwardCpInviteC2CMessageIfNeeded(
|
||||||
sourceMessage: message,
|
sourceMessage: message,
|
||||||
data: data,
|
data: data,
|
||||||
@ -895,6 +938,14 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
|||||||
invite: invite,
|
invite: invite,
|
||||||
isSender: isSender,
|
isSender: isSender,
|
||||||
);
|
);
|
||||||
|
_echoCpInviteC2CMessageToSenderIfNeeded(
|
||||||
|
sourceMessage: message,
|
||||||
|
data: data,
|
||||||
|
applyId: applyId,
|
||||||
|
invite: invite,
|
||||||
|
currentProfile: currentProfile,
|
||||||
|
isSender: isSender,
|
||||||
|
);
|
||||||
if (isSender) {
|
if (isSender) {
|
||||||
final receiver = _cpInviteReceiverFromMessageData(data);
|
final receiver = _cpInviteReceiverFromMessageData(data);
|
||||||
_cpInviteLog(
|
_cpInviteLog(
|
||||||
@ -984,6 +1035,21 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
|||||||
return null;
|
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) {
|
bool _handleCpInviteResultMessage(Map<String, dynamic> data) {
|
||||||
final result = _cpInviteResultFromMessageData(data);
|
final result = _cpInviteResultFromMessageData(data);
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
@ -1363,6 +1429,10 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
|||||||
if (!isSender || sourceMessage?.isSelf == true) {
|
if (!isSender || sourceMessage?.isSelf == true) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
final noticeSource = data["noticeSource"]?.toString() ?? "";
|
||||||
|
if (noticeSource == "RECEIVER_ECHO" || noticeSource == "LOCAL_OUTGOING") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
final normalizedApplyId = applyId.trim();
|
final normalizedApplyId = applyId.trim();
|
||||||
if (normalizedApplyId.isEmpty ||
|
if (normalizedApplyId.isEmpty ||
|
||||||
!_forwardedCpInviteC2CMessageKeys.add(normalizedApplyId)) {
|
!_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) {
|
String _cpInviteReceiverIdFromMessageData(Map<String, dynamic> data) {
|
||||||
final content = _cpInviteContentMap(data);
|
final content = _cpInviteContentMap(data);
|
||||||
return _firstCpInviteValue([
|
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({
|
Future<void> _sendCpBuildCustomMessage({
|
||||||
required String receiverId,
|
required String receiverId,
|
||||||
required Map<String, dynamic> data,
|
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(
|
void _sendIncomingCpInviteRejectedNotice(
|
||||||
SCSystemInvitMessageRes invite,
|
SCSystemInvitMessageRes invite,
|
||||||
RtcProvider rtcProvider,
|
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/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_user_identity_res.dart';
|
||||||
import 'package:yumi/shared/business_logic/models/res/sc_vip_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 {
|
class SocialChatUserProfileManager extends ChangeNotifier {
|
||||||
static const List<String> _cpProfileRelationTypes = [
|
static const List<String> _cpProfileRelationTypes = [
|
||||||
@ -188,10 +189,20 @@ class SocialChatUserProfileManager extends ChangeNotifier {
|
|||||||
final normalizedType = _normalizeCpRelationType(
|
final normalizedType = _normalizeCpRelationType(
|
||||||
cpPair.relationType ?? relationType,
|
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(
|
final enrichedPair = cpPair.copyWith(
|
||||||
relationType: normalizedType,
|
relationType: normalizedType,
|
||||||
cpValue: cpPair.cpValue ?? cabin?.cpVal?.toDouble(),
|
cpValue: cpPair.cpValue ?? cabin?.cpVal?.toDouble(),
|
||||||
levelText: _cpLevelTextFromCabin(cabin),
|
levelText: rightsLevelText ?? cabinLevelText,
|
||||||
dismissCost: cpPair.dismissCost ?? _cpDismissCostFromCabin(cabin),
|
dismissCost: cpPair.dismissCost ?? _cpDismissCostFromCabin(cabin),
|
||||||
);
|
);
|
||||||
if (normalizedType == 'CP') {
|
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) {
|
bool _hasCpPair(CPRes? pair) {
|
||||||
return (pair?.cpUserId?.trim().isNotEmpty ?? false) ||
|
return (pair?.cpUserId?.trim().isNotEmpty ?? false) ||
|
||||||
(pair?.cpUserAvatar?.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) {
|
int? _cpDismissCostFromCabin(SCCpCabinRes? cabin) {
|
||||||
final directCost = cabin?.dismissCost;
|
final directCost = cabin?.dismissCost;
|
||||||
if (directCost != null) {
|
if (directCost != null) {
|
||||||
@ -340,16 +368,6 @@ class SocialChatUserProfileManager extends ChangeNotifier {
|
|||||||
return int.tryParse(value?.toString() ?? "");
|
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(
|
Future<SocialChatUserProfile> _withCurrentVipLevel(
|
||||||
SocialChatUserProfile profile,
|
SocialChatUserProfile profile,
|
||||||
String userId,
|
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/country_res.dart';
|
||||||
import 'package:yumi/shared/business_logic/models/res/gift_backpack_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/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_index_banner_res.dart';
|
||||||
import 'package:yumi/shared/business_logic/models/res/sc_room_emoji_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 _hasGiftListLoaded = false;
|
||||||
bool _isFetchingGiftBackpack = false;
|
bool _isFetchingGiftBackpack = false;
|
||||||
bool _hasGiftBackpackLoaded = false;
|
bool _hasGiftBackpackLoaded = false;
|
||||||
|
bool _isFetchingFirstRechargeRewardHome = false;
|
||||||
|
bool _hasFirstRechargeRewardHomeLoaded = false;
|
||||||
|
SCFirstRechargeRewardHomeRes? _firstRechargeRewardHome;
|
||||||
|
|
||||||
Map<String, List<SocialChatGiftRes>> giftByTab = {};
|
Map<String, List<SocialChatGiftRes>> giftByTab = {};
|
||||||
Map<String, List<SocialChatGiftRes>> activityGiftByTab = {};
|
Map<String, List<SocialChatGiftRes>> activityGiftByTab = {};
|
||||||
@ -59,10 +63,40 @@ class SCAppGeneralManager extends ChangeNotifier {
|
|||||||
bool get isGiftBackpackLoading =>
|
bool get isGiftBackpackLoading =>
|
||||||
_isFetchingGiftBackpack && !_hasGiftBackpackLoaded;
|
_isFetchingGiftBackpack && !_hasGiftBackpackLoaded;
|
||||||
|
|
||||||
|
bool get shouldShowFirstRechargeReward =>
|
||||||
|
_firstRechargeRewardHome?.shouldShowRewardDialog ?? false;
|
||||||
|
|
||||||
|
SCFirstRechargeRewardHomeRes? get firstRechargeRewardHome =>
|
||||||
|
_firstRechargeRewardHome;
|
||||||
|
|
||||||
bool isGiftTabLoading(String type) {
|
bool isGiftTabLoading(String type) {
|
||||||
return type == backpackGiftTab ? isGiftBackpackLoading : isGiftListLoading;
|
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 {
|
Future<void> fetchCountryList() async {
|
||||||
if (_isFetchingCountryList) {
|
if (_isFetchingCountryList) {
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import 'package:flutter/foundation.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:in_app_purchase/in_app_purchase.dart';
|
import 'package:in_app_purchase/in_app_purchase.dart';
|
||||||
import 'package:yumi/services/auth/user_profile_manager.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:provider/provider.dart';
|
||||||
import 'package:yumi/shared/tools/sc_loading_manager.dart';
|
import 'package:yumi/shared/tools/sc_loading_manager.dart';
|
||||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||||
@ -23,7 +24,7 @@ class IOSPaymentProcessor extends ChangeNotifier {
|
|||||||
// 只复制必要的状态变量
|
// 只复制必要的状态变量
|
||||||
bool _isAvailable = false;
|
bool _isAvailable = false;
|
||||||
String _errorMessage = '';
|
String _errorMessage = '';
|
||||||
List<SelecteProductConfig> _products = [];
|
final List<SelecteProductConfig> _products = [];
|
||||||
|
|
||||||
late BuildContext context;
|
late BuildContext context;
|
||||||
Map<String, SCProductConfigRes> productMap = {};
|
Map<String, SCProductConfigRes> productMap = {};
|
||||||
@ -125,11 +126,11 @@ class IOSPaymentProcessor extends ChangeNotifier {
|
|||||||
// 发起iOS购买
|
// 发起iOS购买
|
||||||
Future<void> processPurchase() async {
|
Future<void> processPurchase() async {
|
||||||
ProductDetails? product;
|
ProductDetails? product;
|
||||||
_products.forEach((d) {
|
for (final d in _products) {
|
||||||
if (d.isSelecte) {
|
if (d.isSelecte) {
|
||||||
product = d.produc;
|
product = d.produc;
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
if (product == null) {
|
if (product == null) {
|
||||||
SCTts.show(SCAppLocalizations.of(context)!.pleaseSelectaItem);
|
SCTts.show(SCAppLocalizations.of(context)!.pleaseSelectaItem);
|
||||||
@ -226,6 +227,14 @@ class IOSPaymentProcessor extends ChangeNotifier {
|
|||||||
|
|
||||||
// 验证iOS支付
|
// 验证iOS支付
|
||||||
Future<void> _verifyIOSPayment(PurchaseDetails purchase) async {
|
Future<void> _verifyIOSPayment(PurchaseDetails purchase) async {
|
||||||
|
final profileManager = Provider.of<SocialChatUserProfileManager>(
|
||||||
|
context,
|
||||||
|
listen: false,
|
||||||
|
);
|
||||||
|
final firstRechargeManager = Provider.of<SCAppGeneralManager>(
|
||||||
|
context,
|
||||||
|
listen: false,
|
||||||
|
);
|
||||||
SCLoadingManager.show();
|
SCLoadingManager.show();
|
||||||
try {
|
try {
|
||||||
String receiptData = purchase.verificationData.serverVerificationData;
|
String receiptData = purchase.verificationData.serverVerificationData;
|
||||||
@ -242,14 +251,9 @@ class IOSPaymentProcessor extends ChangeNotifier {
|
|||||||
// 🔥 iOS关键:必须完成交易
|
// 🔥 iOS关键:必须完成交易
|
||||||
await iap.completePurchase(purchase);
|
await iap.completePurchase(purchase);
|
||||||
// 更新用户信息
|
// 更新用户信息
|
||||||
Provider.of<SocialChatUserProfileManager>(
|
profileManager.fetchUserProfileData();
|
||||||
context,
|
profileManager.balance();
|
||||||
listen: false,
|
_refreshFirstRechargeRewardState(firstRechargeManager);
|
||||||
).fetchUserProfileData();
|
|
||||||
Provider.of<SocialChatUserProfileManager>(
|
|
||||||
context,
|
|
||||||
listen: false,
|
|
||||||
).balance();
|
|
||||||
SCTts.show('Purchase is successful!');
|
SCTts.show('Purchase is successful!');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.toString().endsWith("${SCErroCode.orderExistsCreated.code}")) {
|
if (e.toString().endsWith("${SCErroCode.orderExistsCreated.code}")) {
|
||||||
@ -270,9 +274,14 @@ class IOSPaymentProcessor extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 交付商品(与安卓相同)
|
void _refreshFirstRechargeRewardState(SCAppGeneralManager manager) {
|
||||||
Future<void> _deliverProduct(String productId) async {
|
try {
|
||||||
// 使用相同的业务逻辑
|
unawaited(manager.refreshFirstRechargeRewardHome(forceRefresh: true));
|
||||||
|
} catch (error) {
|
||||||
|
if (kDebugMode) {
|
||||||
|
debugPrint('[FirstRecharge][ios_payment] refresh state failed: $error');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// iOS错误处理
|
// 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:in_app_purchase_android/in_app_purchase_android.dart';
|
||||||
import 'package:yumi/app_localizations.dart';
|
import 'package:yumi/app_localizations.dart';
|
||||||
import 'package:yumi/services/auth/user_profile_manager.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:provider/provider.dart';
|
||||||
import 'package:yumi/shared/tools/sc_string_utils.dart';
|
import 'package:yumi/shared/tools/sc_string_utils.dart';
|
||||||
import 'package:yumi/shared/tools/sc_loading_manager.dart';
|
import 'package:yumi/shared/tools/sc_loading_manager.dart';
|
||||||
@ -298,12 +299,17 @@ class AndroidPaymentProcessor extends ChangeNotifier {
|
|||||||
context,
|
context,
|
||||||
listen: false,
|
listen: false,
|
||||||
);
|
);
|
||||||
|
final firstRechargeManager = Provider.of<SCAppGeneralManager>(
|
||||||
|
context,
|
||||||
|
listen: false,
|
||||||
|
);
|
||||||
try {
|
try {
|
||||||
// 如果是消耗型商品,尝试消耗
|
// 如果是消耗型商品,尝试消耗
|
||||||
if (_getProductType(purchase.productID) == 'consumable') {
|
if (_getProductType(purchase.productID) == 'consumable') {
|
||||||
await consumePurchase(purchase);
|
await consumePurchase(purchase);
|
||||||
// 消耗后重新获取余额
|
// 消耗后重新获取余额
|
||||||
profileManager.balance();
|
profileManager.balance();
|
||||||
|
_refreshFirstRechargeRewardState(firstRechargeManager);
|
||||||
SCTts.show('outstanding purchases have been reinstated');
|
SCTts.show('outstanding purchases have been reinstated');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -317,6 +323,10 @@ class AndroidPaymentProcessor extends ChangeNotifier {
|
|||||||
context,
|
context,
|
||||||
listen: false,
|
listen: false,
|
||||||
);
|
);
|
||||||
|
final firstRechargeManager = Provider.of<SCAppGeneralManager>(
|
||||||
|
context,
|
||||||
|
listen: false,
|
||||||
|
);
|
||||||
try {
|
try {
|
||||||
// 1. 基本验证
|
// 1. 基本验证
|
||||||
if (purchase.verificationData.serverVerificationData.isEmpty) {
|
if (purchase.verificationData.serverVerificationData.isEmpty) {
|
||||||
@ -359,6 +369,7 @@ class AndroidPaymentProcessor extends ChangeNotifier {
|
|||||||
// 8. 更新用户余额
|
// 8. 更新用户余额
|
||||||
profileManager.fetchUserProfileData();
|
profileManager.fetchUserProfileData();
|
||||||
profileManager.balance();
|
profileManager.balance();
|
||||||
|
_refreshFirstRechargeRewardState(firstRechargeManager);
|
||||||
|
|
||||||
SCTts.show('purchase successful');
|
SCTts.show('purchase successful');
|
||||||
} catch (e) {
|
} 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 {
|
Future<void> _ensurePurchaseConsumed(PurchaseDetails purchase) async {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -1162,6 +1162,10 @@ class CPRes {
|
|||||||
_levelText = _asString(
|
_levelText = _asString(
|
||||||
source['levelText'] ??
|
source['levelText'] ??
|
||||||
source['levelName'] ??
|
source['levelName'] ??
|
||||||
|
source['level'] ??
|
||||||
|
source['cpLevel'] ??
|
||||||
|
source['cabinLevel'] ??
|
||||||
|
source['lv'] ??
|
||||||
source['name'] ??
|
source['name'] ??
|
||||||
source['title'],
|
source['title'],
|
||||||
);
|
);
|
||||||
|
|||||||
@ -4,6 +4,7 @@ class SCCpRightsConfigRes {
|
|||||||
this.cpUserId,
|
this.cpUserId,
|
||||||
this.relationshipStatus,
|
this.relationshipStatus,
|
||||||
this.intimacyValue,
|
this.intimacyValue,
|
||||||
|
this.dismissConsumeGold,
|
||||||
List<SCCpRightsLevelRes>? levels,
|
List<SCCpRightsLevelRes>? levels,
|
||||||
}) : levels = levels ?? const [];
|
}) : levels = levels ?? const [];
|
||||||
|
|
||||||
@ -18,6 +19,11 @@ class SCCpRightsConfigRes {
|
|||||||
cpUserId: _asString(source['cpUserId']),
|
cpUserId: _asString(source['cpUserId']),
|
||||||
relationshipStatus: _asString(source['relationshipStatus']),
|
relationshipStatus: _asString(source['relationshipStatus']),
|
||||||
intimacyValue: _asInt(source['intimacyValue']),
|
intimacyValue: _asInt(source['intimacyValue']),
|
||||||
|
dismissConsumeGold: _asInt(
|
||||||
|
source['dismissConsumeGold'] ??
|
||||||
|
source['dismissCost'] ??
|
||||||
|
source['dismissGold'],
|
||||||
|
),
|
||||||
levels:
|
levels:
|
||||||
rawLevels is List
|
rawLevels is List
|
||||||
? rawLevels
|
? rawLevels
|
||||||
@ -31,6 +37,7 @@ class SCCpRightsConfigRes {
|
|||||||
final String? cpUserId;
|
final String? cpUserId;
|
||||||
final String? relationshipStatus;
|
final String? relationshipStatus;
|
||||||
final int? intimacyValue;
|
final int? intimacyValue;
|
||||||
|
final int? dismissConsumeGold;
|
||||||
final List<SCCpRightsLevelRes> levels;
|
final List<SCCpRightsLevelRes> levels;
|
||||||
|
|
||||||
static String? _asString(dynamic value) {
|
static String? _asString(dynamic value) {
|
||||||
|
|||||||
@ -41,7 +41,7 @@ class SCSystemInvitMessageRes {
|
|||||||
}
|
}
|
||||||
|
|
||||||
SCSystemInvitMessageRes.fromJson(dynamic json) {
|
SCSystemInvitMessageRes.fromJson(dynamic json) {
|
||||||
_account = json['account'];
|
_account = _firstNonBlank(json, const ['account', 'applyUserAccount']);
|
||||||
_countryCode = json['countryCode'];
|
_countryCode = json['countryCode'];
|
||||||
_countryName = json['countryName'];
|
_countryName = json['countryName'];
|
||||||
_userAvatar = json['userAvatar'];
|
_userAvatar = json['userAvatar'];
|
||||||
@ -61,9 +61,20 @@ class SCSystemInvitMessageRes {
|
|||||||
'avatarFrameCover',
|
'avatarFrameCover',
|
||||||
]);
|
]);
|
||||||
_userNickname = json['userNickname'];
|
_userNickname = json['userNickname'];
|
||||||
_actualAccount = json['actualAccount'];
|
_actualAccount = _firstNonBlank(json, const [
|
||||||
|
'actualAccount',
|
||||||
|
'applyActualAccount',
|
||||||
|
'applyUserActualAccount',
|
||||||
|
'inviterActualAccount',
|
||||||
|
'inviterUserId',
|
||||||
|
'inviterId',
|
||||||
|
'senderUserId',
|
||||||
|
'senderId',
|
||||||
|
'fromUserId',
|
||||||
|
'userId',
|
||||||
|
]);
|
||||||
_applyType = json['applyType'];
|
_applyType = json['applyType'];
|
||||||
_content = json['content'];
|
_content = _contentString(json['content']);
|
||||||
_relationType = _firstNonBlank(json, _relationTypeKeys);
|
_relationType = _firstNonBlank(json, _relationTypeKeys);
|
||||||
_dismissEndTime = _asInt(json['dismissEndTime']);
|
_dismissEndTime = _asInt(json['dismissEndTime']);
|
||||||
_giftCover = json['giftCover'];
|
_giftCover = json['giftCover'];
|
||||||
@ -80,6 +91,20 @@ class SCSystemInvitMessageRes {
|
|||||||
return int.tryParse(value?.toString() ?? '');
|
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? _account;
|
||||||
String? _countryCode;
|
String? _countryCode;
|
||||||
String? _countryName;
|
String? _countryName;
|
||||||
@ -140,7 +165,10 @@ class SCSystemInvitMessageRes {
|
|||||||
!_hasInviteIdentity(root)) {
|
!_hasInviteIdentity(root)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
final source = <String, dynamic>{...(content.isNotEmpty ? content : root)};
|
final source = <String, dynamic>{
|
||||||
|
...root,
|
||||||
|
if (content.isNotEmpty) ...content,
|
||||||
|
};
|
||||||
final relationType =
|
final relationType =
|
||||||
_firstNonBlank(source, _relationTypeKeys) ??
|
_firstNonBlank(source, _relationTypeKeys) ??
|
||||||
_firstNonBlank(root, _relationTypeKeys) ??
|
_firstNonBlank(root, _relationTypeKeys) ??
|
||||||
|
|||||||
@ -94,6 +94,7 @@ abstract class SocialChatRoomRepository {
|
|||||||
String? roomId,
|
String? roomId,
|
||||||
SCGiveAwayGiftRoomAcceptsCmd? accepts,
|
SCGiveAwayGiftRoomAcceptsCmd? accepts,
|
||||||
String? dynamicContentId,
|
String? dynamicContentId,
|
||||||
|
String? relationType,
|
||||||
});
|
});
|
||||||
|
|
||||||
///举报
|
///举报
|
||||||
@ -182,6 +183,7 @@ abstract class SocialChatRoomRepository {
|
|||||||
Object? gameId,
|
Object? gameId,
|
||||||
String? dynamicContentId,
|
String? dynamicContentId,
|
||||||
String? songId,
|
String? songId,
|
||||||
|
String? relationType,
|
||||||
});
|
});
|
||||||
|
|
||||||
///查询榜单前三名
|
///查询榜单前三名
|
||||||
@ -199,6 +201,7 @@ abstract class SocialChatRoomRepository {
|
|||||||
String? roomId,
|
String? roomId,
|
||||||
num? seatIndex,
|
num? seatIndex,
|
||||||
String? songId,
|
String? songId,
|
||||||
|
String? relationType,
|
||||||
});
|
});
|
||||||
|
|
||||||
///查询房间火箭状态
|
///查询房间火箭状态
|
||||||
|
|||||||
@ -610,6 +610,7 @@ class SCChatRoomRepository implements SocialChatRoomRepository {
|
|||||||
String? roomId,
|
String? roomId,
|
||||||
SCGiveAwayGiftRoomAcceptsCmd? accepts,
|
SCGiveAwayGiftRoomAcceptsCmd? accepts,
|
||||||
String? dynamicContentId,
|
String? dynamicContentId,
|
||||||
|
String? relationType,
|
||||||
}) async {
|
}) async {
|
||||||
Map<String, dynamic> params = {};
|
Map<String, dynamic> params = {};
|
||||||
if (roomId != null) {
|
if (roomId != null) {
|
||||||
@ -625,6 +626,9 @@ class SCChatRoomRepository implements SocialChatRoomRepository {
|
|||||||
if (dynamicContentId != null) {
|
if (dynamicContentId != null) {
|
||||||
params["dynamicContentId"] = dynamicContentId;
|
params["dynamicContentId"] = dynamicContentId;
|
||||||
}
|
}
|
||||||
|
if (relationType != null && relationType.trim().isNotEmpty) {
|
||||||
|
params["relationType"] = relationType.trim();
|
||||||
|
}
|
||||||
|
|
||||||
_giftRepoLog(
|
_giftRepoLog(
|
||||||
'request giveGift endpoint=/gift/batch '
|
'request giveGift endpoint=/gift/batch '
|
||||||
@ -632,6 +636,7 @@ class SCChatRoomRepository implements SocialChatRoomRepository {
|
|||||||
'roomId=$roomId '
|
'roomId=$roomId '
|
||||||
'quantity=$quantity '
|
'quantity=$quantity '
|
||||||
'checkCombo=$checkCombo '
|
'checkCombo=$checkCombo '
|
||||||
|
'relationType=$relationType '
|
||||||
'dynamicContentId=$dynamicContentId '
|
'dynamicContentId=$dynamicContentId '
|
||||||
'acceptUserIds=${acceptUserIds.join(",")} '
|
'acceptUserIds=${acceptUserIds.join(",")} '
|
||||||
'payload=${_stringifyGiftDebug(params)}',
|
'payload=${_stringifyGiftDebug(params)}',
|
||||||
@ -953,6 +958,7 @@ class SCChatRoomRepository implements SocialChatRoomRepository {
|
|||||||
Object? gameId,
|
Object? gameId,
|
||||||
String? dynamicContentId,
|
String? dynamicContentId,
|
||||||
String? songId,
|
String? songId,
|
||||||
|
String? relationType,
|
||||||
}) async {
|
}) async {
|
||||||
Map<String, dynamic> params = {};
|
Map<String, dynamic> params = {};
|
||||||
if (roomId != null) {
|
if (roomId != null) {
|
||||||
@ -969,6 +975,9 @@ class SCChatRoomRepository implements SocialChatRoomRepository {
|
|||||||
.toList();
|
.toList();
|
||||||
params["dynamicContentId"] = dynamicContentId;
|
params["dynamicContentId"] = dynamicContentId;
|
||||||
params["songId"] = songId;
|
params["songId"] = songId;
|
||||||
|
if (relationType != null && relationType.trim().isNotEmpty) {
|
||||||
|
params["relationType"] = relationType.trim();
|
||||||
|
}
|
||||||
|
|
||||||
_giftRepoLog(
|
_giftRepoLog(
|
||||||
'request giveLuckyGift endpoint=/gift/give/lucky-gift '
|
'request giveLuckyGift endpoint=/gift/give/lucky-gift '
|
||||||
@ -976,6 +985,7 @@ class SCChatRoomRepository implements SocialChatRoomRepository {
|
|||||||
'roomId=$roomId '
|
'roomId=$roomId '
|
||||||
'quantity=$quantity '
|
'quantity=$quantity '
|
||||||
'checkCombo=$checkCombo '
|
'checkCombo=$checkCombo '
|
||||||
|
'relationType=$relationType '
|
||||||
'gameId=$gameId '
|
'gameId=$gameId '
|
||||||
'dynamicContentId=$dynamicContentId '
|
'dynamicContentId=$dynamicContentId '
|
||||||
'songId=$songId '
|
'songId=$songId '
|
||||||
@ -1042,6 +1052,7 @@ class SCChatRoomRepository implements SocialChatRoomRepository {
|
|||||||
String? roomId,
|
String? roomId,
|
||||||
num? seatIndex,
|
num? seatIndex,
|
||||||
String? songId,
|
String? songId,
|
||||||
|
String? relationType,
|
||||||
}) async {
|
}) async {
|
||||||
final params = <String, dynamic>{
|
final params = <String, dynamic>{
|
||||||
"id": id,
|
"id": id,
|
||||||
@ -1058,6 +1069,18 @@ class SCChatRoomRepository implements SocialChatRoomRepository {
|
|||||||
if (songId != null && songId.trim().isNotEmpty) {
|
if (songId != null && songId.trim().isNotEmpty) {
|
||||||
params["songId"] = songId;
|
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?>(
|
final result = await http.post<Object?>(
|
||||||
"/gift/backpack/give-away",
|
"/gift/backpack/give-away",
|
||||||
data: params,
|
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/data_sources/models/enum/sc_gift_type.dart';
|
||||||
import 'package:yumi/shared/tools/sc_cp_relation_notice_utils.dart';
|
import 'package:yumi/shared/tools/sc_cp_relation_notice_utils.dart';
|
||||||
|
|
||||||
bool scIsCpGiftTab(String? tabType) =>
|
bool scIsCpGiftTab(String? tabType) {
|
||||||
tabType?.trim().toUpperCase() == SCGiftType.CP.name;
|
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) {
|
String scCpRelationTypeForGift(SocialChatGiftRes gift) {
|
||||||
final explicit = _normalizeExplicitGiftRelationType(gift.relationType);
|
final explicit = _normalizeExplicitGiftRelationType(gift.relationType);
|
||||||
@ -18,6 +30,7 @@ String scCpRelationTypeForGift(SocialChatGiftRes gift) {
|
|||||||
gift.giftName,
|
gift.giftName,
|
||||||
gift.special,
|
gift.special,
|
||||||
gift.standardId,
|
gift.standardId,
|
||||||
|
gift.giftTab,
|
||||||
]) {
|
]) {
|
||||||
final inferred = _inferCpRelationTypeFromGiftText(value);
|
final inferred = _inferCpRelationTypeFromGiftText(value);
|
||||||
if (inferred != null) {
|
if (inferred != null) {
|
||||||
@ -33,10 +46,13 @@ String? _normalizeExplicitGiftRelationType(String? relationType) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
final normalized = scNormalizeCpRelationType(raw);
|
final normalized = scNormalizeCpRelationType(raw);
|
||||||
if (normalized != "CP" || _isCpRelationText(raw)) {
|
if (normalized != "CP") {
|
||||||
return normalized;
|
return normalized;
|
||||||
}
|
}
|
||||||
return _inferCpRelationTypeFromGiftText(raw) ?? normalized;
|
if (_isCpRelationText(raw)) {
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
return _inferCpRelationTypeFromGiftText(raw);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _isCpRelationText(String value) {
|
bool _isCpRelationText(String value) {
|
||||||
@ -49,7 +65,14 @@ String? _inferCpRelationTypeFromGiftText(String? value) {
|
|||||||
if (upper == null || upper.isEmpty) {
|
if (upper == null || upper.isEmpty) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (_hasRelationToken(upper, const ["SIS", "SISTER", "SISTERS"])) {
|
if (_hasRelationToken(upper, const [
|
||||||
|
"SIS",
|
||||||
|
"SISTER",
|
||||||
|
"SISTERS",
|
||||||
|
"姐妹",
|
||||||
|
"姊妹",
|
||||||
|
"闺蜜",
|
||||||
|
])) {
|
||||||
return "SISTERS";
|
return "SISTERS";
|
||||||
}
|
}
|
||||||
if (_hasRelationToken(upper, const [
|
if (_hasRelationToken(upper, const [
|
||||||
@ -61,10 +84,13 @@ String? _inferCpRelationTypeFromGiftText(String? value) {
|
|||||||
"SWORN_BROS",
|
"SWORN_BROS",
|
||||||
"SWORN_BROTHER",
|
"SWORN_BROTHER",
|
||||||
"SWORN_BROTHERS",
|
"SWORN_BROTHERS",
|
||||||
|
"兄弟",
|
||||||
|
"哥们",
|
||||||
|
"兄妹",
|
||||||
])) {
|
])) {
|
||||||
return "BROTHER";
|
return "BROTHER";
|
||||||
}
|
}
|
||||||
if (_hasRelationToken(upper, const ["CP", "COUPLE"])) {
|
if (_hasRelationToken(upper, const ["CP", "COUPLE", "情侣", "恋人", "伴侣"])) {
|
||||||
return "CP";
|
return "CP";
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@ -73,6 +99,9 @@ String? _inferCpRelationTypeFromGiftText(String? value) {
|
|||||||
bool _hasRelationToken(String upperText, List<String> tokens) {
|
bool _hasRelationToken(String upperText, List<String> tokens) {
|
||||||
for (final token in tokens) {
|
for (final token in tokens) {
|
||||||
final escaped = RegExp.escape(token);
|
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)) {
|
if (RegExp('(^|[^A-Z0-9])$escaped([^A-Z0-9]|\$)').hasMatch(upperText)) {
|
||||||
return true;
|
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/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';
|
import 'package:yumi/shared/tools/sc_cp_relation_notice_utils.dart';
|
||||||
|
|
||||||
class SCCpRelationPairInfo {
|
class SCCpRelationPairInfo {
|
||||||
@ -113,18 +114,5 @@ Set<String> _relationKeys(CPRes relation) {
|
|||||||
String _normalizedRelationKey(String? value) => value?.trim() ?? "";
|
String _normalizedRelationKey(String? value) => value?.trim() ?? "";
|
||||||
|
|
||||||
int? _cpLevelFromRelation(CPRes relation) {
|
int? _cpLevelFromRelation(CPRes relation) {
|
||||||
final levelText = relation.levelText?.trim() ?? "";
|
return scCpRelationLevelIndexOrNull(relation.levelText);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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';
|
import 'package:yumi/ui_kit/components/text/sc_text.dart';
|
||||||
|
|
||||||
class SCDialogUtils {
|
class SCDialogUtils {
|
||||||
static Future<void> showFirstRechargeDialog(BuildContext context) {
|
static Future<void> showFirstRechargeDialog(
|
||||||
return FirstRechargeDialog.showFromRemote(context);
|
BuildContext context, {
|
||||||
|
bool forceRefresh = false,
|
||||||
|
}) {
|
||||||
|
return FirstRechargeDialog.showFromRemote(
|
||||||
|
context,
|
||||||
|
forceRefresh: forceRefresh,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void showDynamicCommentOptDialog(
|
static void showDynamicCommentOptDialog(
|
||||||
|
|||||||
@ -6,9 +6,9 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||||
import 'package:provider/provider.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/services/payment/google_payment_manager.dart';
|
||||||
import 'package:yumi/shared/business_logic/models/res/sc_first_recharge_reward_res.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/shared/tools/sc_loading_manager.dart';
|
||||||
import 'package:yumi/ui_kit/components/sc_debounce_widget.dart';
|
import 'package:yumi/ui_kit/components/sc_debounce_widget.dart';
|
||||||
import 'package:yumi/ui_kit/components/sc_tts.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 {
|
try {
|
||||||
SCLoadingManager.show(context: context);
|
SCLoadingManager.show(context: context);
|
||||||
final home = await SCConfigRepositoryImp().firstRechargeRewardHome();
|
final home = await Provider.of<SCAppGeneralManager>(
|
||||||
|
context,
|
||||||
|
listen: false,
|
||||||
|
).refreshFirstRechargeRewardHome(forceRefresh: forceRefresh);
|
||||||
if (!context.mounted) {
|
if (!context.mounted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!home.shouldShowRewardDialog) {
|
if (home == null || !home.shouldShowRewardDialog) {
|
||||||
SmartDialog.dismiss(tag: dialogTag);
|
SmartDialog.dismiss(tag: dialogTag);
|
||||||
return;
|
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/enum/message_status.dart';
|
||||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.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_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:tencent_cloud_chat_sdk/models/v2_tim_text_elem.dart';
|
||||||
import 'package:yumi/app_localizations.dart';
|
import 'package:yumi/app_localizations.dart';
|
||||||
import 'package:yumi/ui_kit/components/dialog/dialog_base.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/ui_kit/components/text/sc_text.dart';
|
||||||
import 'package:yumi/app/constants/sc_global_config.dart';
|
import 'package:yumi/app/constants/sc_global_config.dart';
|
||||||
import 'package:yumi/app/routes/sc_fluro_navigator.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/tools/sc_date_utils.dart';
|
||||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart';
|
import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart';
|
||||||
import 'package:yumi/shared/business_logic/models/res/login_res.dart';
|
import 'package:yumi/shared/business_logic/models/res/login_res.dart';
|
||||||
@ -406,7 +408,15 @@ class _ConversationItemState extends State<ConversationItem> {
|
|||||||
!_isSystemConversation &&
|
!_isSystemConversation &&
|
||||||
conversation.conversationID != "customer" &&
|
conversation.conversationID != "customer" &&
|
||||||
conversation.conversationID != "article" &&
|
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 {
|
String get _displayAvatar {
|
||||||
final userAvatar = user?.userAvatar ?? "";
|
final userAvatar = user?.userAvatar ?? "";
|
||||||
@ -490,20 +500,21 @@ class _ConversationItemState extends State<ConversationItem> {
|
|||||||
}*/
|
}*/
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
if (_isC2CConversation && (conversation.userID?.isEmpty ?? true)) {
|
final routeConversation = _routeConversation();
|
||||||
|
if (_isC2CConversation && (routeConversation.userID?.isEmpty ?? true)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (conversation.type == ConversationType.V2TIM_GROUP &&
|
if (routeConversation.type == ConversationType.V2TIM_GROUP &&
|
||||||
(conversation.groupID?.isEmpty ?? true)) {
|
(routeConversation.groupID?.isEmpty ?? true)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
conversation.unreadCount = 0;
|
conversation.unreadCount = 0;
|
||||||
var bool = await Provider.of<RtmProvider>(
|
var bool = await Provider.of<RtmProvider>(
|
||||||
context,
|
context,
|
||||||
listen: false,
|
listen: false,
|
||||||
).startConversation(conversation);
|
).startConversation(routeConversation);
|
||||||
if (!bool) return;
|
if (!bool) return;
|
||||||
var json = jsonEncode(widget.conversation.toJson());
|
var json = jsonEncode(routeConversation.toJson());
|
||||||
final route =
|
final route =
|
||||||
_isSystemConversation ? SCChatRouter.systemChat : SCChatRouter.chat;
|
_isSystemConversation ? SCChatRouter.systemChat : SCChatRouter.chat;
|
||||||
SCNavigatorUtils.push(
|
SCNavigatorUtils.push(
|
||||||
@ -662,10 +673,146 @@ class _ConversationItemState extends State<ConversationItem> {
|
|||||||
|
|
||||||
void loadUserInfo() {
|
void loadUserInfo() {
|
||||||
if (!_shouldLoadUserInfo || user != null) return;
|
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;
|
if (!mounted) return;
|
||||||
user = value;
|
user = value;
|
||||||
setState(() {});
|
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_utils.dart';
|
||||||
import 'package:yumi/shared/tools/sc_room_top_layer_guard.dart';
|
import 'package:yumi/shared/tools/sc_room_top_layer_guard.dart';
|
||||||
import 'package:yumi/shared/tools/sc_string_utils.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/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/models/enum/sc_props_type.dart';
|
||||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.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) {
|
int _cpLevelIndex(String levelText) {
|
||||||
final levelMatch = RegExp(
|
return scCpRelationLevelIndex(levelText);
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String _cpLevelDisplayText(String rawText) {
|
String _cpLevelDisplayText(String rawText) {
|
||||||
final text = rawText.trim();
|
return scCpRelationLevelDisplayText(rawText);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _relationHasPartner(SocialChatUserProfile? profile, CPRes relation) {
|
bool _relationHasPartner(SocialChatUserProfile? profile, CPRes relation) {
|
||||||
@ -1485,7 +1466,12 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
|
|||||||
expAwayText: progress.expAwayText,
|
expAwayText: progress.expAwayText,
|
||||||
nextLevelText: progress.nextLevelText,
|
nextLevelText: progress.nextLevelText,
|
||||||
cancelText: cancelText,
|
cancelText: cancelText,
|
||||||
onCancel: () => _showCpDismissConfirm(context, cp),
|
onCancel:
|
||||||
|
() => _showCpDismissConfirm(
|
||||||
|
context,
|
||||||
|
cp,
|
||||||
|
dismissCost: progress.dismissCost,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1514,7 +1500,11 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
|
|||||||
cp.leftUserId,
|
cp.leftUserId,
|
||||||
relationType: cp.relationType,
|
relationType: cp.relationType,
|
||||||
);
|
);
|
||||||
return _RoomCpProgressData.fromCabin(cabin, cp);
|
return _RoomCpProgressData.fromCabin(
|
||||||
|
cabin,
|
||||||
|
cp,
|
||||||
|
fallbackDismissCost: latestFallback.dismissCost,
|
||||||
|
);
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
return latestFallback;
|
return latestFallback;
|
||||||
}
|
}
|
||||||
@ -1522,8 +1512,9 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
|
|||||||
|
|
||||||
Future<void> _showCpDismissConfirm(
|
Future<void> _showCpDismissConfirm(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
_RoomProfileCpData cp,
|
_RoomProfileCpData cp, {
|
||||||
) async {
|
int? dismissCost,
|
||||||
|
}) async {
|
||||||
final l10n = SCAppLocalizations.of(context)!;
|
final l10n = SCAppLocalizations.of(context)!;
|
||||||
final partnerUserId = cp.rightUserId.trim();
|
final partnerUserId = cp.rightUserId.trim();
|
||||||
if (partnerUserId.isEmpty) {
|
if (partnerUserId.isEmpty) {
|
||||||
@ -1532,7 +1523,7 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
|
|||||||
await showRoomCpDismissConfirmDialog(
|
await showRoomCpDismissConfirmDialog(
|
||||||
context,
|
context,
|
||||||
message: l10n.cpDismissConfirmMessage(
|
message: l10n.cpDismissConfirmMessage(
|
||||||
cp.dismissCost == null ? "--" : "${cp.dismissCost}",
|
dismissCost == null ? "--" : "$dismissCost",
|
||||||
),
|
),
|
||||||
cancelText: l10n.cancel,
|
cancelText: l10n.cancel,
|
||||||
confirmText: l10n.confirm,
|
confirmText: l10n.confirm,
|
||||||
@ -1821,6 +1812,7 @@ class _RoomCpProgressData {
|
|||||||
required this.targetValue,
|
required this.targetValue,
|
||||||
required this.expAwayText,
|
required this.expAwayText,
|
||||||
required this.nextLevelText,
|
required this.nextLevelText,
|
||||||
|
this.dismissCost,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory _RoomCpProgressData.fallback(_RoomProfileCpData cp) {
|
factory _RoomCpProgressData.fallback(_RoomProfileCpData cp) {
|
||||||
@ -1834,6 +1826,7 @@ class _RoomCpProgressData {
|
|||||||
nextLevelText: _levelShortText(
|
nextLevelText: _levelShortText(
|
||||||
_nextLevelIndex(_levelIndex(cp.levelText)),
|
_nextLevelIndex(_levelIndex(cp.levelText)),
|
||||||
),
|
),
|
||||||
|
dismissCost: cp.dismissCost,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1847,13 +1840,15 @@ class _RoomCpProgressData {
|
|||||||
currentValue: currentValue,
|
currentValue: currentValue,
|
||||||
levels: levels,
|
levels: levels,
|
||||||
cp: cp,
|
cp: cp,
|
||||||
|
dismissCost: config.dismissConsumeGold ?? cp.dismissCost,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
factory _RoomCpProgressData.fromCabin(
|
factory _RoomCpProgressData.fromCabin(
|
||||||
SCCpCabinRes cabin,
|
SCCpCabinRes cabin,
|
||||||
_RoomProfileCpData cp,
|
_RoomProfileCpData cp, {
|
||||||
) {
|
int? fallbackDismissCost,
|
||||||
|
}) {
|
||||||
final currentValue =
|
final currentValue =
|
||||||
(cabin.cpVal?.toDouble() ??
|
(cabin.cpVal?.toDouble() ??
|
||||||
cabin.cpPairUserProfileCO?.cpValue ??
|
cabin.cpPairUserProfileCO?.cpValue ??
|
||||||
@ -1866,6 +1861,7 @@ class _RoomCpProgressData {
|
|||||||
currentValue: currentValue,
|
currentValue: currentValue,
|
||||||
levels: levels,
|
levels: levels,
|
||||||
cp: cp,
|
cp: cp,
|
||||||
|
dismissCost: cabin.dismissCost ?? fallbackDismissCost ?? cp.dismissCost,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1873,6 +1869,7 @@ class _RoomCpProgressData {
|
|||||||
required double currentValue,
|
required double currentValue,
|
||||||
required List<_CpProgressLevel> levels,
|
required List<_CpProgressLevel> levels,
|
||||||
required _RoomProfileCpData cp,
|
required _RoomProfileCpData cp,
|
||||||
|
int? dismissCost,
|
||||||
}) {
|
}) {
|
||||||
final sortedLevels =
|
final sortedLevels =
|
||||||
levels.where((level) => level.requiredValue != null).toList()..sort(
|
levels.where((level) => level.requiredValue != null).toList()..sort(
|
||||||
@ -1908,6 +1905,7 @@ class _RoomCpProgressData {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
: _levelShortText(nextLevel.level, fallback: nextLevel.levelName),
|
: _levelShortText(nextLevel.level, fallback: nextLevel.levelName),
|
||||||
|
dismissCost: dismissCost,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1916,6 +1914,7 @@ class _RoomCpProgressData {
|
|||||||
final double targetValue;
|
final double targetValue;
|
||||||
final String expAwayText;
|
final String expAwayText;
|
||||||
final String nextLevelText;
|
final String nextLevelText;
|
||||||
|
final int? dismissCost;
|
||||||
|
|
||||||
static _CpProgressLevel? _currentLevelFromConfig(
|
static _CpProgressLevel? _currentLevelFromConfig(
|
||||||
List<_CpProgressLevel> levels,
|
List<_CpProgressLevel> levels,
|
||||||
@ -1957,15 +1956,7 @@ class _RoomCpProgressData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static int _levelIndex(String levelText) {
|
static int _levelIndex(String levelText) {
|
||||||
final levelMatch = RegExp(
|
return scCpRelationLevelIndex(levelText);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static int _nextLevelIndex(int level) {
|
static int _nextLevelIndex(int level) {
|
||||||
|
|||||||
@ -61,6 +61,15 @@ void main() {
|
|||||||
expect(scCpRelationTypeForGift(cpGift), 'CP');
|
expect(scCpRelationTypeForGift(cpGift), 'CP');
|
||||||
expect(scIsCpGift(sisterGift), isTrue);
|
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', () {
|
group('SCSystemInvitMessageRes avatar frame', () {
|
||||||
@ -87,6 +96,18 @@ void main() {
|
|||||||
expect(invite?.relationType, 'BROTHER');
|
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', () {
|
test('infers relation type from gift payload when needed', () {
|
||||||
final invite = SCSystemInvitMessageRes.fromMessageData({
|
final invite = SCSystemInvitMessageRes.fromMessageData({
|
||||||
'content': {
|
'content': {
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:yumi/shared/business_logic/models/res/login_res.dart';
|
import 'package:yumi/shared/business_logic/models/res/login_res.dart';
|
||||||
|
import 'package:yumi/shared/tools/sc_cp_relation_level_utils.dart';
|
||||||
import 'package:yumi/shared/tools/sc_cp_relation_pair_utils.dart';
|
import 'package:yumi/shared/tools/sc_cp_relation_pair_utils.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
@ -62,4 +63,20 @@ void main() {
|
|||||||
expect(scCpRelationTypeBetweenProfiles(left, right), isNull);
|
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,
|
'cpUserId': null,
|
||||||
'relationshipStatus': 'NONE',
|
'relationshipStatus': 'NONE',
|
||||||
'intimacyValue': 0,
|
'intimacyValue': 0,
|
||||||
|
'dismissConsumeGold': 88,
|
||||||
'levels': [
|
'levels': [
|
||||||
{
|
{
|
||||||
'level': 1,
|
'level': 1,
|
||||||
@ -32,6 +33,7 @@ void main() {
|
|||||||
expect(config.cpUserId, isNull);
|
expect(config.cpUserId, isNull);
|
||||||
expect(config.relationshipStatus, 'NONE');
|
expect(config.relationshipStatus, 'NONE');
|
||||||
expect(config.intimacyValue, 0);
|
expect(config.intimacyValue, 0);
|
||||||
|
expect(config.dismissConsumeGold, 88);
|
||||||
expect(config.levels, hasLength(1));
|
expect(config.levels, hasLength(1));
|
||||||
expect(config.levels.first.levelName, 'CP 1级');
|
expect(config.levels.first.levelName, 'CP 1级');
|
||||||
expect(config.levels.first.requiredIntimacyValue, 0);
|
expect(config.levels.first.requiredIntimacyValue, 0);
|
||||||
@ -49,6 +51,7 @@ void main() {
|
|||||||
'cpUserId': 123,
|
'cpUserId': 123,
|
||||||
'relationshipStatus': 'ACTIVE',
|
'relationshipStatus': 'ACTIVE',
|
||||||
'intimacyValue': 1200,
|
'intimacyValue': 1200,
|
||||||
|
'dismissConsumeGold': 120,
|
||||||
'levels': [
|
'levels': [
|
||||||
{
|
{
|
||||||
'level': 2,
|
'level': 2,
|
||||||
@ -65,6 +68,7 @@ void main() {
|
|||||||
expect(config.cpUserId, '123');
|
expect(config.cpUserId, '123');
|
||||||
expect(config.relationshipStatus, 'ACTIVE');
|
expect(config.relationshipStatus, 'ACTIVE');
|
||||||
expect(config.intimacyValue, 1200);
|
expect(config.intimacyValue, 1200);
|
||||||
|
expect(config.dismissConsumeGold, 120);
|
||||||
expect(config.levels.first.level, 2);
|
expect(config.levels.first.level, 2);
|
||||||
expect(config.levels.first.unlocked, true);
|
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:26:58 准备热加载进场弹窗候选兜底逻辑,并通过前后台切换触发一次 entry popup 验证日志。
|
||||||
- [ ] 2026-05-22 17:31:21 当前 iPhone 17 Pro flutter run 会话已停止,避免后台继续占用;候选兜底逻辑已通过 flutter analyze,未再做前后台日志复验。
|
- [ ] 2026-05-22 17:31:21 当前 iPhone 17 Pro flutter run 会话已停止,避免后台继续占用;候选兜底逻辑已通过 flutter analyze,未再做前后台日志复验。
|
||||||
- [x] 2026-05-22 17:32:46 本轮完成:首充奖励按有效接口 item 动态渲染;启动弹窗顺序调整为注册奖励 > 每日签到 > 进场弹窗 > 首充弹窗;进场弹窗接口已接入并支持路径降级与候选图片兜底。
|
- [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