- 会员控制功能

- bug修复
This commit is contained in:
linshujie 2026-09-10 18:10:24 +08:00
parent f0a0b3d33f
commit 44420e6cab
85 changed files with 1376 additions and 295 deletions

View File

@ -255,7 +255,6 @@
30D74BF42FEB6F5B0050EB2C /* LocationPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D74BF12FEB6F5B0050EB2C /* LocationPickerView.swift */; }; 30D74BF42FEB6F5B0050EB2C /* LocationPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D74BF12FEB6F5B0050EB2C /* LocationPickerView.swift */; };
30D74D1F2FEBB09B0050EB2C /* CreateScheduleVM.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D74D1E2FEBB09B0050EB2C /* CreateScheduleVM.swift */; }; 30D74D1F2FEBB09B0050EB2C /* CreateScheduleVM.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D74D1E2FEBB09B0050EB2C /* CreateScheduleVM.swift */; };
30D87CDB2FDFA9EE00E958FD /* MQTTService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D87CDA2FDFA9EE00E958FD /* MQTTService.swift */; }; 30D87CDB2FDFA9EE00E958FD /* MQTTService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D87CDA2FDFA9EE00E958FD /* MQTTService.swift */; };
55C1A0113034101000786011 /* PhoneStatusCollector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55C1A0103034101000786010 /* PhoneStatusCollector.swift */; };
30D87CDD2FDFF07500E958FD /* InteractionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D87CDC2FDFF07500E958FD /* InteractionView.swift */; }; 30D87CDD2FDFF07500E958FD /* InteractionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D87CDC2FDFF07500E958FD /* InteractionView.swift */; };
30D87CDF2FDFF1A100E958FD /* QuickMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D87CDE2FDFF1A100E958FD /* QuickMessageView.swift */; }; 30D87CDF2FDFF1A100E958FD /* QuickMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D87CDE2FDFF1A100E958FD /* QuickMessageView.swift */; };
30D87D042FE1336300E958FD /* NavigationVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D87D012FE1336300E958FD /* NavigationVC.swift */; }; 30D87D042FE1336300E958FD /* NavigationVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D87D012FE1336300E958FD /* NavigationVC.swift */; };
@ -315,7 +314,6 @@
55B217B13022F0A000784711 /* GroupItineraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B217B03022F0A000784710 /* GroupItineraryView.swift */; }; 55B217B13022F0A000784711 /* GroupItineraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B217B03022F0A000784710 /* GroupItineraryView.swift */; };
55B217C13022F0A000784721 /* MinePhotoWallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B217C03022F0A000784720 /* MinePhotoWallView.swift */; }; 55B217C13022F0A000784721 /* MinePhotoWallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B217C03022F0A000784720 /* MinePhotoWallView.swift */; };
55B217C33022F0A000784723 /* MemberPhoneReportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B217C23022F0A000784722 /* MemberPhoneReportView.swift */; }; 55B217C33022F0A000784723 /* MemberPhoneReportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B217C23022F0A000784722 /* MemberPhoneReportView.swift */; };
55C1A0213034102000786021 /* PrivilegeDeniedOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55C1A0203034102000786020 /* PrivilegeDeniedOverlay.swift */; };
55B217C53022F0A000784725 /* MemberAppUsageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B217C43022F0A000784724 /* MemberAppUsageView.swift */; }; 55B217C53022F0A000784725 /* MemberAppUsageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B217C43022F0A000784724 /* MemberAppUsageView.swift */; };
55B217D13022F0A000784727 /* UnlockCountManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B217D03022F0A000784726 /* UnlockCountManager.swift */; }; 55B217D13022F0A000784727 /* UnlockCountManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B217D03022F0A000784726 /* UnlockCountManager.swift */; };
55B217D23023010000784731 /* TodayTrackDetailVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B217D13023010000784730 /* TodayTrackDetailVC.swift */; }; 55B217D23023010000784731 /* TodayTrackDetailVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B217D13023010000784730 /* TodayTrackDetailVC.swift */; };
@ -379,6 +377,8 @@
55B261010000000100786601 /* PrivacySettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B261000000000100786600 /* PrivacySettingsViewController.swift */; }; 55B261010000000100786601 /* PrivacySettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B261000000000100786600 /* PrivacySettingsViewController.swift */; };
55BF752D2FFE53F70055DA57 /* LocationPermissionPopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55BF752C2FFE53F70055DA57 /* LocationPermissionPopView.swift */; }; 55BF752D2FFE53F70055DA57 /* LocationPermissionPopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55BF752C2FFE53F70055DA57 /* LocationPermissionPopView.swift */; };
55BF75352FFF91690055DA57 /* InteractionEmojiCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55BF75342FFF91690055DA57 /* InteractionEmojiCell.swift */; }; 55BF75352FFF91690055DA57 /* InteractionEmojiCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55BF75342FFF91690055DA57 /* InteractionEmojiCell.swift */; };
55C1A0113034101000786011 /* PhoneStatusCollector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55C1A0103034101000786010 /* PhoneStatusCollector.swift */; };
55C1A0213034102000786021 /* PrivilegeDeniedOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55C1A0203034102000786020 /* PrivilegeDeniedOverlay.swift */; };
55C219C13024C00100784731 /* SearchLocationHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55C219C13024C00100784730 /* SearchLocationHeaderView.swift */; }; 55C219C13024C00100784731 /* SearchLocationHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55C219C13024C00100784730 /* SearchLocationHeaderView.swift */; };
55C4C0242FDB91C8009215C2 /* CheckPermissionGuideVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55C4C0232FDB91C8009215C2 /* CheckPermissionGuideVC.swift */; }; 55C4C0242FDB91C8009215C2 /* CheckPermissionGuideVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55C4C0232FDB91C8009215C2 /* CheckPermissionGuideVC.swift */; };
60BEE25E61F3DE24976A7A47 /* ManagedSettingsUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FE44DF1C46BDA7B98BC7859 /* ManagedSettingsUI.framework */; }; 60BEE25E61F3DE24976A7A47 /* ManagedSettingsUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FE44DF1C46BDA7B98BC7859 /* ManagedSettingsUI.framework */; };
@ -706,7 +706,6 @@
30D74BF12FEB6F5B0050EB2C /* LocationPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationPickerView.swift; sourceTree = "<group>"; }; 30D74BF12FEB6F5B0050EB2C /* LocationPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationPickerView.swift; sourceTree = "<group>"; };
30D74D1E2FEBB09B0050EB2C /* CreateScheduleVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateScheduleVM.swift; sourceTree = "<group>"; }; 30D74D1E2FEBB09B0050EB2C /* CreateScheduleVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateScheduleVM.swift; sourceTree = "<group>"; };
30D87CDA2FDFA9EE00E958FD /* MQTTService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MQTTService.swift; sourceTree = "<group>"; }; 30D87CDA2FDFA9EE00E958FD /* MQTTService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MQTTService.swift; sourceTree = "<group>"; };
55C1A0103034101000786010 /* PhoneStatusCollector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhoneStatusCollector.swift; sourceTree = "<group>"; };
30D87CDC2FDFF07500E958FD /* InteractionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractionView.swift; sourceTree = "<group>"; }; 30D87CDC2FDFF07500E958FD /* InteractionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractionView.swift; sourceTree = "<group>"; };
30D87CDE2FDFF1A100E958FD /* QuickMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickMessageView.swift; sourceTree = "<group>"; }; 30D87CDE2FDFF1A100E958FD /* QuickMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickMessageView.swift; sourceTree = "<group>"; };
30D87D012FE1336300E958FD /* NavigationVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationVC.swift; sourceTree = "<group>"; }; 30D87D012FE1336300E958FD /* NavigationVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationVC.swift; sourceTree = "<group>"; };
@ -770,7 +769,6 @@
55B217B03022F0A000784710 /* GroupItineraryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupItineraryView.swift; sourceTree = "<group>"; }; 55B217B03022F0A000784710 /* GroupItineraryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupItineraryView.swift; sourceTree = "<group>"; };
55B217C03022F0A000784720 /* MinePhotoWallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MinePhotoWallView.swift; sourceTree = "<group>"; }; 55B217C03022F0A000784720 /* MinePhotoWallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MinePhotoWallView.swift; sourceTree = "<group>"; };
55B217C23022F0A000784722 /* MemberPhoneReportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberPhoneReportView.swift; sourceTree = "<group>"; }; 55B217C23022F0A000784722 /* MemberPhoneReportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberPhoneReportView.swift; sourceTree = "<group>"; };
55C1A0203034102000786020 /* PrivilegeDeniedOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivilegeDeniedOverlay.swift; sourceTree = "<group>"; };
55B217C43022F0A000784724 /* MemberAppUsageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberAppUsageView.swift; sourceTree = "<group>"; }; 55B217C43022F0A000784724 /* MemberAppUsageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberAppUsageView.swift; sourceTree = "<group>"; };
55B217D03022F0A000784726 /* UnlockCountManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnlockCountManager.swift; sourceTree = "<group>"; }; 55B217D03022F0A000784726 /* UnlockCountManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnlockCountManager.swift; sourceTree = "<group>"; };
55B217D13023010000784730 /* TodayTrackDetailVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodayTrackDetailVC.swift; sourceTree = "<group>"; }; 55B217D13023010000784730 /* TodayTrackDetailVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodayTrackDetailVC.swift; sourceTree = "<group>"; };
@ -834,6 +832,8 @@
55B261000000000100786600 /* PrivacySettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacySettingsViewController.swift; sourceTree = "<group>"; }; 55B261000000000100786600 /* PrivacySettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacySettingsViewController.swift; sourceTree = "<group>"; };
55BF752C2FFE53F70055DA57 /* LocationPermissionPopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationPermissionPopView.swift; sourceTree = "<group>"; }; 55BF752C2FFE53F70055DA57 /* LocationPermissionPopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationPermissionPopView.swift; sourceTree = "<group>"; };
55BF75342FFF91690055DA57 /* InteractionEmojiCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractionEmojiCell.swift; sourceTree = "<group>"; }; 55BF75342FFF91690055DA57 /* InteractionEmojiCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractionEmojiCell.swift; sourceTree = "<group>"; };
55C1A0103034101000786010 /* PhoneStatusCollector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhoneStatusCollector.swift; sourceTree = "<group>"; };
55C1A0203034102000786020 /* PrivilegeDeniedOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivilegeDeniedOverlay.swift; sourceTree = "<group>"; };
55C219C13024C00100784730 /* SearchLocationHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchLocationHeaderView.swift; sourceTree = "<group>"; }; 55C219C13024C00100784730 /* SearchLocationHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchLocationHeaderView.swift; sourceTree = "<group>"; };
55C4C0232FDB91C8009215C2 /* CheckPermissionGuideVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckPermissionGuideVC.swift; sourceTree = "<group>"; }; 55C4C0232FDB91C8009215C2 /* CheckPermissionGuideVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckPermissionGuideVC.swift; sourceTree = "<group>"; };
5E5F5DC8694A001BF0C47147 /* PairGuideStepsView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PairGuideStepsView.swift; path = AppRestrict/PairGuideStepsView.swift; sourceTree = "<group>"; }; 5E5F5DC8694A001BF0C47147 /* PairGuideStepsView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PairGuideStepsView.swift; path = AppRestrict/PairGuideStepsView.swift; sourceTree = "<group>"; };
@ -2937,7 +2937,7 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual; CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 2;
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = LRDLWZ2Y83; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = LRDLWZ2Y83;
ENABLE_USER_SCRIPT_SANDBOXING = "$(inherited)"; ENABLE_USER_SCRIPT_SANDBOXING = "$(inherited)";
@ -2992,7 +2992,7 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual; CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 2;
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = LRDLWZ2Y83; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = LRDLWZ2Y83;
ENABLE_USER_SCRIPT_SANDBOXING = "$(inherited)"; ENABLE_USER_SCRIPT_SANDBOXING = "$(inherited)";
@ -3225,7 +3225,7 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual; CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 2;
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = LRDLWZ2Y83; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = LRDLWZ2Y83;
ENABLE_USER_SCRIPT_SANDBOXING = "$(inherited)"; ENABLE_USER_SCRIPT_SANDBOXING = "$(inherited)";

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "vip_required@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "vip_required@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

View File

@ -0,0 +1,9 @@
{
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"provides-namespace" : true
}
}

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "app_usage@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "app_usage@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "phone_report@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "phone_report@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "today_track@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "today_track@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View File

@ -0,0 +1,9 @@
{
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"provides-namespace" : true
}
}

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "crown@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "crown@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 681 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "limited_offer@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "limited_offer@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "media_locked_bg@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "media_locked_bg@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "narrow_button_bg@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "narrow_button_bg@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "realtime_location@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "realtime_location@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "wide_button_bg@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "wide_button_bg@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

View File

@ -15,6 +15,7 @@ class AppContextManager: NSObject {
var systemConfig: SystemConfigModel? var systemConfig: SystemConfigModel?
var imToken: String? var imToken: String?
var imTokenUserId: String?
var account: UserConfigModel? { var account: UserConfigModel? {
if let loginAccount = loginAccount { if let loginAccount = loginAccount {
@ -129,6 +130,8 @@ class AppContextManager: NSObject {
Defaults[\.userConfig] = nil Defaults[\.userConfig] = nil
Defaults[\.defaultGroupKey] = "" Defaults[\.defaultGroupKey] = ""
loginAccount = nil loginAccount = nil
imToken = nil
imTokenUserId = nil
} }
} }

View File

@ -127,7 +127,11 @@ extension ApiManager {
DLToast.show(text: combineMessage) DLToast.show(text: combineMessage)
} }
} }
return .failure(handleError(with: code, domain: "Data Error", message: combineMessage, data: response)) return .failure(handleError(with: code,
domain: "Data Error",
message: combineMessage,
isShowTip: code != GatewayStatusCode.needVip.rawValue,
data: response))
case let .failure(error): case let .failure(error):
let message = error.gatewayMessage ?? "请检查网络连接" let message = error.gatewayMessage ?? "请检查网络连接"
if error.innerError?.code == NSURLErrorCancelled { if error.innerError?.code == NSURLErrorCancelled {
@ -181,10 +185,15 @@ extension ApiManager {
// MARK: - VIP // MARK: - VIP
private func handleNeedVip(_ msg: String) { private func handleNeedVip(_ msg: String) {
let message = msg
.split(separator: ",", maxSplits: 1, omittingEmptySubsequences: false)
.first
.map(String.init)?
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
MainAsync { MainAsync {
ConfirmPopVC.showAlert(title: "温馨提示", message: msg, confirmText: "去开通", confirmBlock: { UIViewController.showConfirmPop(title: "温馨提示", message: message, confirmText: "去开通", confirmBlock: {
AppRouter.push(Route.vipRecharge) AppRouter.push(Route.vipRecharge)
}) }, cancelText: "取消")
} }
} }
} }

View File

@ -92,6 +92,7 @@ struct GroupUsersResponse: BaseModelProtocol {
var message: String? var message: String?
var model: GroupInfoModel? var model: GroupInfoModel?
var list: [GroupMemberModel] = [] var list: [GroupMemberModel] = []
var vip = GroupMemberVip()
var reviewCount: Int = 0 var reviewCount: Int = 0
init?(map: Map) {} init?(map: Map) {}
@ -101,6 +102,7 @@ struct GroupUsersResponse: BaseModelProtocol {
message <- map["message"] message <- map["message"]
model <- map["data.group"] model <- map["data.group"]
list <- map["data.mates"] list <- map["data.mates"]
vip <- map["data.vip"]
reviewCount <- (map["data.reviewCount"], kStrTransformInt) reviewCount <- (map["data.reviewCount"], kStrTransformInt)
if reviewCount == 0 { if reviewCount == 0 {
reviewCount <- (map["data.review_count"], kStrTransformInt) reviewCount <- (map["data.review_count"], kStrTransformInt)
@ -286,6 +288,27 @@ struct GroupPrivilege: Mappable, Equatable {
} }
} }
struct GroupMemberVip: Mappable, Equatable {
var app_usage_record: Bool?
var phone_report: Bool?
var trip_analysis_others: Bool?
var app_lock: Bool?
var pigeon_mail: Bool?
var view_phone: Bool?
init() {}
init?(map: Map) {}
mutating func mapping(map: Map) {
app_usage_record <- (map["app_usage_record"], kOptionalFlexibleBool)
phone_report <- (map["phone_report"], kOptionalFlexibleBool)
trip_analysis_others <- (map["trip_analysis_others"], kOptionalFlexibleBool)
app_lock <- (map["app_lock"], kOptionalFlexibleBool)
pigeon_mail <- (map["pigeon_mail"], kOptionalFlexibleBool)
view_phone <- (map["view_phone"], kOptionalFlexibleBool)
}
}
private let kFlexibleBool = TransformOf<Bool, Any>(fromJSON: { value in private let kFlexibleBool = TransformOf<Bool, Any>(fromJSON: { value in
switch value { switch value {
case let value as Bool: case let value as Bool:
@ -304,6 +327,31 @@ private let kFlexibleBool = TransformOf<Bool, Any>(fromJSON: { value in
value value
}) })
private let kOptionalFlexibleBool = TransformOf<Bool, Any>(fromJSON: { value in
switch value {
case let value as Bool:
return value
case let value as NSNumber:
return value.boolValue
case let value as Int:
return value != 0
case let value as String:
let text = value.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
switch text {
case "1", "true", "yes":
return true
case "0", "false", "no":
return false
default:
return nil
}
default:
return nil
}
}, toJSON: { value in
value
})
extension UIImageView { extension UIImageView {
@MainActor @MainActor
func setGroupIcon(_ model: GroupInfoModel) { func setGroupIcon(_ model: GroupInfoModel) {
@ -424,7 +472,13 @@ struct GroupMemberModel: Mappable, Equatable {
/// 1 2 3 4 /// 1 2 3 4
var level: Int = 1 var level: Int = 1
var shouldShowVipAvatarFrame: Bool {
AppContextManager.shared.systemConfig?.isIntercept != true && level != 1
}
var vipIcon: UIImage { var vipIcon: UIImage {
guard AppContextManager.shared.systemConfig?.isIntercept != true else {
return UIImage()
}
switch level { switch level {
case 2: // case 2: //
return UIImage(named: "VipIcon/2") ?? UIImage() return UIImage(named: "VipIcon/2") ?? UIImage()

View File

@ -135,7 +135,7 @@ final class AppRestrictVC: BaseViewController {
.subscribe(onNext: { _ in .subscribe(onNext: { _ in
DLToast.show(text: "配对成功") DLToast.show(text: "配对成功")
}, onError: { error in }, onError: { error in
DLToast.show(text: error.gatewayMessage ?? error.localizedDescription) DLToast.show(error)
}) })
.disposed(by: disposeBag) .disposed(by: disposeBag)
} }
@ -278,7 +278,7 @@ final class AppRestrictVC: BaseViewController {
DLToast.show(text: "删除成功") DLToast.show(text: "删除成功")
}, onError: { [weak self] error in }, onError: { [weak self] error in
self?.deletingTokenStrings.remove(tokenString) self?.deletingTokenStrings.remove(tokenString)
DLToast.show(text: error.gatewayMessage ?? error.localizedDescription) DLToast.show(error)
}) })
.disposed(by: UserService.disposeBag) .disposed(by: UserService.disposeBag)
} }

View File

@ -35,7 +35,8 @@ final class FeatureIntroVC: BaseViewController {
super.viewDidAppear(animated) super.viewDidAppear(animated)
rootView.reloadFeatureItems() rootView.reloadFeatureItems()
guard let config = AppContextManager.shared.systemConfig else { return } guard AppContextManager.shared.systemConfig?.isIntercept == false,
let config = AppContextManager.shared.systemConfig else { return }
bannerList = config.bannerList bannerList = config.bannerList
rootView.cycleScrollView.isHidden = config.bannerList.filter { $0.enable == true }.count == 0 rootView.cycleScrollView.isHidden = config.bannerList.filter { $0.enable == true }.count == 0
rootView.cycleScrollView.imageURLStringsGroup = config.bannerList.filter { $0.enable == true }.map { $0.img } rootView.cycleScrollView.imageURLStringsGroup = config.bannerList.filter { $0.enable == true }.map { $0.img }

View File

@ -96,7 +96,7 @@ class CreateGroupViewModel {
self?.groupType = response.list[0].type self?.groupType = response.list[0].type
self?.groupTemplates.onNext(response.list) self?.groupTemplates.onNext(response.list)
}, onError: { error in }, onError: { error in
DLToast.showError(text: error.localizedDescription) DLToast.showError(error)
}) })
.disposed(by: disposeBag) .disposed(by: disposeBag)
} }
@ -135,7 +135,7 @@ class CreateGroupViewModel {
CreateGroupVipPopView.show() CreateGroupVipPopView.show()
} }
else { else {
DLToast.show(text: error.localizedDescription) DLToast.show(error)
} }
}).disposed(by: disposeBag) }).disposed(by: disposeBag)
} }

View File

@ -96,16 +96,7 @@ final class GroupChatVC: BaseViewController {
setupKeyboard() setupKeyboard()
IQKeyboardManager.shared.isEnabled = false IQKeyboardManager.shared.isEnabled = false
IQKeyboardManager.shared.resignOnTouchOutside = false IQKeyboardManager.shared.resignOnTouchOutside = false
rootView.updateVipMediaAccess(isUnlocked: isVipUnlocked)
//
// let isCircleOwner = GroupOwnerMatcher.matches(
// userId: AppContextManager.shared.userId,
// groupKey: viewModel.groupId
// )
// let canSend = isCircleOwner || AppContextManager.shared.vip > 1
// rootView.disableIMView.isHidden = canSend
// rootView.quickActionBar.isUserInteractionEnabled = canSend
// rootView.quickActionBar.alpha = canSend ? 1 : 0.35
} }
override func viewWillDisappear(_ animated: Bool) { override func viewWillDisappear(_ animated: Bool) {
@ -581,6 +572,22 @@ final class GroupChatVC: BaseViewController {
} }
// MARK: - Actions // MARK: - Actions
private var isVipUnlocked: Bool {
guard AppContextManager.shared.systemConfig?.isIntercept == false else {
return false
}
return AppContextManager.shared.vip > 1
}
private func requireVipForMedia() -> Bool {
guard isVipUnlocked else {
AppRouter.push(Route.vipRecharge)
return false
}
return true
}
private func reactiveAction() { private func reactiveAction() {
rootView.backBtn.rx.tap rootView.backBtn.rx.tap
.subscribe(onNext: { _ in .subscribe(onNext: { _ in
@ -630,7 +637,7 @@ final class GroupChatVC: BaseViewController {
rootView.galleryBtn.rx.tap rootView.galleryBtn.rx.tap
.subscribe(onNext: { [weak self] in .subscribe(onNext: { [weak self] in
guard let self = self else { return } guard let self, self.requireVipForMedia() else { return }
self.rootView.dismissAllPanels() self.rootView.dismissAllPanels()
self.showAlbum() self.showAlbum()
}) })
@ -638,8 +645,9 @@ final class GroupChatVC: BaseViewController {
rootView.cameraBtn.rx.tap rootView.cameraBtn.rx.tap
.subscribe(onNext: { [weak self] in .subscribe(onNext: { [weak self] in
self?.rootView.dismissAllPanels() guard let self, self.requireVipForMedia() else { return }
self?.showCamera() self.rootView.dismissAllPanels()
self.showCamera()
}) })
.disposed(by: disposeBag) .disposed(by: disposeBag)
@ -699,16 +707,19 @@ final class GroupChatVC: BaseViewController {
// MARK: - // MARK: -
private func showAlbum() { private func showAlbum() {
guard requireVipForMedia() else { return }
let picker = PhotoPickerVC(maxCount: 9) let picker = PhotoPickerVC(maxCount: 9)
picker.onConfirm = { [weak self] images in picker.onConfirm = { [weak self] images in
guard let self, self.requireVipForMedia() else { return }
for image in images { for image in images {
self?.sendImageMessage(image) self.sendImageMessage(image)
} }
} }
present(picker, animated: true) present(picker, animated: true)
} }
private func showCamera() { private func showCamera() {
guard requireVipForMedia() else { return }
guard UIImagePickerController.isSourceTypeAvailable(.camera) else { guard UIImagePickerController.isSourceTypeAvailable(.camera) else {
DLToast.showError(text: "当前设备不支持拍照") DLToast.showError(text: "当前设备不支持拍照")
return return
@ -748,6 +759,11 @@ final class GroupChatVC: BaseViewController {
private var currentVoiceHover: VoiceRecordView.HoverTarget = .none private var currentVoiceHover: VoiceRecordView.HoverTarget = .none
private func enterVoiceInputMode() { private func enterVoiceInputMode() {
guard isVipUnlocked else {
rootView.dismissAllPanels()
rootView.setVoiceInputMode(true)
return
}
let status = AVCaptureDevice.authorizationStatus(for: .audio) let status = AVCaptureDevice.authorizationStatus(for: .audio)
switch status { switch status {
case .authorized: case .authorized:
@ -831,6 +847,7 @@ final class GroupChatVC: BaseViewController {
} }
private func startVoiceRecording() { private func startVoiceRecording() {
guard requireVipForMedia() else { return }
guard !isVoiceRecording else { return } guard !isVoiceRecording else { return }
AudioServicesPlaySystemSound(1519) AudioServicesPlaySystemSound(1519)
currentVoiceHover = .none currentVoiceHover = .none
@ -945,28 +962,30 @@ final class GroupChatVC: BaseViewController {
} }
private func sendVoiceMessage(url: URL, durationMs: Int) { private func sendVoiceMessage(url: URL, durationMs: Int) {
let msg = OIMMessageInfo.createSoundMessage(fromFullPath: url.path, duration: durationMs) guard requireVipForMedia() else {
viewModel.cacheMessage(msg) deleteRecordFile()
OIMManager.manager.sendMessage(msg, rootView.voiceRecordView.reset()
recvID: "", return
groupID: viewModel.groupId, }
offlinePushInfo: nil, GroupIMService.shared.sendGroupMessage(groupId: viewModel.groupId,
onSuccess: { [weak self] returnedMsg in makeMessage: {
OIMMessageInfo.createSoundMessage(fromFullPath: url.path, duration: durationMs)
},
onPrepared: { [weak self] message in
self?.viewModel.cacheMessage(message)
},
onSuccess: { [weak self] sentMessage in
DispatchQueue.main.async { DispatchQueue.main.async {
guard let self else { return } guard let self else { return }
let sentMessage = returnedMsg ?? msg
if sentMessage.soundElem == nil {
sentMessage.soundElem = msg.soundElem
}
sentMessage.soundElem?.soundPath = url.path sentMessage.soundElem?.soundPath = url.path
self.viewModel.onReceiveMessage(sentMessage) self.viewModel.onReceiveMessage(sentMessage)
} }
}, },
onProgress: nil as OIMNumberCallback?,
onFailure: { [weak self] code, errMsg in onFailure: { [weak self] code, errMsg in
print("Voice send failed: \(code) \(errMsg ?? "")") print("Voice send failed: \(code) \(errMsg ?? "")")
DispatchQueue.main.async { DispatchQueue.main.async {
self?.dl.show(text: (errMsg?.isEmpty == false ? errMsg : "语音发送失败,请重试") ?? "语音发送失败,请重试") let text = GroupIMService.shared.displaySendError(errMsg, fallback: "语音发送失败,请重试")
self?.dl.show(text: text)
} }
}) })
} }
@ -1014,7 +1033,7 @@ final class GroupChatVC: BaseViewController {
}, onError: { [weak self] error in }, onError: { [weak self] error in
guard let self, self.groupInfoRequestID == requestID else { return } guard let self, self.groupInfoRequestID == requestID else { return }
if showError { if showError {
DLToast.showError(text: error.localizedDescription) DLToast.showError(error)
} }
}) })
.disposed(by: disposeBag) .disposed(by: disposeBag)
@ -1186,6 +1205,7 @@ private class MessageListenerProxy: NSObject, OIMAdvancedMsgListener {
extension GroupChatVC { extension GroupChatVC {
private func sendImageMessage(_ image: UIImage) { private func sendImageMessage(_ image: UIImage) {
guard requireVipForMedia() else { return }
let sendImage = image.cgImage != nil ? image.fixOrientation() : image let sendImage = image.cgImage != nil ? image.fixOrientation() : image
guard let data = sendImage.jpegData(compressionQuality: 0.8) else { return } guard let data = sendImage.jpegData(compressionQuality: 0.8) else { return }
let dir = NSTemporaryDirectory() let dir = NSTemporaryDirectory()
@ -1195,18 +1215,21 @@ extension GroupChatVC {
let displaySize = ChatImageLayout.displaySize(width: sendImage.size.width, height: sendImage.size.height) let displaySize = ChatImageLayout.displaySize(width: sendImage.size.width, height: sendImage.size.height)
let msg = OIMMessageInfo.createImageMessage(fromFullPath: fileURL.path) var localId = ""
// 使 SDK clientMsgID ID便 GroupIMService.shared.sendGroupMessage(groupId: viewModel.groupId,
let localId = msg.clientMsgID ?? UUID().uuidString makeMessage: {
viewModel.cacheMessage(msg) OIMMessageInfo.createImageMessage(fromFullPath: fileURL.path)
},
// loading onPrepared: { [weak self] message in
let localMsg = ChatMessage( guard let self else { return }
localId = message.clientMsgID ?? UUID().uuidString
self.viewModel.cacheMessage(message)
let localMessage = ChatMessage(
id: localId, id: localId,
isSelf: true, isSelf: true,
senderId: AppContextManager.shared.userId, senderId: AppContextManager.shared.userId,
avatar: viewModel.getUserAvatar(id: AppContextManager.shared.userId), avatar: self.viewModel.getUserAvatar(id: AppContextManager.shared.userId),
headPic: viewModel.getUserHeadPic(id: AppContextManager.shared.userId), headPic: self.viewModel.getUserHeadPic(id: AppContextManager.shared.userId),
senderName: AppContextManager.shared.name, senderName: AppContextManager.shared.name,
content: "", content: "",
voiceUrl: "", voiceUrl: "",
@ -1217,17 +1240,13 @@ extension GroupChatVC {
showTime: false, showTime: false,
isUploading: true isUploading: true
) )
viewModel.appendLocalMessage(.imageSend(localMsg)) self.viewModel.appendLocalMessage(.imageSend(localMessage))
OIMManager.manager.sendMessage(msg, },
recvID: "", onSuccess: { [weak self] sentMessage in
groupID: viewModel.groupId,
offlinePushInfo: nil,
onSuccess: { [weak self] returnedMsg in
// URLloading // URLloading
// returnedMsg SDK OIMMessageInfo URL let networkUrl = sentMessage.pictureElem?.sourcePicture?.url
let networkUrl = returnedMsg?.pictureElem?.sourcePicture?.url ?? sentMessage.pictureElem?.bigPicture?.url
?? returnedMsg?.pictureElem?.bigPicture?.url ?? sentMessage.pictureElem?.sourcePath
?? returnedMsg?.pictureElem?.sourcePath
?? "" ?? ""
self?.viewModel.updateLocalMessage(id: localId) { chatMsg in self?.viewModel.updateLocalMessage(id: localId) { chatMsg in
// URL // URL
@ -1238,13 +1257,16 @@ extension GroupChatVC {
} }
try? FileManager.default.removeItem(at: fileURL) try? FileManager.default.removeItem(at: fileURL)
}, },
onProgress: nil as OIMNumberCallback?,
onFailure: { [weak self] code, errMsg in onFailure: { [weak self] code, errMsg in
print("Image send failed: \(code) \(errMsg ?? "")") print("Image send failed: \(code) \(errMsg ?? "")")
// loading // loading
self?.viewModel.updateLocalMessage(id: localId) { chatMsg in self?.viewModel.updateLocalMessage(id: localId) { chatMsg in
chatMsg.isUploading = false chatMsg.isUploading = false
} }
DispatchQueue.main.async {
let text = GroupIMService.shared.displaySendError(errMsg, fallback: "图片发送失败,请重试")
self?.dl.show(text: text)
}
}) })
} }
} }

View File

@ -161,6 +161,7 @@ class GroupChatView: UIView {
/// ///
private var isVoiceInputMode = false private var isVoiceInputMode = false
private var isVipMediaUnlocked = true
func setVoiceInputMode(_ enabled: Bool) { func setVoiceInputMode(_ enabled: Bool) {
isVoiceInputMode = enabled isVoiceInputMode = enabled
@ -171,9 +172,22 @@ class GroupChatView: UIView {
refreshComposer() refreshComposer()
} }
func updateVipMediaAccess(isUnlocked: Bool) {
isVipMediaUnlocked = isUnlocked
let lockedBackground = isUnlocked ? nil : UIImage(named: "VipFeature/media_locked_bg")
galleryBtn.setBackgroundImage(lockedBackground, for: .normal)
cameraBtn.setBackgroundImage(lockedBackground, for: .normal)
galleryBtn.backgroundColor = isUnlocked ? .white : .clear
cameraBtn.backgroundColor = isUnlocked ? .white : .clear
voiceBtn.setImage(UIImage(named: "IM/voice_input"), for: .normal)
textField.placeholder = "发消息或者按住说话"
refreshComposer()
}
func refreshComposer() { func refreshComposer() {
let hasText = !(textField.text ?? "").trimmingCharacters(in: .whitespacesAndNewlines).isEmpty let hasText = !(textField.text ?? "").trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
if isVoiceInputMode { if isVoiceInputMode {
updateHoldSpeakAppearance()
textField.isHidden = true textField.isHidden = true
emojiBtn.isHidden = true emojiBtn.isHidden = true
voiceBtn.isHidden = true voiceBtn.isHidden = true
@ -191,6 +205,22 @@ class GroupChatView: UIView {
layoutIfNeeded() layoutIfNeeded()
} }
private func updateHoldSpeakAppearance() {
var configuration = UIButton.Configuration.plain()
configuration.title = "按住 说话"
configuration.image = isVipMediaUnlocked ? nil : UIImage(named: "VipFeature/crown")
configuration.imagePlacement = .leading
configuration.imagePadding = isVipMediaUnlocked ? 0 : 10
configuration.contentInsets = .zero
configuration.baseForegroundColor = UIColor(hexStr: "#293445")
configuration.titleTextAttributesTransformer = UIConfigurationTextAttributesTransformer { attributes in
var attributes = attributes
attributes.font = .systemFont(ofSize: 16, weight: .medium)
return attributes
}
holdSpeakBtn.configuration = configuration
}
func dismissAllPanels(excludeTextField: Bool = false) { func dismissAllPanels(excludeTextField: Bool = false) {
let needsReset = !emojiPanelView.isHidden let needsReset = !emojiPanelView.isHidden
|| textField.isFirstResponder || textField.isFirstResponder

View File

@ -333,65 +333,62 @@ final class GroupChatViewModel {
let atAll = pendingAtAll let atAll = pendingAtAll
let mentions = pendingMentions let mentions = pendingMentions
let msg: OIMMessageInfo
if atAll {
msg = OIMMessageInfo.createText(atAllMessage: trimmed,
displayText: "@所有人",
message: quote)
} else if !mentions.isEmpty {
let ids = mentions.map(\.userId)
let infos: [OIMAtInfo] = mentions.map { m in
let info = OIMAtInfo()
info.atUserID = m.userId
info.groupNickname = m.nickname
return info
}
msg = OIMMessageInfo.createText(atMessage: trimmed,
atUsersID: ids,
atUsersInfo: infos,
message: quote)
} else if let quote {
msg = OIMMessageInfo.createQuoteMessage(trimmed, message: quote)
} else {
msg = OIMMessageInfo.createTextMessage(trimmed)
}
clearPendingQuote() clearPendingQuote()
clearPendingMentions() clearPendingMentions()
OIMManager.manager.sendMessage(msg, GroupIMService.shared.sendGroupMessage(groupId: groupId,
recvID: "", makeMessage: {
groupID: groupId, if atAll {
offlinePushInfo: nil, return OIMMessageInfo.createText(atAllMessage: trimmed,
onSuccess: { [weak self] _ in displayText: "@所有人",
self?.cacheMessage(msg) message: quote)
self?.appendMessage(msg) }
if !mentions.isEmpty {
let ids = mentions.map(\.userId)
let infos: [OIMAtInfo] = mentions.map { mention in
let info = OIMAtInfo()
info.atUserID = mention.userId
info.groupNickname = mention.nickname
return info
}
return OIMMessageInfo.createText(atMessage: trimmed,
atUsersID: ids,
atUsersInfo: infos,
message: quote)
}
if let quote {
return OIMMessageInfo.createQuoteMessage(trimmed, message: quote)
}
return OIMMessageInfo.createTextMessage(trimmed)
},
onSuccess: { [weak self] message in
self?.cacheMessage(message)
self?.appendMessage(message)
}, },
onProgress: nil as OIMNumberCallback?,
onFailure: { code, errMsg in onFailure: { code, errMsg in
print("send failed: \(code) \(errMsg ?? "")") print("send failed: \(code) \(errMsg ?? "")")
DispatchQueue.main.async { DispatchQueue.main.async {
DLToast.showError(text: errMsg ?? "发送失败") let text = GroupIMService.shared.displaySendError(errMsg, fallback: "消息发送失败,请重试")
DLToast.showError(text: text)
} }
}) })
} }
func sendLocation(_ location: ChatLocationPayload) { func sendLocation(_ location: ChatLocationPayload) {
let msg = OIMMessageInfo.createLocationMessage(location.encodedDescription, GroupIMService.shared.sendGroupMessage(groupId: groupId,
makeMessage: {
OIMMessageInfo.createLocationMessage(location.encodedDescription,
latitude: location.latitude, latitude: location.latitude,
longitude: location.longitude) longitude: location.longitude)
OIMManager.manager.sendMessage(msg,
recvID: "",
groupID: groupId,
offlinePushInfo: nil,
onSuccess: { [weak self] _ in
self?.cacheMessage(msg)
self?.appendMessage(msg)
}, },
onProgress: nil as OIMNumberCallback?, onSuccess: { [weak self] message in
self?.cacheMessage(message)
self?.appendMessage(message)
},
onFailure: { code, errMsg in onFailure: { code, errMsg in
DispatchQueue.main.async { DispatchQueue.main.async {
DLToast.showError(text: errMsg ?? "位置发送失败(\(code))") let text = GroupIMService.shared.displaySendError(errMsg, fallback: "位置发送失败,请重试")
DLToast.showError(text: text)
} }
}) })
} }

View File

@ -15,8 +15,12 @@ final class GroupIMService {
private var isInited = false private var isInited = false
private var isLogining = false private var isLogining = false
private var isLoggingOut = false
private var isSessionInvalidated = false
private var loginGeneration = 0
/// ///
private var pendingLoginCompletions: [(Bool) -> Void] = [] private var pendingLoginCompletions: [(userId: String, completion: (Bool) -> Void)] = []
private var pendingLogoutCompletions: [(Bool) -> Void] = []
private let disposeBag = DisposeBag() private let disposeBag = DisposeBag()
private init() {} private init() {}
@ -34,85 +38,273 @@ final class GroupIMService {
onConnecting: {}, onConnecting: {},
onConnectFailure: { _, _ in }, onConnectFailure: { _, _ in },
onConnectSuccess: {}, onConnectSuccess: {},
onKickedOffline: {}, onKickedOffline: {
onUserTokenExpired: {}, GroupIMService.shared.invalidateSession()
onUserTokenInvalid: { _ in }) },
onUserTokenExpired: {
GroupIMService.shared.invalidateSession()
},
onUserTokenInvalid: { _ in
GroupIMService.shared.invalidateSession()
})
} }
// MARK: - Login // MARK: - Login
/// token /// token
func ensureLogin(completion: @escaping (Bool) -> Void) { func ensureLogin(completion: @escaping (Bool) -> Void) {
login(completion: completion) runOnMain { [weak self] in
self?.login(completion: completion)
}
} }
func login(completion: @escaping (Bool) -> Void) { private func login(completion: @escaping (Bool) -> Void) {
if OIMManager.manager.getLoginStatus() == .logged {
completion(true)
return
}
if AppContextManager.shared.isGuest { if AppContextManager.shared.isGuest {
completion(false) completion(false)
return return
} }
let userId = AppContextManager.shared.userId let userId = AppContextManager.shared.userId.trimmingCharacters(in: .whitespacesAndNewlines)
guard !userId.isEmpty else { guard !userId.isEmpty else {
completion(false) completion(false)
return return
} }
pendingLoginCompletions.append(completion) if !isSessionInvalidated,
guard !isLogining else { return } OIMManager.manager.getLoginStatus() == .logged,
isLogining = true OIMManager.manager.getLoginUserID() == userId {
completion(true)
let existing = AppContextManager.shared.imToken?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
if !existing.isEmpty {
performSDKLogin(userId: userId, token: existing)
return return
} }
pendingLoginCompletions.append((userId, completion))
startLoginIfNeeded()
}
private func startLoginIfNeeded() {
guard !isLogining, !isLoggingOut, !pendingLoginCompletions.isEmpty else { return }
let userId = AppContextManager.shared.userId.trimmingCharacters(in: .whitespacesAndNewlines)
guard !AppContextManager.shared.isGuest, !userId.isEmpty else {
let callbacks = pendingLoginCompletions
pendingLoginCompletions.removeAll()
callbacks.forEach { $0.completion(false) }
return
}
let staleCompletions = pendingLoginCompletions.filter { $0.userId != userId }
pendingLoginCompletions.removeAll { $0.userId != userId }
staleCompletions.forEach { $0.completion(false) }
guard !pendingLoginCompletions.isEmpty else { return }
let status = OIMManager.manager.getLoginStatus()
if status == .logged {
if !isSessionInvalidated, OIMManager.manager.getLoginUserID() == userId {
finishLogin(success: true, userId: userId)
} else {
clearCachedToken()
loginGeneration += 1
beginSDKLogout()
}
return
}
if status == .logging {
clearCachedToken()
loginGeneration += 1
beginSDKLogout()
return
}
isLogining = true
loginGeneration += 1
let generation = loginGeneration
let existing = AppContextManager.shared.imToken?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
if !existing.isEmpty, AppContextManager.shared.imTokenUserId == userId {
performSDKLogin(userId: userId, token: existing, generation: generation, canRefreshToken: true)
return
}
clearCachedToken()
requestTokenAndLogin(userId: userId, generation: generation)
}
private func requestTokenAndLogin(userId: String, generation: Int) {
UserService.imToken() UserService.imToken()
.subscribe(onNext: { [weak self] response in .subscribe(onNext: { [weak self] response in
guard let self else { return } guard let self else { return }
guard let data = response.data, guard self.loginGeneration == generation else { return }
let token = data["token"] as? String, guard AppContextManager.shared.userId == userId else {
!token.isEmpty else { self.finishLogin(success: false, userId: userId)
self.finishLogin(success: false)
return return
} }
AppContextManager.shared.imToken = token guard let data = response.data,
self.performSDKLogin(userId: userId, token: token) let token = data["token"] as? String,
!token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
self.finishLogin(success: false, userId: userId)
return
}
let normalizedToken = token.trimmingCharacters(in: .whitespacesAndNewlines)
AppContextManager.shared.imToken = normalizedToken
AppContextManager.shared.imTokenUserId = userId
self.performSDKLogin(userId: userId,
token: normalizedToken,
generation: generation,
canRefreshToken: false)
}, onError: { [weak self] _ in }, onError: { [weak self] _ in
self?.finishLogin(success: false) guard let self, self.loginGeneration == generation else { return }
self.finishLogin(success: false, userId: userId)
}) })
.disposed(by: disposeBag) .disposed(by: disposeBag)
} }
private func performSDKLogin(userId: String, token: String) { private func performSDKLogin(userId: String,
token: String,
generation: Int,
canRefreshToken: Bool) {
OIMManager.manager.login(userId, token: token) { [weak self] _ in OIMManager.manager.login(userId, token: token) { [weak self] _ in
self?.finishLogin(success: true) guard let self, self.loginGeneration == generation else { return }
let isCurrentUser = AppContextManager.shared.userId == userId
&& OIMManager.manager.getLoginUserID() == userId
self.isSessionInvalidated = !isCurrentUser
self.finishLogin(success: isCurrentUser, userId: userId)
} onFailure: { [weak self] code, msg in } onFailure: { [weak self] code, msg in
guard let self, self.loginGeneration == generation else { return }
print("OpenIM login failed: \(code) \(msg ?? "")") print("OpenIM login failed: \(code) \(msg ?? "")")
self?.finishLogin(success: false) if canRefreshToken, AppContextManager.shared.userId == userId {
self.clearCachedToken()
self.requestTokenAndLogin(userId: userId, generation: generation)
} else {
self.finishLogin(success: false, userId: userId)
}
} }
} }
private func finishLogin(success: Bool) { private func finishLogin(success: Bool, userId: String) {
isLogining = false isLogining = false
let callbacks = pendingLoginCompletions let callbacks = pendingLoginCompletions.filter { $0.userId == userId }
pendingLoginCompletions.removeAll() pendingLoginCompletions.removeAll { $0.userId == userId }
callbacks.forEach { $0(success) } callbacks.forEach { $0.completion(success) }
startLoginIfNeeded()
} }
// MARK: - Logout // MARK: - Logout
func logout(completion: ((Bool) -> Void)? = nil) { func logout(completion: ((Bool) -> Void)? = nil) {
runOnMain { [weak self] in
guard let self else { return }
self.clearCachedToken()
self.isSessionInvalidated = true
self.loginGeneration += 1
self.isLogining = false
let loginCallbacks = self.pendingLoginCompletions
self.pendingLoginCompletions.removeAll()
loginCallbacks.forEach { $0.completion(false) }
if let completion {
self.pendingLogoutCompletions.append(completion)
}
self.beginSDKLogout()
}
}
private func beginSDKLogout() {
guard !isLoggingOut else { return }
guard OIMManager.manager.getLoginStatus() != .logout else {
finishLogout(success: true)
return
}
isLoggingOut = true
OIMManager.manager.logoutWith(onSuccess: { (_: String?) in OIMManager.manager.logoutWith(onSuccess: { (_: String?) in
completion?(true) GroupIMService.shared.finishLogout(success: true)
}, onFailure: { (code: Int, msg: String?) in }, onFailure: { [weak self] (code: Int, msg: String?) in
print("OpenIM logout failed: \(code) \(msg ?? "")") print("OpenIM logout failed: \(code) \(msg ?? "")")
completion?(false) self?.finishLogout(success: false)
}) })
} }
private func finishLogout(success: Bool) {
isLoggingOut = false
let callbacks = pendingLogoutCompletions
pendingLogoutCompletions.removeAll()
callbacks.forEach { $0(success) }
if success {
startLoginIfNeeded()
} else {
let loginCallbacks = pendingLoginCompletions
pendingLoginCompletions.removeAll()
loginCallbacks.forEach { $0.completion(false) }
}
}
private func clearCachedToken() {
AppContextManager.shared.imToken = nil
AppContextManager.shared.imTokenUserId = nil
}
private func invalidateSession() {
runOnMain { [weak self] in
self?.isSessionInvalidated = true
self?.clearCachedToken()
}
}
private func runOnMain(_ action: @escaping () -> Void) {
if Thread.isMainThread {
action()
} else {
DispatchQueue.main.async(execute: action)
}
}
// MARK: - Send
func sendGroupMessage(groupId: String,
makeMessage: @escaping () -> OIMMessageInfo,
onPrepared: ((OIMMessageInfo) -> Void)? = nil,
onSuccess: ((OIMMessageInfo) -> Void)?,
onProgress: ((Int) -> Void)? = nil,
onFailure: ((Int, String?) -> Void)?) {
let normalizedGroupId = groupId.trimmingCharacters(in: .whitespacesAndNewlines)
guard !normalizedGroupId.isEmpty else {
onFailure?(-1, "圈子信息无效")
return
}
ensureLogin { success in
let userId = AppContextManager.shared.userId
guard success,
OIMManager.manager.getLoginStatus() == .logged,
OIMManager.manager.getLoginUserID() == userId else {
onFailure?(-1, "消息服务连接失败,请重试")
return
}
let message = makeMessage()
guard !(message.clientMsgID ?? "").isEmpty else {
onFailure?(-1, "消息创建失败,请重试")
return
}
onPrepared?(message)
OIMManager.manager.sendMessage(message,
recvID: "",
groupID: normalizedGroupId,
offlinePushInfo: nil,
onSuccess: { returnedMessage in
onSuccess?(returnedMessage ?? message)
},
onProgress: { progress in
onProgress?(progress)
},
onFailure: { code, errorMessage in
onFailure?(code, errorMessage)
})
}
}
func displaySendError(_ message: String?, fallback: String) -> String {
let text = message?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
guard !text.isEmpty else { return fallback }
let lowercased = text.lowercased()
if lowercased.contains("json") || lowercased.contains("unexpected end") {
return fallback
}
return text
}
// MARK: - Get Joined Groups // MARK: - Get Joined Groups
func getJoinedGroups(completion: @escaping ([OIMGroupInfo]) -> Void) { func getJoinedGroups(completion: @escaping ([OIMGroupInfo]) -> Void) {
OIMManager.manager.getJoinedGroupListWith(onSuccess: { groups in OIMManager.manager.getJoinedGroupListWith(onSuccess: { groups in

View File

@ -86,7 +86,7 @@ final class GroupMemberListVC: BaseViewController {
self.viewModel.loadData(group: group, members: response.list) self.viewModel.loadData(group: group, members: response.list)
}, onError: { error in }, onError: { error in
DLToast.dismiss() DLToast.dismiss()
DLToast.showError(text: error.localizedDescription) DLToast.showError(error)
}) })
.disposed(by: disposeBag) .disposed(by: disposeBag)
} }

View File

@ -264,7 +264,7 @@ final class GroupMemberListCell: UICollectionViewCell {
nameLabel.text = model.showName nameLabel.text = model.showName
nameLabel.textColor = UIColor(hexStr: isCurrentUser ? "#16B3FF" : "#0F2846") nameLabel.textColor = UIColor(hexStr: isCurrentUser ? "#16B3FF" : "#0F2846")
selectionImageView.isHidden = !isSelected selectionImageView.isHidden = !isSelected
vipBackgroundView.isHidden = model.level == 1 vipBackgroundView.isHidden = !model.shouldShowVipAvatarFrame
let isSOS = model.status == 1 let isSOS = model.status == 1
updateSOSBlink(isSOS) updateSOSBlink(isSOS)

View File

@ -176,7 +176,7 @@ class GroupSettingVC: BaseViewController {
self.requestReviewBadge() self.requestReviewBadge()
}, onError: { error in }, onError: { error in
DLToast.dismiss() DLToast.dismiss()
DLToast.showError(text: error.localizedDescription) DLToast.showError(error)
}) })
.disposed(by: disposeBag) .disposed(by: disposeBag)
} }

View File

@ -24,6 +24,7 @@ final class GroupViewController: BaseViewController {
private var groupRefreshWorkItem: DispatchWorkItem? private var groupRefreshWorkItem: DispatchWorkItem?
private var groupInfoRequestID = UUID() private var groupInfoRequestID = UUID()
private var hasAppeared = false private var hasAppeared = false
private var isEnteringGroupChat = false
override func loadView() { override func loadView() {
rootView = GroupView(frame: UIScreen.main.bounds) rootView = GroupView(frame: UIScreen.main.bounds)
@ -184,10 +185,10 @@ final class GroupViewController: BaseViewController {
rootView.createdTableView.rx.modelSelected(GroupCellData.self).asObservable(), rootView.createdTableView.rx.modelSelected(GroupCellData.self).asObservable(),
rootView.joinedTableView.rx.modelSelected(GroupCellData.self).asObservable() rootView.joinedTableView.rx.modelSelected(GroupCellData.self).asObservable()
) )
.subscribe(onNext: { data in .subscribe(onNext: { [weak self] data in
let groupId = data.group.group_key let groupId = data.group.group_key
guard !groupId.isEmpty else { return } guard !groupId.isEmpty else { return }
AppRouter.push(Route.groupChat, userInfo: ["groupId": groupId]) self?.openGroupChat(groupId: groupId)
}) })
.disposed(by: disposeBag) .disposed(by: disposeBag)
@ -215,6 +216,24 @@ final class GroupViewController: BaseViewController {
.disposed(by: disposeBag) .disposed(by: disposeBag)
} }
private func openGroupChat(groupId: String) {
guard !isEnteringGroupChat else { return }
isEnteringGroupChat = true
DLToast.showLoading()
GroupIMService.shared.ensureLogin { [weak self] success in
DispatchQueue.main.async {
guard let self else { return }
self.isEnteringGroupChat = false
DLToast.dismiss()
guard success else {
DLToast.showError(text: "消息服务登录失败,请重试")
return
}
AppRouter.push(Route.groupChat, userInfo: ["groupId": groupId])
}
}
}
private func bindItineraryPage() { private func bindItineraryPage() {
let page = rootView.itineraryPage let page = rootView.itineraryPage

View File

@ -78,7 +78,7 @@ final class MailboxDetailViewController: UIViewController {
DLToast.dismiss() DLToast.dismiss()
self?.isLoading = false self?.isLoading = false
self?.detailView.setControlsEnabled(true) self?.detailView.setControlsEnabled(true)
DLToast.showError(text: error.localizedDescription) DLToast.showError(error)
}) })
.disposed(by: disposeBag) .disposed(by: disposeBag)
} }

View File

@ -187,7 +187,7 @@ final class MailboxViewController: BaseViewController {
self.updateContentState(showLoading: false) self.updateContentState(showLoading: false)
self.loadPendingCategoryIfNeeded() self.loadPendingCategoryIfNeeded()
}, onError: { [weak self] error in }, onError: { [weak self] error in
self?.finishLoadingWithError(message: error.localizedDescription) self?.finishLoadingWithError(error: error)
}) })
.disposed(by: disposeBag) .disposed(by: disposeBag)
} }
@ -228,6 +228,13 @@ final class MailboxViewController: BaseViewController {
DLToast.showError(text: message) DLToast.showError(text: message)
} }
private func finishLoadingWithError(error: Error) {
isLoading = false
finishRefreshControls()
updateContentState(showLoading: false)
DLToast.showError(error)
}
private func finishRefreshControls() { private func finishRefreshControls() {
rootView.tableView.mj_header?.endRefreshing() rootView.tableView.mj_header?.endRefreshing()
if reachedEnd { if reachedEnd {
@ -281,7 +288,7 @@ final class MailboxViewController: BaseViewController {
self.present(detailController, animated: true) self.present(detailController, animated: true)
}, onError: { error in }, onError: { error in
DLToast.dismiss() DLToast.dismiss()
DLToast.showError(text: error.localizedDescription) DLToast.showError(error)
}) })
.disposed(by: disposeBag) .disposed(by: disposeBag)
} }
@ -343,7 +350,7 @@ final class MailboxViewController: BaseViewController {
DLToast.showSuccess(text: "已清空") DLToast.showSuccess(text: "已清空")
}, onError: { error in }, onError: { error in
DLToast.dismiss() DLToast.dismiss()
DLToast.showError(text: error.localizedDescription) DLToast.showError(error)
}) })
.disposed(by: disposeBag) .disposed(by: disposeBag)
} }

View File

@ -230,7 +230,7 @@ final class MemberInfoVC: BaseViewController {
self.refreshDetail() self.refreshDetail()
}, onError: { error in }, onError: { error in
DLToast.dismiss() DLToast.dismiss()
DLToast.show(text: error.localizedDescription) DLToast.show(error)
}).disposed(by: disposeBag) }).disposed(by: disposeBag)
} }
@ -248,7 +248,7 @@ final class MemberInfoVC: BaseViewController {
case .success: case .success:
self.presentRelationPicker() self.presentRelationPicker()
case let .failure(error): case let .failure(error):
DLToast.showError(text: error.localizedDescription) DLToast.showError(error)
} }
} }
} }
@ -291,7 +291,7 @@ final class MemberInfoVC: BaseViewController {
self.refreshDetail() self.refreshDetail()
}, onError: { error in }, onError: { error in
DLToast.dismiss() DLToast.dismiss()
DLToast.show(text: error.localizedDescription) DLToast.show(error)
}).disposed(by: disposeBag) }).disposed(by: disposeBag)
} }
@ -310,7 +310,7 @@ final class MemberInfoVC: BaseViewController {
self?.syncUserConfig() self?.syncUserConfig()
}, onError: { error in }, onError: { error in
DLToast.dismiss() DLToast.dismiss()
DLToast.show(text: error.localizedDescription) DLToast.show(error)
}).disposed(by: disposeBag) }).disposed(by: disposeBag)
} }
@ -324,7 +324,7 @@ final class MemberInfoVC: BaseViewController {
self?.syncUserConfig() self?.syncUserConfig()
}, onError: { error in }, onError: { error in
DLToast.dismiss() DLToast.dismiss()
DLToast.show(text: error.localizedDescription) DLToast.show(error)
}).disposed(by: disposeBag) }).disposed(by: disposeBag)
} }
@ -340,7 +340,7 @@ final class MemberInfoVC: BaseViewController {
self?.syncUserConfig() self?.syncUserConfig()
}, onError: { error in }, onError: { error in
DLToast.dismiss() DLToast.dismiss()
DLToast.show(text: error.localizedDescription) DLToast.show(error)
}).disposed(by: disposeBag) }).disposed(by: disposeBag)
} }

View File

@ -115,7 +115,7 @@ class RemoveMemberVC: BaseViewController {
self.viewModel.loadData(response.list) self.viewModel.loadData(response.list)
}, onError: { error in }, onError: { error in
DLToast.dismiss() DLToast.dismiss()
DLToast.showError(text: error.localizedDescription) DLToast.showError(error)
}) })
.disposed(by: disposeBag) .disposed(by: disposeBag)
} }

View File

@ -25,6 +25,7 @@ final class CreateBubbleSetupView: UIView {
private let subtitleLab = UILabel() private let subtitleLab = UILabel()
private let titleLab = UILabel() private let titleLab = UILabel()
private let hourPicker = BubbleHourPickerView() private let hourPicker = BubbleHourPickerView()
private let vipOfferImageView = UIImageView(image: UIImage(named: "VipFeature/limited_offer"))
private let sheetOverlay = UIView() private let sheetOverlay = UIView()
private let sheetPanel = UIView() private let sheetPanel = UIView()
@ -83,6 +84,7 @@ final class CreateBubbleSetupView: UIView {
addSubview(subtitleLab) addSubview(subtitleLab)
addSubview(hourPicker) addSubview(hourPicker)
addSubview(confirmBtn) addSubview(confirmBtn)
addSubview(vipOfferImageView)
heroView.showsOuterDecorBubbles = false heroView.showsOuterDecorBubbles = false
heroView.layoutChain heroView.layoutChain
@ -113,6 +115,25 @@ final class CreateBubbleSetupView: UIView {
confirmBtn.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal) confirmBtn.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal)
confirmBtn.cornerRadius = 20 confirmBtn.cornerRadius = 20
confirmBtn.layoutChain.left(30).right(30).bottom(34).height(56) confirmBtn.layoutChain.left(30).right(30).bottom(34).height(56)
vipOfferImageView.contentMode = .scaleAspectFit
vipOfferImageView.isHidden = true
vipOfferImageView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
vipOfferImageView.trailingAnchor.constraint(equalTo: confirmBtn.trailingAnchor, constant: -6),
vipOfferImageView.bottomAnchor.constraint(equalTo: confirmBtn.topAnchor, constant: 10),
vipOfferImageView.widthAnchor.constraint(equalToConstant: 94),
vipOfferImageView.heightAnchor.constraint(equalToConstant: 32)
])
}
func updateVipAccess(isUnlocked: Bool) {
confirmBtn.setTitle(isUnlocked ? "确定" : "开通会员 解锁功能", for: .normal)
confirmBtn.setBackgroundImage(
UIImage(named: isUnlocked ? "Common/button_bg_2" : "VipFeature/wide_button_bg"),
for: .normal
)
vipOfferImageView.isHidden = isUnlocked
} }
private func setupSheet() { private func setupSheet() {

View File

@ -26,6 +26,7 @@ class CreateBubbleVC: BaseViewController {
override func viewWillAppear(_ animated: Bool) { override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated) super.viewWillAppear(animated)
rootView.updateVipAccess(isUnlocked: isVipUnlocked)
requestUserStatus() requestUserStatus()
} }
@ -48,6 +49,10 @@ class CreateBubbleVC: BaseViewController {
rootView.setupView.confirmBtn.rx.tap rootView.setupView.confirmBtn.rx.tap
.subscribe(onNext: { [weak self] in .subscribe(onNext: { [weak self] in
guard let self else { return } guard let self else { return }
guard self.isVipUnlocked else {
AppRouter.push(Route.vipRecharge)
return
}
let hours = self.rootView.setupView.selectedHour.value let hours = self.rootView.setupView.selectedHour.value
let message = self.rootView.setupView.buildMessage(for: hours) let message = self.rootView.setupView.buildMessage(for: hours)
self.rootView.setupView.showNotifySheet(message: message) self.rootView.setupView.showNotifySheet(message: message)
@ -80,18 +85,21 @@ class CreateBubbleVC: BaseViewController {
} }
private func submitCreate() { private func submitCreate() {
guard let systemConfig = AppContextManager.shared.systemConfig, systemConfig.isIntercept == false else { guard isVipUnlocked else {
let hours = rootView.setupView.selectedHour.value rootView.setupView.dismissNotifySheet()
requestSetBubble(enable: true, keep_time: hours) AppRouter.push(Route.vipRecharge)
return return
} }
if AppContextManager.shared.vip > 1 {
let hours = rootView.setupView.selectedHour.value let hours = rootView.setupView.selectedHour.value
requestSetBubble(enable: true, keep_time: hours) requestSetBubble(enable: true, keep_time: hours)
} else {
CreateBubblePopView.show()
} }
private var isVipUnlocked: Bool {
guard AppContextManager.shared.systemConfig?.isIntercept == false else {
return false
}
return AppContextManager.shared.vip > 1
} }
private func restoreCachedBubbleIfNeeded() { private func restoreCachedBubbleIfNeeded() {
@ -119,7 +127,7 @@ class CreateBubbleVC: BaseViewController {
self.applyUserStatus(model) self.applyUserStatus(model)
}, onError: { [weak self] error in }, onError: { [weak self] error in
guard let self, requestVersion == self.statusRequestVersion else { return } guard let self, requestVersion == self.statusRequestVersion else { return }
DLToast.showError(text: error.localizedDescription) DLToast.showError(error)
}) })
.disposed(by: disposeBag) .disposed(by: disposeBag)
} }

View File

@ -90,4 +90,8 @@ class CreateBubbleView: UIView {
func updateNavTitleForSheet(_ visible: Bool) { func updateNavTitleForSheet(_ visible: Bool) {
navView.titleLabel.text = visible ? "通知您的圈子" : "设置气泡时间" navView.titleLabel.text = visible ? "通知您的圈子" : "设置气泡时间"
} }
func updateVipAccess(isUnlocked: Bool) {
setupView.updateVipAccess(isUnlocked: isUnlocked)
}
} }

View File

@ -158,6 +158,7 @@ class GroupMemberCell: UITableViewCell {
ownView.isHidden = !isOwn ownView.isHidden = !isOwn
avaterImgView.setHeadPic(model.displayHeadPic) avaterImgView.setHeadPic(model.displayHeadPic)
vipIcon.image = model.vipIcon vipIcon.image = model.vipIcon
vipIcon.isHidden = !model.shouldShowVipAvatarFrame
nameLab.text = model.showName nameLab.text = model.showName
// //
locationLab.text = "" + model.lastLocation locationLab.text = "" + model.lastLocation

View File

@ -10,14 +10,25 @@ import Network
import CoreLocation import CoreLocation
import SwiftyUserDefaults import SwiftyUserDefaults
enum MemberFeatureAccessState: Equatable {
case available
case vipRequired
case privilegeDenied
}
class GroupMemberView2: UIView { class GroupMemberView2: UIView {
private var navBarHeightConstraint: NSLayoutConstraint? private var navBarHeightConstraint: NSLayoutConstraint?
private var todayTrackTopToInteraction: NSLayoutConstraint? private var todayTrackTopToInteraction: NSLayoutConstraint?
private var todayTrackTopToMember: NSLayoutConstraint? private var todayTrackTopToMember: NSLayoutConstraint?
private var appUsageTopToPhoneReportConstraint: NSLayoutConstraint?
private var appUsageTopToTodayTrackConstraint: NSLayoutConstraint?
private var todayTrackBottomConstraint: NSLayoutConstraint?
private var phoneReportBottomConstraint: NSLayoutConstraint? private var phoneReportBottomConstraint: NSLayoutConstraint?
private var appUsageBottomConstraint: NSLayoutConstraint? private var appUsageBottomConstraint: NSLayoutConstraint?
private var isAppUsageVisible = false
private var currentMemberModel: GroupMemberModel? private var currentMemberModel: GroupMemberModel?
private var currentGroupVip = GroupMemberVip()
private var emojiEchoByUserId: [String: String] = [:] private var emojiEchoByUserId: [String: String] = [:]
private var messageEchoByUserId: [String: String] = [:] private var messageEchoByUserId: [String: String] = [:]
private var phoneStatusObserver: NSObjectProtocol? private var phoneStatusObserver: NSObjectProtocol?
@ -26,6 +37,7 @@ class GroupMemberView2: UIView {
private var localWeatherSnapshotKey: String? private var localWeatherSnapshotKey: String?
private var localWeatherUpdatedAt: Date? private var localWeatherUpdatedAt: Date?
private var pendingWeatherLocationKey: String? private var pendingWeatherLocationKey: String?
private(set) var isTodayTrackVipRequired = false
var onCoupleMemberTap: ((GroupMemberModel) -> Void)? var onCoupleMemberTap: ((GroupMemberModel) -> Void)?
private static let weatherRefreshInterval: TimeInterval = 15 * 60 private static let weatherRefreshInterval: TimeInterval = 15 * 60
@ -43,9 +55,11 @@ class GroupMemberView2: UIView {
func setupMemberInfo( func setupMemberInfo(
_ model: GroupMemberModel, _ model: GroupMemberModel,
isOwner: Bool, isOwner: Bool,
phoneUsage: PhoneUsageTodayModel? phoneUsage: PhoneUsageTodayModel?,
groupVip: GroupMemberVip
) { ) {
currentMemberModel = model currentMemberModel = model
currentGroupVip = groupVip
memberNameLab.text = model.showName memberNameLab.text = model.showName
ownView.isHidden = !isOwner ownView.isHidden = !isOwner
relationIconView.configure(relationIdx: model.extra.relation_idx) relationIconView.configure(relationIdx: model.extra.relation_idx)
@ -64,9 +78,7 @@ class GroupMemberView2: UIView {
} }
applyPhoneUsage(phoneUsage) applyPhoneUsage(phoneUsage)
if isCurrentUser { if isCurrentUser, !isPhoneDeviceVipRequired {
applyLocalDeviceInfo()
startLocalPhoneStatusObserving()
requestLocalWeather() requestLocalWeather()
} }
updateRealtimeInteractionVisibility(isCurrentUser: isCurrentUser) updateRealtimeInteractionVisibility(isCurrentUser: isCurrentUser)
@ -75,6 +87,7 @@ class GroupMemberView2: UIView {
func clearMemberInfo() { func clearMemberInfo() {
currentMemberModel = nil currentMemberModel = nil
currentGroupVip = GroupMemberVip()
memberNameLab.text = "" memberNameLab.text = ""
ownView.isHidden = true ownView.isHidden = true
relationIconView.configure(relationIdx: "") relationIconView.configure(relationIdx: "")
@ -92,16 +105,23 @@ class GroupMemberView2: UIView {
func updatePhoneUsage(_ phoneUsage: PhoneUsageTodayModel?, for userId: String) { func updatePhoneUsage(_ phoneUsage: PhoneUsageTodayModel?, for userId: String) {
guard currentMemberModel?.user_id == userId else { return } guard currentMemberModel?.user_id == userId else { return }
applyPhoneUsage(phoneUsage) applyPhoneUsage(phoneUsage)
if Self.isCurrentUser(userId) { if Self.isCurrentUser(userId), !isPhoneDeviceVipRequired {
applyLocalDeviceInfo() applyLocalDeviceInfo()
} }
} }
func refreshLocalDeviceInfoIfNeeded() { func refreshLocalDeviceInfoIfNeeded() {
guard isShowingCurrentUser else { return } guard isShowingCurrentUser, !isPhoneDeviceVipRequired else { return }
applyLocalDeviceInfo() applyLocalDeviceInfo()
} }
func refreshInterceptState() {
memberCV.reloadData()
coupleLeftSlot.refreshVipVisibility()
coupleRightSlot.refreshVipVisibility()
updateFeatureVisibilityLayout()
}
func applyEmojiEcho(name: String, for userId: String) { func applyEmojiEcho(name: String, for userId: String) {
emojiEchoByUserId[userId] = name emojiEchoByUserId[userId] = name
if currentMemberModel?.user_id == userId { if currentMemberModel?.user_id == userId {
@ -186,7 +206,9 @@ class GroupMemberView2: UIView {
} }
private func applyPhoneUsage(_ usage: PhoneUsageTodayModel?) { private func applyPhoneUsage(_ usage: PhoneUsageTodayModel?) {
if isShowingCurrentUser { if isPhoneDeviceVipRequired {
stopLocalPhoneStatusObserving()
} else if isShowingCurrentUser {
applyLocalDeviceInfo() applyLocalDeviceInfo()
startLocalPhoneStatusObserving() startLocalPhoneStatusObserving()
} else { } else {
@ -197,51 +219,95 @@ class GroupMemberView2: UIView {
applyDeviceInfo(usage?.phoneInfo) applyDeviceInfo(usage?.phoneInfo)
} }
} }
applyPhoneDeviceAccessState()
if isShowingOfflineOther { if isShowingOfflineOther {
phoneReportView.configure(with: nil) phoneReportView.configure(with: nil)
appUsageView.configure(with: nil) appUsageView.configure(with: nil)
applyPrivilegeMasks(appUsagePreview: nil) applyFeatureAccessStates(appUsagePreview: nil)
applyOfflineStayPoints() applyOfflineStayPoints()
return return
} }
phoneReportView.configure(with: Self.phoneReportPreview(from: usage)) phoneReportView.configure(with: Self.phoneReportPreview(from: usage))
let appUsagePreview = MemberAppUsagePreview(topApps: usage?.topApps ?? []) let appUsagePreview = MemberAppUsagePreview(topApps: usage?.topApps ?? [])
appUsageView.configure(with: appUsagePreview) appUsageView.configure(with: appUsagePreview)
applyPrivilegeMasks(appUsagePreview: appUsagePreview) applyFeatureAccessStates(appUsagePreview: appUsagePreview)
applyStayPoints(usage?.stayPoints ?? []) applyStayPoints(usage?.stayPoints ?? [])
} }
var isPhoneReportDenied: Bool { phoneReportView.isDenied } var isPhoneReportDenied: Bool { phoneReportView.isDenied }
var isAppUsageDenied: Bool { appUsageView.isDenied } var isAppUsageDenied: Bool { appUsageView.isDenied }
var isPhoneReportVipRequired: Bool { phoneReportView.isVipRequired }
var isAppUsageVipRequired: Bool { appUsageView.isVipRequired }
var onPhoneReportRemind: (() -> Void)? var onPhoneReportRemind: (() -> Void)?
var onAppUsageRemind: (() -> Void)? var onAppUsageRemind: (() -> Void)?
private func applyPrivilegeMasks(appUsagePreview: MemberAppUsagePreview? = nil) { private func applyFeatureAccessStates(appUsagePreview: MemberAppUsagePreview?) {
guard currentMemberModel != nil else { guard currentMemberModel != nil else {
phoneReportView.setDenied(false) isTodayTrackVipRequired = false
appUsageView.setDenied(false) todayTrackVipView.isHidden = true
phoneReportView.setAccessState(.available)
appUsageView.setAccessState(.available)
updateAppUsageVisibility(appUsagePreview != nil) updateAppUsageVisibility(appUsagePreview != nil)
return return
} }
let isSelf = isShowingCurrentUser let isSelf = isShowingCurrentUser
let privilege = currentMemberModel?.extra.privilege ?? GroupPrivilege() let memberPrivilege = currentMemberModel?.extra.privilege ?? GroupPrivilege()
let phoneDenied = !isSelf && !privilege.phone_report let phoneState = Self.accessState(
let usageDenied = !isSelf && !privilege.app_usage_record vipAllowed: currentGroupVip.phone_report,
phoneReportView.setDenied(phoneDenied) privilegeAllowed: isSelf || memberPrivilege.phone_report
)
let appUsageState = Self.accessState(
vipAllowed: currentGroupVip.app_usage_record,
privilegeAllowed: isSelf || memberPrivilege.app_usage_record
)
isTodayTrackVipRequired = !isSelf && currentGroupVip.trip_analysis_others == false
todayTrackVipView.isHidden = !isTodayTrackVipRequired
phoneReportView.setAccessState(phoneState)
phoneReportView.onDeniedAction = { [weak self] in self?.onPhoneReportRemind?() } phoneReportView.onDeniedAction = { [weak self] in self?.onPhoneReportRemind?() }
appUsageView.setDenied(usageDenied) appUsageView.setAccessState(appUsageState)
appUsageView.onDeniedAction = { [weak self] in self?.onAppUsageRemind?() } appUsageView.onDeniedAction = { [weak self] in self?.onAppUsageRemind?() }
let showUsage = appUsagePreview != nil || usageDenied let showUsage = appUsagePreview != nil || appUsageState != .available
if appUsagePreview == nil, usageDenied { if appUsagePreview == nil, appUsageState != .available {
appUsageView.configure(with: nil) appUsageView.configure(with: nil)
} }
updateAppUsageVisibility(showUsage) updateAppUsageVisibility(showUsage)
} }
private static func accessState(
vipAllowed: Bool?,
privilegeAllowed: Bool
) -> MemberFeatureAccessState {
if vipAllowed == false {
return .vipRequired
}
if !privilegeAllowed {
return .privilegeDenied
}
return .available
}
private var isShowingCurrentUser: Bool { private var isShowingCurrentUser: Bool {
Self.isCurrentUser(currentMemberModel?.user_id) Self.isCurrentUser(currentMemberModel?.user_id)
} }
private var isPhoneDeviceVipRequired: Bool {
currentMemberModel != nil && !isShowingCurrentUser && currentGroupVip.view_phone == false
}
private func applyPhoneDeviceAccessState() {
let requiresVip = isPhoneDeviceVipRequired
phoneModelView.isHidden = requiresVip
batteryView.isHidden = requiresVip
networkView.isHidden = requiresVip
weatherView.isHidden = requiresVip
phoneDeviceVipButton.isHidden = !requiresVip
}
@objc private func handlePhoneDeviceVipTap() {
AppRouter.push(Route.vipRecharge)
}
private static func isCurrentUser(_ userId: String?) -> Bool { private static func isCurrentUser(_ userId: String?) -> Bool {
let memberId = userId?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" let memberId = userId?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
let selfId = AppContextManager.shared.userId.trimmingCharacters(in: .whitespacesAndNewlines) let selfId = AppContextManager.shared.userId.trimmingCharacters(in: .whitespacesAndNewlines)
@ -280,7 +346,7 @@ class GroupMemberView2: UIView {
object: nil, object: nil,
queue: .main queue: .main
) { [weak self] _ in ) { [weak self] _ in
guard let self, self.isShowingCurrentUser else { return } guard let self, self.isShowingCurrentUser, !self.isPhoneDeviceVipRequired else { return }
self.applyLocalDeviceInfo() self.applyLocalDeviceInfo()
} }
} }
@ -309,6 +375,7 @@ class GroupMemberView2: UIView {
} }
private func requestLocalWeather() { private func requestLocalWeather() {
guard !isPhoneDeviceVipRequired else { return }
guard let latitude = Defaults[\.currentLatitude], guard let latitude = Defaults[\.currentLatitude],
let longitude = Defaults[\.currentLongitude] else { let longitude = Defaults[\.currentLongitude] else {
if localWeatherSnapshot == nil { if localWeatherSnapshot == nil {
@ -342,7 +409,7 @@ class GroupMemberView2: UIView {
MemberWeatherService.shared.weather(for: location) { [weak self] result in MemberWeatherService.shared.weather(for: location) { [weak self] result in
guard let self, self.weatherRequestID == requestID else { return } guard let self, self.weatherRequestID == requestID else { return }
self.pendingWeatherLocationKey = nil self.pendingWeatherLocationKey = nil
guard self.isShowingCurrentUser else { return } guard self.isShowingCurrentUser, !self.isPhoneDeviceVipRequired else { return }
switch result { switch result {
case .success(let snapshot): case .success(let snapshot):
self.localWeatherSnapshot = snapshot self.localWeatherSnapshot = snapshot
@ -369,14 +436,20 @@ class GroupMemberView2: UIView {
} }
private func updateAppUsageVisibility(_ visible: Bool) { private func updateAppUsageVisibility(_ visible: Bool) {
if visible { isAppUsageVisible = visible
phoneReportBottomConstraint?.isActive = false
appUsageBottomConstraint?.isActive = true
} else {
appUsageBottomConstraint?.isActive = false
phoneReportBottomConstraint?.isActive = true
}
appUsageView.isHidden = !visible appUsageView.isHidden = !visible
updateFeatureVisibilityLayout()
}
private func updateFeatureVisibilityLayout() {
let hidesPhoneReport = AppContextManager.shared.systemConfig?.isIntercept == true
phoneReportView.isHidden = hidesPhoneReport
appUsageTopToPhoneReportConstraint?.isActive = !hidesPhoneReport
appUsageTopToTodayTrackConstraint?.isActive = hidesPhoneReport
phoneReportBottomConstraint?.isActive = !hidesPhoneReport && !isAppUsageVisible
appUsageBottomConstraint?.isActive = isAppUsageVisible
todayTrackBottomConstraint?.isActive = hidesPhoneReport && !isAppUsageVisible
setNeedsLayout() setNeedsLayout()
} }
@ -594,8 +667,16 @@ class GroupMemberView2: UIView {
scrollContentView.addSubview(appUsageView) scrollContentView.addSubview(appUsageView)
appUsageView.layoutChain appUsageView.layoutChain
.topToBottomOfView(phoneReportView, offset: 15)
.edgesHorzontal() .edgesHorzontal()
appUsageTopToPhoneReportConstraint = appUsageView.topAnchor.constraint(
equalTo: phoneReportView.bottomAnchor,
constant: 15
)
appUsageTopToPhoneReportConstraint?.isActive = true
appUsageTopToTodayTrackConstraint = appUsageView.topAnchor.constraint(
equalTo: todayTrackView.bottomAnchor,
constant: 15
)
phoneReportBottomConstraint = phoneReportView.bottomAnchor.constraint( phoneReportBottomConstraint = phoneReportView.bottomAnchor.constraint(
equalTo: scrollContentView.bottomAnchor, equalTo: scrollContentView.bottomAnchor,
@ -605,6 +686,10 @@ class GroupMemberView2: UIView {
equalTo: scrollContentView.bottomAnchor, equalTo: scrollContentView.bottomAnchor,
constant: -(49 + kSafeBottomMargin) constant: -(49 + kSafeBottomMargin)
) )
todayTrackBottomConstraint = todayTrackView.bottomAnchor.constraint(
equalTo: scrollContentView.bottomAnchor,
constant: -(49 + kSafeBottomMargin)
)
phoneReportBottomConstraint?.isActive = true phoneReportBottomConstraint?.isActive = true
return view return view
@ -864,6 +949,7 @@ class GroupMemberView2: UIView {
view.addSubview(batteryView) view.addSubview(batteryView)
view.addSubview(networkView) view.addSubview(networkView)
view.addSubview(weatherView) view.addSubview(weatherView)
view.addSubview(phoneDeviceVipButton)
phoneModelView.layoutChain phoneModelView.layoutChain
.topToBottomOfView(locationLab, offset: 20) .topToBottomOfView(locationLab, offset: 20)
@ -885,6 +971,12 @@ class GroupMemberView2: UIView {
.right() .right()
.widthToView(phoneModelView) .widthToView(phoneModelView)
phoneDeviceVipButton.layoutChain
.centerX()
.centerY(phoneModelView)
.width(174)
.height(34)
return view return view
}() }()
@ -1147,6 +1239,17 @@ class GroupMemberView2: UIView {
return label return label
}() }()
private lazy var phoneDeviceVipButton: UIButton = {
let button = UIButton(type: .custom)
button.setTitle("开通会员 解锁功能", for: .normal)
button.setTitleColor(.white, for: .normal)
button.titleLabel?.font = FontManager.boboBold(14)
button.setBackgroundImage(UIImage(named: "VipFeature/narrow_button_bg"), for: .normal)
button.isHidden = true
button.addTarget(self, action: #selector(handlePhoneDeviceVipTap), for: .touchUpInside)
return button
}()
// MARK: - / APP使 // MARK: - / APP使
lazy var phoneReportView = MemberPhoneReportView() lazy var phoneReportView = MemberPhoneReportView()
lazy var appUsageView: MemberAppUsageView = { lazy var appUsageView: MemberAppUsageView = {
@ -1360,6 +1463,25 @@ class GroupMemberView2: UIView {
.topToBottomOfView(stayTxtLab, offset: 14) .topToBottomOfView(stayTxtLab, offset: 14)
.leftToView(stayTxtLab) .leftToView(stayTxtLab)
view.addSubview(todayTrackVipView)
todayTrackVipView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
todayTrackVipView.topAnchor.constraint(equalTo: infoView.topAnchor),
todayTrackVipView.leadingAnchor.constraint(equalTo: infoView.leadingAnchor),
todayTrackVipView.trailingAnchor.constraint(equalTo: infoView.trailingAnchor),
todayTrackVipView.bottomAnchor.constraint(equalTo: infoView.bottomAnchor)
])
return view
}()
private lazy var todayTrackVipView: UIImageView = {
let view = UIImageView(image: UIImage(named: "MemberVipRequired/today_track"))
view.contentMode = .scaleAspectFill
view.clipsToBounds = true
view.cornerRadius = 16
view.isHidden = true
view.isUserInteractionEnabled = false
return view return view
}() }()
@ -1507,7 +1629,11 @@ private final class CoupleMemberSlot: UIControl {
avatarImageView.setHeadPic(model?.displayHeadPic) avatarImageView.setHeadPic(model?.displayHeadPic)
avatarImageView.layer.borderColor = UIColor(hexStr: isSelected ? "#FFD43B" : "#FFFFFF").cgColor avatarImageView.layer.borderColor = UIColor(hexStr: isSelected ? "#FFD43B" : "#FFFFFF").cgColor
nameLabel.text = model?.showName nameLabel.text = model?.showName
vipImageView.isHidden = model?.level == 1 || model == nil refreshVipVisibility()
}
func refreshVipVisibility() {
vipImageView.isHidden = member?.shouldShowVipAvatarFrame != true
} }
private func setupUI() { private func setupUI() {

View File

@ -330,7 +330,12 @@ class HomeViewController: BaseViewController {
// //
rootView.groupMemberView.todayTrackView.rx.tapGesture.subscribe(onNext: { [weak self] _ in rootView.groupMemberView.todayTrackView.rx.tapGesture.subscribe(onNext: { [weak self] _ in
self?.openTodayTrackDetail() guard let self else { return }
if self.rootView.groupMemberView.isTodayTrackVipRequired {
AppRouter.push(Route.vipRecharge)
} else {
self.openTodayTrackDetail()
}
}).disposed(by: disposeBag) }).disposed(by: disposeBag)
// //
@ -344,13 +349,23 @@ class HomeViewController: BaseViewController {
// //
rootView.groupMemberView.phoneReportView.rx.tapGesture.subscribe(onNext: { [weak self] _ in rootView.groupMemberView.phoneReportView.rx.tapGesture.subscribe(onNext: { [weak self] _ in
guard let self, !self.rootView.groupMemberView.isPhoneReportDenied else { return } guard let self else { return }
if self.rootView.groupMemberView.isPhoneReportVipRequired {
AppRouter.push(Route.vipRecharge)
return
}
guard !self.rootView.groupMemberView.isPhoneReportDenied else { return }
self.openPhoneReportDetail() self.openPhoneReportDetail()
}).disposed(by: disposeBag) }).disposed(by: disposeBag)
// APP // APP
rootView.groupMemberView.appUsageView.rx.tapGesture.subscribe(onNext: { [weak self] _ in rootView.groupMemberView.appUsageView.rx.tapGesture.subscribe(onNext: { [weak self] _ in
guard let self, !self.rootView.groupMemberView.isAppUsageDenied else { return } guard let self else { return }
if self.rootView.groupMemberView.isAppUsageVipRequired {
AppRouter.push(Route.vipRecharge)
return
}
guard !self.rootView.groupMemberView.isAppUsageDenied else { return }
self.openAppUsageReport() self.openAppUsageReport()
}).disposed(by: disposeBag) }).disposed(by: disposeBag)
@ -379,6 +394,7 @@ class HomeViewController: BaseViewController {
guard let self = self, let model = response.model else { return } guard let self = self, let model = response.model else { return }
Defaults[\.loginToken] = model.token Defaults[\.loginToken] = model.token
AppContextManager.shared.systemConfig = model.config AppContextManager.shared.systemConfig = model.config
self.rootView.groupMemberView.refreshInterceptState()
// rootView.searchLottieView.isHidden = model.config?.isIntercept == true // rootView.searchLottieView.isHidden = model.config?.isIntercept == true
self.requestUserInfo { [weak self] in self.requestUserInfo { [weak self] in
self?.requestGroupInfo() self?.requestGroupInfo()
@ -595,7 +611,7 @@ class HomeViewController: BaseViewController {
done(true) done(true)
}, onError: { error in }, onError: { error in
DLToast.dismiss() DLToast.dismiss()
DLToast.show(text: error.localizedDescription) DLToast.show(error)
done(false) done(false)
}) })
.disposed(by: disposeBag) .disposed(by: disposeBag)
@ -660,7 +676,7 @@ class HomeViewController: BaseViewController {
}, onError: { [weak self] error in }, onError: { [weak self] error in
guard let self, self.groupInfoRequestID == requestID else { return } guard let self, self.groupInfoRequestID == requestID else { return }
if showError { if showError {
DLToast.showError(text: error.localizedDescription) DLToast.showError(error)
} }
if retryAllowed { if retryAllowed {
self.scheduleGroupDataRetry(changes: changes) self.scheduleGroupDataRetry(changes: changes)
@ -833,7 +849,7 @@ class HomeViewController: BaseViewController {
return return
} }
self.viewModel.updateMembers(response.list) self.viewModel.updateGroupUsers(response.list, vip: response.vip)
// status=0 // status=0
self.syncMemberAnnotations(self.viewModel.memberList) self.syncMemberAnnotations(self.viewModel.memberList)
self.refreshMQTTSubscriptions(self.viewModel.memberList) self.refreshMQTTSubscriptions(self.viewModel.memberList)
@ -855,7 +871,7 @@ class HomeViewController: BaseViewController {
self.groupInfoRequestID == requestID, self.groupInfoRequestID == requestID,
self.viewModel.groupModel?.default_group_key == groupKey else { return } self.viewModel.groupModel?.default_group_key == groupKey else { return }
if showError { if showError {
DLToast.showError(text: error.localizedDescription) DLToast.showError(error)
} }
if retryAllowed { if retryAllowed {
self.scheduleGroupDataRetry(changes: changes) self.scheduleGroupDataRetry(changes: changes)
@ -945,7 +961,7 @@ class HomeViewController: BaseViewController {
GroupService.operate(opType: "setdefault", requestData: ["group_key" : groupKey]) GroupService.operate(opType: "setdefault", requestData: ["group_key" : groupKey])
.subscribe(onNext: { _ in }, onError: { error in .subscribe(onNext: { _ in }, onError: { error in
DLToast.showError(text: error.localizedDescription) DLToast.showError(error)
}) })
.disposed(by: disposeBag) .disposed(by: disposeBag)
} }
@ -1168,7 +1184,8 @@ class HomeViewController: BaseViewController {
rootView.groupMemberView.setupMemberInfo( rootView.groupMemberView.setupMemberInfo(
model, model,
isOwner: viewModel.isGroupOwn(id: model.user_id), isOwner: viewModel.isGroupOwn(id: model.user_id),
phoneUsage: usage phoneUsage: usage,
groupVip: viewModel.groupVip
) )
} }
@ -1424,7 +1441,7 @@ extension HomeViewController {
self?.queryUnlockRequests() self?.queryUnlockRequests()
done(true) done(true)
}, onError: { error in }, onError: { error in
DLToast.show(text: error.gatewayMessage ?? "解锁失败") DLToast.show(error, fallbackText: "解锁失败")
done(false) done(false)
}) })
.disposed(by: disposeBag) .disposed(by: disposeBag)

View File

@ -87,6 +87,7 @@ class HomeViewModel {
} }
private(set) var memberList: [GroupMemberModel] = [] private(set) var memberList: [GroupMemberModel] = []
private(set) var groupVip = GroupMemberVip()
func markMemberInBubble(userId: String) { func markMemberInBubble(userId: String) {
updateMemberStatus(userId: userId, status: 2) updateMemberStatus(userId: userId, status: 2)
@ -112,7 +113,13 @@ class HomeViewModel {
sectionedItems.onNext(memberList.mapSection()) sectionedItems.onNext(memberList.mapSection())
} }
func updateGroupUsers(_ list: [GroupMemberModel], vip: GroupMemberVip) {
groupVip = vip
updateMembers(list)
}
func clearMembers() { func clearMembers() {
groupVip = GroupMemberVip()
updateMembers([]) updateMembers([])
} }

View File

@ -339,7 +339,7 @@ final class LockedAppPopView: UIView {
DLToast.show(text: "已向圈主发送解锁请求") DLToast.show(text: "已向圈主发送解锁请求")
Self.dismiss() Self.dismiss()
}, onError: { error in }, onError: { error in
DLToast.show(text: error.gatewayMessage ?? "发送失败") DLToast.show(error, fallbackText: "发送失败")
}) })
.disposed(by: disposeBag) .disposed(by: disposeBag)
} }

View File

@ -74,8 +74,10 @@ final class MemberAppUsageView: UIView {
private let usageTitleLab = UILabel() private let usageTitleLab = UILabel()
private let topTitleLab = UILabel() private let topTitleLab = UILabel()
private let topStack = UIStackView() private let topStack = UIStackView()
private let vipRequiredView = UIImageView()
private let deniedOverlay = PrivilegeDeniedOverlay() private let deniedOverlay = PrivilegeDeniedOverlay()
private(set) var isDenied = false private(set) var isDenied = false
private(set) var isVipRequired = false
var onDeniedAction: (() -> Void)? var onDeniedAction: (() -> Void)?
override init(frame: CGRect) { override init(frame: CGRect) {
@ -88,8 +90,9 @@ final class MemberAppUsageView: UIView {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
func setDenied(_ denied: Bool) { func setAccessState(_ state: MemberFeatureAccessState) {
isDenied = denied isDenied = state == .privilegeDenied
isVipRequired = state == .vipRequired
deniedOverlay.configure( deniedOverlay.configure(
title: "TA未开启设置", title: "TA未开启设置",
message: "无法查看app使用记录", message: "无法查看app使用记录",
@ -97,9 +100,13 @@ final class MemberAppUsageView: UIView {
imageName: "PrivilegeDenied/app_usage" imageName: "PrivilegeDenied/app_usage"
) )
deniedOverlay.onAction = { [weak self] in self?.onDeniedAction?() } deniedOverlay.onAction = { [weak self] in self?.onDeniedAction?() }
deniedOverlay.setVisible(denied) deniedOverlay.setVisible(isDenied)
deniedOverlay.layer.cornerRadius = cardView.layer.cornerRadius deniedOverlay.layer.cornerRadius = cardView.layer.cornerRadius
gestureRecognizers?.forEach { $0.isEnabled = !denied } vipRequiredView.isHidden = !isVipRequired
[usageTitleLab, topTitleLab, donutView, totalTimeLab, totalHintLab, topStack].forEach {
$0.isHidden = isVipRequired
}
gestureRecognizers?.forEach { $0.isEnabled = !isDenied }
} }
func configure(with preview: MemberAppUsagePreview?) { func configure(with preview: MemberAppUsagePreview?) {
@ -200,6 +207,14 @@ final class MemberAppUsageView: UIView {
.left(160) .left(160)
.right(14) .right(14)
vipRequiredView.image = UIImage(named: "MemberVipRequired/app_usage")
vipRequiredView.contentMode = .scaleAspectFill
vipRequiredView.clipsToBounds = true
vipRequiredView.isHidden = true
vipRequiredView.isUserInteractionEnabled = false
cardView.addSubview(vipRequiredView)
vipRequiredView.layoutChain.edges()
cardView.bringSubviewToFront(deniedOverlay) cardView.bringSubviewToFront(deniedOverlay)
} }

View File

@ -52,8 +52,10 @@ final class MemberPhoneReportView: UIView {
private let unlockIcon = UIImageView() private let unlockIcon = UIImageView()
private let unlockTitleLab = UILabel() private let unlockTitleLab = UILabel()
private let unlockValueLab = UILabel() private let unlockValueLab = UILabel()
private let vipRequiredView = UIImageView()
private let deniedOverlay = PrivilegeDeniedOverlay() private let deniedOverlay = PrivilegeDeniedOverlay()
private(set) var isDenied = false private(set) var isDenied = false
private(set) var isVipRequired = false
var onDeniedAction: (() -> Void)? var onDeniedAction: (() -> Void)?
override init(frame: CGRect) { override init(frame: CGRect) {
@ -66,8 +68,9 @@ final class MemberPhoneReportView: UIView {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
func setDenied(_ denied: Bool) { func setAccessState(_ state: MemberFeatureAccessState) {
isDenied = denied isDenied = state == .privilegeDenied
isVipRequired = state == .vipRequired
deniedOverlay.configure( deniedOverlay.configure(
title: "TA未开启设置", title: "TA未开启设置",
message: "无法查看手机报告", message: "无法查看手机报告",
@ -75,9 +78,12 @@ final class MemberPhoneReportView: UIView {
imageName: "PrivilegeDenied/phone_report" imageName: "PrivilegeDenied/phone_report"
) )
deniedOverlay.onAction = { [weak self] in self?.onDeniedAction?() } deniedOverlay.onAction = { [weak self] in self?.onDeniedAction?() }
deniedOverlay.setVisible(denied) deniedOverlay.setVisible(isDenied)
deniedOverlay.layer.cornerRadius = cardView.layer.cornerRadius deniedOverlay.layer.cornerRadius = cardView.layer.cornerRadius
gestureRecognizers?.forEach { $0.isEnabled = !denied } vipRequiredView.isHidden = !isVipRequired
leftPanel.isHidden = isVipRequired
rightPanel.isHidden = isVipRequired
gestureRecognizers?.forEach { $0.isEnabled = !isDenied }
} }
func configure(with preview: MemberPhoneReportPreview?) { func configure(with preview: MemberPhoneReportPreview?) {
@ -259,6 +265,14 @@ final class MemberPhoneReportView: UIView {
.leftToRightOfView(unlockValueLab, offset: 3) .leftToRightOfView(unlockValueLab, offset: 3)
.bottomToView(unlockValueLab, offset: -4) .bottomToView(unlockValueLab, offset: -4)
vipRequiredView.image = UIImage(named: "MemberVipRequired/phone_report")
vipRequiredView.contentMode = .scaleAspectFill
vipRequiredView.clipsToBounds = true
vipRequiredView.isHidden = true
vipRequiredView.isUserInteractionEnabled = false
cardView.addSubview(vipRequiredView)
vipRequiredView.layoutChain.edges()
cardView.bringSubviewToFront(deniedOverlay) cardView.bringSubviewToFront(deniedOverlay)
} }
} }

View File

@ -137,7 +137,7 @@ final class SOSListViewController: BaseViewController {
self.rootView.tableView.reloadData() self.rootView.tableView.reloadData()
}, onError: { [weak self] error in }, onError: { [weak self] error in
self?.finishLoading() self?.finishLoading()
DLToast.showError(text: error.gatewayMessage ?? error.localizedDescription) DLToast.showError(error)
}) })
.disposed(by: disposeBag) .disposed(by: disposeBag)
} }

View File

@ -83,7 +83,7 @@ final class SOSViewController: BaseViewController {
self.rootView.setState(model.status == 1 ? .active : .idle) self.rootView.setState(model.status == 1 ? .active : .idle)
}, onError: { [weak self] error in }, onError: { [weak self] error in
guard let self, requestVersion == self.statusRequestVersion else { return } guard let self, requestVersion == self.statusRequestVersion else { return }
DLToast.showError(text: error.localizedDescription) DLToast.showError(error)
}) })
.disposed(by: disposeBag) .disposed(by: disposeBag)
} }

View File

@ -43,14 +43,20 @@ class SearchLocationResultVC: BaseViewController {
private func applyResultPresentation() { private func applyResultPresentation() {
handleCode() handleCode()
let isVip = isVipUnlocked() let isVip = isVipUnlocked()
rootView.vipMaskView.isHidden = isVip rootView.updateVipAccess(isUnlocked: isVip)
if !isVip { if !isVip {
rootView.actionBtn.setTitle("开通会员", for: .normal) rootView.actionBtn.setTitle("开通会员 解锁功能", for: .normal)
rootView.actionBtn.isHidden = false rootView.actionBtn.isHidden = false
} }
} }
private func reactiveAction() { private func reactiveAction() {
rootView.vipMaskView.rx.tap
.subscribe(onNext: {
AppRouter.push(Route.vipRecharge)
})
.disposed(by: disposeBag)
rootView.actionBtn.rx.tap.subscribe(onNext: { _ in rootView.actionBtn.rx.tap.subscribe(onNext: { _ in
guard self.isVipUnlocked() else { guard self.isVipUnlocked() else {
AppRouter.push(Route.vipRecharge) AppRouter.push(Route.vipRecharge)

View File

@ -9,6 +9,7 @@ import Lottie
class SearchLocationResultView: UIView { class SearchLocationResultView: UIView {
let headerView = SearchLocationHeaderView() let headerView = SearchLocationHeaderView()
private var vipResultHeightConstraint: NSLayoutConstraint?
private func setupUI() { private func setupUI() {
addSubview(navBgView) addSubview(navBgView)
@ -44,6 +45,17 @@ class SearchLocationResultView: UIView {
.height(showInviteRow ? 72 : 0) .height(showInviteRow ? 72 : 0)
} }
func updateVipAccess(isUnlocked: Bool) {
normalResultContentView.isHidden = !isUnlocked
vipMaskView.isHidden = isUnlocked
vipOfferImageView.isHidden = isUnlocked
vipResultHeightConstraint?.isActive = !isUnlocked
actionBtn.setBackgroundImage(
UIImage(named: isUnlocked ? "Common/button_bg_2" : "VipFeature/wide_button_bg"),
for: .normal
)
}
lazy var navBgView: UIImageView = { lazy var navBgView: UIImageView = {
let iv = UIImageView() let iv = UIImageView()
iv.image = UIImage(named: "Common/navBar_bg_2") iv.image = UIImage(named: "Common/navBar_bg_2")
@ -73,6 +85,7 @@ class SearchLocationResultView: UIView {
resultCard.layoutChain resultCard.layoutChain
.topToBottomOfView(headerView, offset: 20) .topToBottomOfView(headerView, offset: 20)
.edgesHorzontal(15) .edgesHorzontal(15)
vipResultHeightConstraint = resultCard.heightAnchor.constraint(equalToConstant: 158)
contentView.addSubview(actionBtn) contentView.addSubview(actionBtn)
actionBtn.layoutChain actionBtn.layoutChain
@ -81,6 +94,15 @@ class SearchLocationResultView: UIView {
.height(56) .height(56)
.bottom(kSafeBottomMargin + 30) .bottom(kSafeBottomMargin + 30)
contentView.addSubview(vipOfferImageView)
vipOfferImageView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
vipOfferImageView.trailingAnchor.constraint(equalTo: actionBtn.trailingAnchor, constant: -6),
vipOfferImageView.bottomAnchor.constraint(equalTo: actionBtn.topAnchor, constant: 10),
vipOfferImageView.widthAnchor.constraint(equalToConstant: 94),
vipOfferImageView.heightAnchor.constraint(equalToConstant: 32)
])
return view return view
}() }()
@ -89,8 +111,25 @@ class SearchLocationResultView: UIView {
card.backgroundColor = UIColor(hexStr: "#EFF9FF") card.backgroundColor = UIColor(hexStr: "#EFF9FF")
card.cornerRadius = 16 card.cornerRadius = 16
card.addSubview(resultStackView)
resultStackView.layoutChain.edges()
resultStackView.addArrangedSubview(normalResultContentView)
card.addSubview(vipMaskView)
vipMaskView.layoutChain.edges()
return card
}()
private lazy var resultStackView: UIStackView = {
let stackView = UIStackView()
stackView.axis = .vertical
return stackView
}()
private lazy var normalResultContentView: UIView = {
let contentView = UIView()
let locationRow = UIView() let locationRow = UIView()
card.addSubview(locationRow) contentView.addSubview(locationRow)
let pinIcon = UIImageView(image: UIImage(named: "Home/member_location")) let pinIcon = UIImageView(image: UIImage(named: "Home/member_location"))
pinIcon.contentMode = .scaleAspectFit pinIcon.contentMode = .scaleAspectFit
@ -115,7 +154,7 @@ class SearchLocationResultView: UIView {
let pillContainer = UIView() let pillContainer = UIView()
pillContainer.backgroundColor = .white pillContainer.backgroundColor = .white
pillContainer.cornerRadius = 14 pillContainer.cornerRadius = 14
card.addSubview(pillContainer) contentView.addSubview(pillContainer)
pillContainer.addSubview(statusPillLab) pillContainer.addSubview(statusPillLab)
statusPillLab.layoutChain.edgesHorzontal(58).edgesVertical(13) statusPillLab.layoutChain.edgesHorzontal(58).edgesVertical(13)
pillContainer.layoutChain pillContainer.layoutChain
@ -123,30 +162,27 @@ class SearchLocationResultView: UIView {
.centerX() .centerX()
.height(48) .height(48)
card.addSubview(disclaimerIcon) contentView.addSubview(disclaimerIcon)
disclaimerIcon.layoutChain disclaimerIcon.layoutChain
.topToBottomOfView(pillContainer, offset: 14) .topToBottomOfView(pillContainer, offset: 14)
.left(24) .left(24)
.width(12) .width(12)
.height(12) .height(12)
card.addSubview(disclaimerLab) contentView.addSubview(disclaimerLab)
disclaimerLab.layoutChain disclaimerLab.layoutChain
.topToBottomOfView(pillContainer, offset: 11) .topToBottomOfView(pillContainer, offset: 11)
.leftToRightOfView(disclaimerIcon, offset: 3) .leftToRightOfView(disclaimerIcon, offset: 3)
.right(23) .right(23)
card.addSubview(inviteView) contentView.addSubview(inviteView)
inviteView.layoutChain inviteView.layoutChain
.topToBottomOfView(disclaimerLab, offset: 0) .topToBottomOfView(disclaimerLab, offset: 0)
.edgesHorzontal(12) .edgesHorzontal(12)
.height(0) .height(0)
.bottom(16) .bottom(16)
card.addSubview(vipMaskView) return contentView
vipMaskView.layoutChain.edges()
return card
}() }()
lazy var phoneAreaLab: UILabel = { lazy var phoneAreaLab: UILabel = {
@ -197,6 +233,13 @@ class SearchLocationResultView: UIView {
return btn return btn
}() }()
private lazy var vipOfferImageView: UIImageView = {
let view = UIImageView(image: UIImage(named: "VipFeature/limited_offer"))
view.contentMode = .scaleAspectFit
view.isHidden = true
return view
}()
var inviteBtn: UIButton { actionBtn } var inviteBtn: UIButton { actionBtn }
lazy var inviteView: UIView = { lazy var inviteView: UIView = {
@ -240,10 +283,9 @@ class SearchLocationResultView: UIView {
return btn return btn
}() }()
lazy var vipMaskView: UIImageView = { lazy var vipMaskView: UIButton = {
let view = UIImageView() let view = UIButton(type: .custom)
view.image = UIImage(named: "SearchLocation/vip_mask") view.setBackgroundImage(UIImage(named: "VipFeature/realtime_location"), for: .normal)
view.contentMode = .scaleAspectFill
view.isHidden = true view.isHidden = true
return view return view
}() }()

View File

@ -84,6 +84,11 @@ final class TodayTrackDetailVC: BaseViewController {
} }
} }
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
viewModel.start()
}
override func viewDidLayoutSubviews() { override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews() super.viewDidLayoutSubviews()
let width = rootView.dateCV.bounds.width let width = rootView.dateCV.bounds.width
@ -125,6 +130,12 @@ final class TodayTrackDetailVC: BaseViewController {
}) })
.disposed(by: disposeBag) .disposed(by: disposeBag)
rootView.vipRequiredBtn.rx.tap
.subscribe(onNext: {
AppRouter.push(Route.vipRecharge)
})
.disposed(by: disposeBag)
viewModel.displayedTrips viewModel.displayedTrips
.observe(on: MainScheduler.instance) .observe(on: MainScheduler.instance)
.subscribe(onNext: { [weak self] trips in .subscribe(onNext: { [weak self] trips in
@ -135,7 +146,22 @@ final class TodayTrackDetailVC: BaseViewController {
viewModel.isEmpty viewModel.isEmpty
.observe(on: MainScheduler.instance) .observe(on: MainScheduler.instance)
.subscribe(onNext: { [weak self] empty in .subscribe(onNext: { [weak self] empty in
self?.rootView.emptyLab.isHidden = !empty guard let self, !self.viewModel.isVipRequired.value else { return }
self.rootView.emptyLab.isHidden = !empty
})
.disposed(by: disposeBag)
viewModel.isVipRequired
.observe(on: MainScheduler.instance)
.distinctUntilChanged()
.subscribe(onNext: { [weak self] required in
guard let self else { return }
self.stopPlayback(resetProgress: true)
self.rootView.setVipRequired(required)
self.updateSheetHeight(isVipRequired: required)
if !required {
self.rootView.emptyLab.isHidden = !self.viewModel.isEmpty.value
}
}) })
.disposed(by: disposeBag) .disposed(by: disposeBag)
@ -174,7 +200,14 @@ final class TodayTrackDetailVC: BaseViewController {
}) })
.disposed(by: disposeBag) .disposed(by: disposeBag)
viewModel.start() }
private func updateSheetHeight(isVipRequired: Bool) {
guard let constraint = rootView.sheetHeightConstraint else { return }
let minHeight: CGFloat = 200 + kSafeBottomMargin
let maxHeight: CGFloat = UIScreen.main.bounds.height * 0.7
let vipHeight: CGFloat = UIScreen.main.bounds.height * 0.47
constraint.constant = isVipRequired ? min(maxHeight, max(minHeight, vipHeight)) : minHeight
} }
private func bindSheetPan() { private func bindSheetPan() {

View File

@ -150,6 +150,14 @@ final class TodayTrackDetailView: UIView {
return lab return lab
}() }()
let vipRequiredBtn: UIButton = {
let btn = UIButton(type: .custom)
btn.setImage(UIImage(named: "Home/HistoryTrack/vip_required"), for: .normal)
btn.imageView?.contentMode = .scaleAspectFit
btn.isHidden = true
return btn
}()
private(set) var sheetHeightConstraint: NSLayoutConstraint? private(set) var sheetHeightConstraint: NSLayoutConstraint?
override init(frame: CGRect) { override init(frame: CGRect) {
@ -263,6 +271,20 @@ final class TodayTrackDetailView: UIView {
emptyLab.layoutChain emptyLab.layoutChain
.centerX() .centerX()
.topToBottomOfView(dateBar, offset: 40) .topToBottomOfView(dateBar, offset: 40)
sheetView.addSubview(vipRequiredBtn)
vipRequiredBtn.layoutChain
.topToBottomOfView(dateBar, offset: 28)
.centerX()
.width(303)
.height(139)
}
func setVipRequired(_ required: Bool) {
vipRequiredBtn.isHidden = !required
timelineTV.isHidden = required
emptyLab.isHidden = true
playbackBar.isHidden = required
} }
func cleanupMap() { func cleanupMap() {

View File

@ -23,6 +23,7 @@ final class TodayTrackDetailViewModel {
let selectedTripId = BehaviorRelay<String?>(value: nil) let selectedTripId = BehaviorRelay<String?>(value: nil)
let displayedTrips = BehaviorRelay<[ScheduleRecordModel]>(value: []) let displayedTrips = BehaviorRelay<[ScheduleRecordModel]>(value: [])
let isEmpty = BehaviorRelay<Bool>(value: true) let isEmpty = BehaviorRelay<Bool>(value: true)
let isVipRequired = BehaviorRelay<Bool>(value: false)
let loading = BehaviorRelay<Bool>(value: false) let loading = BehaviorRelay<Bool>(value: false)
private let disposeBag = DisposeBag() private let disposeBag = DisposeBag()
@ -75,6 +76,7 @@ final class TodayTrackDetailViewModel {
selectedTripId.accept(nil) selectedTripId.accept(nil)
trips.accept([]) trips.accept([])
displayedTrips.accept([]) displayedTrips.accept([])
isVipRequired.accept(false)
isEmpty.accept(false) isEmpty.accept(false)
loadTrips(userId: userId, dateKey: dateKey) loadTrips(userId: userId, dateKey: dateKey)
} }
@ -93,14 +95,19 @@ final class TodayTrackDetailViewModel {
.subscribe(onNext: { [weak self] response in .subscribe(onNext: { [weak self] response in
guard let self, self.requestToken == token else { return } guard let self, self.requestToken == token else { return }
self.applyTrips(response.trips) self.applyTrips(response.trips)
}, onError: { [weak self] _ in }, onError: { [weak self] error in
guard let self, self.requestToken == token else { return } guard let self, self.requestToken == token else { return }
if error.gatewayStatusCode == 22000 {
self.applyVipRequired()
return
}
self.applyTrips([]) self.applyTrips([])
}) })
.disposed(by: disposeBag) .disposed(by: disposeBag)
} }
private func applyTrips(_ list: [ScheduleRecordModel]) { private func applyTrips(_ list: [ScheduleRecordModel]) {
isVipRequired.accept(false)
let sorted = list.sorted { $0.start_time < $1.start_time } let sorted = list.sorted { $0.start_time < $1.start_time }
trips.accept(sorted) trips.accept(sorted)
if selectedTripId.value == nil || !sorted.contains(where: { $0.id == selectedTripId.value }) { if selectedTripId.value == nil || !sorted.contains(where: { $0.id == selectedTripId.value }) {
@ -110,6 +117,15 @@ final class TodayTrackDetailViewModel {
loading.accept(false) loading.accept(false)
} }
private func applyVipRequired() {
selectedTripId.accept(nil)
trips.accept([])
displayedTrips.accept([])
isEmpty.accept(false)
isVipRequired.accept(true)
loading.accept(false)
}
private func applyDisplayedTrips() { private func applyDisplayedTrips() {
let all = trips.value let all = trips.value
if let id = selectedTripId.value, let one = all.first(where: { $0.id == id }) { if let id = selectedTripId.value, let one = all.first(where: { $0.id == id }) {

View File

@ -13,10 +13,12 @@ final class LockDistractVC: BaseViewController {
private var groupModel: GroupModel? private var groupModel: GroupModel?
private var ownedGroups: [GroupInfoModel] = [] private var ownedGroups: [GroupInfoModel] = []
private var activeGroupKey: String = "" private var activeGroupKey: String = ""
private var groupVip = GroupMemberVip()
private var members: [GroupMemberModel] = [] private var members: [GroupMemberModel] = []
private var selectedIndex = 0 private var selectedIndex = 0
private var lockAppsDisposable: Disposable? private var lockAppsDisposable: Disposable?
private let lockStateSubscriptionOwner = UUID() private let lockStateSubscriptionOwner = UUID()
private var hasAppeared = false
override var isNavigationBarHidden: Bool { true } override var isNavigationBarHidden: Bool { true }
@ -29,9 +31,18 @@ final class LockDistractVC: BaseViewController {
super.viewDidLoad() super.viewDidLoad()
view.backgroundColor = UIColor(hexStr: "#FAFAFA") view.backgroundColor = UIColor(hexStr: "#FAFAFA")
bind() bind()
applyGroupVip(groupVip)
loadGroupMembers() loadGroupMembers()
} }
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if hasAppeared {
refreshGroupVip()
}
hasAppeared = true
}
@MainActor deinit { @MainActor deinit {
MQTTService.shared.updateLockStateSubscriptions( MQTTService.shared.updateLockStateSubscriptions(
owner: lockStateSubscriptionOwner, owner: lockStateSubscriptionOwner,
@ -145,6 +156,7 @@ final class LockDistractVC: BaseViewController {
private func loadUsers(for groupKey: String) { private func loadUsers(for groupKey: String) {
guard !groupKey.isEmpty else { guard !groupKey.isEmpty else {
applyGroupVip(GroupMemberVip())
applyMembers([]) applyMembers([])
return return
} }
@ -154,9 +166,11 @@ final class LockDistractVC: BaseViewController {
DLToast.showError(text: response.message ?? "获取圈子成员失败") DLToast.showError(text: response.message ?? "获取圈子成员失败")
return return
} }
self?.applyMembers(response.list) guard let self, self.activeGroupKey == groupKey else { return }
self.applyGroupVip(response.vip)
self.applyMembers(response.list)
}, onError: { error in }, onError: { error in
DLToast.showError(text: error.localizedDescription) DLToast.showError(error)
}) })
.disposed(by: disposeBag) .disposed(by: disposeBag)
} }
@ -173,6 +187,28 @@ final class LockDistractVC: BaseViewController {
loadLockableApps() loadLockableApps()
} }
private var isAppLockVipUnlocked: Bool {
groupVip.app_lock == true
}
private func applyGroupVip(_ vip: GroupMemberVip) {
groupVip = vip
rootView.updateVipAccess(isUnlocked: isAppLockVipUnlocked)
}
private func refreshGroupVip() {
let groupKey = activeGroupKey
guard !groupKey.isEmpty else { return }
GroupService.groupUsers(groupKey: groupKey)
.subscribe(onNext: { [weak self] response in
guard let self,
self.activeGroupKey == groupKey,
response.isValid(for: groupKey) else { return }
self.applyGroupVip(response.vip)
}, onError: { _ in })
.disposed(by: disposeBag)
}
private func stepMember(_ delta: Int) { private func stepMember(_ delta: Int) {
guard !members.isEmpty else { return } guard !members.isEmpty else { return }
let count = members.count let count = members.count
@ -258,7 +294,7 @@ final class LockDistractVC: BaseViewController {
self.rootView.configureLockableApps([]) self.rootView.configureLockableApps([])
self.rootView.configureLocked(false) self.rootView.configureLocked(false)
self.rootView.setTodayLockCount(0) self.rootView.setTodayLockCount(0)
DLToast.show(text: error.gatewayMessage ?? "获取可锁应用失败") DLToast.show(error, fallbackText: "获取可锁应用失败")
}) })
lockAppsDisposable?.disposed(by: disposeBag) lockAppsDisposable?.disposed(by: disposeBag)
} }
@ -290,10 +326,11 @@ final class LockDistractVC: BaseViewController {
} }
DLToast.showLoading() DLToast.showLoading()
UserService.requestPhoneUnlock(os: os, groupKey: activeGroupKey, tokens: tokens) UserService.requestPhoneUnlock(os: os, groupKey: activeGroupKey, tokens: tokens)
.subscribe(onNext: { _ in .subscribe(onNext: { [weak self] _ in
DLToast.show(text: "已向圈主发送解锁请求") DLToast.show(text: "已向圈主发送解锁请求")
self?.refreshGroupVip()
}, onError: { error in }, onError: { error in
DLToast.show(text: error.gatewayMessage ?? "发送失败") DLToast.show(error, fallbackText: "发送失败")
}) })
.disposed(by: disposeBag) .disposed(by: disposeBag)
} }
@ -322,14 +359,19 @@ final class LockDistractVC: BaseViewController {
self?.rootView.lockBtn.isEnabled = true self?.rootView.lockBtn.isEnabled = true
DLToast.show(text: "已解锁") DLToast.show(text: "已解锁")
self?.loadLockableApps() self?.loadLockableApps()
self?.refreshGroupVip()
}, onError: { [weak self] error in }, onError: { [weak self] error in
self?.rootView.lockBtn.isEnabled = true self?.rootView.lockBtn.isEnabled = true
DLToast.show(text: error.gatewayMessage ?? "解锁失败") DLToast.show(error, fallbackText: "解锁失败")
}) })
.disposed(by: disposeBag) .disposed(by: disposeBag)
} }
private func performPrimaryAction() { private func performPrimaryAction() {
if rootView.primaryAction == .lock, !isAppLockVipUnlocked {
AppRouter.push(Route.vipRecharge)
return
}
switch rootView.primaryAction { switch rootView.primaryAction {
case .requestUnlock: case .requestUnlock:
requestUnlockForLockedApps() requestUnlockForLockedApps()
@ -343,6 +385,10 @@ final class LockDistractVC: BaseViewController {
} }
private func lockSelectedApps() { private func lockSelectedApps() {
guard isAppLockVipUnlocked else {
AppRouter.push(Route.vipRecharge)
return
}
guard let member = selectedMember, !activeGroupKey.isEmpty else { guard let member = selectedMember, !activeGroupKey.isEmpty else {
DLToast.show(text: "请选择要锁定的成员") DLToast.show(text: "请选择要锁定的成员")
return return
@ -368,9 +414,10 @@ final class LockDistractVC: BaseViewController {
self?.rootView.lockBtn.isEnabled = true self?.rootView.lockBtn.isEnabled = true
DLToast.show(text: "锁定成功") DLToast.show(text: "锁定成功")
self?.loadLockableApps() self?.loadLockableApps()
self?.refreshGroupVip()
}, onError: { [weak self] error in }, onError: { [weak self] error in
self?.rootView.lockBtn.isEnabled = true self?.rootView.lockBtn.isEnabled = true
DLToast.show(text: error.gatewayMessage ?? "锁定失败") DLToast.show(error, fallbackText: "锁定失败")
}) })
.disposed(by: disposeBag) .disposed(by: disposeBag)
} }

View File

@ -35,6 +35,7 @@ final class LockDistractView: UIView {
private(set) var isLocked = false private(set) var isLocked = false
private(set) var isCurrentUser = false private(set) var isCurrentUser = false
private(set) var primaryAction: LockDistractPrimaryAction = .lock private(set) var primaryAction: LockDistractPrimaryAction = .lock
private(set) var isVipUnlocked = true
private let copyMaxLength = 20 private let copyMaxLength = 20
private var lockBtnHeightConstraint: NSLayoutConstraint? private var lockBtnHeightConstraint: NSLayoutConstraint?
private var bottomStackToAppsConstraint: NSLayoutConstraint? private var bottomStackToAppsConstraint: NSLayoutConstraint?
@ -110,6 +111,11 @@ final class LockDistractView: UIView {
appCollectionView.reloadData() appCollectionView.reloadData()
} }
func updateVipAccess(isUnlocked: Bool) {
isVipUnlocked = isUnlocked
applyLockedState()
}
var selectedLockApps: [PhoneLockAppItem] { var selectedLockApps: [PhoneLockAppItem] {
lockableApps.enumerated().compactMap { index, app in lockableApps.enumerated().compactMap { index, app in
selectedAppIndexes.contains(index) ? app : nil selectedAppIndexes.contains(index) ? app : nil
@ -167,16 +173,23 @@ final class LockDistractView: UIView {
appCollectionView.reloadData() appCollectionView.reloadData()
lockBtn.isHidden = lockedByOthers lockBtn.isHidden = lockedByOthers
lockBtnHeightConstraint?.constant = lockedByOthers ? 0 : 56 lockBtnHeightConstraint?.constant = lockedByOthers ? 0 : 56
vipOfferImageView.isHidden = lockedByOthers || primaryAction != .lock || isVipUnlocked
switch primaryAction { switch primaryAction {
case .lock: case .lock:
tipLab.text = "锁定功能需要在对方开启权限情况下才能使用哟~" tipLab.text = "锁定功能需要在对方开启权限情况下才能使用哟~"
lockBtn.setTitle("锁定", for: .normal) lockBtn.setTitle(isVipUnlocked ? "锁定" : "开通会员 解锁功能", for: .normal)
lockBtn.setBackgroundImage(
UIImage(named: isVipUnlocked ? "Common/button_bg_2" : "VipFeature/wide_button_bg"),
for: .normal
)
case .requestUnlock: case .requestUnlock:
tipLab.text = "已被其他圈主锁定,请先申请解锁哟~" tipLab.text = "已被其他圈主锁定,请先申请解锁哟~"
lockBtn.setTitle("请求解锁", for: .normal) lockBtn.setTitle("请求解锁", for: .normal)
lockBtn.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal)
case .unlock: case .unlock:
tipLab.text = "TA已经被你锁定解锁后才能重新设置哟~" tipLab.text = "TA已经被你锁定解锁后才能重新设置哟~"
lockBtn.setTitle("解锁", for: .normal) lockBtn.setTitle("解锁", for: .normal)
lockBtn.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal)
case .hidden: case .hidden:
tipLab.text = "该成员的APP已被其他圈主锁定~" tipLab.text = "该成员的APP已被其他圈主锁定~"
} }
@ -195,6 +208,7 @@ final class LockDistractView: UIView {
addSubview(footerBar) addSubview(footerBar)
footerBar.addSubview(tipRow) footerBar.addSubview(tipRow)
footerBar.addSubview(lockBtn) footerBar.addSubview(lockBtn)
footerBar.addSubview(vipOfferImageView)
contentView.addSubview(navBgView) contentView.addSubview(navBgView)
contentView.addSubview(statsRow) contentView.addSubview(statsRow)
@ -230,6 +244,14 @@ final class LockDistractView: UIView {
.bottomToTopOfView(lockBtn, offset: -8) .bottomToTopOfView(lockBtn, offset: -8)
.top(10) .top(10)
vipOfferImageView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
vipOfferImageView.trailingAnchor.constraint(equalTo: lockBtn.trailingAnchor, constant: -6),
vipOfferImageView.bottomAnchor.constraint(equalTo: lockBtn.topAnchor, constant: 10),
vipOfferImageView.widthAnchor.constraint(equalToConstant: 94),
vipOfferImageView.heightAnchor.constraint(equalToConstant: 32)
])
scrollView.layoutChain scrollView.layoutChain
.top() .top()
.edgesHorzontal() .edgesHorzontal()
@ -705,6 +727,13 @@ final class LockDistractView: UIView {
return btn return btn
}() }()
lazy var vipOfferImageView: UIImageView = {
let view = UIImageView(image: UIImage(named: "VipFeature/limited_offer"))
view.contentMode = .scaleAspectFit
view.isHidden = true
return view
}()
private func makeSectionTitle(_ text: String) -> UIView { private func makeSectionTitle(_ text: String) -> UIView {
let row = UIView() let row = UIView()
let star = UIImageView(image: UIImage(named: "LockDistract/section_star")) let star = UIImageView(image: UIImage(named: "LockDistract/section_star"))

View File

@ -85,9 +85,11 @@ class LoginView: UIView {
addSubview(rightSeparator) addSubview(rightSeparator)
addSubview(agreementView) addSubview(agreementView)
agreementView.addSubview(checkBox) addSubview(checkBox)
agreementView.addSubview(agreementTV) agreementView.addSubview(agreementTV)
agreementView.addSubview(agreementLabel) agreementView.addSubview(agreementLabel)
// agreementView.addSubview(checkBox)
bgMaskImage.layoutChain.edges(excludingEdge: .bottom).heightToWidth(320/375) bgMaskImage.layoutChain.edges(excludingEdge: .bottom).heightToWidth(320/375)
@ -176,15 +178,13 @@ class LoginView: UIView {
agreementLabel.layoutChain.edges() agreementLabel.layoutChain.edges()
checkBox.layoutChain checkBox.layoutChain
.left() .rightToLeftOfView(agreementView, offset: 0)
.centerY() .centerY(agreementView)
.width(12) .width(12)
.height(12) .height(12)
agreementTV.layoutChain agreementTV.layoutChain
.leftToRightOfView(checkBox, offset: 0) .edges()
.right()
.top()
phoneLoginBtn.layoutChain phoneLoginBtn.layoutChain
.bottom(kSafeBottomMargin + 30) .bottom(kSafeBottomMargin + 30)
@ -457,7 +457,7 @@ class LoginView: UIView {
let btn = UIButton(type: .custom) let btn = UIButton(type: .custom)
btn.setImage(UIImage(named: "Login/checkbox"), for: .normal) btn.setImage(UIImage(named: "Login/checkbox"), for: .normal)
btn.setImage(UIImage(named: "Login/selected"), for: .selected) btn.setImage(UIImage(named: "Login/selected"), for: .selected)
btn.extendEdgeInsets = UIEdgeInsets(top: 20, left: 100, bottom: 100, right: 20) btn.extendEdgeInsets = UIEdgeInsets(top: 20, left: 150, bottom: 150, right: 50)
return btn return btn
}() }()

View File

@ -66,8 +66,8 @@ class LoginViewController: BaseViewController {
return return
} }
guard let phone = self.rootView.phoneInputTF.text, !phone.isEmpty else { guard let phone = self.rootView.phoneInputTF.text, !phone.isEmpty, phone.count == 11 else {
DLToast.showError(text: "请输入手机号码") DLToast.showError(text: "请输入正确手机号码")
return return
} }

View File

@ -66,7 +66,8 @@ class OneTapLoginView: UIView {
inputContainerView.addSubview(loginButton) inputContainerView.addSubview(loginButton)
addSubview(agreementView) addSubview(agreementView)
agreementView.addSubview(checkBox) addSubview(checkBox)
// agreementView.addSubview(checkBox)
agreementView.addSubview(agreementLabel) agreementView.addSubview(agreementLabel)
agreementView.addSubview(agreementTV) agreementView.addSubview(agreementTV)
@ -107,8 +108,8 @@ class OneTapLoginView: UIView {
.edgesHorzontal(30) .edgesHorzontal(30)
checkBox.layoutChain checkBox.layoutChain
.left() .rightToLeftOfView(agreementView, offset: 0)
.top(10) .centerY(agreementView)
.width(12) .width(12)
.height(12) .height(12)
@ -119,10 +120,7 @@ class OneTapLoginView: UIView {
.bottom() .bottom()
agreementTV.layoutChain agreementTV.layoutChain
.leftToRightOfView(checkBox, offset: 0) .edges()
.right()
.top()
.bottom()
} }
override init(frame: CGRect) { override init(frame: CGRect) {

View File

@ -122,7 +122,7 @@ class AccountVC: BaseViewController {
NotificationCenter.default.post(name: .RefreshUserConfigNotification, object: nil) NotificationCenter.default.post(name: .RefreshUserConfigNotification, object: nil)
}, onError: { error in }, onError: { error in
DLToast.dismiss() DLToast.dismiss()
DLToast.show(text: error.localizedDescription) DLToast.show(error)
}).disposed(by: disposeBag) }).disposed(by: disposeBag)
} }
@ -140,7 +140,7 @@ class AccountVC: BaseViewController {
NotificationCenter.default.post(name: .RefreshUserConfigNotification, object: nil) NotificationCenter.default.post(name: .RefreshUserConfigNotification, object: nil)
}, onError: { error in }, onError: { error in
DLToast.dismiss() DLToast.dismiss()
DLToast.show(text: error.localizedDescription) DLToast.show(error)
}).disposed(by: disposeBag) }).disposed(by: disposeBag)
} }
@ -156,7 +156,7 @@ class AccountVC: BaseViewController {
NotificationCenter.default.post(name: .RefreshUserConfigNotification, object: nil) NotificationCenter.default.post(name: .RefreshUserConfigNotification, object: nil)
}, onError: { error in }, onError: { error in
DLToast.dismiss() DLToast.dismiss()
DLToast.show(text: error.localizedDescription) DLToast.show(error)
}).disposed(by: disposeBag) }).disposed(by: disposeBag)
} }

View File

@ -194,7 +194,7 @@ final class FeedbackVC: BaseViewController {
pendingDraft = nil pendingDraft = nil
isSubmitting = false isSubmitting = false
rootView.setSubmitting(false) rootView.setSubmitting(false)
DLToast.show(text: error.localizedDescription) DLToast.show(error)
} }
private func compressedJPEG(from image: UIImage) -> Data? { private func compressedJPEG(from image: UIImage) -> Data? {

View File

@ -127,7 +127,7 @@ final class MineViewController: BaseViewController {
groupInfo: response.model groupInfo: response.model
) )
}, onError: { error in }, onError: { error in
DLToast.showError(text: error.localizedDescription) DLToast.showError(error)
}) })
.disposed(by: disposeBag) .disposed(by: disposeBag)
} }

View File

@ -79,7 +79,7 @@ final class MoodStatusVC: BaseViewController {
} }
}, onError: { [weak self] error in }, onError: { [weak self] error in
guard let self, requestVersion == self.statusRequestVersion else { return } guard let self, requestVersion == self.statusRequestVersion else { return }
DLToast.showError(text: error.localizedDescription) DLToast.showError(error)
}) })
.disposed(by: disposeBag) .disposed(by: disposeBag)
} }

View File

@ -84,7 +84,7 @@ class AvatarIconListVC: BaseViewController {
self.rootView.setPreviewImage(image) self.rootView.setPreviewImage(image)
self.onSelectIcon?(url) self.onSelectIcon?(url)
}, onError: { error in }, onError: { error in
DLToast.show(text: error.gatewayMessage ?? "更换失败") DLToast.show(error, fallbackText: "更换失败")
}) })
.disposed(by: disposeBag) .disposed(by: disposeBag)
} }

View File

@ -93,7 +93,7 @@ final class PrivacySettingsViewController: BaseViewController {
}, onError: { [weak self] error in }, onError: { [weak self] error in
self?.groups = [] self?.groups = []
self?.rootView.configure(groups: [], selectedGroupKey: "") self?.rootView.configure(groups: [], selectedGroupKey: "")
DLToast.showError(text: error.localizedDescription) DLToast.showError(error)
}) })
.disposed(by: disposeBag) .disposed(by: disposeBag)
} }
@ -127,7 +127,7 @@ final class PrivacySettingsViewController: BaseViewController {
}, onError: { [weak self] error in }, onError: { [weak self] error in
self?.rootView.setSettingsEnabled(!(self?.groups.isEmpty ?? true)) self?.rootView.setSettingsEnabled(!(self?.groups.isEmpty ?? true))
self?.applySelectedPrivilege() self?.applySelectedPrivilege()
DLToast.showError(text: error.localizedDescription) DLToast.showError(error)
}) })
.disposed(by: disposeBag) .disposed(by: disposeBag)
} }
@ -242,7 +242,7 @@ final class PrivacySettingsViewController: BaseViewController {
self.isSubmitting = false self.isSubmitting = false
self.rootView.setSettingsEnabled(!self.groups.isEmpty) self.rootView.setSettingsEnabled(!self.groups.isEmpty)
self.revertPrivilege(groupKey: groupKey, previous: previous) self.revertPrivilege(groupKey: groupKey, previous: previous)
DLToast.showError(text: error.localizedDescription) DLToast.showError(error)
}) })
.disposed(by: disposeBag) .disposed(by: disposeBag)
} }

View File

@ -13,6 +13,7 @@ final class PigeonMessageVC: BaseViewController {
private var rootView: PigeonMessageView! private var rootView: PigeonMessageView!
private var groupModel: GroupModel? private var groupModel: GroupModel?
private var activeGroupKey: String = "" private var activeGroupKey: String = ""
private var groupVip = GroupMemberVip()
private var members: [GroupMemberModel] = [] private var members: [GroupMemberModel] = []
private var selectedMembers: [GroupMemberModel] = [] private var selectedMembers: [GroupMemberModel] = []
private var selectedImage: UIImage? private var selectedImage: UIImage?
@ -26,6 +27,7 @@ final class PigeonMessageVC: BaseViewController {
private var playbackTimer: Timer? private var playbackTimer: Timer?
private var recordedDuration: TimeInterval = 0 private var recordedDuration: TimeInterval = 0
private var isPressingRecord = false private var isPressingRecord = false
private var hasAppeared = false
override func loadView() { override func loadView() {
rootView = PigeonMessageView(frame: UIScreen.main.bounds) rootView = PigeonMessageView(frame: UIScreen.main.bounds)
@ -36,9 +38,18 @@ final class PigeonMessageVC: BaseViewController {
super.viewDidLoad() super.viewDidLoad()
bindActions() bindActions()
setupRecordingGesture() setupRecordingGesture()
applyGroupVip(groupVip)
loadMembers() loadMembers()
} }
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if hasAppeared {
refreshGroupVip()
}
hasAppeared = true
}
override func viewWillDisappear(_ animated: Bool) { override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated) super.viewWillDisappear(animated)
guard isMovingFromParent || navigationController?.isBeingDismissed == true else { return } guard isMovingFromParent || navigationController?.isBeingDismissed == true else { return }
@ -102,13 +113,14 @@ final class PigeonMessageVC: BaseViewController {
self.loadUsers(for: self.activeGroupKey, showLoading: false) self.loadUsers(for: self.activeGroupKey, showLoading: false)
}, onError: { error in }, onError: { error in
DLToast.dismiss() DLToast.dismiss()
DLToast.showError(text: error.localizedDescription) DLToast.showError(error)
}).disposed(by: disposeBag) }).disposed(by: disposeBag)
} }
private func loadUsers(for groupKey: String, showLoading: Bool) { private func loadUsers(for groupKey: String, showLoading: Bool) {
guard !groupKey.isEmpty else { guard !groupKey.isEmpty else {
DLToast.dismiss() DLToast.dismiss()
applyGroupVip(GroupMemberVip())
applyMembers([]) applyMembers([])
return return
} }
@ -120,10 +132,12 @@ final class PigeonMessageVC: BaseViewController {
DLToast.showError(text: response.message ?? "获取圈子成员失败") DLToast.showError(text: response.message ?? "获取圈子成员失败")
return return
} }
self?.applyMembers(response.list) guard let self, self.activeGroupKey == groupKey else { return }
self.applyGroupVip(response.vip)
self.applyMembers(response.list)
}, onError: { error in }, onError: { error in
DLToast.dismiss() DLToast.dismiss()
DLToast.showError(text: error.localizedDescription) DLToast.showError(error)
}) })
.disposed(by: disposeBag) .disposed(by: disposeBag)
} }
@ -134,6 +148,28 @@ final class PigeonMessageVC: BaseViewController {
rootView.configureMembers(members, selectedIds: []) rootView.configureMembers(members, selectedIds: [])
} }
private var isPigeonVipUnlocked: Bool {
groupVip.pigeon_mail == true
}
private func applyGroupVip(_ vip: GroupMemberVip) {
groupVip = vip
rootView.updateVipAccess(isUnlocked: isPigeonVipUnlocked)
}
private func refreshGroupVip() {
let groupKey = activeGroupKey
guard !groupKey.isEmpty else { return }
GroupService.groupUsers(groupKey: groupKey)
.subscribe(onNext: { [weak self] response in
guard let self,
self.activeGroupKey == groupKey,
response.isValid(for: groupKey) else { return }
self.applyGroupVip(response.vip)
}, onError: { _ in })
.disposed(by: disposeBag)
}
private func updateCurrentGroupDisplay() { private func updateCurrentGroupDisplay() {
let groupName = groupModel?.groups.first { let groupName = groupModel?.groups.first {
$0.group_key == activeGroupKey $0.group_key == activeGroupKey
@ -158,7 +194,7 @@ final class PigeonMessageVC: BaseViewController {
self.presentGroupPicker(model) self.presentGroupPicker(model)
}, onError: { error in }, onError: { error in
DLToast.dismiss() DLToast.dismiss()
DLToast.showError(text: error.localizedDescription) DLToast.showError(error)
}).disposed(by: disposeBag) }).disposed(by: disposeBag)
} }
@ -480,6 +516,10 @@ final class PigeonMessageVC: BaseViewController {
} }
@objc private func sendMessage() { @objc private func sendMessage() {
guard isPigeonVipUnlocked else {
AppRouter.push(Route.vipRecharge)
return
}
if let wait = pigeonSendWaitSeconds() { if let wait = pigeonSendWaitSeconds() {
DLToast.show(text: "发送过于频繁,请\(Self.formatWaitDuration(wait))后再试") DLToast.show(text: "发送过于频繁,请\(Self.formatWaitDuration(wait))后再试")
return return
@ -580,10 +620,11 @@ final class PigeonMessageVC: BaseViewController {
self.cleanupAudio(deleteFile: true) self.cleanupAudio(deleteFile: true)
self.rootView.clearVoiceDraft() self.rootView.clearVoiceDraft()
self.recordPigeonSendSuccess() self.recordPigeonSendSuccess()
self.refreshGroupVip()
DLToast.show(text: "发送成功") DLToast.show(text: "发送成功")
}, onError: { error in }, onError: { error in
DLToast.dismiss() DLToast.dismiss()
DLToast.show(text: error.localizedDescription) DLToast.show(error)
}) })
.disposed(by: disposeBag) .disposed(by: disposeBag)
} }

View File

@ -82,6 +82,7 @@ final class PigeonMessageView: UIView, UITextFieldDelegate {
private let recordingHintLabel = UILabel() private let recordingHintLabel = UILabel()
private let voiceTimeLabel = UILabel() private let voiceTimeLabel = UILabel()
private let waveformView = PigeonWaveformView() private let waveformView = PigeonWaveformView()
private let vipOfferImageView = UIImageView(image: UIImage(named: "VipFeature/limited_offer"))
private var selectedImageHeightConstraint: NSLayoutConstraint? private var selectedImageHeightConstraint: NSLayoutConstraint?
private var isRecording = false private var isRecording = false
@ -252,6 +253,15 @@ final class PigeonMessageView: UIView, UITextFieldDelegate {
updateMode() updateMode()
} }
func updateVipAccess(isUnlocked: Bool) {
sendButton.setTitle(isUnlocked ? "发送" : "开通会员 解锁功能", for: .normal)
sendButton.setBackgroundImage(
UIImage(named: isUnlocked ? "Common/button_bg_2" : "VipFeature/wide_button_bg"),
for: .normal
)
vipOfferImageView.isHidden = isUnlocked
}
private func setupUI() { private func setupUI() {
let headerView = PigeonHeaderGradientView() let headerView = PigeonHeaderGradientView()
addSubview(headerView) addSubview(headerView)
@ -384,6 +394,17 @@ final class PigeonMessageView: UIView, UITextFieldDelegate {
.topToBottomOfView(modeContainer, offset: 12) .topToBottomOfView(modeContainer, offset: 12)
.edgesHorzontal(15) .edgesHorzontal(15)
.bottom(16) .bottom(16)
vipOfferImageView.contentMode = .scaleAspectFit
vipOfferImageView.isHidden = true
addSubview(vipOfferImageView)
vipOfferImageView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
vipOfferImageView.trailingAnchor.constraint(equalTo: sendButton.trailingAnchor, constant: -6),
vipOfferImageView.bottomAnchor.constraint(equalTo: sendButton.topAnchor, constant: 10),
vipOfferImageView.widthAnchor.constraint(equalToConstant: 94),
vipOfferImageView.heightAnchor.constraint(equalToConstant: 32)
])
} }
private func setupRecipientCard(_ card: UIView) { private func setupRecipientCard(_ card: UIView) {

View File

@ -118,7 +118,7 @@ class CreateScheduleVC: BaseViewController, MAMapViewDelegate {
CreateScheduleVipPopView.show() CreateScheduleVipPopView.show()
} }
else { else {
self.dl.show(text: error.localizedDescription) self.dl.show(error)
} }
}).disposed(by: disposeBag) }).disposed(by: disposeBag)
} }

View File

@ -495,7 +495,7 @@ class FullscreenWebView: WKWebView {
@objc private func image(_ image: UIImage, didFinishSavingWithError error: NSError?, context: UnsafeRawPointer) { @objc private func image(_ image: UIImage, didFinishSavingWithError error: NSError?, context: UnsafeRawPointer) {
if let error = error { if let error = error {
error.isShowErrorTips ? DLToast.showError(text: error.localizedDescription) : () DLToast.showError(error)
} else { } else {
DLToast.showSuccess(text: "oapp_Success".localizedString) DLToast.showSuccess(text: "oapp_Success".localizedString)
} }

View File

@ -267,7 +267,7 @@ struct GroupService {
} }
DLToast.show(text: "已提醒") DLToast.show(text: "已提醒")
}, onError: { error in }, onError: { error in
DLToast.show(text: error.localizedDescription) DLToast.show(error)
}) })
.disposed(by: disposeBag) .disposed(by: disposeBag)
} }

View File

@ -300,6 +300,15 @@ public class DLToast {
show(text: text, icon: UIImage(named: "fail"), in: view, completion: completion) show(text: text, icon: UIImage(named: "fail"), in: view, completion: completion)
} }
public static func showError(_ error: Error,
in view: UIView? = nil,
completion: (() -> Void)? = nil) {
guard error.isShowErrorTips else { return }
showError(text: error.gatewayMessage ?? error.localizedDescription,
in: view,
completion: completion)
}
/// ///
/// - Parameters: /// - Parameters:
/// - text: /// - text:
@ -338,6 +347,16 @@ public class DLToast {
} }
} }
public static func show(_ error: Error,
fallbackText: String? = nil,
in view: UIView? = nil,
completion: (() -> Void)? = nil) {
guard error.isShowErrorTips else { return }
show(text: error.gatewayMessage ?? fallbackText ?? error.localizedDescription,
in: view,
completion: completion)
}
/// toast /// toast
/// - Parameters: /// - Parameters:
/// - view: /// - view:
@ -385,6 +404,11 @@ extension DLWrapper where Base: UIView {
DLToast.showError(text: text, in: base, completion: completion) DLToast.showError(text: text, in: base, completion: completion)
} }
public func showError(_ error: Error,
completion: (() -> Void)? = nil) {
DLToast.showError(error, in: base, completion: completion)
}
/// ///
/// - Parameters: /// - Parameters:
/// - text: /// - text:
@ -409,6 +433,12 @@ extension DLWrapper where Base: UIView {
DLToast.show(text: text, icon: icon, config: config, style: style, in: base, completion: completion) DLToast.show(text: text, icon: icon, config: config, style: style, in: base, completion: completion)
} }
public func show(_ error: Error,
fallbackText: String? = nil,
completion: (() -> Void)? = nil) {
DLToast.show(error, fallbackText: fallbackText, in: base, completion: completion)
}
/// toast /// toast
/// - Parameters: /// - Parameters:
/// - completion: /// - completion:
@ -446,6 +476,11 @@ extension DLWrapper where Base: UIViewController {
DLToast.showError(text: text, in: base.view, completion: completion) DLToast.showError(text: text, in: base.view, completion: completion)
} }
public func showError(_ error: Error,
completion: (() -> Void)? = nil) {
DLToast.showError(error, in: base.view, completion: completion)
}
/// ///
/// - Parameters: /// - Parameters:
/// - text: /// - text:
@ -470,6 +505,12 @@ extension DLWrapper where Base: UIViewController {
DLToast.show(text: text, icon: icon, config: config, style: style, in: base.view, completion: completion) DLToast.show(text: text, icon: icon, config: config, style: style, in: base.view, completion: completion)
} }
public func show(_ error: Error,
fallbackText: String? = nil,
completion: (() -> Void)? = nil) {
DLToast.show(error, fallbackText: fallbackText, in: base.view, completion: completion)
}
/// toast /// toast
/// - Parameters: /// - Parameters:
/// - completion: /// - completion: