|
|
@ -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)";
|
||||||
|
|
|
||||||
22
QuickLocation/Assets.xcassets/Home/HistoryTrack/vip_required.imageset/Contents.json
vendored
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
QuickLocation/Assets.xcassets/Home/HistoryTrack/vip_required.imageset/vip_required@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
QuickLocation/Assets.xcassets/Home/HistoryTrack/vip_required.imageset/vip_required@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 31 KiB |
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
},
|
||||||
|
"properties" : {
|
||||||
|
"provides-namespace" : true
|
||||||
|
}
|
||||||
|
}
|
||||||
22
QuickLocation/Assets.xcassets/MemberVipRequired/app_usage.imageset/Contents.json
vendored
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
QuickLocation/Assets.xcassets/MemberVipRequired/app_usage.imageset/app_usage@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
QuickLocation/Assets.xcassets/MemberVipRequired/app_usage.imageset/app_usage@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 58 KiB |
22
QuickLocation/Assets.xcassets/MemberVipRequired/phone_report.imageset/Contents.json
vendored
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
QuickLocation/Assets.xcassets/MemberVipRequired/phone_report.imageset/phone_report@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
QuickLocation/Assets.xcassets/MemberVipRequired/phone_report.imageset/phone_report@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 46 KiB |
22
QuickLocation/Assets.xcassets/MemberVipRequired/today_track.imageset/Contents.json
vendored
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
QuickLocation/Assets.xcassets/MemberVipRequired/today_track.imageset/today_track@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
QuickLocation/Assets.xcassets/MemberVipRequired/today_track.imageset/today_track@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 37 KiB |
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
},
|
||||||
|
"properties" : {
|
||||||
|
"provides-namespace" : true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 681 B |
|
After Width: | Height: | Size: 1.2 KiB |
22
QuickLocation/Assets.xcassets/VipFeature/limited_offer.imageset/Contents.json
vendored
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
QuickLocation/Assets.xcassets/VipFeature/limited_offer.imageset/limited_offer@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
QuickLocation/Assets.xcassets/VipFeature/limited_offer.imageset/limited_offer@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
22
QuickLocation/Assets.xcassets/VipFeature/media_locked_bg.imageset/Contents.json
vendored
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
QuickLocation/Assets.xcassets/VipFeature/media_locked_bg.imageset/media_locked_bg@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
QuickLocation/Assets.xcassets/VipFeature/media_locked_bg.imageset/media_locked_bg@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
22
QuickLocation/Assets.xcassets/VipFeature/narrow_button_bg.imageset/Contents.json
vendored
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
QuickLocation/Assets.xcassets/VipFeature/narrow_button_bg.imageset/narrow_button_bg@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
QuickLocation/Assets.xcassets/VipFeature/narrow_button_bg.imageset/narrow_button_bg@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
22
QuickLocation/Assets.xcassets/VipFeature/realtime_location.imageset/Contents.json
vendored
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
QuickLocation/Assets.xcassets/VipFeature/realtime_location.imageset/realtime_location@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
QuickLocation/Assets.xcassets/VipFeature/realtime_location.imageset/realtime_location@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 29 KiB |
22
QuickLocation/Assets.xcassets/VipFeature/wide_button_bg.imageset/Contents.json
vendored
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
QuickLocation/Assets.xcassets/VipFeature/wide_button_bg.imageset/wide_button_bg@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
QuickLocation/Assets.xcassets/VipFeature/wide_button_bg.imageset/wide_button_bg@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 7.4 KiB |
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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: "取消")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 }
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,39 +1215,38 @@ 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 }
|
||||||
id: localId,
|
localId = message.clientMsgID ?? UUID().uuidString
|
||||||
isSelf: true,
|
self.viewModel.cacheMessage(message)
|
||||||
senderId: AppContextManager.shared.userId,
|
let localMessage = ChatMessage(
|
||||||
avatar: viewModel.getUserAvatar(id: AppContextManager.shared.userId),
|
id: localId,
|
||||||
headPic: viewModel.getUserHeadPic(id: AppContextManager.shared.userId),
|
isSelf: true,
|
||||||
senderName: AppContextManager.shared.name,
|
senderId: AppContextManager.shared.userId,
|
||||||
content: "",
|
avatar: self.viewModel.getUserAvatar(id: AppContextManager.shared.userId),
|
||||||
voiceUrl: "",
|
headPic: self.viewModel.getUserHeadPic(id: AppContextManager.shared.userId),
|
||||||
imageUrl: fileURL.path,
|
senderName: AppContextManager.shared.name,
|
||||||
imageWidth: displaySize.width,
|
content: "",
|
||||||
imageHeight: displaySize.height,
|
voiceUrl: "",
|
||||||
timestamp: Date().timeIntervalSince1970,
|
imageUrl: fileURL.path,
|
||||||
showTime: false,
|
imageWidth: displaySize.width,
|
||||||
isUploading: true
|
imageHeight: displaySize.height,
|
||||||
)
|
timestamp: Date().timeIntervalSince1970,
|
||||||
viewModel.appendLocalMessage(.imageSend(localMsg))
|
showTime: false,
|
||||||
OIMManager.manager.sendMessage(msg,
|
isUploading: true
|
||||||
recvID: "",
|
)
|
||||||
groupID: viewModel.groupId,
|
self.viewModel.appendLocalMessage(.imageSend(localMessage))
|
||||||
offlinePushInfo: nil,
|
},
|
||||||
onSuccess: { [weak self] returnedMsg in
|
onSuccess: { [weak self] sentMessage in
|
||||||
// 服务端返回后,更新本地消息为服务端图片URL,去掉loading
|
// 服务端返回后,更新本地消息为服务端图片URL,去掉loading
|
||||||
// 注意: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)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
},
|
},
|
||||||
onProgress: nil as OIMNumberCallback?,
|
onSuccess: { [weak self] message in
|
||||||
onFailure: { code, errMsg in
|
self?.cacheMessage(message)
|
||||||
|
self?.appendMessage(message)
|
||||||
|
},
|
||||||
|
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,
|
||||||
latitude: location.latitude,
|
makeMessage: {
|
||||||
longitude: location.longitude)
|
OIMMessageInfo.createLocationMessage(location.encodedDescription,
|
||||||
OIMManager.manager.sendMessage(msg,
|
latitude: location.latitude,
|
||||||
recvID: "",
|
longitude: location.longitude)
|
||||||
groupID: groupId,
|
|
||||||
offlinePushInfo: nil,
|
|
||||||
onSuccess: { [weak self] _ in
|
|
||||||
self?.cacheMessage(msg)
|
|
||||||
self?.appendMessage(msg)
|
|
||||||
},
|
},
|
||||||
onProgress: nil as OIMNumberCallback?,
|
onSuccess: { [weak self] message in
|
||||||
onFailure: { code, errMsg in
|
self?.cacheMessage(message)
|
||||||
|
self?.appendMessage(message)
|
||||||
|
},
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
let hours = rootView.setupView.selectedHour.value
|
||||||
|
requestSetBubble(enable: true, keep_time: hours)
|
||||||
|
}
|
||||||
|
|
||||||
if AppContextManager.shared.vip > 1 {
|
private var isVipUnlocked: Bool {
|
||||||
let hours = rootView.setupView.selectedHour.value
|
guard AppContextManager.shared.systemConfig?.isIntercept == false else {
|
||||||
requestSetBubble(enable: true, keep_time: hours)
|
return false
|
||||||
} else {
|
|
||||||
CreateBubblePopView.show()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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([])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}()
|
}()
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
||||||
|
|
|
||||||
|
|
@ -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 }) {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"))
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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? {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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: 完成回调
|
||||||
|
|
|
||||||