- bug修改

- 功能调整
This commit is contained in:
linshujie 2026-09-08 18:34:05 +08:00
parent 5674603e94
commit b2ccc36d15
97 changed files with 18689 additions and 11841 deletions

13
Podfile
View File

@ -2,6 +2,15 @@
source 'https://gitee.com/mirrors/CocoaPods-Specs.git'
platform :ios, '16.0'
use_frameworks!
# AdHoc is a custom configuration. Without this mapping CocoaPods only
# generates Debug/Release xcfilelists, and AdHoc builds fail to embed pods.
project 'QuickLocation.xcodeproj', {
'Debug' => :debug,
'Release' => :release,
'AdHoc' => :release,
}
target 'QuickLocation' do
# Comment the next line if you don't want to use dynamic frameworks
use_frameworks!
@ -139,6 +148,10 @@ post_install do |installer|
end
end
if File.basename(path) == 'Pods-QuickLocation.adhoc.xcconfig' && content !~ /SWIFT_ACTIVE_COMPILATION_CONDITIONS/
content += "\nSWIFT_ACTIVE_COMPILATION_CONDITIONS = $(inherited) AdHoc\n"
end
File.write(path, content)
end
end

View File

@ -374,6 +374,6 @@ SPEC CHECKSUMS:
YYImage: 1e1b62a9997399593e4b9c4ecfbbabbf1d3f3b54
ZXSDK: 786338c0a18e98e03eda00699c3bfd2700b97117
PODFILE CHECKSUM: f676321d820ce55c18e69318732ac59e39a57e43
PODFILE CHECKSUM: 3272194e421676580050601835b59f7c4d288888
COCOAPODS: 1.16.2

2
Pods/Manifest.lock generated
View File

@ -374,6 +374,6 @@ SPEC CHECKSUMS:
YYImage: 1e1b62a9997399593e4b9c4ecfbbabbf1d3f3b54
ZXSDK: 786338c0a18e98e03eda00699c3bfd2700b97117
PODFILE CHECKSUM: f676321d820ce55c18e69318732ac59e39a57e43
PODFILE CHECKSUM: 3272194e421676580050601835b59f7c4d288888
COCOAPODS: 1.16.2

File diff suppressed because it is too large Load Diff

View File

@ -175,6 +175,53 @@ code_sign_if_enabled() {
fi
}
if [[ "$CONFIGURATION" == "AdHoc" ]]; then
install_framework "${BUILT_PRODUCTS_DIR}/Alamofire/Alamofire.framework"
install_framework "${BUILT_PRODUCTS_DIR}/BRPickerView/BRPickerView.framework"
install_framework "${BUILT_PRODUCTS_DIR}/CocoaLumberjack/CocoaLumberjack.framework"
install_framework "${BUILT_PRODUCTS_DIR}/CocoaMQTT/CocoaMQTT.framework"
install_framework "${BUILT_PRODUCTS_DIR}/Differentiator/Differentiator.framework"
install_framework "${BUILT_PRODUCTS_DIR}/HXPHPicker/HXPHPicker.framework"
install_framework "${BUILT_PRODUCTS_DIR}/IQKeyboardCore/IQKeyboardCore.framework"
install_framework "${BUILT_PRODUCTS_DIR}/IQKeyboardManagerSwift/IQKeyboardManagerSwift.framework"
install_framework "${BUILT_PRODUCTS_DIR}/IQKeyboardNotification/IQKeyboardNotification.framework"
install_framework "${BUILT_PRODUCTS_DIR}/IQKeyboardReturnManager/IQKeyboardReturnManager.framework"
install_framework "${BUILT_PRODUCTS_DIR}/IQKeyboardToolbar/IQKeyboardToolbar.framework"
install_framework "${BUILT_PRODUCTS_DIR}/IQKeyboardToolbarManager/IQKeyboardToolbarManager.framework"
install_framework "${BUILT_PRODUCTS_DIR}/IQTextInputViewNotification/IQTextInputViewNotification.framework"
install_framework "${BUILT_PRODUCTS_DIR}/IQTextView/IQTextView.framework"
install_framework "${BUILT_PRODUCTS_DIR}/Kingfisher/Kingfisher.framework"
install_framework "${BUILT_PRODUCTS_DIR}/KingfisherWebP/KingfisherWebP.framework"
install_framework "${BUILT_PRODUCTS_DIR}/MBProgressHUD/MBProgressHUD.framework"
install_framework "${BUILT_PRODUCTS_DIR}/MJExtension/MJExtension.framework"
install_framework "${BUILT_PRODUCTS_DIR}/MJRefresh/MJRefresh.framework"
install_framework "${BUILT_PRODUCTS_DIR}/MarqueeLabel/MarqueeLabel.framework"
install_framework "${BUILT_PRODUCTS_DIR}/Masonry/Masonry.framework"
install_framework "${BUILT_PRODUCTS_DIR}/Moya/Moya.framework"
install_framework "${BUILT_PRODUCTS_DIR}/MqttCocoaAsyncSocket/MqttCocoaAsyncSocket.framework"
install_framework "${BUILT_PRODUCTS_DIR}/ObjectMapper/ObjectMapper.framework"
install_framework "${BUILT_PRODUCTS_DIR}/Popover/Popover.framework"
install_framework "${BUILT_PRODUCTS_DIR}/RxCocoa/RxCocoa.framework"
install_framework "${BUILT_PRODUCTS_DIR}/RxDataSources/RxDataSources.framework"
install_framework "${BUILT_PRODUCTS_DIR}/RxGesture/RxGesture.framework"
install_framework "${BUILT_PRODUCTS_DIR}/RxRelay/RxRelay.framework"
install_framework "${BUILT_PRODUCTS_DIR}/RxSwift/RxSwift.framework"
install_framework "${BUILT_PRODUCTS_DIR}/RxSwiftExt/RxSwiftExt.framework"
install_framework "${BUILT_PRODUCTS_DIR}/SDCycleScrollView/SDCycleScrollView.framework"
install_framework "${BUILT_PRODUCTS_DIR}/SDWebImage/SDWebImage.framework"
install_framework "${BUILT_PRODUCTS_DIR}/SGQRCode/SGQRCode.framework"
install_framework "${BUILT_PRODUCTS_DIR}/SnapKit/SnapKit.framework"
install_framework "${BUILT_PRODUCTS_DIR}/SwiftDate/SwiftDate.framework"
install_framework "${BUILT_PRODUCTS_DIR}/SwiftKeychainWrapper/SwiftKeychainWrapper.framework"
install_framework "${BUILT_PRODUCTS_DIR}/SwiftyJSON/SwiftyJSON.framework"
install_framework "${BUILT_PRODUCTS_DIR}/SwiftyUserDefaults/SwiftyUserDefaults.framework"
install_framework "${BUILT_PRODUCTS_DIR}/TagListView/TagListView.framework"
install_framework "${BUILT_PRODUCTS_DIR}/URLNavigator/URLNavigator.framework"
install_framework "${BUILT_PRODUCTS_DIR}/YBImageBrowser/YBImageBrowser.framework"
install_framework "${BUILT_PRODUCTS_DIR}/YYImage/YYImage.framework"
install_framework "${BUILT_PRODUCTS_DIR}/libwebp/libwebp.framework"
install_framework "${BUILT_PRODUCTS_DIR}/lottie-ios/Lottie.framework"
fi
if [[ "$CONFIGURATION" == "Debug" ]]; then
install_framework "${BUILT_PRODUCTS_DIR}/Alamofire/Alamofire.framework"
install_framework "${BUILT_PRODUCTS_DIR}/BRPickerView/BRPickerView.framework"

View File

@ -106,6 +106,11 @@ if [[ "$CONFIGURATION" == "Release" ]]; then
install_resource "${PODS_ROOT}/AMapNavi-NO-IDFA/AMapNaviKit.framework/AMap.bundle"
install_resource "${PODS_ROOT}/AlipaySDK-iOS/AlipaySDK.bundle"
fi
if [[ "$CONFIGURATION" == "AdHoc" ]]; then
install_resource "${PODS_ROOT}/AMapNavi-NO-IDFA/AMapNaviKit.framework/AMapNavi.bundle"
install_resource "${PODS_ROOT}/AMapNavi-NO-IDFA/AMapNaviKit.framework/AMap.bundle"
install_resource "${PODS_ROOT}/AlipaySDK-iOS/AlipaySDK.bundle"
fi
mkdir -p "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}"
rsync -avr --copy-links --no-relative --exclude '*/.svn/*' --files-from="$RESOURCES_TO_COPY" / "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}"

View File

@ -19,9 +19,6 @@
305A768E2FCA8C7000227D26 /* SignPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A74CD2FCA8C7000227D26 /* SignPlugin.swift */; };
305A768F2FCA8C7000227D26 /* SystemAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A74CE2FCA8C7000227D26 /* SystemAPI.swift */; };
305A76902FCA8C7000227D26 /* UserAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A74CF2FCA8C7000227D26 /* UserAPI.swift */; };
55B230033032000100785004 /* PigeonAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B230023032000100785003 /* PigeonAPI.swift */; };
55B230013032000100785002 /* UploadService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B230003032000100785001 /* UploadService.swift */; };
55B230053032000100785006 /* PigeonService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B230043032000100785005 /* PigeonService.swift */; };
305A76912FCA8C7000227D26 /* Constant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A74D22FCA8C7000227D26 /* Constant.swift */; };
305A76922FCA8C7000227D26 /* LogUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A74D32FCA8C7000227D26 /* LogUtils.swift */; };
305A76932FCA8C7000227D26 /* AddImageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A74D52FCA8C7000227D26 /* AddImageCell.swift */; };
@ -90,7 +87,6 @@
305A76D22FCA8C7000227D26 /* ThemeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A75292FCA8C7000227D26 /* ThemeManager.swift */; };
305A76D32FCA8C7000227D26 /* URLManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A752B2FCA8C7000227D26 /* URLManager.swift */; };
305A76D42FCA8C7000227D26 /* GroupModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A752E2FCA8C7000227D26 /* GroupModel.swift */; };
55B230073032000100785008 /* PigeonModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B230063032000100785007 /* PigeonModel.swift */; };
305A76D52FCA8C7000227D26 /* SystemResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A752F2FCA8C7000227D26 /* SystemResponse.swift */; };
305A76D62FCA8C7000227D26 /* ImagePlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A75312FCA8C7000227D26 /* ImagePlugin.swift */; };
305A76D72FCA8C7000227D26 /* NotEmpty.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A75322FCA8C7000227D26 /* NotEmpty.swift */; };
@ -166,13 +162,6 @@
305A79902FCAC61A00227D26 /* InviteMemberVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A798F2FCAC61A00227D26 /* InviteMemberVC.swift */; };
3062E8B52FCE6BBA00CEF511 /* ScanVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3062E8B42FCE6BBA00CEF511 /* ScanVC.swift */; };
3062E8B72FCE6BFE00CEF511 /* ScanView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3062E8B62FCE6BFE00CEF511 /* ScanView.swift */; };
30A1C0A12FEB000100CA0001 /* CameraCaptureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A1C0A02FEB000100CA0001 /* CameraCaptureView.swift */; };
30A1C0A32FEB000100CA0001 /* CameraCaptureVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A1C0A22FEB000100CA0001 /* CameraCaptureVC.swift */; };
30B2D0B12FEB111100CA0001 /* PhotoPickerStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B2D0B02FEB111100CA0001 /* PhotoPickerStore.swift */; };
30B2D0B32FEB111100CA0001 /* PhotoPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B2D0B22FEB111100CA0001 /* PhotoPickerView.swift */; };
30B2D0B52FEB111100CA0001 /* PhotoPickerAlbumView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B2D0B42FEB111100CA0001 /* PhotoPickerAlbumView.swift */; };
30B2D0B72FEB111100CA0001 /* PhotoPickerPreviewVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B2D0B62FEB111100CA0001 /* PhotoPickerPreviewVC.swift */; };
30B2D0B92FEB111100CA0001 /* PhotoPickerVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B2D0B82FEB111100CA0001 /* PhotoPickerVC.swift */; };
3062E8BA2FCEAC6500CEF511 /* CreateGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3062E8B92FCEAC6500CEF511 /* CreateGroupView.swift */; };
3062E8BC2FCEAC7100CEF511 /* CreateGroupVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3062E8BB2FCEAC7100CEF511 /* CreateGroupVC.swift */; };
3062E8BE2FCEBD0E00CEF511 /* GroupIconListVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3062E8BD2FCEBD0E00CEF511 /* GroupIconListVC.swift */; };
@ -185,6 +174,8 @@
307073E52FD18A20004C37CC /* GroupChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 307073E32FD18A20004C37CC /* GroupChatView.swift */; };
307073E62FD18A20004C37CC /* GroupChatVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 307073E22FD18A20004C37CC /* GroupChatVC.swift */; };
307073EA2FD2715A004C37CC /* GroupChatViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 307073E92FD2715A004C37CC /* GroupChatViewModel.swift */; };
30A1C0A12FEB000100CA0001 /* CameraCaptureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A1C0A02FEB000100CA0001 /* CameraCaptureView.swift */; };
30A1C0A32FEB000100CA0001 /* CameraCaptureVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A1C0A22FEB000100CA0001 /* CameraCaptureVC.swift */; };
30A7A9112FCAEE3D00105780 /* GroupListPopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A7A9102FCAEE3D00105780 /* GroupListPopView.swift */; };
30A87A4B2FEE1DAB0095E7C6 /* ItineraryDetailVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A87A4A2FEE1DAB0095E7C6 /* ItineraryDetailVC.swift */; };
30A87A4D2FEE1DB40095E7C6 /* ItineraryDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A87A4C2FEE1DB40095E7C6 /* ItineraryDetailView.swift */; };
@ -208,6 +199,11 @@
30ACD54E2FF7942E00174861 /* PromotionalActivitiesVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30ACD54D2FF7942E00174861 /* PromotionalActivitiesVC.swift */; };
30ACD5502FF7943A00174861 /* PromotionalActivitiesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30ACD54F2FF7943A00174861 /* PromotionalActivitiesView.swift */; };
30ACD67B2FF7B36A00174861 /* PopupQueueManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30ACD67A2FF7B36A00174861 /* PopupQueueManager.swift */; };
30B2D0B12FEB111100CA0001 /* PhotoPickerStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B2D0B02FEB111100CA0001 /* PhotoPickerStore.swift */; };
30B2D0B32FEB111100CA0001 /* PhotoPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B2D0B22FEB111100CA0001 /* PhotoPickerView.swift */; };
30B2D0B52FEB111100CA0001 /* PhotoPickerAlbumView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B2D0B42FEB111100CA0001 /* PhotoPickerAlbumView.swift */; };
30B2D0B72FEB111100CA0001 /* PhotoPickerPreviewVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B2D0B62FEB111100CA0001 /* PhotoPickerPreviewVC.swift */; };
30B2D0B92FEB111100CA0001 /* PhotoPickerVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B2D0B82FEB111100CA0001 /* PhotoPickerVC.swift */; };
30B74B3A2FF2115A00F6744D /* GroupScheduleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B74B392FF2115A00F6744D /* GroupScheduleView.swift */; };
30B74B3C2FF2117900F6744D /* GroupScheduleVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B74B3B2FF2117900F6744D /* GroupScheduleVC.swift */; };
30B74B412FF2437E00F6744D /* GroupMemberListVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B74B402FF2437E00F6744D /* GroupMemberListVC.swift */; };
@ -259,7 +255,7 @@
30D74BF42FEB6F5B0050EB2C /* LocationPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D74BF12FEB6F5B0050EB2C /* LocationPickerView.swift */; };
30D74D1F2FEBB09B0050EB2C /* CreateScheduleVM.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D74D1E2FEBB09B0050EB2C /* CreateScheduleVM.swift */; };
30D87CDB2FDFA9EE00E958FD /* MQTTService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D87CDA2FDFA9EE00E958FD /* MQTTService.swift */; };
55B240013034000100786001 /* MemberWeatherService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B240023034000100786002 /* MemberWeatherService.swift */; };
55C1A0113034101000786011 /* PhoneStatusCollector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55C1A0103034101000786010 /* PhoneStatusCollector.swift */; };
30D87CDD2FDFF07500E958FD /* InteractionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D87CDC2FDFF07500E958FD /* InteractionView.swift */; };
30D87CDF2FDFF1A100E958FD /* QuickMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D87CDE2FDFF1A100E958FD /* QuickMessageView.swift */; };
30D87D042FE1336300E958FD /* NavigationVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D87D012FE1336300E958FD /* NavigationVC.swift */; };
@ -319,6 +315,7 @@
55B217B13022F0A000784711 /* GroupItineraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B217B03022F0A000784710 /* GroupItineraryView.swift */; };
55B217C13022F0A000784721 /* MinePhotoWallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B217C03022F0A000784720 /* MinePhotoWallView.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 */; };
55B217D13022F0A000784727 /* UnlockCountManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B217D03022F0A000784726 /* UnlockCountManager.swift */; };
55B217D23023010000784731 /* TodayTrackDetailVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B217D13023010000784730 /* TodayTrackDetailVC.swift */; };
@ -327,22 +324,15 @@
55B217D83023010000784737 /* PhoneReportDetailVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B217D73023010000784736 /* PhoneReportDetailVC.swift */; };
55B217DA3023010000784739 /* PhoneReportDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B217D93023010000784738 /* PhoneReportDetailView.swift */; };
55B217DC302301000078473B /* PhoneReportDetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B217DB302301000078473A /* PhoneReportDetailViewModel.swift */; };
55B240013040000100784901 /* AppUsageReportVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B240003040000100784900 /* AppUsageReportVC.swift */; };
55B240033040000100784903 /* AppUsageReportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B240023040000100784902 /* AppUsageReportView.swift */; };
55B240053040000100784905 /* AppUsageReportViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B240043040000100784904 /* AppUsageReportViewModel.swift */; };
55B218A13024A00100784702 /* AboutVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B218A13024A00100784701 /* AboutVC.swift */; };
55B218A13024A00100784704 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B218A13024A00100784703 /* AboutView.swift */; };
55B218A13024A00100784706 /* CancelAccountVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B218A13024A00100784705 /* CancelAccountVC.swift */; };
55B218A13024A00100784708 /* CancelAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B218A13024A00100784707 /* CancelAccountView.swift */; };
55B218A13024A0010078470A /* MemberInfoVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B218A13024A00100784709 /* MemberInfoVC.swift */; };
55B218A13024A0010078470C /* MemberInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B218A13024A0010078470B /* MemberInfoView.swift */; };
55B233013035000100785041 /* GenderSelectPopVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B233003035000100785040 /* GenderSelectPopVC.swift */; };
55B234013036000100785051 /* RelationSelectPopVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B234003036000100785050 /* RelationSelectPopVC.swift */; };
55B234033036000100785053 /* RelationStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B234023036000100785052 /* RelationStore.swift */; };
55B218B13024B00100784722 /* FeatureIntroVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B218B13024B00100784721 /* FeatureIntroVC.swift */; };
55B218B13024B00100784724 /* FeatureIntroView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B218B13024B00100784723 /* FeatureIntroView.swift */; };
55B219A2302A000100784751 /* UnlockRequestPopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B219A1302A000100784750 /* UnlockRequestPopView.swift */; };
55B250023050000100784902 /* LockedAppPopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B250013050000100784901 /* LockedAppPopView.swift */; };
55B219B2302A000100784761 /* ReceiveMessagePopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B219B1302A000100784760 /* ReceiveMessagePopView.swift */; };
55B219B4302A000100784763 /* ReceiveMessageReportPopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B219B3302A000100784762 /* ReceiveMessageReportPopView.swift */; };
55B219C13024C00100784722 /* BubbleHeroView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B219C13024C00100784721 /* BubbleHeroView.swift */; };
@ -355,6 +345,7 @@
55B219C8302A000100784778 /* PigeonMessageHistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B219C7302A000100784777 /* PigeonMessageHistoryView.swift */; };
55B21A02302B0001007847A2 /* SOSAnimationViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B21A01302B0001007847A1 /* SOSAnimationViews.swift */; };
55B21A04302B0001007847A4 /* SOSReceiveAlertView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B21A03302B0001007847A3 /* SOSReceiveAlertView.swift */; };
55B21A06302B0001007847A6 /* SOSListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B21A05302B0001007847A5 /* SOSListViewController.swift */; };
55B21D6E3023117900784774 /* 优设标题黑_猫啃网.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 55B21D6D3023117900784774 /* 优设标题黑_猫啃网.ttf */; };
55B220013031000100784801 /* FeedbackVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B220003031000100784800 /* FeedbackVC.swift */; };
55B220033031000100784803 /* FeedbackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B220023031000100784802 /* FeedbackView.swift */; };
@ -365,6 +356,27 @@
55B221270000000100784827 /* MoodStatusCatalog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B221260000000100784826 /* MoodStatusCatalog.swift */; };
55B222010000000100784831 /* AddEmojiPopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B222000000000100784830 /* AddEmojiPopView.swift */; };
55B222030000000100784833 /* AddMessagePopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B222020000000100784832 /* AddMessagePopView.swift */; };
55B230013032000100785002 /* UploadService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B230003032000100785001 /* UploadService.swift */; };
55B230033032000100785004 /* PigeonAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B230023032000100785003 /* PigeonAPI.swift */; };
55B230053032000100785006 /* PigeonService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B230043032000100785005 /* PigeonService.swift */; };
55B230073032000100785008 /* PigeonModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B230063032000100785007 /* PigeonModel.swift */; };
55B230093032000100785010 /* LockDistractPermissionPopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B230083032000100785009 /* LockDistractPermissionPopView.swift */; };
55B232013034000100785031 /* PigeonUploadPopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B232003034000100785030 /* PigeonUploadPopView.swift */; };
55B232033034000100785033 /* PigeonWatermarkCameraVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B232023034000100785032 /* PigeonWatermarkCameraVC.swift */; };
55B232053034000100785035 /* PigeonWatermarkCameraView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B232043034000100785034 /* PigeonWatermarkCameraView.swift */; };
55B233013035000100785041 /* GenderSelectPopVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B233003035000100785040 /* GenderSelectPopVC.swift */; };
55B234013036000100785051 /* RelationSelectPopVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B234003036000100785050 /* RelationSelectPopVC.swift */; };
55B234033036000100785053 /* RelationStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B234023036000100785052 /* RelationStore.swift */; };
55B240013034000100786001 /* MemberWeatherService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B240023034000100786002 /* MemberWeatherService.swift */; };
55B240013040000100784901 /* AppUsageReportVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B240003040000100784900 /* AppUsageReportVC.swift */; };
55B240033040000100784903 /* AppUsageReportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B240023040000100784902 /* AppUsageReportView.swift */; };
55B240053040000100784905 /* AppUsageReportViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B240043040000100784904 /* AppUsageReportViewModel.swift */; };
55B250023038000100786502 /* MailboxModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B250013038000100786501 /* MailboxModel.swift */; };
55B250023050000100784902 /* LockedAppPopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B250013050000100784901 /* LockedAppPopView.swift */; };
55B250043038000100786504 /* MailboxView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B250033038000100786503 /* MailboxView.swift */; };
55B250063038000100786506 /* MailboxViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B250053038000100786505 /* MailboxViewController.swift */; };
55B250083038000100786508 /* MailboxDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B250073038000100786507 /* MailboxDetailViewController.swift */; };
55B261010000000100786601 /* PrivacySettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B261000000000100786600 /* PrivacySettingsViewController.swift */; };
55BF752D2FFE53F70055DA57 /* LocationPermissionPopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55BF752C2FFE53F70055DA57 /* LocationPermissionPopView.swift */; };
55BF75352FFF91690055DA57 /* InteractionEmojiCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55BF75342FFF91690055DA57 /* InteractionEmojiCell.swift */; };
55C219C13024C00100784731 /* SearchLocationHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55C219C13024C00100784730 /* SearchLocationHeaderView.swift */; };
@ -386,10 +398,6 @@
C15D41A1770F5B4A02E5FC59 /* AppRestrictShared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AB29D0618C4BE76F2CA5261 /* AppRestrictShared.swift */; };
C3C4ACC1AE3CAF5BA21328B7 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 226CA98495C2884EBB1D1372 /* Foundation.framework */; };
C6A99DEE361AB31477F88475 /* LockDistractView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC46E80B848E4012D192E4CE /* LockDistractView.swift */; };
55B230093032000100785010 /* LockDistractPermissionPopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B230083032000100785009 /* LockDistractPermissionPopView.swift */; };
55B232013034000100785031 /* PigeonUploadPopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B232003034000100785030 /* PigeonUploadPopView.swift */; };
55B232033034000100785033 /* PigeonWatermarkCameraVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B232023034000100785032 /* PigeonWatermarkCameraVC.swift */; };
55B232053034000100785035 /* PigeonWatermarkCameraView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B232043034000100785034 /* PigeonWatermarkCameraView.swift */; };
CC53DE2CA88E529129547A52 /* ShieldConfigurationExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 0C47E0012491DB1371FD5E53 /* ShieldConfigurationExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
D3C101B198090A9F4D93E44F /* FamilyControls.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4CD6B965A124276661A84C4C /* FamilyControls.framework */; };
D60B79067C80CD6AF50D63FE /* FamilyControls.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4CD6B965A124276661A84C4C /* FamilyControls.framework */; };
@ -437,6 +445,7 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
01BC26D9997CBD9CF070EA6A /* Pods-QuickLocation.adhoc.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-QuickLocation.adhoc.xcconfig"; path = "Target Support Files/Pods-QuickLocation/Pods-QuickLocation.adhoc.xcconfig"; sourceTree = "<group>"; };
08002267FC4390B20D3510B3 /* ShieldConfigurationExtension.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ShieldConfigurationExtension.swift; path = ShieldConfigurationExtension/ShieldConfigurationExtension.swift; sourceTree = "<group>"; };
0C47E0012491DB1371FD5E53 /* ShieldConfigurationExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ShieldConfigurationExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
1AB29D0618C4BE76F2CA5261 /* AppRestrictShared.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AppRestrictShared.swift; path = AppRestrictShared/AppRestrictShared.swift; sourceTree = "<group>"; };
@ -452,9 +461,6 @@
305A74CD2FCA8C7000227D26 /* SignPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignPlugin.swift; sourceTree = "<group>"; };
305A74CE2FCA8C7000227D26 /* SystemAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemAPI.swift; sourceTree = "<group>"; };
305A74CF2FCA8C7000227D26 /* UserAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAPI.swift; sourceTree = "<group>"; };
55B230023032000100785003 /* PigeonAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PigeonAPI.swift; sourceTree = "<group>"; };
55B230003032000100785001 /* UploadService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadService.swift; sourceTree = "<group>"; };
55B230043032000100785005 /* PigeonService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PigeonService.swift; sourceTree = "<group>"; };
305A74D12FCA8C7000227D26 /* adhoc.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = adhoc.xcconfig; sourceTree = "<group>"; };
305A74D22FCA8C7000227D26 /* Constant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constant.swift; sourceTree = "<group>"; };
305A74D32FCA8C7000227D26 /* LogUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogUtils.swift; sourceTree = "<group>"; };
@ -526,7 +532,6 @@
305A75292FCA8C7000227D26 /* ThemeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeManager.swift; sourceTree = "<group>"; };
305A752B2FCA8C7000227D26 /* URLManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLManager.swift; sourceTree = "<group>"; };
305A752E2FCA8C7000227D26 /* GroupModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupModel.swift; sourceTree = "<group>"; };
55B230063032000100785007 /* PigeonModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PigeonModel.swift; sourceTree = "<group>"; };
305A752F2FCA8C7000227D26 /* SystemResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemResponse.swift; sourceTree = "<group>"; };
305A75312FCA8C7000227D26 /* ImagePlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePlugin.swift; sourceTree = "<group>"; };
305A75322FCA8C7000227D26 /* NotEmpty.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotEmpty.swift; sourceTree = "<group>"; };
@ -607,13 +612,6 @@
305A798F2FCAC61A00227D26 /* InviteMemberVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteMemberVC.swift; sourceTree = "<group>"; };
3062E8B42FCE6BBA00CEF511 /* ScanVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanVC.swift; sourceTree = "<group>"; };
3062E8B62FCE6BFE00CEF511 /* ScanView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanView.swift; sourceTree = "<group>"; };
30A1C0A02FEB000100CA0001 /* CameraCaptureView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraCaptureView.swift; sourceTree = "<group>"; };
30A1C0A22FEB000100CA0001 /* CameraCaptureVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraCaptureVC.swift; sourceTree = "<group>"; };
30B2D0B02FEB111100CA0001 /* PhotoPickerStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoPickerStore.swift; sourceTree = "<group>"; };
30B2D0B22FEB111100CA0001 /* PhotoPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoPickerView.swift; sourceTree = "<group>"; };
30B2D0B42FEB111100CA0001 /* PhotoPickerAlbumView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoPickerAlbumView.swift; sourceTree = "<group>"; };
30B2D0B62FEB111100CA0001 /* PhotoPickerPreviewVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoPickerPreviewVC.swift; sourceTree = "<group>"; };
30B2D0B82FEB111100CA0001 /* PhotoPickerVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoPickerVC.swift; sourceTree = "<group>"; };
3062E8B92FCEAC6500CEF511 /* CreateGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateGroupView.swift; sourceTree = "<group>"; };
3062E8BB2FCEAC7100CEF511 /* CreateGroupVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateGroupVC.swift; sourceTree = "<group>"; };
3062E8BD2FCEBD0E00CEF511 /* GroupIconListVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupIconListVC.swift; sourceTree = "<group>"; };
@ -626,6 +624,8 @@
307073E22FD18A20004C37CC /* GroupChatVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupChatVC.swift; sourceTree = "<group>"; };
307073E32FD18A20004C37CC /* GroupChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupChatView.swift; sourceTree = "<group>"; };
307073E92FD2715A004C37CC /* GroupChatViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupChatViewModel.swift; sourceTree = "<group>"; };
30A1C0A02FEB000100CA0001 /* CameraCaptureView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraCaptureView.swift; sourceTree = "<group>"; };
30A1C0A22FEB000100CA0001 /* CameraCaptureVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraCaptureVC.swift; sourceTree = "<group>"; };
30A7A9102FCAEE3D00105780 /* GroupListPopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupListPopView.swift; sourceTree = "<group>"; };
30A87A4A2FEE1DAB0095E7C6 /* ItineraryDetailVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItineraryDetailVC.swift; sourceTree = "<group>"; };
30A87A4C2FEE1DB40095E7C6 /* ItineraryDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItineraryDetailView.swift; sourceTree = "<group>"; };
@ -649,6 +649,11 @@
30ACD54D2FF7942E00174861 /* PromotionalActivitiesVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PromotionalActivitiesVC.swift; sourceTree = "<group>"; };
30ACD54F2FF7943A00174861 /* PromotionalActivitiesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PromotionalActivitiesView.swift; sourceTree = "<group>"; };
30ACD67A2FF7B36A00174861 /* PopupQueueManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopupQueueManager.swift; sourceTree = "<group>"; };
30B2D0B02FEB111100CA0001 /* PhotoPickerStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoPickerStore.swift; sourceTree = "<group>"; };
30B2D0B22FEB111100CA0001 /* PhotoPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoPickerView.swift; sourceTree = "<group>"; };
30B2D0B42FEB111100CA0001 /* PhotoPickerAlbumView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoPickerAlbumView.swift; sourceTree = "<group>"; };
30B2D0B62FEB111100CA0001 /* PhotoPickerPreviewVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoPickerPreviewVC.swift; sourceTree = "<group>"; };
30B2D0B82FEB111100CA0001 /* PhotoPickerVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoPickerVC.swift; sourceTree = "<group>"; };
30B74B392FF2115A00F6744D /* GroupScheduleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupScheduleView.swift; sourceTree = "<group>"; };
30B74B3B2FF2117900F6744D /* GroupScheduleVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupScheduleVC.swift; sourceTree = "<group>"; };
30B74B402FF2437E00F6744D /* GroupMemberListVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupMemberListVC.swift; sourceTree = "<group>"; };
@ -701,7 +706,7 @@
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>"; };
30D87CDA2FDFA9EE00E958FD /* MQTTService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MQTTService.swift; sourceTree = "<group>"; };
55B240023034000100786002 /* MemberWeatherService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberWeatherService.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>"; };
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>"; };
@ -765,6 +770,7 @@
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>"; };
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>"; };
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>"; };
@ -773,22 +779,15 @@
55B217D73023010000784736 /* PhoneReportDetailVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhoneReportDetailVC.swift; sourceTree = "<group>"; };
55B217D93023010000784738 /* PhoneReportDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhoneReportDetailView.swift; sourceTree = "<group>"; };
55B217DB302301000078473A /* PhoneReportDetailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhoneReportDetailViewModel.swift; sourceTree = "<group>"; };
55B240003040000100784900 /* AppUsageReportVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppUsageReportVC.swift; sourceTree = "<group>"; };
55B240023040000100784902 /* AppUsageReportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppUsageReportView.swift; sourceTree = "<group>"; };
55B240043040000100784904 /* AppUsageReportViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppUsageReportViewModel.swift; sourceTree = "<group>"; };
55B218A13024A00100784701 /* AboutVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutVC.swift; sourceTree = "<group>"; };
55B218A13024A00100784703 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = "<group>"; };
55B218A13024A00100784705 /* CancelAccountVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CancelAccountVC.swift; sourceTree = "<group>"; };
55B218A13024A00100784707 /* CancelAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CancelAccountView.swift; sourceTree = "<group>"; };
55B218A13024A00100784709 /* MemberInfoVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberInfoVC.swift; sourceTree = "<group>"; };
55B218A13024A0010078470B /* MemberInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberInfoView.swift; sourceTree = "<group>"; };
55B233003035000100785040 /* GenderSelectPopVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenderSelectPopVC.swift; sourceTree = "<group>"; };
55B234003036000100785050 /* RelationSelectPopVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelationSelectPopVC.swift; sourceTree = "<group>"; };
55B234023036000100785052 /* RelationStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelationStore.swift; sourceTree = "<group>"; };
55B218B13024B00100784721 /* FeatureIntroVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureIntroVC.swift; sourceTree = "<group>"; };
55B218B13024B00100784723 /* FeatureIntroView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureIntroView.swift; sourceTree = "<group>"; };
55B219A1302A000100784750 /* UnlockRequestPopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnlockRequestPopView.swift; sourceTree = "<group>"; };
55B250013050000100784901 /* LockedAppPopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockedAppPopView.swift; sourceTree = "<group>"; };
55B219B1302A000100784760 /* ReceiveMessagePopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiveMessagePopView.swift; sourceTree = "<group>"; };
55B219B3302A000100784762 /* ReceiveMessageReportPopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiveMessageReportPopView.swift; sourceTree = "<group>"; };
55B219C13024C00100784721 /* BubbleHeroView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BubbleHeroView.swift; sourceTree = "<group>"; };
@ -801,6 +800,7 @@
55B219C7302A000100784777 /* PigeonMessageHistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PigeonMessageHistoryView.swift; sourceTree = "<group>"; };
55B21A01302B0001007847A1 /* SOSAnimationViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOSAnimationViews.swift; sourceTree = "<group>"; };
55B21A03302B0001007847A3 /* SOSReceiveAlertView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOSReceiveAlertView.swift; sourceTree = "<group>"; };
55B21A05302B0001007847A5 /* SOSListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOSListViewController.swift; sourceTree = "<group>"; };
55B21D6D3023117900784774 /* 优设标题黑_猫啃网.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "优设标题黑_猫啃网.ttf"; sourceTree = "<group>"; };
55B220003031000100784800 /* FeedbackVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbackVC.swift; sourceTree = "<group>"; };
55B220023031000100784802 /* FeedbackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbackView.swift; sourceTree = "<group>"; };
@ -811,6 +811,27 @@
55B221260000000100784826 /* MoodStatusCatalog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoodStatusCatalog.swift; sourceTree = "<group>"; };
55B222000000000100784830 /* AddEmojiPopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEmojiPopView.swift; sourceTree = "<group>"; };
55B222020000000100784832 /* AddMessagePopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddMessagePopView.swift; sourceTree = "<group>"; };
55B230003032000100785001 /* UploadService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadService.swift; sourceTree = "<group>"; };
55B230023032000100785003 /* PigeonAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PigeonAPI.swift; sourceTree = "<group>"; };
55B230043032000100785005 /* PigeonService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PigeonService.swift; sourceTree = "<group>"; };
55B230063032000100785007 /* PigeonModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PigeonModel.swift; sourceTree = "<group>"; };
55B230083032000100785009 /* LockDistractPermissionPopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockDistractPermissionPopView.swift; sourceTree = "<group>"; };
55B232003034000100785030 /* PigeonUploadPopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PigeonUploadPopView.swift; sourceTree = "<group>"; };
55B232023034000100785032 /* PigeonWatermarkCameraVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PigeonWatermarkCameraVC.swift; sourceTree = "<group>"; };
55B232043034000100785034 /* PigeonWatermarkCameraView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PigeonWatermarkCameraView.swift; sourceTree = "<group>"; };
55B233003035000100785040 /* GenderSelectPopVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenderSelectPopVC.swift; sourceTree = "<group>"; };
55B234003036000100785050 /* RelationSelectPopVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelationSelectPopVC.swift; sourceTree = "<group>"; };
55B234023036000100785052 /* RelationStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelationStore.swift; sourceTree = "<group>"; };
55B240003040000100784900 /* AppUsageReportVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppUsageReportVC.swift; sourceTree = "<group>"; };
55B240023034000100786002 /* MemberWeatherService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberWeatherService.swift; sourceTree = "<group>"; };
55B240023040000100784902 /* AppUsageReportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppUsageReportView.swift; sourceTree = "<group>"; };
55B240043040000100784904 /* AppUsageReportViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppUsageReportViewModel.swift; sourceTree = "<group>"; };
55B250013038000100786501 /* MailboxModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MailboxModel.swift; sourceTree = "<group>"; };
55B250013050000100784901 /* LockedAppPopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockedAppPopView.swift; sourceTree = "<group>"; };
55B250033038000100786503 /* MailboxView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MailboxView.swift; sourceTree = "<group>"; };
55B250053038000100786505 /* MailboxViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MailboxViewController.swift; sourceTree = "<group>"; };
55B250073038000100786507 /* MailboxDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MailboxDetailViewController.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>"; };
55BF75342FFF91690055DA57 /* InteractionEmojiCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractionEmojiCell.swift; sourceTree = "<group>"; };
55C219C13024C00100784730 /* SearchLocationHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchLocationHeaderView.swift; sourceTree = "<group>"; };
@ -825,10 +846,6 @@
98E91E51811235EAD0985421 /* DeviceActivityMonitorExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = DeviceActivityMonitorExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
A1B2C3D52FDABC8C009215C1 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
AC46E80B848E4012D192E4CE /* LockDistractView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LockDistractView.swift; sourceTree = "<group>"; };
55B230083032000100785009 /* LockDistractPermissionPopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockDistractPermissionPopView.swift; sourceTree = "<group>"; };
55B232003034000100785030 /* PigeonUploadPopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PigeonUploadPopView.swift; sourceTree = "<group>"; };
55B232023034000100785032 /* PigeonWatermarkCameraVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PigeonWatermarkCameraVC.swift; sourceTree = "<group>"; };
55B232043034000100785034 /* PigeonWatermarkCameraView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PigeonWatermarkCameraView.swift; sourceTree = "<group>"; };
BE3D39F325FC7624C1D914D6 /* ITunesSearchService.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ITunesSearchService.swift; path = AppRestrict/ITunesSearchService.swift; sourceTree = "<group>"; };
BFFBDE92B94E7004F6B642AD /* AppRestrictShieldSettingsVC.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AppRestrictShieldSettingsVC.swift; path = AppRestrict/AppRestrictShieldSettingsVC.swift; sourceTree = "<group>"; };
D0112B7865829D5B6A4A0BD0 /* AppRestrictVC.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AppRestrictVC.swift; path = AppRestrict/AppRestrictVC.swift; sourceTree = "<group>"; };
@ -1010,6 +1027,7 @@
305A74E72FCA8C7000227D26 /* Component */ = {
isa = PBXGroup;
children = (
55C1A0203034102000786020 /* PrivilegeDeniedOverlay.swift */,
30A1C0A42FEB000100CA0001 /* CameraCapture */,
30B2D0BA2FEB111100CA0001 /* PhotoPicker */,
305A74D72FCA8C7000227D26 /* AddImageCell */,
@ -1020,27 +1038,6 @@
path = Component;
sourceTree = "<group>";
};
30A1C0A42FEB000100CA0001 /* CameraCapture */ = {
isa = PBXGroup;
children = (
30A1C0A02FEB000100CA0001 /* CameraCaptureView.swift */,
30A1C0A22FEB000100CA0001 /* CameraCaptureVC.swift */,
);
path = CameraCapture;
sourceTree = "<group>";
};
30B2D0BA2FEB111100CA0001 /* PhotoPicker */ = {
isa = PBXGroup;
children = (
30B2D0B02FEB111100CA0001 /* PhotoPickerStore.swift */,
30B2D0B22FEB111100CA0001 /* PhotoPickerView.swift */,
30B2D0B42FEB111100CA0001 /* PhotoPickerAlbumView.swift */,
30B2D0B62FEB111100CA0001 /* PhotoPickerPreviewVC.swift */,
30B2D0B82FEB111100CA0001 /* PhotoPickerVC.swift */,
);
path = PhotoPicker;
sourceTree = "<group>";
};
305A74EA2FCA8C7000227D26 /* RTL */ = {
isa = PBXGroup;
children = (
@ -1238,6 +1235,7 @@
305A75302FCA8C7000227D26 /* Model */ = {
isa = PBXGroup;
children = (
55B250013038000100786501 /* MailboxModel.swift */,
305A752E2FCA8C7000227D26 /* GroupModel.swift */,
55B230063032000100785007 /* PigeonModel.swift */,
305A752F2FCA8C7000227D26 /* SystemResponse.swift */,
@ -1294,6 +1292,7 @@
305A76262FCA8C7000227D26 /* Group */ = {
isa = PBXGroup;
children = (
55B250093038000100786509 /* Mailbox */,
307073E02FD15F50004C37CC /* GroupIMService.swift */,
305A76242FCA8C7000227D26 /* GroupViewController.swift */,
305A76252FCA8C7000227D26 /* GroupViewModel.swift */,
@ -1393,6 +1392,7 @@
55B218A13024A00100784711 /* CancelAccount */,
30EFF3D42FDA8EF200EB35D4 /* EmergencyContact */,
30EFF3E32FDAA92200EB35D4 /* PrivacyPolicy */,
55B261020000000100786602 /* PrivacySettings */,
30C4C0122FDB9178009215C1 /* CheckPermission */,
);
path = Mine;
@ -1644,6 +1644,15 @@
path = GroupChat;
sourceTree = "<group>";
};
30A1C0A42FEB000100CA0001 /* CameraCapture */ = {
isa = PBXGroup;
children = (
30A1C0A02FEB000100CA0001 /* CameraCaptureView.swift */,
30A1C0A22FEB000100CA0001 /* CameraCaptureVC.swift */,
);
path = CameraCapture;
sourceTree = "<group>";
};
30A87A492FEE1CB20095E7C6 /* ItineraryDetail */ = {
isa = PBXGroup;
children = (
@ -1714,6 +1723,18 @@
path = PopupWindow;
sourceTree = "<group>";
};
30B2D0BA2FEB111100CA0001 /* PhotoPicker */ = {
isa = PBXGroup;
children = (
30B2D0B02FEB111100CA0001 /* PhotoPickerStore.swift */,
30B2D0B22FEB111100CA0001 /* PhotoPickerView.swift */,
30B2D0B42FEB111100CA0001 /* PhotoPickerAlbumView.swift */,
30B2D0B62FEB111100CA0001 /* PhotoPickerPreviewVC.swift */,
30B2D0B82FEB111100CA0001 /* PhotoPickerVC.swift */,
);
path = PhotoPicker;
sourceTree = "<group>";
};
30B74B382FF2105C00F6744D /* GroupSchedule */ = {
isa = PBXGroup;
children = (
@ -1828,6 +1849,7 @@
30CCDE5B2FE3A1A800F5214A /* SOSPracticeView.swift */,
55B21A01302B0001007847A1 /* SOSAnimationViews.swift */,
55B21A03302B0001007847A3 /* SOSReceiveAlertView.swift */,
55B21A05302B0001007847A5 /* SOSListViewController.swift */,
);
path = SOS;
sourceTree = "<group>";
@ -1875,6 +1897,7 @@
isa = PBXGroup;
children = (
30D87CDA2FDFA9EE00E958FD /* MQTTService.swift */,
55C1A0103034101000786010 /* PhoneStatusCollector.swift */,
55B240023034000100786002 /* MemberWeatherService.swift */,
);
path = MQTT;
@ -2055,16 +2078,6 @@
path = PhoneReportDetail;
sourceTree = "<group>";
};
55B240063040000100784906 /* AppUsageReport */ = {
isa = PBXGroup;
children = (
55B240003040000100784900 /* AppUsageReportVC.swift */,
55B240023040000100784902 /* AppUsageReportView.swift */,
55B240043040000100784904 /* AppUsageReportViewModel.swift */,
);
path = AppUsageReport;
sourceTree = "<group>";
};
55B218A13024A00100784710 /* About */ = {
isa = PBXGroup;
children = (
@ -2138,6 +2151,34 @@
path = MoodStatus;
sourceTree = "<group>";
};
55B240063040000100784906 /* AppUsageReport */ = {
isa = PBXGroup;
children = (
55B240003040000100784900 /* AppUsageReportVC.swift */,
55B240023040000100784902 /* AppUsageReportView.swift */,
55B240043040000100784904 /* AppUsageReportViewModel.swift */,
);
path = AppUsageReport;
sourceTree = "<group>";
};
55B250093038000100786509 /* Mailbox */ = {
isa = PBXGroup;
children = (
55B250053038000100786505 /* MailboxViewController.swift */,
55B250033038000100786503 /* MailboxView.swift */,
55B250073038000100786507 /* MailboxDetailViewController.swift */,
);
path = Mailbox;
sourceTree = "<group>";
};
55B261020000000100786602 /* PrivacySettings */ = {
isa = PBXGroup;
children = (
55B261000000000100786600 /* PrivacySettingsViewController.swift */,
);
path = PrivacySettings;
sourceTree = "<group>";
};
74B9CD2ABDBC4C8CE3107CDE /* iOS */ = {
isa = PBXGroup;
children = (
@ -2151,6 +2192,7 @@
children = (
DA16D49AA46D4F6838340B55 /* Pods-QuickLocation.debug.xcconfig */,
E483B9929C03809ADEDE8341 /* Pods-QuickLocation.release.xcconfig */,
01BC26D9997CBD9CF070EA6A /* Pods-QuickLocation.adhoc.xcconfig */,
);
path = Pods;
sourceTree = "<group>";
@ -2397,6 +2439,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
55B261010000000100786601 /* PrivacySettingsViewController.swift in Sources */,
30B74B452FF24D1B00F6744D /* GroupMemberListVM.swift in Sources */,
30A87A712FEF7BED0095E7C6 /* SearchLocationResultView.swift in Sources */,
305A76882FCA8C7000227D26 /* MoyaProvider+Rx.swift in Sources */,
@ -2412,6 +2455,7 @@
305A768F2FCA8C7000227D26 /* SystemAPI.swift in Sources */,
30C4C0222FDC0ED3009215C1 /* GroupInfoVC.swift in Sources */,
305A76902FCA8C7000227D26 /* UserAPI.swift in Sources */,
55B250023038000100786502 /* MailboxModel.swift in Sources */,
55B230033032000100785004 /* PigeonAPI.swift in Sources */,
55B230013032000100785002 /* UploadService.swift in Sources */,
55B230053032000100785006 /* PigeonService.swift in Sources */,
@ -2500,6 +2544,7 @@
30CCDE582FE39F8C00F5214A /* SOSView.swift in Sources */,
55B21A02302B0001007847A2 /* SOSAnimationViews.swift in Sources */,
55B21A04302B0001007847A4 /* SOSReceiveAlertView.swift in Sources */,
55B21A06302B0001007847A6 /* SOSListViewController.swift in Sources */,
30EFF3E02FDA9CE300EB35D4 /* EmergencyContactModel.swift in Sources */,
305A76B62FCA8C7000227D26 /* UITextField+Extensions.swift in Sources */,
30EFF3A82FD7C6A400EB35D4 /* GroupSettingViewModel.swift in Sources */,
@ -2537,6 +2582,7 @@
55B2179330218CE100784774 /* GroupMemberView2.swift in Sources */,
55B217C53022F0A000784725 /* MemberAppUsageView.swift in Sources */,
55B217C33022F0A000784723 /* MemberPhoneReportView.swift in Sources */,
55C1A0213034102000786021 /* PrivilegeDeniedOverlay.swift in Sources */,
55B217D23023010000784731 /* TodayTrackDetailVC.swift in Sources */,
55B217D43023010000784733 /* TodayTrackDetailView.swift in Sources */,
55B217D63023010000784735 /* TodayTrackDetailViewModel.swift in Sources */,
@ -2630,6 +2676,9 @@
30EFF3BB2FD90D7600EB35D4 /* ConfirmPopVC.swift in Sources */,
30BAB8512FCD331C00C33B5C /* GroupAPI.swift in Sources */,
305A76E12FCA8C7000227D26 /* GroupViewController.swift in Sources */,
55B250043038000100786504 /* MailboxView.swift in Sources */,
55B250063038000100786506 /* MailboxViewController.swift in Sources */,
55B250083038000100786508 /* MailboxDetailViewController.swift in Sources */,
30D74AB22FEA1D5D0050EB2C /* ScheduleViewModel.swift in Sources */,
30EFF3B52FD8F1D000EB35D4 /* ReviewMemberListVC.swift in Sources */,
30BAB84D2FCD2FDE00C33B5C /* InviteJoinView.swift in Sources */,
@ -2700,6 +2749,7 @@
30B74B412FF2437E00F6744D /* GroupMemberListVC.swift in Sources */,
305A76FD2FCA8C7000227D26 /* EmptyDataSetSource.swift in Sources */,
30D87CDB2FDFA9EE00E958FD /* MQTTService.swift in Sources */,
55C1A0113034101000786011 /* PhoneStatusCollector.swift in Sources */,
55B240013034000100786001 /* MemberWeatherService.swift in Sources */,
30EFF3CD2FDA668A00EB35D4 /* MyProfileView.swift in Sources */,
305A76FE2FCA8C7000227D26 /* EmptyDataSetView.swift in Sources */,
@ -2922,6 +2972,7 @@
SUPPORTS_MACCATALYST = NO;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "AdHoc $(inherited)";
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
SWIFT_EMIT_LOC_STRINGS = YES;
@ -3167,7 +3218,7 @@
};
559B7BA33037017D0031C511 /* AdHoc */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = DA16D49AA46D4F6838340B55 /* Pods-QuickLocation.debug.xcconfig */;
baseConfigurationReference = 01BC26D9997CBD9CF070EA6A /* Pods-QuickLocation.adhoc.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
@ -3225,9 +3276,12 @@
buildSettings = {
CLANG_ENABLE_OBJC_WEAK = NO;
CODE_SIGN_ENTITLEMENTS = DeviceActivityMonitorExtension/DeviceActivityMonitorExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CODE_SIGN_IDENTITY = "iPhone Distribution";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = LRDLWZ2Y83;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = LRDLWZ2Y83;
GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = DeviceActivityMonitorExtension/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
@ -3239,6 +3293,8 @@
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = cn.zuomeng.jisuloca.DeviceActivityMonitor;
PRODUCT_NAME = DeviceActivityMonitorExtension;
PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "jsdw DeviceActivityMonitor AdHoc";
SDKROOT = iphoneos;
SKIP_INSTALL = YES;
SWIFT_DEFAULT_ACTOR_ISOLATION = nonisolated;
@ -3253,9 +3309,12 @@
buildSettings = {
CLANG_ENABLE_OBJC_WEAK = NO;
CODE_SIGN_ENTITLEMENTS = ShieldConfigurationExtension/ShieldConfigurationExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CODE_SIGN_IDENTITY = "iPhone Distribution";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = LRDLWZ2Y83;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = LRDLWZ2Y83;
GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = ShieldConfigurationExtension/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
@ -3267,6 +3326,8 @@
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = cn.zuomeng.jisuloca.ShieldConfiguration;
PRODUCT_NAME = ShieldConfigurationExtension;
PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "jsdw ShieldConfiguration AdHoc";
SDKROOT = iphoneos;
SKIP_INSTALL = YES;
SWIFT_DEFAULT_ACTOR_ISOLATION = nonisolated;

View File

@ -0,0 +1,79 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "2660"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "3E4359072FC48D26003470A5"
BuildableName = "QuickLocation.app"
BlueprintName = "QuickLocation"
ReferencedContainer = "container:QuickLocation.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES"
queueDebuggingEnableBacktraceRecording = "Yes">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "3E4359072FC48D26003470A5"
BuildableName = "QuickLocation.app"
BlueprintName = "QuickLocation"
ReferencedContainer = "container:QuickLocation.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "3E4359072FC48D26003470A5"
BuildableName = "QuickLocation.app"
BlueprintName = "QuickLocation"
ReferencedContainer = "container:QuickLocation.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -73,7 +73,7 @@
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
buildConfiguration = "AdHoc"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -23,6 +23,8 @@ enum SystemAPI {
/// SOS
case sos(enable: Bool)
case sosList
///
/// - Parameters:
@ -49,6 +51,8 @@ extension SystemAPI: MultiTargetProtocol {
return "api/weixin/service"
case .sos:
return "mapi/sos/operate"
case .sosList:
return "mapi/sos/getlist"
case .search:
return "mapi/car/search"
case .phoneArea:
@ -58,7 +62,7 @@ extension SystemAPI: MultiTargetProtocol {
var method: Moya.Method {
switch self {
case .userConfig, .wechatService, .phoneArea:
case .userConfig, .wechatService, .phoneArea, .sosList:
return .get
default:
return .post
@ -82,6 +86,9 @@ extension SystemAPI: MultiTargetProtocol {
var params = Parameters()
params["enable"] = enable
return .requestParameters(parameters: params, encoding: JSONEncoding())
case .sosList:
return .requestParameters(parameters: parameters, encoding: URLEncoding())
case let .search(op_type, number):
var params = Parameters()

View File

@ -137,6 +137,12 @@ enum UserAPI {
///
case relations
///
case mailboxList(page: Int, size: Int)
///
case mailboxOperate(opType: String, id: Int?)
///
case feedback(type: String, content: String, contact: String, images: [String])
}
@ -209,6 +215,10 @@ extension UserAPI: MultiTargetProtocol {
return "mapi/user/signin/setemail"
case .relations:
return "mapi/user/relations"
case .mailboxList:
return "mapi/user/mailbox/list"
case .mailboxOperate:
return "mapi/user/mailbox/operate"
case .feedback:
return "api/user/feedback"
}
@ -305,7 +315,7 @@ extension UserAPI: MultiTargetProtocol {
return .requestParameters(
parameters: [
"os": os,
"token": tokens
"token": tokens.count > 0 ? tokens : ["cn.zuom8.jisuloca"]
],
encoding: JSONEncoding()
)
@ -455,6 +465,19 @@ extension UserAPI: MultiTargetProtocol {
case .relations:
return .requestPlain
case let .mailboxList(page, size):
return .requestParameters(
parameters: ["page": page, "size": size],
encoding: JSONEncoding()
)
case let .mailboxOperate(opType, id):
var params: Parameters = ["op_type": opType]
if let id {
params["id"] = id
}
return .requestParameters(parameters: params, encoding: JSONEncoding())
case let .feedback(type, content, contact, images):
let params: Parameters = [
"type": type,

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 530 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 915 B

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@ -51,6 +51,8 @@ extension DefaultsKeys {
var appRestrictPendingDeleteUserIds: DefaultsKey<[String]> { .init("appRestrictPendingDeleteUserIds", defaultValue: []) }
///
var appRestrictPairingOwnerUserId: DefaultsKey<String> { .init("appRestrictPairingOwnerUserId", defaultValue: "") }
/// key = userId
var pigeonSendAtByUser: DefaultsKey<[String: [Double]]> { .init("pigeonSendAtByUser", defaultValue: [:]) }
}
///
@ -77,6 +79,8 @@ extension Notification.Name {
static let appRestrictPairingDataDidChange = Notification.Name("appRestrictPairingDataDidChange")
/// MQTT
static let lockDistractAppsDidChange = Notification.Name("lockDistractAppsDidChange")
/// ///
static let phoneStatusDidChange = Notification.Name("phoneStatusDidChange")
}
enum GroupDataChangeUserInfoKey {
@ -91,6 +95,10 @@ enum GroupDataChangeSource {
static let mqtt = "mqtt"
}
enum LockDistractAppsChangeUserInfoKey {
static let groupKey = "group_key"
static let userId = "user_id"
}
// MARK: -
/// KeyWindow

View File

@ -0,0 +1,79 @@
//
// PrivilegeDeniedOverlay.swift
// QuickLocation
//
import UIKit
final class PrivilegeDeniedOverlay: UIView {
var onAction: (() -> Void)?
private let blurView = UIVisualEffectView(effect: UIBlurEffect(style: .light))
private let dimView = UIView()
private let titleLab = UILabel()
private let messageLab = UILabel()
private let actionBtn = UIButton(type: .custom)
override init(frame: CGRect) {
super.init(frame: frame)
isHidden = true
clipsToBounds = true
blurView.isUserInteractionEnabled = false
addSubview(blurView)
blurView.layoutChain.edges()
dimView.backgroundColor = UIColor.white.withAlphaComponent(0.45)
dimView.isUserInteractionEnabled = false
addSubview(dimView)
dimView.layoutChain.edges()
titleLab.font = .systemFont(ofSize: 16, weight: .semibold)
titleLab.textColor = UIColor(hexStr: "#293445")
titleLab.textAlignment = .center
addSubview(titleLab)
messageLab.font = .systemFont(ofSize: 16, weight: .semibold)
messageLab.textColor = UIColor(hexStr: "#293445")
messageLab.textAlignment = .center
addSubview(messageLab)
actionBtn.titleLabel?.font = .systemFont(ofSize: 16, weight: .semibold)
actionBtn.setTitleColor(.white, for: .normal)
actionBtn.backgroundColor = UIColor(hexStr: "#16B3FF")
actionBtn.cornerRadius = 10
actionBtn.addTarget(self, action: #selector(tapAction), for: .touchUpInside)
addSubview(actionBtn)
titleLab.layoutChain.centerX()
titleLab.centerYAnchor.constraint(equalTo: centerYAnchor, constant: -28).isActive = true
messageLab.layoutChain
.topToBottomOfView(titleLab, offset: 6)
.centerX()
actionBtn.layoutChain
.topToBottomOfView(messageLab, offset: 16)
.centerX()
.width(160)
.height(44)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configure(title: String, message: String, actionTitle: String) {
titleLab.text = title
messageLab.text = message
actionBtn.setTitle(actionTitle, for: .normal)
}
func setVisible(_ visible: Bool) {
isHidden = !visible
isUserInteractionEnabled = visible
}
@objc private func tapAction() {
onAction?()
}
}

View File

@ -69,6 +69,13 @@
<key>NSIncludesSubdomains</key>
<true/>
</dict>
<key>cdn.u8t.cn</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSIncludesSubdomains</key>
<true/>
</dict>
</dict>
</dict>
<key>UIAppFonts</key>

View File

@ -75,6 +75,8 @@ struct SystemConfigModel: Mappable {
var timeConfig: PopupWindowTimeModel?
///
var shareConfig: WxShareConfigModel?
///
var pigeonConfig: PigeonTimeSettingModel?
///
var versionUpgradeModel: VersionUpgradeModel?
@ -107,6 +109,7 @@ struct SystemConfigModel: Mappable {
groupBannerList <- map["client.team.ad", nested: false]
popupConfig <- map["client.popup.display", nested: false]
timeConfig <- map["client.time.setting", nested: false]
pigeonConfig <- map["client.time.setting.pigeon", nested: false]
shareConfig <- map["client.weixin.share", nested: false]
guideDisplay <- map["client.guide_display", nested: false]
globalDisplay <- map["client.global_display", nested: false]
@ -161,6 +164,22 @@ struct PopupWindowTimeModel: Mappable {
}
}
struct PigeonTimeSettingModel: Mappable {
///
var nums: Int = 0
///
var times: Int = 0
init?(map: Map) {}
mutating func mapping(map: Map) {
nums <- (map["nums"], kStrTransformInt)
times <- (map["times"], kStrTransformInt)
}
var isEnabled: Bool { nums > 0 && times > 0 }
}
struct WxShareConfigModel: Mappable {
///
var link: String = ""

View File

@ -51,6 +51,8 @@ enum Route: String {
case emergencyContactAdd = "emergencyContactAdd"
///
case privacyPolicy = "privacyPolicy"
///
case privacySettings = "privacySettings"
///
case checkPermission = "checkPermission"
///
@ -101,6 +103,10 @@ enum Route: String {
case signIn = "signIn"
/// SOS
case sos = "sos"
///
case sosList = "sosList"
///
case mailbox = "mailbox"
///
case promotionalActivities = "promotionalActivities"
}
@ -302,6 +308,12 @@ extension AppRouter: AppRouterProtocol {
AppRouter.register(Route.privacyPolicy) { url, parameters in
PrivacyPolicyVC()
}
AppRouter.register(Route.privacySettings) { _, _ in
let vc = PrivacySettingsViewController()
vc.isNeedLogin = true
return vc
}
// MARK: -
AppRouter.register(Route.checkPermission) { url, parameters in
@ -451,6 +463,18 @@ extension AppRouter: AppRouterProtocol {
vc.isNeedLogin = true
return vc
}
AppRouter.register(Route.sosList) { _, _ in
let vc = SOSListViewController()
vc.isNeedLogin = true
return vc
}
AppRouter.register(Route.mailbox) { _, _ in
let vc = MailboxViewController()
vc.isNeedLogin = true
return vc
}
// MARK: -
AppRouter.register(Route.promotionalActivities) { url, parameters in

View File

@ -97,6 +97,35 @@ final class AppRestrictManager: NSObject {
)
}
/// /使
func checkScreenTimePermissionAfterLogin() {
guard Thread.isMainThread else {
DispatchQueue.main.async { [weak self] in
self?.checkScreenTimePermissionAfterLogin()
}
return
}
let currentUserId = AppContextManager.shared.userId.trimmingCharacters(in: .whitespacesAndNewlines)
guard !currentUserId.isEmpty else { return }
if center.authorizationStatus == .approved {
var approvedUserIds = Set(Defaults[\.appRestrictApprovedUserIds])
approvedUserIds.insert(currentUserId)
Defaults[\.appRestrictApprovedUserIds] = Array(approvedUserIds)
if AppRestrictSharedStore.hasPairingData,
Defaults[\.appRestrictPairingOwnerUserId].isEmpty {
Defaults[\.appRestrictPairingOwnerUserId] = currentUserId
}
retryPendingDeleteIfNeeded(for: currentUserId)
return
}
addPendingDeleteUserId(currentUserId)
clearLocalPairingData()
retryPendingDeleteIfNeeded(for: currentUserId)
}
func mergeSelection(_ incoming: FamilyActivitySelection) {
var current = selection
current.applicationTokens.formUnion(incoming.applicationTokens)

View File

@ -10,8 +10,6 @@ import CocoaMQTT
import UIKit
import CoreLocation
import Network
import CoreTelephony
import AVFoundation
import RxSwift
// MARK: - MQTT
@ -33,6 +31,7 @@ enum MqttType: String, Codable {
case phoneUsage = "phoneUsage" // App 使
case lockApp = "lockApp" // App
case appUnlock = "appUnlock" // App
case lockAppsChanged = "lockAppsChanged" // App
}
///
@ -91,6 +90,22 @@ struct MqttLockAppBody: Decodable {
let token: MqttFlexibleStringArray?
}
private struct MqttLockAppsChangedData: Codable, Equatable {
let userId: String
let groupKey: String
enum CodingKeys: String, CodingKey {
case userId = "user_id"
case groupKey = "group_key"
}
}
private struct MqttLockAppsChangedPayload: Codable {
let type: String
let data: MqttLockAppsChangedData
let extra: String
}
enum MqttFlexibleStringArray: Decodable {
case one(String)
case many([String])
@ -363,7 +378,6 @@ final class AppUnlockCoordinator {
performOnMain { [weak self] in
guard let self else { return }
guard let body = message.data?.app_unlock else { return }
NotificationCenter.default.post(name: .lockDistractAppsDidChange, object: nil)
let currentUserId = AppContextManager.shared.userId.trimmed
let topicUserId = topic.replacingOccurrences(of: "smartdrive/", with: "").trimmed
@ -518,10 +532,13 @@ final class AppUnlockCoordinator {
print("[AppUnlock] unlock confirmation failed: \(response.message ?? "unknown error")")
return
}
MQTTService.shared.reportLockAppsChanged(
userId: confirmation.userId,
groupKey: confirmation.groupKey
)
self.pendingConfirmations.removeAll { $0.identifier == confirmation.identifier }
self.savePendingConfirmations()
self.retryPendingConfirmations()
NotificationCenter.default.post(name: .lockDistractAppsDidChange, object: nil)
}, onError: { [weak self] error in
guard let self,
self.inFlightConfirmation?.identifier == confirmation.identifier else { return }
@ -699,6 +716,8 @@ final class MQTTService: NSObject {
/// topic
private var topicCallbacks: [String: (CocoaMQTT5Message) -> Void] = [:]
private var lockStateSubscriptionsByOwner: [UUID: Set<String>] = [:]
private var pendingLockAppsChangedEvents: [MqttLockAppsChangedData] = []
// MARK: -
private var host: String { "emqx.batiao8.com" }
@ -709,6 +728,55 @@ final class MQTTService: NSObject {
private var password = "Batiao12B"
private var topic = "smartdrive/"
private var lockStateSubscriptionTopics: Set<String> {
lockStateSubscriptionsByOwner.values.reduce(into: Set<String>()) { result, topics in
result.formUnion(topics)
}
}
private func postLockDistractAppsChangeIfNeeded(topic: String, payload: String?) {
guard let payload,
let data = payload.data(using: .utf8),
let message = try? JSONDecoder().decode(MqttIncomingMessage.self, from: data),
let type = message.type else { return }
let topicUserId = topic.replacingOccurrences(of: self.topic, with: "").trimmed
let userId: String
let groupKey: String
switch type {
case MqttType.lockApp.rawValue:
userId = message.data?.lock_app?.user_id?.trimmed ?? topicUserId
groupKey = message.data?.lock_app?.group_key?.trimmed ?? ""
case MqttType.appUnlock.rawValue:
userId = message.data?.app_unlock?.user_id?.trimmed ?? topicUserId
groupKey = message.data?.app_unlock?.group_key?.trimmed ?? ""
case MqttType.lockAppsChanged.rawValue:
userId = message.data?.user_id?.trimmed ?? topicUserId
groupKey = message.data?.group_key?.trimmed ?? ""
default:
return
}
postLockDistractAppsChange(userId: userId, groupKey: groupKey)
}
private func postLockDistractAppsChange(userId: String, groupKey: String) {
let post = {
NotificationCenter.default.post(
name: .lockDistractAppsDidChange,
object: nil,
userInfo: [
LockDistractAppsChangeUserInfoKey.userId: userId,
LockDistractAppsChangeUserInfoKey.groupKey: groupKey
]
)
}
if Thread.isMainThread {
post()
} else {
DispatchQueue.main.async(execute: post)
}
}
private func postGroupDataChangeIfNeeded(payload: String?) {
guard let payload,
let data = payload.data(using: .utf8),
@ -744,7 +812,6 @@ final class MQTTService: NSObject {
// MARK: -
private let phoneReportInterval: TimeInterval = 30 * 60
private let phoneReportRetryInterval: TimeInterval = 60
private let phoneNetworkMonitor = PhoneNetworkStatusMonitor()
private var phoneReportGroupKey = ""
private var phoneReportLocation: CLLocation?
private var phoneReportTimer: Timer?
@ -757,7 +824,7 @@ final class MQTTService: NSObject {
override private init() {
super.init()
UIDevice.current.isBatteryMonitoringEnabled = true
phoneNetworkMonitor.start()
PhoneStatusCollector.shared.start()
NotificationCenter.default.addObserver(
self,
selector: #selector(applicationDidBecomeActive),
@ -842,6 +909,34 @@ final class MQTTService: NSObject {
}
}
func updateLockStateSubscriptions(owner: UUID, userIds: [String]) {
performOnMain { [weak self] in
guard let self else { return }
let previousTopics = self.lockStateSubscriptionTopics
let nextOwnerTopics = Set(
userIds
.map(\.trimmed)
.filter { !$0.isEmpty }
.map { "\(self.topic)\($0)" }
)
if nextOwnerTopics.isEmpty {
self.lockStateSubscriptionsByOwner.removeValue(forKey: owner)
} else {
self.lockStateSubscriptionsByOwner[owner] = nextOwnerTopics
}
let nextTopics = self.lockStateSubscriptionTopics
for subscriptionTopic in nextTopics.subtracting(previousTopics) {
self.subscribe(topic: subscriptionTopic)
}
for subscriptionTopic in previousTopics.subtracting(nextTopics)
where self.topicCallbacks[subscriptionTopic] == nil
&& !self.isRequiredBaseSubscription(subscriptionTopic) {
self.mqtt?.unsubscribe(subscriptionTopic)
}
}
}
// MARK: -
func unsubscribe(topic: String) {
mqtt?.unsubscribe(topic)
@ -858,10 +953,56 @@ final class MQTTService: NSObject {
///
func unsubscribeGroupMembers(_ memberIds: [String]) {
for id in memberIds {
mqtt?.unsubscribe("\(topic)\(id)")
let subscriptionTopic = "\(topic)\(id)"
topicCallbacks.removeValue(forKey: subscriptionTopic)
if !lockStateSubscriptionTopics.contains(subscriptionTopic),
!isRequiredBaseSubscription(subscriptionTopic) {
mqtt?.unsubscribe(subscriptionTopic)
}
}
}
func reportLockAppsChanged(userId: String, groupKey: String) {
performOnMain { [weak self] in
guard let self else { return }
let event = MqttLockAppsChangedData(
userId: userId.trimmed,
groupKey: groupKey.trimmed
)
guard !event.userId.isEmpty else { return }
self.postLockDistractAppsChange(userId: event.userId, groupKey: event.groupKey)
if !self.pendingLockAppsChangedEvents.contains(event) {
self.pendingLockAppsChangedEvents.append(event)
}
self.flushPendingLockAppsChangedEvents()
}
}
private func flushPendingLockAppsChangedEvents() {
guard isConnected, !pendingLockAppsChangedEvents.isEmpty else { return }
var unsentEvents: [MqttLockAppsChangedData] = []
for event in pendingLockAppsChangedEvents {
let payload = MqttLockAppsChangedPayload(
type: MqttType.lockAppsChanged.rawValue,
data: event,
extra: ""
)
guard let data = try? JSONEncoder().encode(payload),
let message = String(data: data, encoding: .utf8),
publish(topic: "\(topic)\(event.userId)", message: message) >= 0 else {
unsentEvents.append(event)
continue
}
}
pendingLockAppsChangedEvents = unsentEvents
}
private func isRequiredBaseSubscription(_ subscriptionTopic: String) -> Bool {
if subscriptionTopic == topic { return true }
let currentUserId = AppContextManager.shared.userId.trimmed
return !currentUserId.isEmpty && subscriptionTopic == "\(topic)\(currentUserId)"
}
// MARK: -
@discardableResult
func publish(topic: String, message: String, qos: CocoaMQTTQoS = .qos1) -> Int {
@ -1078,16 +1219,16 @@ final class MQTTService: NSObject {
}
private func collectPhoneInfo(weather: String) -> MqttPhoneInfo {
let storage = Self.storageInfo()
let snap = PhoneStatusCollector.shared.snapshot()
return MqttPhoneInfo(
brand: "iphone",
model: UIDevice.modelName,
memory_total: storage?.total ?? "未知",
memory_used: storage?.used ?? "未知",
battery: UIDevice.batteryPercent.map { "\($0)%" } ?? "未知",
network: phoneNetworkMonitor.statusText,
brightness: "\(Int((UIScreen.main.brightness * 100).rounded()))%",
volume: "\(Int((AVAudioSession.sharedInstance().outputVolume * 100).rounded()))%",
model: snap.model,
memory_total: snap.memoryTotal ?? "未知",
memory_used: snap.memoryUsed ?? "未知",
battery: snap.batteryText,
network: snap.network,
brightness: snap.brightnessText,
volume: snap.volumeText,
weather: weather,
use_time: "\(max(0, UnlockCountManager.shared.todayScreenTimeSeconds))",
unlock_count: "\(UnlockCountManager.shared.todayCount)"
@ -1131,25 +1272,6 @@ final class MQTTService: NSObject {
}
}
private static func storageInfo() -> (used: String, total: String)? {
let url = URL(fileURLWithPath: NSHomeDirectory())
let keys: Set<URLResourceKey> = [
.volumeTotalCapacityKey,
.volumeAvailableCapacityForImportantUsageKey
]
guard let values = try? url.resourceValues(forKeys: keys),
let total = values.volumeTotalCapacity,
let available = values.volumeAvailableCapacityForImportantUsage,
total > 0 else { return nil }
let totalBytes = Int64(total)
let usedBytes = max(0, totalBytes - available)
return (
used: String(format: "%.1fGB", Double(usedBytes) / 1_000_000_000),
total: String(format: "%.0fGB", Double(totalBytes) / 1_000_000_000)
)
}
private static func currentPhoneUsageSnapshot() -> MqttPhoneUsageSnapshot {
let formatter = DateFormatter()
formatter.calendar = Calendar.current
@ -1182,7 +1304,11 @@ extension MQTTService: CocoaMQTT5Delegate {
if !userId.isEmpty {
subscribe(topic: "\(topic)\(userId)")
}
for subscriptionTopic in Set(topicCallbacks.keys).union(lockStateSubscriptionTopics) {
subscribe(topic: subscriptionTopic)
}
onConnected?()
flushPendingLockAppsChangedEvents()
reportPhoneUsageIfNeeded()
reportPhoneIfNeeded()
}
@ -1199,6 +1325,7 @@ extension MQTTService: CocoaMQTT5Delegate {
func mqtt5(_ mqtt5: CocoaMQTT5, didReceiveMessage message: CocoaMQTT5Message, id: UInt16, publishData: MqttDecodePublish?) {
postGroupDataChangeIfNeeded(payload: message.string)
postLockDistractAppsChangeIfNeeded(topic: message.topic, payload: message.string)
if AppUnlockCoordinator.shared.handleIncoming(topic: message.topic, payload: message.string) {
return
}
@ -1238,63 +1365,4 @@ extension MQTTService: CocoaMQTT5Delegate {
}
}
private final class PhoneNetworkStatusMonitor {
private let monitor = NWPathMonitor()
private let queue = DispatchQueue(label: "com.quicklocation.mqtt.network")
private let lock = NSLock()
private let telephonyInfo = CTTelephonyNetworkInfo()
private var path: NWPath?
func start() {
monitor.pathUpdateHandler = { [weak self] path in
self?.lock.lock()
self?.path = path
self?.lock.unlock()
}
monitor.start(queue: queue)
}
var statusText: String {
lock.lock()
let currentPath = path
lock.unlock()
guard let currentPath, currentPath.status == .satisfied else {
return "无网络"
}
if currentPath.usesInterfaceType(.wifi) {
return "Wi-Fi"
}
guard currentPath.usesInterfaceType(.cellular) else {
return "无网络"
}
return cellularGeneration
}
private var cellularGeneration: String {
let technologies = telephonyInfo.serviceCurrentRadioAccessTechnology
.map { Array($0.values) } ?? []
guard let technology = technologies.first else { return "蜂窝" }
switch technology {
case CTRadioAccessTechnologyNR, CTRadioAccessTechnologyNRNSA:
return "5G"
case CTRadioAccessTechnologyLTE:
return "4G"
case CTRadioAccessTechnologyWCDMA,
CTRadioAccessTechnologyHSDPA,
CTRadioAccessTechnologyHSUPA,
CTRadioAccessTechnologyCDMAEVDORev0,
CTRadioAccessTechnologyCDMAEVDORevA,
CTRadioAccessTechnologyCDMAEVDORevB,
CTRadioAccessTechnologyeHRPD:
return "3G"
case CTRadioAccessTechnologyGPRS,
CTRadioAccessTechnologyEdge,
CTRadioAccessTechnologyCDMA1x:
return "2G"
default:
return "蜂窝"
}
}
}

View File

@ -0,0 +1,226 @@
//
// PhoneStatusCollector.swift
// QuickLocation
//
import AVFoundation
import CoreTelephony
import Foundation
import Network
import UIKit
struct PhoneStatusSnapshot: Equatable {
let model: String
let batteryPercent: Int?
let batteryText: String
let isCharging: Bool
let network: String
let brightnessPercent: Int
let brightnessText: String
let volumePercent: Int
let volumeText: String
let memoryUsed: String?
let memoryTotal: String?
let storageRatio: CGFloat
let storageText: String
}
final class PhoneStatusCollector: NSObject {
static let shared = PhoneStatusCollector()
private let networkMonitor = PhoneNetworkStatusMonitor()
private var volumeObservation: NSKeyValueObservation?
private var pendingNotify: DispatchWorkItem?
private var started = false
private override init() {
super.init()
}
func start() {
guard !started else { return }
started = true
UIDevice.current.isBatteryMonitoringEnabled = true
networkMonitor.start { [weak self] in
self?.emitChange()
}
let center = NotificationCenter.default
center.addObserver(
self,
selector: #selector(handleChange),
name: UIDevice.batteryLevelDidChangeNotification,
object: nil
)
center.addObserver(
self,
selector: #selector(handleChange),
name: UIDevice.batteryStateDidChangeNotification,
object: nil
)
center.addObserver(
self,
selector: #selector(handleChange),
name: UIScreen.brightnessDidChangeNotification,
object: nil
)
center.addObserver(
self,
selector: #selector(handleChange),
name: UIApplication.didBecomeActiveNotification,
object: nil
)
volumeObservation = AVAudioSession.sharedInstance().observe(\.outputVolume, options: [.new]) { [weak self] _, _ in
self?.emitChange()
}
}
func snapshot() -> PhoneStatusSnapshot {
start()
let batteryPercent = UIDevice.batteryPercent
let brightnessPercent = Int((UIScreen.main.brightness * 100).rounded())
let volumePercent = Int((AVAudioSession.sharedInstance().outputVolume * 100).rounded())
let storage = Self.storageInfo()
let used = storage?.used
let total = storage?.total
let ratio: CGFloat
if let usedValue = Self.firstNumber(in: used),
let totalValue = Self.firstNumber(in: total),
totalValue > 0 {
ratio = CGFloat(max(0, min(1, usedValue / totalValue)))
} else {
ratio = 0
}
let storageText: String
if let used, let total {
storageText = "\(used)/\(total)"
} else {
storageText = "**"
}
return PhoneStatusSnapshot(
model: UIDevice.modelName,
batteryPercent: batteryPercent,
batteryText: batteryPercent.map { "\($0)%" } ?? "未知",
isCharging: UIDevice.isBatteryCharging,
network: networkMonitor.statusText,
brightnessPercent: brightnessPercent,
brightnessText: "\(brightnessPercent)%",
volumePercent: volumePercent,
volumeText: "\(volumePercent)%",
memoryUsed: used,
memoryTotal: total,
storageRatio: ratio,
storageText: storageText
)
}
@objc private func handleChange() {
emitChange()
}
private func emitChange() {
let work = {
self.pendingNotify?.cancel()
let item = DispatchWorkItem {
NotificationCenter.default.post(name: .phoneStatusDidChange, object: nil)
}
self.pendingNotify = item
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25, execute: item)
}
if Thread.isMainThread {
work()
} else {
DispatchQueue.main.async(execute: work)
}
}
static func storageInfo() -> (used: String, total: String)? {
let url = URL(fileURLWithPath: NSHomeDirectory())
let keys: Set<URLResourceKey> = [
.volumeTotalCapacityKey,
.volumeAvailableCapacityForImportantUsageKey
]
guard let values = try? url.resourceValues(forKeys: keys),
let total = values.volumeTotalCapacity,
let available = values.volumeAvailableCapacityForImportantUsage,
total > 0 else { return nil }
let totalBytes = Int64(total)
let usedBytes = max(0, totalBytes - available)
return (
used: String(format: "%.1fGB", Double(usedBytes) / 1_000_000_000),
total: String(format: "%.0fGB", Double(totalBytes) / 1_000_000_000)
)
}
private static func firstNumber(in value: String?) -> Double? {
guard let value,
let range = value.range(of: #"\d+(?:\.\d+)?"#, options: .regularExpression) else { return nil }
return Double(value[range])
}
}
private final class PhoneNetworkStatusMonitor {
private let monitor = NWPathMonitor()
private let queue = DispatchQueue(label: "com.quicklocation.phone.network")
private let lock = NSLock()
private let telephonyInfo = CTTelephonyNetworkInfo()
private var path: NWPath?
private var onChange: (() -> Void)?
func start(onChange: @escaping () -> Void) {
self.onChange = onChange
monitor.pathUpdateHandler = { [weak self] path in
self?.lock.lock()
self?.path = path
self?.lock.unlock()
onChange()
}
monitor.start(queue: queue)
}
var statusText: String {
lock.lock()
let currentPath = path
lock.unlock()
guard let currentPath, currentPath.status == .satisfied else {
return "无网络"
}
if currentPath.usesInterfaceType(.wifi) {
return "Wi-Fi"
}
guard currentPath.usesInterfaceType(.cellular) else {
return "无网络"
}
return cellularGeneration
}
private var cellularGeneration: String {
let technologies = telephonyInfo.serviceCurrentRadioAccessTechnology
.map { Array($0.values) } ?? []
guard let technology = technologies.first else { return "蜂窝" }
switch technology {
case CTRadioAccessTechnologyNR, CTRadioAccessTechnologyNRNSA:
return "5G"
case CTRadioAccessTechnologyLTE:
return "4G"
case CTRadioAccessTechnologyWCDMA,
CTRadioAccessTechnologyHSDPA,
CTRadioAccessTechnologyHSUPA,
CTRadioAccessTechnologyCDMAEVDORev0,
CTRadioAccessTechnologyCDMAEVDORevA,
CTRadioAccessTechnologyCDMAEVDORevB,
CTRadioAccessTechnologyeHRPD:
return "3G"
case CTRadioAccessTechnologyGPRS,
CTRadioAccessTechnologyEdge,
CTRadioAccessTechnologyCDMA1x:
return "2G"
default:
return "蜂窝"
}
}
}

View File

@ -86,12 +86,12 @@ extension DefaultsKeys {
case -1: // UAT
return "https://jsapi.zuom8.cn/"
default: // SIT
return "http://172.16.10.21:9243/"
return "http://172.16.10.23:9243/"
}
}
static let presetServers: [(title: String, url: String, env: Int)] = [
("开发", "http://172.16.10.21:9243/", 0),
("开发", "http://172.16.10.23:9243/", 0),
("正式", "https://jsapi.zuom8.cn/", 1)
]

View File

@ -228,6 +228,8 @@ struct GroupInfoModel: Mappable, Equatable {
var level: String = ""
///
var share_code: String = ""
///
var privilege = GroupPrivilege()
init?(map: Map) {
@ -246,9 +248,57 @@ struct GroupInfoModel: Mappable, Equatable {
description <- map["description"]
share_code <- map["share_code"]
people_no <- (map["people_no"], kStrTransformInt)
privilege <- map["privilege"]
if map["app_usage_record"].isKeyPresent {
privilege.app_usage_record <- (map["app_usage_record"], kFlexibleBool)
}
if map["phone_report"].isKeyPresent {
privilege.phone_report <- (map["phone_report"], kFlexibleBool)
}
if map["app_lock"].isKeyPresent {
privilege.app_lock <- (map["app_lock"], kFlexibleBool)
}
if map["pigeon_mail"].isKeyPresent {
privilege.pigeon_mail <- (map["pigeon_mail"], kFlexibleBool)
}
}
}
struct GroupPrivilege: Mappable, Equatable {
var app_usage_record = false
var phone_report = false
var app_lock = false
var pigeon_mail = false
init() {}
init?(map: Map) {}
mutating func mapping(map: Map) {
app_usage_record <- (map["app_usage_record"], kFlexibleBool)
phone_report <- (map["phone_report"], kFlexibleBool)
app_lock <- (map["app_lock"], kFlexibleBool)
pigeon_mail <- (map["pigeon_mail"], kFlexibleBool)
}
}
private let kFlexibleBool = 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()
return text == "1" || text == "true" || text == "yes"
default:
return false
}
}, toJSON: { value in
value
})
extension UIImageView {
@MainActor
func setGroupIcon(_ model: GroupInfoModel) {
@ -279,17 +329,58 @@ extension GroupInfoModel: IdentifiableType {
struct CommonGroupModel: Mappable, Equatable {
var group_key: String = ""
var group_name: String = ""
var icon: String = ""
var icon_index: Int = 1
var group_template: GroupTemplateModel?
var groupIconURL: String {
let templateIcon = group_template?.icon.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
if !templateIcon.isEmpty { return templateIcon }
return icon.trimmingCharacters(in: .whitespacesAndNewlines)
}
var groupIcon: UIImage {
UIImage(named: "GroupIcon/\(icon_index)")
?? UIImage(named: "GroupIcon/1")
?? UIImage()
}
init(groupKey: String, groupName: String) {
group_key = groupKey
group_name = groupName
}
init(info: GroupInfoModel) {
group_key = info.group_key
group_name = info.name
icon = info.groupIconURL
icon_index = info.icon_index
group_template = info.group_template
}
init?(map: Map) {}
mutating func mapping(map: Map) {
group_key <- map["group_key"]
group_name <- map["group_name"]
if group_name.isEmpty {
group_name <- map["name"]
}
icon <- map["icon"]
icon_index <- (map["icon_index"], kStrTransformInt)
group_template <- map["group_template"]
if icon.isEmpty {
icon = group_template?.icon ?? ""
}
}
mutating func apply(_ info: GroupInfoModel) {
if group_name.isEmpty || group_name == "未知圈子" {
group_name = info.name
}
if icon.isEmpty { icon = info.groupIconURL }
if icon_index <= 1 { icon_index = info.icon_index }
if group_template == nil { group_template = info.group_template }
}
static func deduplicated(_ groups: [CommonGroupModel]) -> [CommonGroupModel] {
@ -415,12 +506,14 @@ struct GroupMemberModel: Mappable, Equatable {
struct MemberExtra: Mappable, Equatable {
var relation_idx: String = ""
var privilege = GroupPrivilege()
init() {}
init?(map: Map) {}
mutating func mapping(map: Map) {
relation_idx <- (map["relation_idx"], kMemberRelationIndexTransform)
privilege <- map["privilege"]
}
static func decode(_ value: Any?) -> MemberExtra {

View File

@ -0,0 +1,100 @@
//
// MailboxModel.swift
// QuickLocation
//
import Foundation
import ObjectMapper
enum MailboxCategory: Int, CaseIterable {
case system = 1
case notice = 2
case interaction = 3
static let displayOrder: [MailboxCategory] = [.interaction, .notice, .system]
var title: String {
switch self {
case .interaction:
return "互动"
case .notice:
return "通知"
case .system:
return "系统"
}
}
var detailTitle: String { "\(title)消息" }
}
struct MailboxMessage: Mappable, Equatable {
var id = 0
var title = ""
var content = ""
var isRead = false
var type = MailboxCategory.system.rawValue
var senderId = ""
var senderIcon = ""
var modifyTime = ""
var category: MailboxCategory {
MailboxCategory(rawValue: type) ?? .system
}
init?(map: Map) {}
mutating func mapping(map: Map) {
id <- (map["id"], kStrTransformInt)
title <- (map["title"], kFlexString)
content <- (map["content"], kFlexString)
isRead <- map["is_read"]
type <- (map["type"], kStrTransformInt)
senderId <- (map["sender_id"], kFlexString)
senderIcon <- (map["sender_icon"], kFlexString)
modifyTime <- (map["mtime"], kFlexString)
}
}
struct MailboxListData: Mappable {
var page = 1
var size = 10
var total = 0
var messages: [MailboxMessage] = []
init?(map: Map) {}
mutating func mapping(map: Map) {
page <- (map["page"], kStrTransformInt)
size <- (map["size"], kStrTransformInt)
total <- (map["total"], kStrTransformInt)
messages <- map["datas"]
}
}
struct MailboxListResponse: BaseModelProtocol {
var code: String?
var message: String?
var data: MailboxListData?
init?(map: Map) {}
mutating func mapping(map: Map) {
code <- (map["code"], kIntTransformStr)
message <- map["message"]
data <- map["data"]
}
}
struct MailboxDetailResponse: BaseModelProtocol {
var code: String?
var message: String?
var data: MailboxMessage?
init?(map: Map) {}
mutating func mapping(map: Map) {
code <- (map["code"], kIntTransformStr)
message <- map["message"]
data <- map["data"]
}
}

View File

@ -26,7 +26,7 @@ final class AppRestrictCell: UITableViewCell {
private let systemIconHost = UIView()
private let linkIconView = UIImageView()
private let catalogIconView = UIImageView()
private let placeholderView = UIView()
private let placeholderView = UIButton(type: .custom)
private let placeholderLab = UILabel()
private let divider = UIView()
@ -87,6 +87,7 @@ final class AppRestrictCell: UITableViewCell {
.centerY()
.width(Metric.appIcon)
.height(Metric.appIcon)
placeholderView.addTarget(self, action: #selector(tapPair), for: .touchUpInside)
placeholderView.addSubview(placeholderLab)
placeholderLab.text = "?"
placeholderLab.font = .systemFont(ofSize: 18, weight: .bold)

View File

@ -110,6 +110,7 @@ final class GroupChatVC: BaseViewController {
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
viewModel.markAsRead()
VoicePlayerManager.shared.stop()
if isVoiceRecording {
finishVoiceRecording(action: .discard)
@ -156,6 +157,7 @@ final class GroupChatVC: BaseViewController {
guard self.rootView.voiceRecordView.isHidden else { return }
guard self.rootView.emojiPanelView.isHidden else { return }
let duration = (noti.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double) ?? 0.25
self.rootView.refreshComposer()
UIView.animate(withDuration: duration) {
self.rootView.bottomBar.layoutChain.bottom(kSafeBottomMargin)
self.rootView.layoutIfNeeded()
@ -652,6 +654,7 @@ final class GroupChatVC: BaseViewController {
guard !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return }
self.viewModel.input.sendMessage.onNext(text)
self.rootView.textField.text = ""
self.rootView.refreshComposer()
self.hideMentionPicker()
})
.disposed(by: disposeBag)
@ -1095,6 +1098,7 @@ extension GroupChatVC: UITextFieldDelegate, UITableViewDataSource {
guard !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return true }
viewModel.input.sendMessage.onNext(text)
textField.text = ""
rootView.refreshComposer()
hideMentionPicker()
return true
}

View File

@ -1750,9 +1750,29 @@ extension GroupChatView: UICollectionViewDelegate {
private enum VoiceBubbleLayout {
static let minWidth: CGFloat = 80
static let maxWidthCap: CGFloat = 220
static let maxWidthRatio: CGFloat = 0.55
static let maxDurationSeconds = 60
static func durationSeconds(from content: String) -> Int {
let raw = content.int
let seconds = raw >= 1000 ? raw / 1000 : raw
return min(Int(max(seconds, 1)), maxDurationSeconds)
}
static func bubbleWidth(durationSeconds: Int) -> CGFloat {
let maxW = min(UIScreen.main.bounds.width * maxWidthRatio, maxWidthCap)
let t = CGFloat(durationSeconds - 1) / CGFloat(maxDurationSeconds - 1)
return (minWidth + (maxW - minWidth) * t).rounded()
}
}
// MARK: -
final class VoiceSendMsgCell: UITableViewCell, VoicePlaybackView {
var menuAnchorView: UIView { bubbleView }
private var bubbleWidthConstraint: NSLayoutConstraint!
private let timeLabel: UILabel = {
let label = UILabel()
@ -1828,10 +1848,12 @@ final class VoiceSendMsgCell: UITableViewCell, VoicePlaybackView {
bubbleView.layoutChain
.topToBottomOfView(senderNameView, offset: 5)
.rightToView(senderNameView)
.width(105).height(39).bottom(10)
.height(39).bottom(10)
bubbleWidthConstraint = bubbleView.widthAnchor.constraint(equalToConstant: VoiceBubbleLayout.minWidth)
bubbleWidthConstraint.isActive = true
playAnimation.layoutChain
.right(36)
.right(10)
.centerY()
.width(20).height(20)
@ -1849,8 +1871,9 @@ final class VoiceSendMsgCell: UITableViewCell, VoicePlaybackView {
timeLabel.text = msg.showTime ? chatFormatTime(msg.timestamp) : nil
avatarView.setHeadPic(msg.headPic)
senderNameView.configure(name: msg.senderName, isOwner: msg.isCircleOwner, relationIdx: msg.relationIdx)
let dur = msg.content.int / 1000
durationLabel.text = dur > 0 ? "\(dur)''" : ""
let dur = VoiceBubbleLayout.durationSeconds(from: msg.content)
durationLabel.text = "\(dur)''"
bubbleWidthConstraint.constant = VoiceBubbleLayout.bubbleWidth(durationSeconds: dur)
voiceUrl = msg.voiceUrl
}
@ -1887,14 +1910,16 @@ final class VoiceSendMsgCell: UITableViewCell, VoicePlaybackView {
// MARK: -
final class VoiceReceivedMsgCell: UITableViewCell, VoicePlaybackView {
var menuAnchorView: UIView { bubbleView }
private var bubbleWidthConstraint: NSLayoutConstraint!
func configure(_ msg: ChatMessage) {
timeLabel.isHidden = !msg.showTime
timeLabel.text = msg.showTime ? chatFormatTime(msg.timestamp) : nil
avatarView.setHeadPic(msg.headPic)
senderNameView.configure(name: msg.senderName, isOwner: msg.isCircleOwner, relationIdx: msg.relationIdx)
let dur = msg.content.int / 1000
durationLabel.text = dur > 0 ? "\(dur)''" : ""
let dur = VoiceBubbleLayout.durationSeconds(from: msg.content)
durationLabel.text = "\(dur)''"
bubbleWidthConstraint.constant = VoiceBubbleLayout.bubbleWidth(durationSeconds: dur)
voiceUrl = msg.voiceUrl
}
@ -1992,12 +2017,13 @@ final class VoiceReceivedMsgCell: UITableViewCell, VoicePlaybackView {
bubbleView.layoutChain
.topToBottomOfView(senderNameView, offset: 5)
.leftToView(senderNameView)
.width(105)
.height(39)
.bottom(10)
bubbleWidthConstraint = bubbleView.widthAnchor.constraint(equalToConstant: VoiceBubbleLayout.minWidth)
bubbleWidthConstraint.isActive = true
playAnimation.layoutChain
.left(36)
.left(10)
.centerY()
.width(20).height(20)

View File

@ -692,7 +692,8 @@ final class GroupChatViewModel {
})
}
private func markAsRead() {
func markAsRead() {
guard !conversationID.isEmpty else { return }
OIMManager.manager.markConversationMessage(asRead: conversationID,
onSuccess: nil,
onFailure: nil)
@ -716,6 +717,9 @@ final class GroupChatViewModel {
// MARK: - Receive
func onReceiveMessage(_ msg: OIMMessageInfo) {
cacheMessage(msg)
if msg.sendID != AppContextManager.shared.userId {
markAsRead()
}
guard let item = toSectionItem(msg) else { return }
let ts = timestampFrom(item: item)
var items = messagesSubject.value

View File

@ -142,6 +142,12 @@ final class GroupViewController: BaseViewController {
})
.disposed(by: disposeBag)
rootView.messageBtn.rx.tap
.subscribe(onNext: {
AppRouter.push(Route.mailbox)
})
.disposed(by: disposeBag)
// Tab(0=joined) / (1=created)
rootView.joinedTabLabel.rx.tapGesture
.subscribe(onNext: { [weak self] _ in self?.switchToSegment(0) })

View File

@ -0,0 +1,392 @@
//
// MailboxDetailViewController.swift
// QuickLocation
//
import UIKit
import RxSwift
import Lottie
final class MailboxDetailViewController: UIViewController {
private var messages: [MailboxMessage]
private var currentIndex: Int
private var currentMessage: MailboxMessage
private let disposeBag = DisposeBag()
private let detailView = MailboxDetailView()
private var isLoading = false
var onMessageRead: ((MailboxMessage) -> Void)?
init(messages: [MailboxMessage], initialMessage: MailboxMessage) {
self.messages = messages
self.currentMessage = initialMessage
self.currentIndex = messages.firstIndex(where: { $0.id == initialMessage.id }) ?? 0
super.init(nibName: nil, bundle: nil)
modalPresentationStyle = .overFullScreen
modalTransitionStyle = .crossDissolve
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func loadView() {
view = detailView
}
override func viewDidLoad() {
super.viewDidLoad()
detailView.closeButton.addTarget(self, action: #selector(closeAction), for: .touchUpInside)
detailView.previousButton.addTarget(self, action: #selector(previousAction), for: .touchUpInside)
detailView.nextButton.addTarget(self, action: #selector(nextAction), for: .touchUpInside)
renderCurrentMessage()
}
private func renderCurrentMessage() {
detailView.configure(
message: currentMessage,
canGoPrevious: currentIndex > 0,
canGoNext: currentIndex + 1 < messages.count
)
}
private func loadMessage(at index: Int) {
guard messages.indices.contains(index), !isLoading else { return }
let cachedMessage = messages[index]
isLoading = true
detailView.setControlsEnabled(false)
DLToast.showLoading()
UserService.mailboxDetail(id: cachedMessage.id)
.subscribe(onNext: { [weak self] response in
guard let self else { return }
DLToast.dismiss()
self.isLoading = false
guard let responseMessage = response.data else {
self.detailView.setControlsEnabled(true)
DLToast.showError(text: response.message ?? "消息详情加载失败")
return
}
var detail = self.mergeDetail(responseMessage, fallback: cachedMessage)
detail.isRead = true
self.messages[index] = detail
self.currentIndex = index
self.currentMessage = detail
self.onMessageRead?(detail)
self.renderCurrentMessage()
}, onError: { [weak self] error in
DLToast.dismiss()
self?.isLoading = false
self?.detailView.setControlsEnabled(true)
DLToast.showError(text: error.localizedDescription)
})
.disposed(by: disposeBag)
}
private func mergeDetail(_ detail: MailboxMessage, fallback: MailboxMessage) -> MailboxMessage {
var merged = detail
if merged.id == 0 { merged.id = fallback.id }
if merged.title.isEmpty { merged.title = fallback.title }
if merged.content.isEmpty { merged.content = fallback.content }
if merged.senderId.isEmpty { merged.senderId = fallback.senderId }
if merged.senderIcon.isEmpty { merged.senderIcon = fallback.senderIcon }
if merged.modifyTime.isEmpty { merged.modifyTime = fallback.modifyTime }
if MailboxCategory(rawValue: merged.type) == nil { merged.type = fallback.type }
return merged
}
@objc private func closeAction() {
dismiss(animated: true)
}
@objc private func previousAction() {
loadMessage(at: currentIndex - 1)
}
@objc private func nextAction() {
loadMessage(at: currentIndex + 1)
}
}
private final class MailboxDetailView: UIView {
let previousButton = UIButton(type: .custom)
let closeButton = UIButton(type: .custom)
let nextButton = UIButton(type: .custom)
private let cardView = UIView()
private let cardBackgroundView = UIImageView()
private let birdImageView = UIImageView(image: UIImage(named: "Mailbox/detail_bird"))
private let typeLabel = UILabel()
private let dividerView = UIView()
private let titleLabel = UILabel()
private let scrollView = UIScrollView()
private let contentContainerView = UIView()
private let contentLabel = UILabel()
private let lottieView = LottieAnimationView()
private let quickMessageLabel = UILabel()
private var animationName: String?
private var contentBottomConstraint: NSLayoutConstraint?
private var cardHeightConstraint: NSLayoutConstraint?
override init(frame: CGRect) {
super.init(frame: frame)
setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configure(message: MailboxMessage, canGoPrevious: Bool, canGoNext: Bool) {
typeLabel.text = message.category.detailTitle
titleLabel.text = message.title
configureContent(message: message)
previousButton.isEnabled = canGoPrevious
nextButton.isEnabled = canGoNext
updateButtonAppearance()
}
func setControlsEnabled(_ enabled: Bool) {
previousButton.isUserInteractionEnabled = enabled
nextButton.isUserInteractionEnabled = enabled
closeButton.isUserInteractionEnabled = enabled
previousButton.alpha = enabled ? (previousButton.isEnabled ? 1 : 0.35) : 0.35
nextButton.alpha = enabled ? (nextButton.isEnabled ? 1 : 0.35) : 0.35
closeButton.alpha = enabled ? 1 : 0.55
}
private func setupUI() {
backgroundColor = UIColor.black.withAlphaComponent(0.68)
cardView.layer.cornerRadius = 31
cardView.clipsToBounds = true
cardBackgroundView.image = UIImage(named: "Mailbox/detail_card")?.resizableImage(
withCapInsets: UIEdgeInsets(top: 70, left: 70, bottom: 70, right: 70),
resizingMode: .stretch
)
cardBackgroundView.contentMode = .scaleToFill
birdImageView.contentMode = .scaleAspectFit
typeLabel.font = .systemFont(ofSize: 18, weight: .medium)
typeLabel.textColor = UIColor(hexStr: "#14324E")
typeLabel.textAlignment = .center
dividerView.backgroundColor = UIColor(hexStr: "#80D8FF", alpha: 0.55)
titleLabel.font = .systemFont(ofSize: 18, weight: .medium)
titleLabel.textColor = UIColor(hexStr: "#333333")
titleLabel.textAlignment = .center
titleLabel.numberOfLines = 0
scrollView.showsVerticalScrollIndicator = false
scrollView.alwaysBounceVertical = false
contentLabel.font = .systemFont(ofSize: 15)
contentLabel.textColor = UIColor(hexStr: "#858585")
contentLabel.textAlignment = .center
contentLabel.numberOfLines = 0
lottieView.contentMode = .scaleAspectFit
lottieView.loopMode = .loop
lottieView.backgroundBehavior = .pauseAndRestore
quickMessageLabel.font = .systemFont(ofSize: 15)
quickMessageLabel.textColor = UIColor(hexStr: "#52606D")
quickMessageLabel.textAlignment = .center
quickMessageLabel.numberOfLines = 0
quickMessageLabel.backgroundColor = UIColor(hexStr: "#DFF5FF")
quickMessageLabel.layer.cornerRadius = 9
quickMessageLabel.clipsToBounds = true
configureControlButton(previousButton, title: "上一条")
configureControlButton(nextButton, title: "下一条")
closeButton.setImage(UIImage(named: "Mailbox/detail_close"), for: .normal)
closeButton.accessibilityLabel = "关闭"
addSubview(cardView)
cardView.addSubview(cardBackgroundView)
addSubview(birdImageView)
cardView.addSubview(typeLabel)
cardView.addSubview(dividerView)
cardView.addSubview(titleLabel)
cardView.addSubview(scrollView)
scrollView.addSubview(contentContainerView)
contentContainerView.addSubview(contentLabel)
contentContainerView.addSubview(lottieView)
contentContainerView.addSubview(quickMessageLabel)
addSubview(previousButton)
addSubview(closeButton)
addSubview(nextButton)
[cardView, cardBackgroundView, birdImageView, typeLabel, dividerView, titleLabel, scrollView,
contentContainerView, contentLabel, lottieView, quickMessageLabel, previousButton, closeButton,
nextButton].forEach { $0.translatesAutoresizingMaskIntoConstraints = false }
let cardWidth = min(kScreenWidth - 48, 360)
let cardHeightConstraint = cardView.heightAnchor.constraint(equalToConstant: 245)
self.cardHeightConstraint = cardHeightConstraint
NSLayoutConstraint.activate([
cardView.centerXAnchor.constraint(equalTo: centerXAnchor),
cardView.centerYAnchor.constraint(equalTo: centerYAnchor, constant: -32),
cardView.widthAnchor.constraint(equalToConstant: cardWidth),
cardHeightConstraint,
cardBackgroundView.topAnchor.constraint(equalTo: cardView.topAnchor),
cardBackgroundView.leadingAnchor.constraint(equalTo: cardView.leadingAnchor),
cardBackgroundView.trailingAnchor.constraint(equalTo: cardView.trailingAnchor),
cardBackgroundView.bottomAnchor.constraint(equalTo: cardView.bottomAnchor),
birdImageView.leadingAnchor.constraint(equalTo: cardView.leadingAnchor, constant: 36),
birdImageView.bottomAnchor.constraint(equalTo: cardView.topAnchor, constant: 35),
birdImageView.widthAnchor.constraint(equalToConstant: 100),
birdImageView.heightAnchor.constraint(equalToConstant: 74),
typeLabel.topAnchor.constraint(equalTo: cardView.topAnchor, constant: 49),
typeLabel.leadingAnchor.constraint(equalTo: cardView.leadingAnchor, constant: 26),
typeLabel.trailingAnchor.constraint(equalTo: cardView.trailingAnchor, constant: -26),
dividerView.topAnchor.constraint(equalTo: typeLabel.bottomAnchor, constant: 15),
dividerView.leadingAnchor.constraint(equalTo: cardView.leadingAnchor, constant: 23),
dividerView.trailingAnchor.constraint(equalTo: cardView.trailingAnchor, constant: -23),
dividerView.heightAnchor.constraint(equalToConstant: 0.5),
titleLabel.topAnchor.constraint(equalTo: dividerView.bottomAnchor, constant: 20),
titleLabel.leadingAnchor.constraint(equalTo: cardView.leadingAnchor, constant: 30),
titleLabel.trailingAnchor.constraint(equalTo: cardView.trailingAnchor, constant: -30),
scrollView.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 13),
scrollView.leadingAnchor.constraint(equalTo: cardView.leadingAnchor, constant: 28),
scrollView.trailingAnchor.constraint(equalTo: cardView.trailingAnchor, constant: -28),
scrollView.bottomAnchor.constraint(equalTo: cardView.bottomAnchor, constant: -25),
contentContainerView.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor),
contentContainerView.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor),
contentContainerView.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor),
contentContainerView.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor),
contentContainerView.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor),
contentLabel.topAnchor.constraint(equalTo: contentContainerView.topAnchor),
contentLabel.leadingAnchor.constraint(equalTo: contentContainerView.leadingAnchor),
contentLabel.trailingAnchor.constraint(equalTo: contentContainerView.trailingAnchor),
lottieView.topAnchor.constraint(equalTo: contentLabel.bottomAnchor, constant: 10),
lottieView.centerXAnchor.constraint(equalTo: contentContainerView.centerXAnchor),
lottieView.widthAnchor.constraint(equalToConstant: 116),
lottieView.heightAnchor.constraint(equalToConstant: 116),
quickMessageLabel.topAnchor.constraint(equalTo: contentLabel.bottomAnchor, constant: 14),
quickMessageLabel.centerXAnchor.constraint(equalTo: contentContainerView.centerXAnchor),
quickMessageLabel.leadingAnchor.constraint(greaterThanOrEqualTo: contentContainerView.leadingAnchor, constant: 20),
quickMessageLabel.trailingAnchor.constraint(lessThanOrEqualTo: contentContainerView.trailingAnchor, constant: -20),
quickMessageLabel.heightAnchor.constraint(greaterThanOrEqualToConstant: 40),
previousButton.topAnchor.constraint(equalTo: cardView.bottomAnchor, constant: 20),
previousButton.leadingAnchor.constraint(equalTo: cardView.leadingAnchor, constant: 34),
previousButton.heightAnchor.constraint(equalToConstant: 40),
nextButton.topAnchor.constraint(equalTo: previousButton.topAnchor),
nextButton.trailingAnchor.constraint(equalTo: cardView.trailingAnchor, constant: -34),
nextButton.heightAnchor.constraint(equalTo: previousButton.heightAnchor),
closeButton.centerXAnchor.constraint(equalTo: centerXAnchor),
closeButton.centerYAnchor.constraint(equalTo: previousButton.centerYAnchor),
closeButton.widthAnchor.constraint(equalToConstant: 24),
closeButton.heightAnchor.constraint(equalToConstant: 24)
])
}
private func configureControlButton(_ button: UIButton, title: String) {
button.setTitle(title, for: .normal)
button.setTitleColor(.white, for: .normal)
button.setTitleColor(UIColor.white.withAlphaComponent(0.45), for: .disabled)
button.titleLabel?.font = .systemFont(ofSize: 16)
}
private func configureContent(message: MailboxMessage) {
animationName = nil
lottieView.stop()
lottieView.animation = nil
lottieView.isHidden = true
quickMessageLabel.isHidden = true
contentLabel.text = message.content
cardHeightConstraint?.constant = message.content.count > 80 ? 300 : 245
guard message.category == .interaction,
let payload = parsePayload(message.content) else {
updateContentBottomAnchor(visibleView: contentLabel)
return
}
contentLabel.text = payload.prefix
switch payload.kind {
case let .animation(name):
cardHeightConstraint?.constant = 330
animationName = name
lottieView.isHidden = false
updateContentBottomAnchor(visibleView: lottieView)
EmojiPanelCell.loadAnimation(for: name) { [weak self] animation in
guard let self, self.animationName == name else { return }
guard let animation else {
self.showFallbackContent(message.content)
return
}
self.lottieView.animation = animation
self.lottieView.currentProgress = 0
self.lottieView.play()
}
case let .quickMessage(text):
cardHeightConstraint?.constant = 265
quickMessageLabel.text = " \(text) "
quickMessageLabel.isHidden = false
updateContentBottomAnchor(visibleView: quickMessageLabel)
}
}
private func updateContentBottomAnchor(visibleView: UIView) {
contentBottomConstraint?.isActive = false
contentBottomConstraint = visibleView.bottomAnchor.constraint(equalTo: contentContainerView.bottomAnchor)
contentBottomConstraint?.isActive = true
}
private func showFallbackContent(_ content: String) {
animationName = nil
lottieView.stop()
lottieView.animation = nil
lottieView.isHidden = true
quickMessageLabel.isHidden = true
contentLabel.text = content
updateContentBottomAnchor(visibleView: contentLabel)
}
private func parsePayload(_ content: String) -> MailboxPayload? {
guard let opening = content.firstIndex(of: "["),
let closing = content[opening...].firstIndex(of: "]"),
let lastOpening = content.lastIndex(of: "[") else {
return nil
}
let codeStart = content.index(after: opening)
let code = String(content[codeStart..<closing])
guard code.count > 1, let prefixCode = code.first else { return nil }
let value = String(code.dropFirst())
let prefix = String(content[..<lastOpening]).trimmingCharacters(in: .whitespacesAndNewlines)
switch prefixCode {
case "1":
guard Int(value) != nil else { return nil }
return MailboxPayload(prefix: prefix, kind: .animation(name: "normal_\(value)"))
case "2":
guard Int(value) != nil else { return nil }
return MailboxPayload(prefix: prefix, kind: .animation(name: "fun_\(value)"))
case "3":
guard let index = Int(value), QuickMessageView.messageList.indices.contains(index) else { return nil }
return MailboxPayload(prefix: prefix, kind: .quickMessage(QuickMessageView.messageList[index]))
default:
return nil
}
}
private func updateButtonAppearance() {
previousButton.alpha = previousButton.isEnabled ? 1 : 0.35
nextButton.alpha = nextButton.isEnabled ? 1 : 0.35
closeButton.alpha = 1
setControlsEnabled(true)
}
}
private struct MailboxPayload {
enum Kind {
case animation(name: String)
case quickMessage(String)
}
let prefix: String
let kind: Kind
}

View File

@ -0,0 +1,494 @@
//
// MailboxView.swift
// QuickLocation
//
import UIKit
import MJRefresh
extension MailboxCategory {
var tintColor: UIColor {
switch self {
case .interaction:
return UIColor(hexStr: "#3ED989")
case .notice:
return UIColor(hexStr: "#C274F7")
case .system:
return UIColor(hexStr: "#55B8F7")
}
}
var backgroundColor: UIColor {
switch self {
case .interaction:
return UIColor(hexStr: "#E7FFF2")
case .notice:
return UIColor(hexStr: "#F9E9FF")
case .system:
return UIColor(hexStr: "#E8F8FF")
}
}
var iconAssetName: String {
switch self {
case .interaction:
return "Mailbox/interaction"
case .notice:
return "Mailbox/notice"
case .system:
return "Mailbox/system"
}
}
var listIconAssetName: String {
switch self {
case .system:
return "Mailbox/system_message"
default:
return iconAssetName
}
}
}
final class MailboxView: UIView {
let backButton = UIButton(type: .custom)
let clearButton = UIButton(type: .custom)
let tableView = UITableView(frame: .zero, style: .plain)
private let headerGradientLayer = CAGradientLayer()
private let titleLabel = UILabel()
private let categoryStackView = UIStackView()
private let contentTopSeparator = UIView()
private let emptyView = UIView()
private let emptyImageView = UIImageView(image: UIImage(named: "empty_data"))
private let emptyLabel = UILabel()
private let loadingIndicator = UIActivityIndicatorView(style: .medium)
private var categoryViews: [MailboxCategory: MailboxCategoryView] = [:]
var onSelectCategory: ((MailboxCategory) -> Void)?
override init(frame: CGRect) {
super.init(frame: frame)
setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
headerGradientLayer.frame = CGRect(x: 0, y: 0, width: bounds.width, height: kNaviHeight + 132)
}
func selectCategory(_ category: MailboxCategory) {
categoryViews.forEach { itemCategory, categoryView in
categoryView.setSelected(itemCategory == category)
}
}
func updateUnreadCounts(_ counts: [MailboxCategory: Int]) {
categoryViews.forEach { category, categoryView in
categoryView.setUnreadCount(counts[category] ?? 0)
}
}
func updateState(hasMessages: Bool, isLoading: Bool, reachedEnd: Bool) {
loadingIndicator.isHidden = !isLoading
if isLoading {
loadingIndicator.startAnimating()
} else {
loadingIndicator.stopAnimating()
}
emptyImageView.isHidden = isLoading || hasMessages || !reachedEnd
emptyLabel.isHidden = emptyImageView.isHidden
tableView.mj_footer?.isHidden = !hasMessages || reachedEnd
if reachedEnd {
tableView.mj_footer?.endRefreshingWithNoMoreData()
} else {
tableView.mj_footer?.resetNoMoreData()
tableView.mj_footer?.endRefreshing()
}
}
private func setupUI() {
backgroundColor = .white
headerGradientLayer.colors = [
UIColor(hexStr: "#DFF9FF").cgColor,
UIColor(hexStr: "#F3FFE9").cgColor,
UIColor.white.cgColor
]
headerGradientLayer.locations = [0, 0.58, 1]
headerGradientLayer.startPoint = CGPoint(x: 0, y: 0)
headerGradientLayer.endPoint = CGPoint(x: 1, y: 1)
layer.insertSublayer(headerGradientLayer, at: 0)
setupNavigationBar()
setupCategories()
setupTableView()
setupEmptyView()
}
private func setupNavigationBar() {
titleLabel.text = "消息"
titleLabel.textColor = UIColor(hexStr: "#18304B")
titleLabel.font = .systemFont(ofSize: 18, weight: .medium)
titleLabel.textAlignment = .center
configureNavigationButton(backButton, symbolName: "chevron.left")
configureNavigationButton(clearButton, symbolName: "trash.fill")
backButton.accessibilityLabel = "返回"
clearButton.accessibilityLabel = "清空全部消息"
addSubview(titleLabel)
addSubview(backButton)
addSubview(clearButton)
titleLabel.translatesAutoresizingMaskIntoConstraints = false
backButton.translatesAutoresizingMaskIntoConstraints = false
clearButton.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
backButton.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 18),
backButton.topAnchor.constraint(equalTo: topAnchor, constant: kStatusBarHeight + 8),
backButton.widthAnchor.constraint(equalToConstant: 36),
backButton.heightAnchor.constraint(equalToConstant: 36),
clearButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -18),
clearButton.centerYAnchor.constraint(equalTo: backButton.centerYAnchor),
clearButton.widthAnchor.constraint(equalToConstant: 36),
clearButton.heightAnchor.constraint(equalToConstant: 36),
titleLabel.centerXAnchor.constraint(equalTo: centerXAnchor),
titleLabel.centerYAnchor.constraint(equalTo: backButton.centerYAnchor)
])
}
private func configureNavigationButton(_ button: UIButton, symbolName: String) {
let configuration = UIImage.SymbolConfiguration(pointSize: 18, weight: .bold)
button.setImage(UIImage(systemName: symbolName, withConfiguration: configuration), for: .normal)
button.tintColor = UIColor(hexStr: "#213148")
button.backgroundColor = UIColor.white.withAlphaComponent(0.88)
button.layer.cornerRadius = 10
button.layer.shadowColor = UIColor.black.withAlphaComponent(0.08).cgColor
button.layer.shadowOpacity = 1
button.layer.shadowRadius = 8
button.layer.shadowOffset = CGSize(width: 0, height: 3)
}
private func setupCategories() {
categoryStackView.axis = .horizontal
categoryStackView.distribution = .fillEqually
categoryStackView.alignment = .fill
MailboxCategory.displayOrder.forEach { category in
let categoryView = MailboxCategoryView(category: category)
categoryView.addTarget(self, action: #selector(categoryTapped(_:)), for: .touchUpInside)
categoryStackView.addArrangedSubview(categoryView)
categoryViews[category] = categoryView
}
contentTopSeparator.backgroundColor = UIColor(hexStr: "#E8E8E8")
addSubview(categoryStackView)
addSubview(contentTopSeparator)
categoryStackView.translatesAutoresizingMaskIntoConstraints = false
contentTopSeparator.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
categoryStackView.topAnchor.constraint(equalTo: topAnchor, constant: kNaviHeight + 14),
categoryStackView.leadingAnchor.constraint(equalTo: leadingAnchor),
categoryStackView.trailingAnchor.constraint(equalTo: trailingAnchor),
categoryStackView.heightAnchor.constraint(equalToConstant: 112),
contentTopSeparator.topAnchor.constraint(equalTo: categoryStackView.bottomAnchor),
contentTopSeparator.leadingAnchor.constraint(equalTo: leadingAnchor),
contentTopSeparator.trailingAnchor.constraint(equalTo: trailingAnchor),
contentTopSeparator.heightAnchor.constraint(equalToConstant: 0.5)
])
}
private func setupTableView() {
tableView.backgroundColor = .white
tableView.separatorStyle = .none
tableView.rowHeight = 66
tableView.showsVerticalScrollIndicator = false
tableView.tableFooterView = UIView()
tableView.contentInset = UIEdgeInsets(top: 8, left: 0, bottom: kSafeBottomMargin + 18, right: 0)
tableView.register(MailboxMessageCell.self, forCellReuseIdentifier: MailboxMessageCell.reuseIdentifier)
addSubview(tableView)
tableView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: contentTopSeparator.bottomAnchor),
tableView.leadingAnchor.constraint(equalTo: leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: bottomAnchor)
])
}
private func setupEmptyView() {
emptyView.isUserInteractionEnabled = false
emptyImageView.contentMode = .scaleAspectFit
emptyLabel.text = "这里怎么什么都没有~"
emptyLabel.textColor = UIColor(hexStr: "#B1B1B1")
emptyLabel.font = .systemFont(ofSize: 14)
emptyLabel.textAlignment = .center
loadingIndicator.color = UIColor(hexStr: "#16B3FF")
addSubview(emptyView)
emptyView.addSubview(emptyImageView)
emptyView.addSubview(emptyLabel)
emptyView.addSubview(loadingIndicator)
emptyView.translatesAutoresizingMaskIntoConstraints = false
emptyImageView.translatesAutoresizingMaskIntoConstraints = false
emptyLabel.translatesAutoresizingMaskIntoConstraints = false
loadingIndicator.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
emptyView.topAnchor.constraint(equalTo: contentTopSeparator.bottomAnchor),
emptyView.leadingAnchor.constraint(equalTo: leadingAnchor),
emptyView.trailingAnchor.constraint(equalTo: trailingAnchor),
emptyView.bottomAnchor.constraint(equalTo: bottomAnchor),
emptyImageView.centerXAnchor.constraint(equalTo: emptyView.centerXAnchor),
emptyImageView.centerYAnchor.constraint(equalTo: emptyView.centerYAnchor, constant: -56),
emptyImageView.widthAnchor.constraint(equalToConstant: 140),
emptyImageView.heightAnchor.constraint(equalToConstant: 120),
emptyLabel.topAnchor.constraint(equalTo: emptyImageView.bottomAnchor, constant: 8),
emptyLabel.centerXAnchor.constraint(equalTo: emptyView.centerXAnchor),
loadingIndicator.centerXAnchor.constraint(equalTo: emptyView.centerXAnchor),
loadingIndicator.centerYAnchor.constraint(equalTo: emptyView.centerYAnchor, constant: -40)
])
}
@objc private func categoryTapped(_ sender: MailboxCategoryView) {
onSelectCategory?(sender.category)
}
}
final class MailboxCategoryView: UIControl {
let category: MailboxCategory
private let iconBackgroundView = UIView()
private let iconImageView = UIImageView()
private let titleLabel = UILabel()
private let underlineView = UIView()
private let badgeLabel = UILabel()
init(category: MailboxCategory) {
self.category = category
super.init(frame: .zero)
setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setSelected(_ selected: Bool) {
titleLabel.textColor = UIColor(hexStr: "#26364A")
underlineView.isHidden = !selected
}
func setUnreadCount(_ count: Int) {
badgeLabel.isHidden = count <= 0
badgeLabel.text = count > 99 ? "99+" : "\(count)"
}
private func setupUI() {
accessibilityLabel = category.title
iconBackgroundView.isUserInteractionEnabled = false
iconBackgroundView.backgroundColor = .clear
iconImageView.image = UIImage(named: category.iconAssetName)
iconImageView.contentMode = .scaleAspectFit
titleLabel.text = category.title
titleLabel.textAlignment = .center
titleLabel.textColor = UIColor(hexStr: "#26364A")
titleLabel.font = .systemFont(ofSize: 15)
underlineView.backgroundColor = UIColor(hexStr: "#13B6FB")
underlineView.layer.cornerRadius = 1.5
underlineView.isHidden = true
badgeLabel.backgroundColor = UIColor(hexStr: "#FF5260")
badgeLabel.textColor = .white
badgeLabel.font = .systemFont(ofSize: 10, weight: .medium)
badgeLabel.textAlignment = .center
badgeLabel.layer.cornerRadius = 9
badgeLabel.clipsToBounds = true
badgeLabel.isHidden = true
addSubview(iconBackgroundView)
iconBackgroundView.addSubview(iconImageView)
addSubview(titleLabel)
addSubview(underlineView)
addSubview(badgeLabel)
[iconBackgroundView, iconImageView, titleLabel, underlineView, badgeLabel].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
}
NSLayoutConstraint.activate([
iconBackgroundView.topAnchor.constraint(equalTo: topAnchor, constant: 12),
iconBackgroundView.centerXAnchor.constraint(equalTo: centerXAnchor),
iconBackgroundView.widthAnchor.constraint(equalToConstant: 50),
iconBackgroundView.heightAnchor.constraint(equalToConstant: 50),
iconImageView.centerXAnchor.constraint(equalTo: iconBackgroundView.centerXAnchor),
iconImageView.centerYAnchor.constraint(equalTo: iconBackgroundView.centerYAnchor),
iconImageView.widthAnchor.constraint(equalToConstant: 50),
iconImageView.heightAnchor.constraint(equalToConstant: 50),
titleLabel.topAnchor.constraint(equalTo: iconBackgroundView.bottomAnchor, constant: 7),
titleLabel.centerXAnchor.constraint(equalTo: centerXAnchor),
underlineView.bottomAnchor.constraint(equalTo: bottomAnchor),
underlineView.centerXAnchor.constraint(equalTo: centerXAnchor),
underlineView.widthAnchor.constraint(equalToConstant: 18),
underlineView.heightAnchor.constraint(equalToConstant: 3),
badgeLabel.centerXAnchor.constraint(equalTo: iconBackgroundView.trailingAnchor, constant: -2),
badgeLabel.centerYAnchor.constraint(equalTo: iconBackgroundView.topAnchor, constant: 2),
badgeLabel.heightAnchor.constraint(equalToConstant: 18),
badgeLabel.widthAnchor.constraint(greaterThanOrEqualToConstant: 18)
])
}
}
final class MailboxMessageCell: UITableViewCell {
static let reuseIdentifier = "MailboxMessageCell"
private let avatarView = UIImageView()
private let defaultIconView = MailboxTypeIconView()
private let unreadDotView = UIView()
private let titleLabel = UILabel()
private let contentLabel = UILabel()
private let timeLabel = UILabel()
private let separatorView = UIView()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareForReuse() {
super.prepareForReuse()
avatarView.setHeadPic(nil)
}
func configure(message: MailboxMessage) {
titleLabel.text = message.title
contentLabel.text = Self.previewText(for: message)
timeLabel.text = Self.timeText(from: message.modifyTime)
unreadDotView.isHidden = message.isRead
let senderIcon = message.senderIcon.trimmingCharacters(in: .whitespacesAndNewlines)
let shouldShowAvatar = message.category != .system && !senderIcon.isEmpty && senderIcon != "0"
avatarView.isHidden = !shouldShowAvatar
defaultIconView.isHidden = shouldShowAvatar
if shouldShowAvatar {
avatarView.setHeadPic(senderIcon)
} else {
defaultIconView.configure(category: message.category)
}
}
private func setupUI() {
selectionStyle = .none
backgroundColor = .white
avatarView.contentMode = .scaleAspectFill
avatarView.layer.cornerRadius = 11
avatarView.clipsToBounds = true
unreadDotView.backgroundColor = UIColor(hexStr: "#FF5260")
unreadDotView.layer.cornerRadius = 4
titleLabel.font = .systemFont(ofSize: 15, weight: .medium)
titleLabel.textColor = UIColor(hexStr: "#26364A")
titleLabel.numberOfLines = 1
contentLabel.font = .systemFont(ofSize: 13)
contentLabel.textColor = UIColor(hexStr: "#AAAAAA")
contentLabel.numberOfLines = 1
timeLabel.font = .systemFont(ofSize: 12)
timeLabel.textColor = UIColor(hexStr: "#BBBBBB")
timeLabel.textAlignment = .right
separatorView.backgroundColor = UIColor(hexStr: "#EEEEEE")
contentView.addSubview(avatarView)
contentView.addSubview(defaultIconView)
contentView.addSubview(unreadDotView)
contentView.addSubview(titleLabel)
contentView.addSubview(contentLabel)
contentView.addSubview(timeLabel)
contentView.addSubview(separatorView)
[avatarView, defaultIconView, unreadDotView, titleLabel, contentLabel, timeLabel, separatorView].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
}
NSLayoutConstraint.activate([
avatarView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 26),
avatarView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
avatarView.widthAnchor.constraint(equalToConstant: 42),
avatarView.heightAnchor.constraint(equalToConstant: 42),
defaultIconView.leadingAnchor.constraint(equalTo: avatarView.leadingAnchor),
defaultIconView.centerYAnchor.constraint(equalTo: avatarView.centerYAnchor),
defaultIconView.widthAnchor.constraint(equalTo: avatarView.widthAnchor),
defaultIconView.heightAnchor.constraint(equalTo: avatarView.heightAnchor),
unreadDotView.centerXAnchor.constraint(equalTo: avatarView.trailingAnchor, constant: -2),
unreadDotView.centerYAnchor.constraint(equalTo: avatarView.topAnchor, constant: 2),
unreadDotView.widthAnchor.constraint(equalToConstant: 8),
unreadDotView.heightAnchor.constraint(equalToConstant: 8),
timeLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
timeLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 15),
timeLabel.widthAnchor.constraint(equalToConstant: 46),
titleLabel.leadingAnchor.constraint(equalTo: avatarView.trailingAnchor, constant: 12),
titleLabel.trailingAnchor.constraint(lessThanOrEqualTo: timeLabel.leadingAnchor, constant: -8),
titleLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 11),
contentLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
contentLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
contentLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 5),
separatorView.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
separatorView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
separatorView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
separatorView.heightAnchor.constraint(equalToConstant: 0.5)
])
}
private static func previewText(for message: MailboxMessage) -> String {
guard message.category == .interaction,
message.content.contains("]"),
let bracketIndex = message.content.lastIndex(of: "[") else {
return message.content
}
return String(message.content[..<bracketIndex]).trimmingCharacters(in: .whitespacesAndNewlines)
}
private static func timeText(from value: String) -> String {
guard value.count >= 16 else { return value }
let startIndex = value.index(value.startIndex, offsetBy: 11)
let endIndex = value.index(startIndex, offsetBy: 5, limitedBy: value.endIndex) ?? value.endIndex
return String(value[startIndex..<endIndex])
}
}
final class MailboxTypeIconView: UIView {
private let imageView = UIImageView()
override init(frame: CGRect) {
super.init(frame: frame)
layer.cornerRadius = 11
clipsToBounds = true
imageView.contentMode = .center
addSubview(imageView)
imageView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
imageView.centerXAnchor.constraint(equalTo: centerXAnchor),
imageView.centerYAnchor.constraint(equalTo: centerYAnchor),
imageView.widthAnchor.constraint(equalTo: widthAnchor),
imageView.heightAnchor.constraint(equalTo: heightAnchor)
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configure(category: MailboxCategory) {
backgroundColor = .clear
imageView.image = UIImage(named: category.listIconAssetName)
}
}

View File

@ -0,0 +1,372 @@
//
// MailboxViewController.swift
// QuickLocation
//
import UIKit
import RxSwift
import MJRefresh
final class MailboxViewController: BaseViewController {
override var isNavigationBarHidden: Bool { true }
private enum LoadKind {
case initial
case refresh
case more
case category
}
private var rootView: MailboxView!
private var messages: [MailboxMessage] = []
private var selectedCategory: MailboxCategory = .interaction
private var currentPage = 0
private var total = 0
private var reachedEnd = false
private var isLoading = false
private var pendingCategory: MailboxCategory?
override func loadView() {
rootView = MailboxView(frame: UIScreen.main.bounds)
view = rootView
}
override func viewDidLoad() {
super.viewDidLoad()
setupTableView()
bindActions()
rootView.selectCategory(selectedCategory)
updateContentState(showLoading: true)
loadPage(
1,
kind: .initial,
replacing: true,
category: selectedCategory,
baselineCount: 0
)
}
private var visibleMessages: [MailboxMessage] {
messages.filter { $0.category == selectedCategory }
}
private func setupTableView() {
rootView.tableView.dataSource = self
rootView.tableView.delegate = self
let header = MJRefreshNormalHeader { [weak self] in
self?.refreshMessages()
}
header.addFeedbackGenerator()
header.stateLabel?.isHidden = true
rootView.tableView.mj_header = header
let footer = MJRefreshAutoNormalFooter { [weak self] in
self?.loadMoreMessages()
}
footer.stateLabel?.isHidden = true
footer.isHidden = true
rootView.tableView.mj_footer = footer
}
private func bindActions() {
rootView.backButton.addTarget(self, action: #selector(backAction), for: .touchUpInside)
rootView.clearButton.addTarget(self, action: #selector(clearAction), for: .touchUpInside)
rootView.onSelectCategory = { [weak self] category in
self?.selectCategory(category)
}
}
private func selectCategory(_ category: MailboxCategory) {
guard selectedCategory != category else { return }
selectedCategory = category
rootView.selectCategory(category)
rootView.tableView.reloadData()
rootView.tableView.setContentOffset(
CGPoint(x: 0, y: -rootView.tableView.adjustedContentInset.top),
animated: false
)
updateContentState(showLoading: isLoading)
ensureMessagesLoaded(for: category)
}
private func refreshMessages() {
guard !isLoading else {
rootView.tableView.mj_header?.endRefreshing()
return
}
loadPage(
1,
kind: .refresh,
replacing: true,
category: selectedCategory,
baselineCount: 0
)
}
private func loadMoreMessages() {
guard !isLoading, !reachedEnd else {
finishRefreshControls()
return
}
let category = selectedCategory
loadPage(
currentPage + 1,
kind: .more,
replacing: false,
category: category,
baselineCount: messages(for: category).count
)
}
private func ensureMessagesLoaded(for category: MailboxCategory) {
guard messages(for: category).isEmpty, !reachedEnd else {
updateContentState(showLoading: false)
return
}
guard !isLoading else {
pendingCategory = category
return
}
loadPage(
currentPage + 1,
kind: .category,
replacing: false,
category: category,
baselineCount: 0
)
}
private func loadPage(
_ page: Int,
kind: LoadKind,
replacing: Bool,
category: MailboxCategory,
baselineCount: Int
) {
guard !isLoading else { return }
isLoading = true
pendingCategory = nil
updateContentState(showLoading: visibleMessages.isEmpty)
UserService.mailboxList(page: page)
.subscribe(onNext: { [weak self] response in
guard let self else { return }
guard let data = response.data else {
self.finishLoadingWithError(message: response.message ?? "消息加载失败")
return
}
if replacing {
self.messages = data.messages
} else {
self.mergeMessages(data.messages)
}
self.currentPage = max(data.page, page)
self.total = max(data.total, self.messages.count)
self.reachedEnd = self.calculateReachedEnd(data: data)
self.rootView.tableView.reloadData()
self.updateUnreadCounts()
let categoryCount = self.messages(for: category).count
if self.selectedCategory == category,
categoryCount <= baselineCount,
!self.reachedEnd {
self.isLoading = false
self.loadPage(
self.currentPage + 1,
kind: kind,
replacing: false,
category: category,
baselineCount: baselineCount
)
return
}
self.isLoading = false
self.finishRefreshControls()
self.updateContentState(showLoading: false)
self.loadPendingCategoryIfNeeded()
}, onError: { [weak self] error in
self?.finishLoadingWithError(message: error.localizedDescription)
})
.disposed(by: disposeBag)
}
private func mergeMessages(_ newMessages: [MailboxMessage]) {
var indices = Dictionary(uniqueKeysWithValues: messages.enumerated().map { ($0.element.id, $0.offset) })
for message in newMessages {
if let index = indices[message.id] {
messages[index] = message
} else {
indices[message.id] = messages.count
messages.append(message)
}
}
}
private func calculateReachedEnd(data: MailboxListData) -> Bool {
if data.total == 0 || data.messages.isEmpty {
return true
}
if messages.count >= data.total {
return true
}
let pageSize = max(data.size, 1)
return max(data.page, currentPage) * pageSize >= data.total
}
private func loadPendingCategoryIfNeeded() {
guard let category = pendingCategory ?? (visibleMessages.isEmpty ? selectedCategory : nil) else { return }
pendingCategory = nil
ensureMessagesLoaded(for: category)
}
private func finishLoadingWithError(message: String) {
isLoading = false
finishRefreshControls()
updateContentState(showLoading: false)
DLToast.showError(text: message)
}
private func finishRefreshControls() {
rootView.tableView.mj_header?.endRefreshing()
if reachedEnd {
rootView.tableView.mj_footer?.endRefreshingWithNoMoreData()
} else {
rootView.tableView.mj_footer?.endRefreshing()
}
}
private func messages(for category: MailboxCategory) -> [MailboxMessage] {
messages.filter { $0.category == category }
}
private func updateUnreadCounts() {
var counts: [MailboxCategory: Int] = [:]
MailboxCategory.displayOrder.forEach { category in
counts[category] = messages(for: category).filter { !$0.isRead }.count
}
rootView.updateUnreadCounts(counts)
}
private func updateContentState(showLoading: Bool) {
rootView.updateState(
hasMessages: !visibleMessages.isEmpty,
isLoading: showLoading,
reachedEnd: reachedEnd
)
}
private func openMessage(_ message: MailboxMessage) {
DLToast.showLoading()
UserService.mailboxDetail(id: message.id)
.subscribe(onNext: { [weak self] response in
guard let self else { return }
DLToast.dismiss()
guard let responseMessage = response.data else {
DLToast.showError(text: response.message ?? "消息详情加载失败")
return
}
var detail = self.mergeDetail(responseMessage, fallback: message)
detail.isRead = true
self.updateMessage(detail)
let categoryMessages = self.messages(for: detail.category)
let detailController = MailboxDetailViewController(
messages: categoryMessages,
initialMessage: detail
)
detailController.onMessageRead = { [weak self] updatedMessage in
self?.updateMessage(updatedMessage)
}
self.present(detailController, animated: true)
}, onError: { error in
DLToast.dismiss()
DLToast.showError(text: error.localizedDescription)
})
.disposed(by: disposeBag)
}
private func updateMessage(_ updatedMessage: MailboxMessage) {
guard let index = messages.firstIndex(where: { $0.id == updatedMessage.id }) else { return }
messages[index] = updatedMessage
rootView.tableView.reloadData()
updateUnreadCounts()
}
private func mergeDetail(_ detail: MailboxMessage, fallback: MailboxMessage) -> MailboxMessage {
var merged = detail
if merged.id == 0 { merged.id = fallback.id }
if merged.title.isEmpty { merged.title = fallback.title }
if merged.content.isEmpty { merged.content = fallback.content }
if merged.senderId.isEmpty { merged.senderId = fallback.senderId }
if merged.senderIcon.isEmpty { merged.senderIcon = fallback.senderIcon }
if merged.modifyTime.isEmpty { merged.modifyTime = fallback.modifyTime }
if MailboxCategory(rawValue: merged.type) == nil { merged.type = fallback.type }
return merged
}
@objc private func backAction() {
navigationController?.popViewController(animated: true)
}
@objc private func clearAction() {
guard !messages.isEmpty else {
DLToast.show(text: "暂无消息")
return
}
showConfirmPop(
title: "清空全部消息",
message: "清空后无法恢复,是否继续?",
confirmText: "清空",
confirmBlock: { [weak self] in
self?.requestClearAll()
},
cancelText: "取消"
)
}
private func requestClearAll() {
DLToast.showLoading()
UserService.clearMailbox()
.subscribe(onNext: { [weak self] response in
guard let self else { return }
DLToast.dismiss()
self.messages = []
self.currentPage = 0
self.total = 0
self.reachedEnd = true
self.pendingCategory = nil
self.rootView.tableView.reloadData()
self.updateUnreadCounts()
self.finishRefreshControls()
self.updateContentState(showLoading: false)
DLToast.showSuccess(text: "已清空")
}, onError: { error in
DLToast.dismiss()
DLToast.showError(text: error.localizedDescription)
})
.disposed(by: disposeBag)
}
}
extension MailboxViewController: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
visibleMessages.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(
withIdentifier: MailboxMessageCell.reuseIdentifier,
for: indexPath
) as? MailboxMessageCell else {
return UITableViewCell()
}
cell.configure(message: visibleMessages[indexPath.row])
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
openMessage(visibleMessages[indexPath.row])
}
}

View File

@ -13,12 +13,26 @@ final class MemberInfoVC: BaseViewController {
private var members: [GroupMemberModel]
private var selectedUserId: String
private var groupModel: GroupModel?
private var routeGroupKey: String
private var isLoadingRelations = false
init(members: [GroupMemberModel], selectedUserId: String, groupModel: GroupModel?) {
private var resolvedGroupKey: String {
let fromModel = groupModel?.default_group_key.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
if !fromModel.isEmpty { return fromModel }
let fromRoute = routeGroupKey.trimmingCharacters(in: .whitespacesAndNewlines)
if !fromRoute.isEmpty { return fromRoute }
let fromGroups = groupModel?.groups.first(where: { !$0.group_key.isEmpty })?.group_key
?? groupModel?.groups.first?.group_key
?? ""
if !fromGroups.isEmpty { return fromGroups }
return AppContextManager.shared.defaultGroupKey.trimmingCharacters(in: .whitespacesAndNewlines)
}
init(members: [GroupMemberModel], selectedUserId: String, groupModel: GroupModel?, groupKey: String = "") {
self.members = members
self.selectedUserId = selectedUserId
self.groupModel = groupModel
self.routeGroupKey = groupKey
super.init(nibName: nil, bundle: nil)
}
@ -35,7 +49,8 @@ final class MemberInfoVC: BaseViewController {
let members = membersJson.compactMap { GroupMemberModel(JSON: $0) }
let selected = (userInfo["userId"] as? String) ?? members.first?.user_id ?? ""
let group = (userInfo["groupJson"] as? [String: Any]).flatMap { GroupModel(JSON: $0) }
self.init(members: members, selectedUserId: selected, groupModel: group)
let groupKey = (userInfo["groupKey"] as? String) ?? ""
self.init(members: members, selectedUserId: selected, groupModel: group, groupKey: groupKey)
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
@ -129,26 +144,23 @@ final class MemberInfoVC: BaseViewController {
var groups = CommonGroupModel.deduplicated(member?.common_groups ?? [])
let groupInfos = groupModel?.groups ?? []
for index in groups.indices where groups[index].group_name.isEmpty {
groups[index].group_name = groupInfos.first {
$0.group_key == groups[index].group_key
}?.name ?? "未知圈子"
for index in groups.indices {
if let info = groupInfos.first(where: { $0.group_key == groups[index].group_key }) {
groups[index].apply(info)
} else if groups[index].group_name.isEmpty {
groups[index].group_name = "未知圈子"
}
}
let currentGroup = groupInfos.first { $0.group_key == groupModel?.default_group_key }
let currentGroup = groupInfos.first { $0.group_key == resolvedGroupKey }
?? groupInfos.first
if let currentGroup, !currentGroup.group_key.isEmpty {
if let index = groups.firstIndex(where: { $0.group_key == currentGroup.group_key }) {
var group = groups.remove(at: index)
if group.group_name.isEmpty || group.group_name == "未知圈子" {
group.group_name = currentGroup.name
}
group.apply(currentGroup)
groups.insert(group, at: 0)
} else {
groups.insert(
CommonGroupModel(groupKey: currentGroup.group_key, groupName: currentGroup.name),
at: 0
)
groups.insert(CommonGroupModel(info: currentGroup), at: 0)
}
}
return CommonGroupModel.deduplicated(groups)
@ -195,7 +207,7 @@ final class MemberInfoVC: BaseViewController {
let trimmed = remark.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty,
let member = members.first(where: { $0.user_id == selectedUserId }) else { return }
let groupKey = groupModel?.default_group_key ?? ""
let groupKey = resolvedGroupKey
guard !groupKey.isEmpty else {
DLToast.show(text: "圈子信息缺失")
return
@ -256,7 +268,7 @@ final class MemberInfoVC: BaseViewController {
private func requestEditRelation(_ idx: String) {
guard let member = members.first(where: { $0.user_id == selectedUserId }) else { return }
let groupKey = groupModel?.default_group_key ?? ""
let groupKey = resolvedGroupKey
guard !groupKey.isEmpty else {
DLToast.show(text: "圈子信息缺失")
return
@ -367,8 +379,28 @@ final class MemberInfoVC: BaseViewController {
private func showCommonGroups(_ groups: [CommonGroupModel]) {
guard groups.count > 1 else { return }
let popup = CommonGroupsPopVC(groups: groups, groupInfos: groupModel?.groups ?? [])
present(popup, animated: true)
let presentPopup: ([CommonGroupModel], [GroupInfoModel]) -> Void = { [weak self] groups, infos in
let popup = CommonGroupsPopVC(groups: groups, groupInfos: infos)
self?.present(popup, animated: true)
}
let infos = groupModel?.groups ?? []
if !infos.isEmpty {
presentPopup(groups, infos)
return
}
DLToast.showLoading()
GroupService.groupInfo().subscribe(onNext: { [weak self] response in
DLToast.dismiss()
if let model = response.model {
self?.groupModel = model
}
presentPopup(self?.resolvedCommonGroups(
for: self?.members.first { $0.user_id == self?.selectedUserId }
) ?? groups, response.model?.groups ?? [])
}, onError: { _ in
DLToast.dismiss()
presentPopup(groups, [])
}).disposed(by: disposeBag)
}
}

View File

@ -177,19 +177,6 @@ final class MemberInfoRowView: UIControl {
stack.isUserInteractionEnabled = false
addSubview(stack)
if row.showsMoreTags {
let moreLabel = UILabel()
moreLabel.text = "..."
moreLabel.font = .systemFont(ofSize: 14, weight: .medium)
moreLabel.textColor = UIColor(hexStr: "#D5923F")
moreLabel.backgroundColor = UIColor(hexStr: "#FFF9DD")
moreLabel.textAlignment = .center
moreLabel.layer.cornerRadius = 7
moreLabel.clipsToBounds = true
stack.addArrangedSubview(moreLabel)
moreLabel.layoutChain.width(26).height(26)
}
let nameLabel = MemberInfoTagLabel()
nameLabel.text = row.value
nameLabel.font = .systemFont(ofSize: 14, weight: .medium)
@ -206,6 +193,17 @@ final class MemberInfoRowView: UIControl {
nameLabel.widthAnchor.constraint(lessThanOrEqualToConstant: 160).isActive = true
if row.showsMoreTags {
let moreLabel = UILabel()
moreLabel.text = "..."
moreLabel.font = .systemFont(ofSize: 14, weight: .medium)
moreLabel.textColor = UIColor(hexStr: "#D5923F")
moreLabel.backgroundColor = UIColor(hexStr: "#FFF9DD")
moreLabel.textAlignment = .center
moreLabel.layer.cornerRadius = 7
moreLabel.clipsToBounds = true
stack.addArrangedSubview(moreLabel)
moreLabel.layoutChain.width(26).height(26)
arrow.contentMode = .scaleAspectFit
stack.addArrangedSubview(arrow)
arrow.layoutChain.width(12).height(12)
@ -368,10 +366,11 @@ private final class CommonGroupReadonlyCell: UITableViewCell {
func configure(group: CommonGroupModel, info: GroupInfoModel?) {
nameLabel.text = group.group_name.isEmpty ? "未知圈子" : group.group_name
if let info {
iconView.setGroupIcon(info)
} else {
iconView.setGroupIcon(url: "", fallback: UIImage(named: "GroupIcon/1"))
}
let url = info?.groupIconURL ?? group.groupIconURL
let fallback = info?.groupIcon ?? group.groupIcon
iconView.setGroupIcon(
url: url,
fallback: fallback.size.width > 0 ? fallback : UIImage(named: "GroupIcon/1")
)
}
}

View File

@ -20,6 +20,8 @@ class GroupMemberView2: UIView {
private var currentMemberModel: GroupMemberModel?
private var emojiEchoByUserId: [String: String] = [:]
private var messageEchoByUserId: [String: String] = [:]
private var phoneStatusObserver: NSObjectProtocol?
private var weatherRequestID = UUID()
var onCoupleMemberTap: ((GroupMemberModel) -> Void)?
private static let batteryNormalColor = UIColor(hexStr: "#47DD00")
@ -45,12 +47,20 @@ class GroupMemberView2: UIView {
updateMoodBadge(model.mood)
statusDotView.backgroundColor = UIColor(hexStr: model.is_online ? "#67EA76" : "#D8D8D8")
statusLab.text = model.is_online ? "在线" : "离线"
locationLab.text = model.lastLocation
let isCurrentUser = model.user_id == AppContextManager.shared.userId
updateDistance(for: model, isCurrentUser: isCurrentUser)
if !model.is_online, !isCurrentUser {
locationLab.text = "**"
distanceView.isHidden = true
} else {
locationLab.text = model.lastLocation
updateDistance(for: model, isCurrentUser: isCurrentUser)
}
applyPhoneUsage(phoneUsage)
if isCurrentUser {
requestLocalWeather()
}
updateRealtimeInteractionVisibility(isCurrentUser: isCurrentUser)
refreshRealtimeInteraction()
}
@ -65,6 +75,7 @@ class GroupMemberView2: UIView {
statusLab.text = ""
locationLab.text = ""
distanceView.isHidden = true
stopLocalPhoneStatusObserving()
applyPhoneUsage(nil)
updateRealtimeInteractionVisibility(isCurrentUser: true)
refreshRealtimeInteraction()
@ -159,14 +170,148 @@ class GroupMemberView2: UIView {
}
private func applyPhoneUsage(_ usage: PhoneUsageTodayModel?) {
applyDeviceInfo(usage?.phoneInfo)
if isShowingCurrentUser {
applyLocalDeviceInfo()
startLocalPhoneStatusObserving()
} else {
stopLocalPhoneStatusObserving()
if isShowingOfflineOther {
applyOfflineDeviceInfo()
} else {
applyDeviceInfo(usage?.phoneInfo)
}
}
if isShowingOfflineOther {
phoneReportView.configure(with: nil)
appUsageView.configure(with: nil)
applyPrivilegeMasks(appUsagePreview: nil)
applyOfflineStayPoints()
return
}
phoneReportView.configure(with: Self.phoneReportPreview(from: usage))
let appUsagePreview = MemberAppUsagePreview(topApps: usage?.topApps ?? [])
appUsageView.configure(with: appUsagePreview)
updateAppUsageVisibility(appUsagePreview != nil)
applyPrivilegeMasks(appUsagePreview: appUsagePreview)
applyStayPoints(usage?.stayPoints ?? [])
}
var isPhoneReportDenied: Bool { phoneReportView.isDenied }
var isAppUsageDenied: Bool { appUsageView.isDenied }
var onPhoneReportRemind: (() -> Void)?
var onAppUsageRemind: (() -> Void)?
private func applyPrivilegeMasks(appUsagePreview: MemberAppUsagePreview? = nil) {
guard currentMemberModel != nil else {
phoneReportView.setDenied(false)
appUsageView.setDenied(false)
updateAppUsageVisibility(appUsagePreview != nil)
return
}
let isSelf = isShowingCurrentUser
let privilege = currentMemberModel?.extra.privilege ?? GroupPrivilege()
let phoneDenied = !isSelf && !privilege.phone_report
let usageDenied = !isSelf && !privilege.app_usage_record
phoneReportView.setDenied(phoneDenied)
phoneReportView.onDeniedAction = { [weak self] in self?.onPhoneReportRemind?() }
appUsageView.setDenied(usageDenied)
appUsageView.onDeniedAction = { [weak self] in self?.onAppUsageRemind?() }
let showUsage = appUsagePreview != nil || usageDenied
if appUsagePreview == nil, usageDenied {
appUsageView.configure(with: nil)
}
updateAppUsageVisibility(showUsage)
}
private var isShowingCurrentUser: Bool {
guard let userId = currentMemberModel?.user_id, !userId.isEmpty else { return false }
return userId == AppContextManager.shared.userId
}
private var isShowingOfflineOther: Bool {
guard let model = currentMemberModel, !isShowingCurrentUser else { return false }
return !model.is_online
}
private func applyOfflineDeviceInfo() {
phoneModelIcon.image = UIImage(named: "Home/member_phone_unknown")
phoneModelLab.text = "**"
phoneModelLab.textColor = Self.batteryDefaultTextColor
networkIcon.image = UIImage(named: "Home/member_network_unknown")
networkLab.text = "**"
networkLab.textColor = Self.batteryDefaultTextColor
applyBatteryUnknown(placeholder: "**")
applyUnknownWeather(text: "**")
}
private func applyOfflineStayPoints() {
stayNumLab.text = "**"
stayLocationView.isHidden = true
stayNodataView.isHidden = false
stayLocationLab.text = ""
stayStatusLab.text = ""
}
private func startLocalPhoneStatusObserving() {
guard phoneStatusObserver == nil else { return }
PhoneStatusCollector.shared.start()
phoneStatusObserver = NotificationCenter.default.addObserver(
forName: .phoneStatusDidChange,
object: nil,
queue: .main
) { [weak self] _ in
guard let self, self.isShowingCurrentUser else { return }
self.applyLocalDeviceInfo()
}
}
private func stopLocalPhoneStatusObserving() {
if let phoneStatusObserver {
NotificationCenter.default.removeObserver(phoneStatusObserver)
self.phoneStatusObserver = nil
}
weatherRequestID = UUID()
}
private func applyLocalDeviceInfo() {
let snap = PhoneStatusCollector.shared.snapshot()
phoneModelIcon.image = UIImage(named: "Home/member_phone")
phoneModelLab.text = snap.model
phoneModelLab.textColor = Self.batteryDefaultTextColor
let hasNetwork = snap.network != "无网络"
networkIcon.image = UIImage(named: hasNetwork ? "Home/member_wifi" : "Home/member_network_unknown")
networkLab.text = snap.network
networkLab.textColor = Self.batteryDefaultTextColor
applyBatteryDisplay(percent: snap.batteryPercent)
}
private func requestLocalWeather() {
let requestID = UUID()
weatherRequestID = requestID
applyUnknownWeather(text: "***")
guard let latitude = Defaults[\.currentLatitude],
let longitude = Defaults[\.currentLongitude] else {
applyUnknownWeather(text: "***")
return
}
let location = CLLocation(latitude: latitude, longitude: longitude)
guard CLLocationCoordinate2DIsValid(location.coordinate) else {
applyUnknownWeather(text: "***")
return
}
MemberWeatherService.shared.weather(for: location) { [weak self] result in
guard let self, self.weatherRequestID == requestID, self.isShowingCurrentUser else { return }
switch result {
case .success(let snapshot):
self.weatherIcon.image = UIImage(named: snapshot.assetName)
self.weatherLab.text = snapshot.text
case .failure:
self.applyUnknownWeather(text: "***")
}
}
}
private func updateAppUsageVisibility(_ visible: Bool) {
if visible {
phoneReportBottomConstraint?.isActive = false
@ -209,11 +354,11 @@ class GroupMemberView2: UIView {
weatherLab.text = text
}
private func applyBatteryUnknown() {
private func applyBatteryUnknown(placeholder: String = "***") {
batteryIcon.image = UIImage(named: "Home/member_battery_unknown")
batteryFillView.isHidden = true
batteryFillView.layoutChain.width(0)
batteryLab.text = "***"
batteryLab.text = placeholder
batteryLab.textColor = Self.batteryDefaultTextColor
}
@ -1283,6 +1428,7 @@ class GroupMemberView2: UIView {
}
deinit {
stopLocalPhoneStatusObserving()
NotificationCenter.default.removeObserver(self)
}

View File

@ -505,8 +505,36 @@ extension HomeView2: UIScrollViewDelegate {
// MARK: - UIGestureRecognizerDelegate
extension HomeView2: UIGestureRecognizerDelegate {
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
guard let pan = gestureRecognizer as? UIPanGestureRecognizer,
pan.view === groupMemberView else { return true }
let memberCV = groupMemberView.memberCV
guard !memberCV.isHidden, isTouch(pan, inside: memberCV) else { return true }
if memberCV.isDragging || memberCV.isDecelerating {
return false
}
let velocity = pan.velocity(in: groupMemberView)
let translation = pan.translation(in: groupMemberView)
let dx = max(abs(velocity.x), abs(translation.x) * 60)
let dy = max(abs(velocity.y), abs(translation.y) * 60)
return dy > dx * 1.8
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
shouldRecognizeSimultaneouslyWith other: UIGestureRecognizer) -> Bool {
if isMemberListPan(other) {
return false
}
return true
}
private func isMemberListPan(_ gesture: UIGestureRecognizer) -> Bool {
let memberCV = groupMemberView.memberCV
if gesture.view === memberCV { return true }
return gesture.view?.isDescendant(of: memberCV) == true
}
private func isTouch(_ pan: UIPanGestureRecognizer, inside view: UIView) -> Bool {
view.bounds.contains(pan.location(in: view))
}
}

View File

@ -343,13 +343,22 @@ class HomeViewController: BaseViewController {
//
rootView.groupMemberView.phoneReportView.rx.tapGesture.subscribe(onNext: { [weak self] _ in
self?.openPhoneReportDetail()
guard let self, !self.rootView.groupMemberView.isPhoneReportDenied else { return }
self.openPhoneReportDetail()
}).disposed(by: disposeBag)
// APP
rootView.groupMemberView.appUsageView.rx.tapGesture.subscribe(onNext: { [weak self] _ in
self?.openAppUsageReport()
guard let self, !self.rootView.groupMemberView.isAppUsageDenied else { return }
self.openAppUsageReport()
}).disposed(by: disposeBag)
rootView.groupMemberView.onPhoneReportRemind = {
DLToast.show(text: "已提醒")
}
rootView.groupMemberView.onAppUsageRemind = {
DLToast.show(text: "已提醒")
}
//
rootView.locationView.rx.tapGesture.subscribe { _ in
@ -1493,7 +1502,6 @@ extension HomeViewController {
let isSelf = (!record.userId.isEmpty && record.userId == selfId)
|| userId == selfId
DispatchQueue.main.async { [weak self] in
NotificationCenter.default.post(name: .lockDistractAppsDidChange, object: nil)
guard isSelf else { return }
AppUnlockCoordinator.shared.registerIncomingLock(tokens: record.tokens)
self?.applyIncomingLocks([record])

View File

@ -74,6 +74,9 @@ final class MemberAppUsageView: UIView {
private let usageTitleLab = UILabel()
private let topTitleLab = UILabel()
private let topStack = UIStackView()
private let deniedOverlay = PrivilegeDeniedOverlay()
private(set) var isDenied = false
var onDeniedAction: (() -> Void)?
override init(frame: CGRect) {
super.init(frame: frame)
@ -85,6 +88,19 @@ final class MemberAppUsageView: UIView {
fatalError("init(coder:) has not been implemented")
}
func setDenied(_ denied: Bool) {
isDenied = denied
deniedOverlay.configure(
title: "TA未开启设置",
message: "无法查看app使用记录",
actionTitle: "提醒TA"
)
deniedOverlay.onAction = { [weak self] in self?.onDeniedAction?() }
deniedOverlay.setVisible(denied)
deniedOverlay.layer.cornerRadius = cardView.layer.cornerRadius
gestureRecognizers?.forEach { $0.isEnabled = !denied }
}
func configure(with preview: MemberAppUsagePreview?) {
topStack.arrangedSubviews.forEach { $0.removeFromSuperview() }
@ -134,6 +150,9 @@ final class MemberAppUsageView: UIView {
.height(176)
.bottom()
cardView.addSubview(deniedOverlay)
deniedOverlay.layoutChain.edges()
usageTitleLab.attributedText = Self.cardTitle("使用时长")
cardView.addSubview(usageTitleLab)
usageTitleLab.layoutChain
@ -179,6 +198,8 @@ final class MemberAppUsageView: UIView {
.top(43)
.left(160)
.right(14)
cardView.bringSubviewToFront(deniedOverlay)
}
private static func cardTitle(_ title: String) -> NSAttributedString {

View File

@ -52,6 +52,9 @@ final class MemberPhoneReportView: UIView {
private let unlockIcon = UIImageView()
private let unlockTitleLab = UILabel()
private let unlockValueLab = UILabel()
private let deniedOverlay = PrivilegeDeniedOverlay()
private(set) var isDenied = false
var onDeniedAction: (() -> Void)?
override init(frame: CGRect) {
super.init(frame: frame)
@ -63,6 +66,19 @@ final class MemberPhoneReportView: UIView {
fatalError("init(coder:) has not been implemented")
}
func setDenied(_ denied: Bool) {
isDenied = denied
deniedOverlay.configure(
title: "TA未开启设置",
message: "无法查看手机报告",
actionTitle: "提醒TA"
)
deniedOverlay.onAction = { [weak self] in self?.onDeniedAction?() }
deniedOverlay.setVisible(denied)
deniedOverlay.layer.cornerRadius = cardView.layer.cornerRadius
gestureRecognizers?.forEach { $0.isEnabled = !denied }
}
func configure(with preview: MemberPhoneReportPreview?) {
if let preview {
screenTimeBadge.configure(seconds: preview.screenTimeSeconds)
@ -103,6 +119,9 @@ final class MemberPhoneReportView: UIView {
.edgesHorzontal(15)
.bottom()
cardView.addSubview(deniedOverlay)
deniedOverlay.layoutChain.edges()
cardView.addSubview(leftPanel)
cardView.addSubview(rightPanel)
leftPanel.backgroundColor = UIColor(hexStr: "#FFFCEA")
@ -238,6 +257,8 @@ final class MemberPhoneReportView: UIView {
unlockValueUnitLab.layoutChain
.leftToRightOfView(unlockValueLab, offset: 3)
.bottomToView(unlockValueLab, offset: -4)
cardView.bringSubviewToFront(deniedOverlay)
}
}

View File

@ -339,7 +339,7 @@ final class PhoneBatteryRingView: UIView {
func configure(percent: Int?, text: String, isCharging: Bool = false) {
valueLab.text = text
bolt.isHidden = false
bolt.isHidden = !isCharging
progress.strokeEnd = CGFloat(max(0, min(100, percent ?? 0))) / 100
}
}

View File

@ -40,6 +40,7 @@ final class PhoneReportDetailViewModel {
private let groupKey: String
private let disposeBag = DisposeBag()
private var statusObserveBag = DisposeBag()
private let placeholder = "**"
private var reportCache: [String: PhoneUsageReportModel] = [:]
private var requestIDs: [String: UUID] = [:]
@ -59,15 +60,30 @@ final class PhoneReportDetailViewModel {
.disposed(by: disposeBag)
}
private func isCurrentUser(_ userId: String) -> Bool {
!userId.isEmpty && userId == AppContextManager.shared.userId
}
private func refresh(userId: String) {
guard !userId.isEmpty, !groupKey.isEmpty else {
let isSelf = isCurrentUser(userId)
updateStatusObserving(isSelf: isSelf)
guard !userId.isEmpty else {
snapshot.accept(Self.emptySnapshot())
return
}
if groupKey.isEmpty {
snapshot.accept(isSelf ? makeLocalStatusSnapshot(report: nil) : Self.emptySnapshot())
return
}
let cacheKey = "\(groupKey)|\(userId)"
if let cached = reportCache[cacheKey] {
snapshot.accept(makeSnapshot(report: cached))
let cached = reportCache[cacheKey]
if isSelf {
snapshot.accept(makeLocalStatusSnapshot(report: cached))
} else if let cached {
snapshot.accept(makeRemoteSnapshot(report: cached))
} else {
snapshot.accept(Self.emptySnapshot())
}
@ -80,35 +96,72 @@ final class PhoneReportDetailViewModel {
.subscribe(onNext: { [weak self] response in
guard let self, self.requestIDs[cacheKey] == requestID else { return }
self.requestIDs.removeValue(forKey: cacheKey)
guard response.code == "0", let report = response.model else { return }
guard response.code == "0", let report = response.model else {
guard self.selectedMemberId.value == userId, self.isCurrentUser(userId) else { return }
self.snapshot.accept(self.makeLocalStatusSnapshot(report: self.reportCache[cacheKey]))
return
}
self.reportCache[cacheKey] = report
guard self.selectedMemberId.value == userId else { return }
self.snapshot.accept(self.makeSnapshot(report: report))
self.snapshot.accept(self.makeSnapshot(report: report, userId: userId))
}, onError: { [weak self] _ in
guard let self, self.requestIDs[cacheKey] == requestID else { return }
self.requestIDs.removeValue(forKey: cacheKey)
guard self.selectedMemberId.value == userId, self.isCurrentUser(userId) else { return }
self.snapshot.accept(self.makeLocalStatusSnapshot(report: self.reportCache[cacheKey]))
})
.disposed(by: disposeBag)
}
private func makeSnapshot(report: PhoneUsageReportModel) -> StatusSnapshot {
let info = report.phoneInfo
let storageRatio = Self.storageRatio(used: info?.memoryUsed, total: info?.memoryTotal)
let screenBars = Self.makeBars(from: report.screenUseTimes, value: { $0.useTime })
let appUsageBars = Self.makeBars(from: report.appUseCounts, value: { $0.count })
private func updateStatusObserving(isSelf: Bool) {
statusObserveBag = DisposeBag()
guard isSelf else { return }
PhoneStatusCollector.shared.start()
NotificationCenter.default.rx.notification(.phoneStatusDidChange)
.observe(on: MainScheduler.instance)
.subscribe(onNext: { [weak self] _ in
guard let self else { return }
let userId = self.selectedMemberId.value
guard self.isCurrentUser(userId) else { return }
let cacheKey = "\(self.groupKey)|\(userId)"
self.snapshot.accept(self.makeLocalStatusSnapshot(report: self.reportCache[cacheKey]))
})
.disposed(by: statusObserveBag)
}
let todayScreenSeconds = Self.todayValue(in: report.screenUseTimes, value: { $0.useTime })
?? Self.nonnegativeInt(from: info?.useTime)
let todayAppUsageCount = Self.todayValue(in: report.appUseCounts, value: { $0.count })
let screenTodayText: String
if let todayScreenSeconds {
screenTodayText = Self.formatDuration(todayScreenSeconds)
} else {
screenTodayText = placeholder
private func makeSnapshot(report: PhoneUsageReportModel, userId: String) -> StatusSnapshot {
if isCurrentUser(userId) {
return makeLocalStatusSnapshot(report: report)
}
let appUsageTodayText = todayAppUsageCount.map { "\($0)" } ?? placeholder
return makeRemoteSnapshot(report: report)
}
private func makeLocalStatusSnapshot(report: PhoneUsageReportModel?) -> StatusSnapshot {
let local = PhoneStatusCollector.shared.snapshot()
let charts = chartFields(from: report)
return StatusSnapshot(
batteryText: local.batteryPercent == nil ? placeholder : local.batteryText,
batteryPercent: local.batteryPercent,
isCharging: local.isCharging,
estimateText: placeholder,
brightnessText: local.brightnessText,
volumeText: local.volumeText,
wifiText: local.network,
deviceText: local.model,
storageText: local.memoryUsed == nil ? placeholder : local.storageText,
storageRatio: local.storageRatio,
brandIconURL: Self.validHTTPURLString(report?.brandIcon ?? "") ?? "",
screenTodayText: charts.screenTodayText,
appUsageTodayText: charts.appUsageTodayText,
screenBars: charts.screenBars,
appUsageBars: charts.appUsageBars
)
}
private func makeRemoteSnapshot(report: PhoneUsageReportModel) -> StatusSnapshot {
let info = report.phoneInfo
let charts = chartFields(from: report)
return StatusSnapshot(
batteryText: Self.displayText(info?.battery, placeholder: placeholder),
batteryPercent: Self.percent(from: info?.battery),
@ -119,15 +172,35 @@ final class PhoneReportDetailViewModel {
wifiText: Self.displayText(info?.network, placeholder: placeholder),
deviceText: Self.displayText(info?.model, placeholder: placeholder),
storageText: Self.storageText(used: info?.memoryUsed, total: info?.memoryTotal, placeholder: placeholder),
storageRatio: storageRatio,
storageRatio: Self.storageRatio(used: info?.memoryUsed, total: info?.memoryTotal),
brandIconURL: Self.validHTTPURLString(report.brandIcon) ?? "",
screenTodayText: screenTodayText,
appUsageTodayText: appUsageTodayText,
screenBars: screenBars,
appUsageBars: appUsageBars
screenTodayText: charts.screenTodayText,
appUsageTodayText: charts.appUsageTodayText,
screenBars: charts.screenBars,
appUsageBars: charts.appUsageBars
)
}
private func chartFields(from report: PhoneUsageReportModel?) -> (
screenTodayText: String,
appUsageTodayText: String,
screenBars: [ChartBar],
appUsageBars: [ChartBar]
) {
guard let report else {
return (placeholder, placeholder, [], [])
}
let info = report.phoneInfo
let screenBars = Self.makeBars(from: report.screenUseTimes, value: { $0.useTime })
let appUsageBars = Self.makeBars(from: report.appUseCounts, value: { $0.count })
let todayScreenSeconds = Self.todayValue(in: report.screenUseTimes, value: { $0.useTime })
?? Self.nonnegativeInt(from: info?.useTime)
let todayAppUsageCount = Self.todayValue(in: report.appUseCounts, value: { $0.count })
let screenTodayText = todayScreenSeconds.map { Self.formatDuration($0) } ?? placeholder
let appUsageTodayText = todayAppUsageCount.map { "\($0)" } ?? placeholder
return (screenTodayText, appUsageTodayText, screenBars, appUsageBars)
}
private static func emptySnapshot() -> StatusSnapshot {
StatusSnapshot(
batteryText: "**",

View File

@ -5,6 +5,19 @@
import UIKit
enum SOSConfiguration {
static let countdownDuration: TimeInterval = 5
static let slideCancelCompletionRatio: CGFloat = 0.9
static var countdownSeconds: Int {
Int(ceil(countdownDuration))
}
static var countdownDescription: String {
"\(countdownSeconds)秒后会将你的SOS和位置发送到你的圈子和紧急联系人。"
}
}
final class SOSPulseButtonView: UIView {
var onTap: (() -> Void)?
@ -194,7 +207,7 @@ final class SOSCountdownRingView: UIView {
private var countdownTimer: Timer?
private var deadline: Date?
private var duration: TimeInterval = 10
private var duration = SOSConfiguration.countdownDuration
private var lastDisplayedValue = -1
private var completionDelivered = false
private var progress: CGFloat = 1
@ -241,7 +254,7 @@ final class SOSCountdownRingView: UIView {
alarmContainer.addSubview(alarmRaysView)
addSubview(alarmContainer)
render(progress: 1, value: 10)
render(progress: 1, value: SOSConfiguration.countdownSeconds)
}
required init?(coder: NSCoder) {
@ -301,7 +314,7 @@ final class SOSCountdownRingView: UIView {
updateAlarmPosition(progress: progress)
}
func start(duration: TimeInterval = 10) {
func start(duration: TimeInterval = SOSConfiguration.countdownDuration) {
stop(reset: false)
self.duration = max(0.1, duration)
deadline = Date().addingTimeInterval(self.duration)
@ -327,7 +340,7 @@ final class SOSCountdownRingView: UIView {
if reset {
completionDelivered = false
lastDisplayedValue = -1
render(progress: 1, value: 10)
render(progress: 1, value: SOSConfiguration.countdownSeconds)
}
}
@ -335,7 +348,7 @@ final class SOSCountdownRingView: UIView {
guard let deadline else { return }
let remaining = max(0, deadline.timeIntervalSinceNow)
let progress = CGFloat(min(1, remaining / duration))
let value = max(0, min(10, Int(ceil(remaining))))
let value = max(0, min(Int(ceil(duration)), Int(ceil(remaining))))
render(progress: progress, value: value)
guard remaining <= 0, !completionDelivered else { return }
@ -564,7 +577,8 @@ final class SOSSlideCancelView: UIView {
@objc private func handlePan(_ gesture: UIPanGestureRecognizer) {
guard !isLocked else { return }
let maxX = max(2, bounds.width - thumbView.bounds.width - 2)
let minimumX: CGFloat = 2
let maxX = max(minimumX, bounds.width - thumbView.bounds.width - minimumX)
switch gesture.state {
case .began:
@ -572,10 +586,11 @@ final class SOSSlideCancelView: UIView {
startX = thumbView.frame.minX
case .changed:
let translation = gesture.translation(in: self)
thumbView.frame.origin.x = min(maxX, max(2, startX + translation.x))
thumbView.frame.origin.x = min(maxX, max(minimumX, startX + translation.x))
case .ended, .cancelled, .failed:
isDragging = false
if thumbView.frame.minX >= maxX - 4 {
let completionX = minimumX + (maxX - minimumX) * SOSConfiguration.slideCancelCompletionRatio
if thumbView.frame.minX >= completionX {
isLocked = true
thumbView.isUserInteractionEnabled = false
onCompleted?()

View File

@ -0,0 +1,354 @@
import UIKit
import ObjectMapper
import RxSwift
struct SOSListResponse: BaseModelProtocol {
var code: String?
var message: String?
var list: [SOSListItem] = []
init?(map: Map) {}
mutating func mapping(map: Map) {
code <- (map["code"], kIntTransformStr)
message <- map["message"]
if message == nil {
message <- map["msg"]
}
list <- map["data"]
if list.isEmpty {
list <- map["data.list"]
}
}
}
struct SOSListItem: Mappable {
var userId = ""
var nickName = ""
var headPic = ""
var lastPosition = ""
var groupKey = ""
var groupName = ""
var sosTime = ""
init?(map: Map) {}
mutating func mapping(map: Map) {
userId <- (map["user_id"], kIntTransformStr)
nickName <- (map["nick_name"], kIntTransformStr)
headPic <- (map["head_pic"], kIntTransformStr)
lastPosition <- (map["last_position"], kIntTransformStr)
groupKey <- (map["group_key"], kIntTransformStr)
groupName <- (map["group_name"], kIntTransformStr)
sosTime <- (map["sos_time"], kIntTransformStr)
}
var displayAddress: String {
let value = lastPosition.trimmed
let parts = value.split(separator: ":", maxSplits: 2, omittingEmptySubsequences: false)
guard parts.count == 3,
Double(parts[0]) != nil,
Double(parts[1]) != nil else { return value }
let address = String(parts[2]).trimmed
return address.isEmpty ? value : address
}
var displayTime: String {
guard let date = parsedSOSDate else { return sosTime }
return Self.displayDateFormatter.string(from: date)
}
private var parsedSOSDate: Date? {
let value = sosTime.trimmed
if let timestamp = TimeInterval(value) {
return Date(timeIntervalSince1970: timestamp > 10_000_000_000 ? timestamp / 1000 : timestamp)
}
for formatter in Self.apiDateFormatters {
if let date = formatter.date(from: value) {
return date
}
}
return ISO8601DateFormatter().date(from: value)
}
private static let apiDateFormatters: [DateFormatter] = [
"yyyy-MM-dd HH:mm:ss",
"yyyy-MM-dd HH:mm",
"yyyy/MM/dd HH:mm:ss",
"yyyy/MM/dd HH:mm"
].map { format in
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = .current
formatter.dateFormat = format
return formatter
}
private static let displayDateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "zh_CN")
formatter.timeZone = .current
formatter.dateFormat = "M月d日 HH:mm"
return formatter
}()
}
final class SOSListViewController: BaseViewController {
private var rootView: SOSListView!
private var items: [SOSListItem] = []
private var isLoading = false
override var isNavigationBarHidden: Bool { true }
override func loadView() {
rootView = SOSListView(frame: UIScreen.main.bounds)
view = rootView
}
override func viewDidLoad() {
super.viewDidLoad()
rootView.tableView.dataSource = self
rootView.tableView.delegate = self
rootView.refreshControl.addTarget(self, action: #selector(refresh), for: .valueChanged)
loadSOSList(showsLoading: true)
}
@objc private func refresh() {
loadSOSList(showsLoading: false)
}
private func loadSOSList(showsLoading: Bool) {
guard !isLoading else { return }
isLoading = true
if showsLoading {
DLToast.showLoading()
}
SystemService.sosList()
.observe(on: MainScheduler.instance)
.subscribe(onNext: { [weak self] response in
guard let self else { return }
self.finishLoading()
guard response.code == "0" else {
DLToast.showError(text: response.message ?? "获取求助列表失败")
return
}
self.items = response.list
self.rootView.showEmpty(self.items.isEmpty)
self.rootView.tableView.reloadData()
}, onError: { [weak self] error in
self?.finishLoading()
DLToast.showError(text: error.gatewayMessage ?? error.localizedDescription)
})
.disposed(by: disposeBag)
}
private func finishLoading() {
isLoading = false
DLToast.dismiss()
rootView.refreshControl.endRefreshing()
}
private func showLocation(for item: SOSListItem) {
guard !item.groupKey.isEmpty, !item.lastPosition.isEmpty else { return }
NotificationCenter.default.post(
name: .ShowMemberLocationNotification,
object: nil,
userInfo: [
"last_position": item.lastPosition,
"group_key": item.groupKey,
"user_id": item.userId
]
)
let mainTabBarController = tabBarController as? MainTabBarController
navigationController?.popToRootViewController(animated: true)
mainTabBarController?.selectTab(at: 0)
}
}
extension SOSListViewController: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
items.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell: SOSListCell = tableView.dequeueReusableCell(for: indexPath)
cell.configure(with: items[indexPath.row])
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
showLocation(for: items[indexPath.row])
}
}
final class SOSListView: UIView {
let tableView = UITableView(frame: .zero, style: .plain)
let refreshControl = UIRefreshControl()
private let navBackgroundView = UIImageView(image: UIImage(named: "Common/navBar_bg_2"))
private let navView = BaseNavigationView(title: "紧急求助")
private let emptyLabel = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func showEmpty(_ isEmpty: Bool) {
emptyLabel.isHidden = !isEmpty
}
private func setupUI() {
backgroundColor = UIColor(hexStr: "#FAFAFA")
navBackgroundView.contentMode = .scaleAspectFill
navBackgroundView.clipsToBounds = true
addSubview(navBackgroundView)
addSubview(navView)
tableView.backgroundColor = .clear
tableView.separatorStyle = .none
tableView.showsVerticalScrollIndicator = false
tableView.estimatedRowHeight = 126
tableView.rowHeight = UITableView.automaticDimension
tableView.contentInset = UIEdgeInsets(top: 14, left: 0, bottom: kSafeBottomMargin + 16, right: 0)
tableView.register(SOSListCell.self)
tableView.refreshControl = refreshControl
addSubview(tableView)
emptyLabel.text = "暂无紧急求助"
emptyLabel.textColor = UIColor(hexStr: "#A7ABB2")
emptyLabel.font = .systemFont(ofSize: 15)
emptyLabel.textAlignment = .center
emptyLabel.isHidden = true
addSubview(emptyLabel)
navBackgroundView.translatesAutoresizingMaskIntoConstraints = false
navView.translatesAutoresizingMaskIntoConstraints = false
tableView.translatesAutoresizingMaskIntoConstraints = false
emptyLabel.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
navBackgroundView.topAnchor.constraint(equalTo: topAnchor),
navBackgroundView.leadingAnchor.constraint(equalTo: leadingAnchor),
navBackgroundView.trailingAnchor.constraint(equalTo: trailingAnchor),
navBackgroundView.heightAnchor.constraint(equalToConstant: max(kNaviHeight + 70, 150)),
navView.topAnchor.constraint(equalTo: topAnchor),
navView.leadingAnchor.constraint(equalTo: leadingAnchor),
navView.trailingAnchor.constraint(equalTo: trailingAnchor),
navView.heightAnchor.constraint(equalToConstant: kNaviHeight),
tableView.topAnchor.constraint(equalTo: navView.bottomAnchor),
tableView.leadingAnchor.constraint(equalTo: leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: bottomAnchor),
emptyLabel.centerXAnchor.constraint(equalTo: centerXAnchor),
emptyLabel.centerYAnchor.constraint(equalTo: centerYAnchor, constant: -20)
])
}
}
final class SOSListCell: UITableViewCell {
private let cardView = UIView()
private let avatarView = UIImageView()
private let titleLabel = UILabel()
private let addressLabel = UILabel()
private let timeLabel = UILabel()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareForReuse() {
super.prepareForReuse()
avatarView.image = nil
}
func configure(with item: SOSListItem) {
avatarView.setHeadPic(item.headPic)
titleLabel.attributedText = makeTitle(item)
addressLabel.text = item.displayAddress.isEmpty ? "位置暂未获取" : item.displayAddress
timeLabel.text = item.displayTime
accessibilityLabel = [item.groupName, item.nickName, "紧急求助中", addressLabel.text, timeLabel.text]
.compactMap { $0 }
.filter { !$0.isEmpty }
.joined(separator: "")
}
private func makeTitle(_ item: SOSListItem) -> NSAttributedString {
let groupName = item.groupName.isEmpty ? "当前" : item.groupName
let nickName = item.nickName.isEmpty ? "圈子成员" : item.nickName
let text = "\(groupName)圈子的 \(nickName) 紧急求助中"
let result = NSMutableAttributedString(
string: text,
attributes: [
.font: UIFont.systemFont(ofSize: 16, weight: .regular),
.foregroundColor: UIColor(hexStr: "#273246")
]
)
let highlightColor = UIColor(hexStr: "#04AFFF")
result.addAttribute(.foregroundColor, value: highlightColor, range: (text as NSString).range(of: groupName))
result.addAttribute(.foregroundColor, value: highlightColor, range: (text as NSString).range(of: nickName))
return result
}
private func setupUI() {
backgroundColor = .clear
selectionStyle = .none
contentView.backgroundColor = .clear
cardView.backgroundColor = .white
cardView.layer.cornerRadius = 22
contentView.addSubview(cardView)
avatarView.contentMode = .scaleAspectFill
avatarView.clipsToBounds = true
avatarView.layer.cornerRadius = 25
cardView.addSubview(avatarView)
titleLabel.numberOfLines = 2
titleLabel.setContentCompressionResistancePriority(.required, for: .vertical)
cardView.addSubview(titleLabel)
addressLabel.font = .systemFont(ofSize: 14)
addressLabel.textColor = UIColor(hexStr: "#8D9198")
addressLabel.numberOfLines = 2
cardView.addSubview(addressLabel)
timeLabel.font = .systemFont(ofSize: 13)
timeLabel.textColor = UIColor(hexStr: "#B6B9BE")
cardView.addSubview(timeLabel)
cardView.translatesAutoresizingMaskIntoConstraints = false
avatarView.translatesAutoresizingMaskIntoConstraints = false
titleLabel.translatesAutoresizingMaskIntoConstraints = false
addressLabel.translatesAutoresizingMaskIntoConstraints = false
timeLabel.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
cardView.topAnchor.constraint(equalTo: contentView.topAnchor),
cardView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 15),
cardView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -15),
cardView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -14),
avatarView.leadingAnchor.constraint(equalTo: cardView.leadingAnchor, constant: 15),
avatarView.topAnchor.constraint(equalTo: cardView.topAnchor, constant: 27),
avatarView.widthAnchor.constraint(equalToConstant: 50),
avatarView.heightAnchor.constraint(equalToConstant: 50),
titleLabel.topAnchor.constraint(equalTo: cardView.topAnchor, constant: 22),
titleLabel.leadingAnchor.constraint(equalTo: avatarView.trailingAnchor, constant: 15),
titleLabel.trailingAnchor.constraint(equalTo: cardView.trailingAnchor, constant: -15),
addressLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 6),
addressLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
addressLabel.trailingAnchor.constraint(equalTo: titleLabel.trailingAnchor),
timeLabel.topAnchor.constraint(equalTo: addressLabel.bottomAnchor, constant: 7),
timeLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
timeLabel.trailingAnchor.constraint(equalTo: titleLabel.trailingAnchor),
timeLabel.bottomAnchor.constraint(equalTo: cardView.bottomAnchor, constant: -16)
])
}
}

View File

@ -258,7 +258,7 @@ final class SOSPracticeView: UIView, UIScrollViewDelegate {
titleLabel.translatesAutoresizingMaskIntoConstraints = false
let bodyLabel = UILabel()
bodyLabel.text = "10秒后会将你的SOS和位置发送到你的圈子和紧急联系人。"
bodyLabel.text = SOSConfiguration.countdownDescription
bodyLabel.textColor = UIColor(hexStr: "#273246")
bodyLabel.font = .systemFont(ofSize: 15, weight: .regular)
bodyLabel.textAlignment = .center
@ -432,7 +432,7 @@ final class SOSPracticeView: UIView, UIScrollViewDelegate {
practicePulseView.setAnimationEnabled(false)
practiceSlider.configure(style: .practiceCountdown)
practiceSlider.setInteractionEnabled(true)
practiceCountdownRing.start(duration: 10)
practiceCountdownRing.start()
case .success:
practiceCountdownRing.stop(reset: false)
practicePulseView.setAnimationEnabled(false)

View File

@ -18,6 +18,7 @@ final class SOSView: UIView {
var onCountdownCompleted: (() -> Void)?
var onCancelActive: (() -> Void)?
var onManageContacts: (() -> Void)?
var onShowSOSList: (() -> Void)?
private(set) var state: SOSPageState = .idle
let sosPracticeView = SOSPracticeView()
@ -83,10 +84,10 @@ final class SOSView: UIView {
idlePulseView.setAnimationEnabled(false)
activePulseView.setAnimationEnabled(false)
countdownTitleLabel.attributedText = Self.cancelTitle
countdownBodyLabel.text = "10秒后会将你的SOS和位置发送到你的圈子和紧急联系人。"
countdownBodyLabel.text = SOSConfiguration.countdownDescription
countdownSlider.configure(style: .countdown)
countdownSlider.setInteractionEnabled(true)
countdownRingView.start(duration: 10)
countdownRingView.start()
case .sending:
countdownRingView.stop(reset: false)
countdownTitleLabel.text = "正在发送"
@ -194,10 +195,15 @@ final class SOSView: UIView {
moreButton.widthAnchor.constraint(equalToConstant: 24).isActive = true
moreButton.heightAnchor.constraint(equalToConstant: 24).isActive = true
moreButton.transform = CGAffineTransform(a: 1.5, b: 0, c: 0, d: 1.5, tx: -7, ty: -7)
moreButton.accessibilityLabel = "更多"
moreButton.accessibilityLabel = "求助列表"
moreButton.addTarget(self, action: #selector(showSOSList), for: .touchUpInside)
navView.addRightButton(moreButton)
}
@objc private func showSOSList() {
onShowSOSList?()
}
private func setupInfoCard() {
infoCard.backgroundColor = UIColor.white.withAlphaComponent(0.93)
infoCard.layer.cornerRadius = 14
@ -417,7 +423,7 @@ final class SOSView: UIView {
contentView.addSubview(countdownTitleLabel)
countdownTitleLabel.translatesAutoresizingMaskIntoConstraints = false
countdownBodyLabel.text = "10秒后会将你的SOS和位置发送到你的圈子和紧急联系人。"
countdownBodyLabel.text = SOSConfiguration.countdownDescription
countdownBodyLabel.textColor = UIColor(hexStr: "#273246")
countdownBodyLabel.font = .systemFont(ofSize: 15, weight: .regular)
countdownBodyLabel.textAlignment = .center

View File

@ -47,6 +47,9 @@ final class SOSViewController: BaseViewController {
rootView.onManageContacts = {
AppRouter.push(Route.emergencyContact)
}
rootView.onShowSOSList = {
AppRouter.push(Route.sosList)
}
}
private func requestEmergencyContacts() {

View File

@ -60,6 +60,9 @@ class LaunchViewController: BaseViewController {
MQTTService.shared.connect()
Defaults[\.loginToken] = model.token
RelationStore.shared.preload()
if #available(iOS 16.0, *) {
AppRestrictManager.shared.checkScreenTimePermissionAfterLogin()
}
AppContextManager.shared.systemConfig = model.config

View File

@ -16,6 +16,7 @@ final class LockDistractVC: BaseViewController {
private var members: [GroupMemberModel] = []
private var selectedIndex = 0
private var lockAppsDisposable: Disposable?
private let lockStateSubscriptionOwner = UUID()
override var isNavigationBarHidden: Bool { true }
@ -31,6 +32,13 @@ final class LockDistractVC: BaseViewController {
loadGroupMembers()
}
@MainActor deinit {
MQTTService.shared.updateLockStateSubscriptions(
owner: lockStateSubscriptionOwner,
userIds: []
)
}
private func bind() {
rootView.navView.onBackTapped = { [weak self] in
self?.navigationController?.popViewController(animated: true)
@ -59,6 +67,15 @@ final class LockDistractVC: BaseViewController {
LockDistractPermissionPopView.show()
}).disposed(by: disposeBag)
rootView.onPrivilegeAction = { [weak self] in
guard let self else { return }
if self.selectedMember?.user_id == AppContextManager.shared.userId {
AppRouter.push(Route.privacySettings)
} else {
DLToast.show(text: "已提醒")
}
}
rootView.lockBtn.rx.tap
.subscribe(onNext: { [weak self] in
self?.performPrimaryAction()
@ -66,6 +83,18 @@ final class LockDistractVC: BaseViewController {
.disposed(by: disposeBag)
NotificationCenter.default.rx.notification(.lockDistractAppsDidChange)
.observe(on: MainScheduler.instance)
.subscribe(onNext: { [weak self] notification in
guard let self else { return }
let groupKey = notification.userInfo?[LockDistractAppsChangeUserInfoKey.groupKey] as? String ?? ""
let userId = notification.userInfo?[LockDistractAppsChangeUserInfoKey.userId] as? String ?? ""
guard groupKey.isEmpty || groupKey == self.activeGroupKey,
userId.isEmpty || userId == self.selectedMember?.user_id else { return }
self.loadLockableApps()
})
.disposed(by: disposeBag)
NotificationCenter.default.rx.notification(UIApplication.didBecomeActiveNotification)
.observe(on: MainScheduler.instance)
.subscribe(onNext: { [weak self] _ in
self?.loadLockableApps()
@ -83,6 +112,10 @@ final class LockDistractVC: BaseViewController {
private func applyOwnedGroups(from model: GroupModel) {
let owned = model.groups.filter(\.is_owner)
guard !owned.isEmpty else {
MQTTService.shared.updateLockStateSubscriptions(
owner: lockStateSubscriptionOwner,
userIds: []
)
DLToast.show(text: "只有圈主才能使用此功能") { [weak self] in
self?.navigationController?.popViewController(animated: true)
}
@ -126,6 +159,10 @@ final class LockDistractVC: BaseViewController {
private func applyMembers(_ members: [GroupMemberModel]) {
self.members = members
MQTTService.shared.updateLockStateSubscriptions(
owner: lockStateSubscriptionOwner,
userIds: members.map(\.user_id)
)
selectedIndex = 0
refreshMember()
rootView.setTodayLockCount(0)
@ -144,16 +181,19 @@ final class LockDistractVC: BaseViewController {
guard members.indices.contains(selectedIndex) else {
rootView.configureMember(name: " ")
rootView.configureLocked(false)
rootView.setPrivilegeDenied(false, isCurrentUser: false)
return
}
let m = members[selectedIndex]
let name = m.showName
let isSelf = m.user_id.trimmed == AppContextManager.shared.userId.trimmed
rootView.configureMember(
name: name,
headPic: m.displayHeadPic,
isCurrentUser: m.user_id.trimmed == AppContextManager.shared.userId.trimmed
isCurrentUser: isSelf
)
rootView.configureLocked(false)
rootView.setPrivilegeDenied(!m.extra.privilege.app_lock, isCurrentUser: isSelf)
}
private func updateGroupName() {
@ -186,6 +226,10 @@ final class LockDistractVC: BaseViewController {
lockAppsDisposable?.dispose()
rootView.configureLockableApps([])
guard let member = selectedMember, !activeGroupKey.isEmpty else { return }
if !member.extra.privilege.app_lock {
rootView.setTodayLockCount(0)
return
}
let targetUserId = member.user_id
let requestGroupKey = activeGroupKey
lockAppsDisposable = UserService.phoneLockApps(userId: targetUserId, groupKey: requestGroupKey)

View File

@ -117,6 +117,24 @@ final class LockDistractView: UIView {
(copyTF.text ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
}
var onPrivilegeAction: (() -> Void)?
private(set) var isPrivilegeDenied = false
private let privilegeOverlay = PrivilegeDeniedOverlay()
func setPrivilegeDenied(_ denied: Bool, isCurrentUser: Bool) {
isPrivilegeDenied = denied
privilegeOverlay.configure(
title: "TA未开启权限",
message: "无法查看可锁应用",
actionTitle: isCurrentUser ? "去开启" : "提醒TA"
)
privilegeOverlay.onAction = { [weak self] in self?.onPrivilegeAction?() }
privilegeOverlay.setVisible(denied)
if denied {
contentView.bringSubviewToFront(privilegeOverlay)
}
}
private func applyLockedState() {
updateLockedEmptyImage()
avatarLockView.isHidden = !isLocked
@ -166,6 +184,7 @@ final class LockDistractView: UIView {
bottomStack.addArrangedSubview(styleSectionTitle)
bottomStack.addArrangedSubview(styleCard)
bottomStack.addArrangedSubview(lockedEmptyView)
contentView.addSubview(privilegeOverlay)
navView.layoutChain
.edges(excludingEdge: .bottom)
@ -228,6 +247,13 @@ final class LockDistractView: UIView {
.edgesHorzontal(16)
.bottom(20)
privilegeOverlay.layoutChain
.topToView(appsSectionTitle)
.left(16)
.right(16)
.bottomToView(bottomStack)
privilegeOverlay.layer.cornerRadius = 26
styleSectionTitle.layoutChain.left(4)
lockedEmptyView.layoutChain
.centerX()

View File

@ -27,6 +27,9 @@ enum LoginSessionHandler {
Defaults[\.loginToken] = model.token
AppContextManager.shared.saveAccount(model)
RelationStore.shared.preload()
if #available(iOS 16.0, *) {
AppRestrictManager.shared.checkScreenTimePermissionAfterLogin()
}
DLToast.showSuccess(text: "登录成功")
DispatchQueue.main.async {
if let userId = model.uid {

View File

@ -30,6 +30,7 @@ class AccountVC: BaseViewController {
// Do any additional setup after loading the view.
reactiveAction()
rootView.refreshProfile()
DispatchQueue.global(qos: .background).async {
let totalByte = AppCacheManager.totalCacheSize()
@ -40,6 +41,11 @@ class AccountVC: BaseViewController {
}
}
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
rootView.refreshProfile()
}
private func reactiveAction() {
rootView.logoutBtn.rx.tap.subscribe(onNext: { _ in
@ -61,6 +67,97 @@ class AccountVC: BaseViewController {
let versionTap = UITapGestureRecognizer(target: self, action: #selector(handleVersionViewTap))
rootView.versionView.addGestureRecognizer(versionTap)
let avatarTap = UITapGestureRecognizer(target: self, action: #selector(handleAvatarTap))
rootView.avatarView.addGestureRecognizer(avatarTap)
let nickTap = UITapGestureRecognizer(target: self, action: #selector(handleNickNameTap))
rootView.nickNameView.addGestureRecognizer(nickTap)
let genderTap = UITapGestureRecognizer(target: self, action: #selector(handleGenderTap))
rootView.genderView.addGestureRecognizer(genderTap)
NotificationCenter.default.rx.notification(.RefreshUserConfigNotification)
.observe(on: MainScheduler.instance)
.subscribe(onNext: { [weak self] _ in
self?.rootView.refreshProfile()
})
.disposed(by: disposeBag)
}
@objc private func handleAvatarTap() {
let vc = AvatarIconListVC(iconIndex: AppContextManager.shared.head_pic)
vc.onSelectIcon = { [weak self] headPic in
self?.requestSetHeadPic(headPic)
}
navigationController?.pushViewController(vc, animated: true)
}
@objc private func handleNickNameTap() {
let vc = TextInputViewController(
title: "昵称",
maxLength: 10,
initialText: AppContextManager.shared.name
) { [weak self] text in
self?.requestSetNickName(text)
}
present(vc, animated: true)
}
@objc private func handleGenderTap() {
let pop = GenderSelectPopVC(currentSex: AppContextManager.shared.sex) { [weak self] sex in
self?.requestSetGender(sex)
}
present(pop, animated: true)
}
private func requestSetHeadPic(_ headPic: String) {
DLToast.showLoading()
UserService.setHeadPic(headPic: headPic).subscribe(onNext: { [weak self] _ in
DLToast.show(text: "更换成功")
if var account = AppContextManager.shared.account {
account.head_pic = headPic
account.avater = HeadPic.isRemote(headPic) ? headPic : ""
AppContextManager.shared.saveAccount(account)
}
self?.rootView.refreshProfile()
NotificationCenter.default.post(name: .RefreshUserConfigNotification, object: nil)
}, onError: { error in
DLToast.dismiss()
DLToast.show(text: error.localizedDescription)
}).disposed(by: disposeBag)
}
private func requestSetNickName(_ name: String) {
let trimmed = name.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return }
DLToast.showLoading()
UserService.setNickName(nick: trimmed).subscribe(onNext: { [weak self] _ in
DLToast.show(text: "修改成功")
if var account = AppContextManager.shared.account {
account.name = trimmed
AppContextManager.shared.saveAccount(account)
}
self?.rootView.refreshProfile()
NotificationCenter.default.post(name: .RefreshUserConfigNotification, object: nil)
}, onError: { error in
DLToast.dismiss()
DLToast.show(text: error.localizedDescription)
}).disposed(by: disposeBag)
}
private func requestSetGender(_ sex: Int) {
DLToast.showLoading()
UserService.setGender(sex: sex).subscribe(onNext: { [weak self] _ in
DLToast.show(text: "修改成功")
if var account = AppContextManager.shared.account {
account.sex = sex
AppContextManager.shared.saveAccount(account)
}
self?.rootView.refreshProfile()
NotificationCenter.default.post(name: .RefreshUserConfigNotification, object: nil)
}, onError: { error in
DLToast.dismiss()
DLToast.show(text: error.localizedDescription)
}).disposed(by: disposeBag)
}
// MARK: - API

View File

@ -33,6 +33,8 @@ class AccountView: UIView {
addSubview(infoView)
infoView.addSubview(infoStackView)
addSubview(profileView)
profileView.addSubview(profileStackView)
addSubview(otherInfoView)
otherInfoView.addSubview(otherInfoStackView)
@ -55,9 +57,18 @@ class AccountView: UIView {
userIdView.layoutChain.height(52)
phoneView.layoutChain.height(52)
profileView.layoutChain
.topToBottomOfView(infoView, offset: 18)
.edgesHorzontal(16)
profileStackView.layoutChain.edges()
avatarView.layoutChain.height(52)
nickNameView.layoutChain.height(52)
genderView.layoutChain.height(52)
otherInfoView.layoutChain
.topToBottomOfView(infoView, offset: 18)
.topToBottomOfView(profileView, offset: 18)
.edgesHorzontal(16)
otherInfoStackView.layoutChain.edges()
@ -193,6 +204,111 @@ class AccountView: UIView {
lab.textColor = UIColor(hexStr: "#1A1A1A")
return lab
}()
lazy var profileView: UIView = {
let view = UIView()
view.backgroundColor = .clear
view.layer.shadowColor = UIColor(hexStr: "#0F2846", alpha: 0.1).cgColor
view.layer.shadowOffset = CGSize(width: 0, height: 0)
view.layer.shadowOpacity = 1
view.layer.shadowRadius = 10
return view
}()
lazy var profileStackView: UIStackView = {
let view = UIStackView(arrangedSubviews: [avatarView, nickNameView, genderView])
view.axis = .vertical
view.alignment = .fill
view.distribution = .fill
view.spacing = 0
view.backgroundColor = .white
view.cornerRadius = 10
return view
}()
lazy var avatarView: UIView = {
makeProfileRow(title: "头像", accessory: avatarImgView, accessorySize: CGSize(width: 32, height: 32), showsLine: true)
}()
lazy var avatarImgView: UIImageView = {
let iv = UIImageView()
iv.contentMode = .scaleAspectFill
iv.clipsToBounds = true
iv.cornerRadius = 16
iv.backgroundColor = UIColor(hexStr: "#E0E0E0")
return iv
}()
lazy var nickNameView: UIView = {
makeProfileRow(title: "昵称", accessory: nickNameLab, showsLine: true)
}()
lazy var nickNameLab: UILabel = {
let lab = UILabel()
lab.font = .systemFont(ofSize: 14, weight: .medium)
lab.textColor = UIColor(hexStr: "#1A1A1A")
lab.textAlignment = .right
return lab
}()
lazy var genderView: UIView = {
makeProfileRow(title: "性别", accessory: genderLab, showsLine: false)
}()
lazy var genderLab: UILabel = {
let lab = UILabel()
lab.font = .systemFont(ofSize: 14, weight: .medium)
lab.textColor = UIColor(hexStr: "#1A1A1A")
lab.textAlignment = .right
return lab
}()
private func makeProfileRow(title: String, accessory: UIView, accessorySize: CGSize? = nil, showsLine: Bool) -> UIView {
let view = UIView()
view.backgroundColor = .clear
let titleLab = UILabel()
titleLab.text = title
titleLab.font = .systemFont(ofSize: 14, weight: .medium)
titleLab.textColor = UIColor(hexStr: "#1A1A1A")
view.addSubview(titleLab)
titleLab.layoutChain
.left(15)
.centerY()
let arrow = UIImageView(image: UIImage(named: "Group/arrow"))
view.addSubview(arrow)
arrow.layoutChain
.right(15)
.width(14)
.height(14)
.centerY()
view.addSubview(accessory)
if let accessorySize {
accessory.layoutChain
.rightToLeftOfView(arrow, offset: -8)
.centerY()
.width(accessorySize.width)
.height(accessorySize.height)
} else {
accessory.layoutChain
.rightToLeftOfView(arrow, offset: -8)
.centerY()
.leftToRightOfView(titleLab, offset: 12)
}
if showsLine {
let line = UIView()
line.backgroundColor = ThemeManager.shared.color.lineColor
view.addSubview(line)
line.layoutChain
.bottom()
.height(0.5)
.edgesHorzontal(15)
}
return view
}
//
lazy var otherInfoView: UIView = {
@ -306,11 +422,28 @@ class AccountView: UIView {
return btn
}()
func refreshProfile() {
avatarImgView.setHeadPic(AppContextManager.shared.head_pic)
nickNameLab.text = AppContextManager.shared.name
genderLab.text = Self.genderText(AppContextManager.shared.sex)
phoneLab.text = AppContextManager.shared.userPhone
}
static func genderText(_ sex: Int) -> String {
switch sex {
case 1: return ""
case 2: return ""
case 0: return "保密"
default: return "未设置"
}
}
override init(frame: CGRect) {
super.init(frame: .zero)
backgroundColor = UIColor(hexStr: "#FAFAFA")
setupUI()
setupRx()
refreshProfile()
}
required init?(coder aDecoder: NSCoder) {

View File

@ -463,6 +463,9 @@ final class MinePhotoWallView: UIView {
daysRow.isHidden = true
return
}
let selfColor = UIColor(hexStr: "#16B3FF")
let otherColor = UIColor(hexStr: "#293445")
let currentUserId = AppContextManager.shared.userId
if isPairMode {
titleLab.isHidden = true
pairHeartImg.isHidden = members.count < 2
@ -474,7 +477,9 @@ final class MinePhotoWallView: UIView {
}
pairLeftLab.text = names.first ?? " "
pairRightLab.text = names.count >= 2 ? names[1] : ""
let selfMember = members.first { $0.user_id == AppContextManager.shared.userId } ?? model
pairLeftLab.textColor = otherColor
pairRightLab.textColor = otherColor
let selfMember = members.first { $0.user_id == currentUserId } ?? model
daysLab.text = selfMember.joinDaysDisplay
} else {
titleLab.isHidden = false
@ -483,6 +488,7 @@ final class MinePhotoWallView: UIView {
pairRightLab.isHidden = true
let nickname = model.showName.trimmingCharacters(in: .whitespacesAndNewlines)
titleLab.text = nickname.isEmpty ? "未设置昵称" : nickname
titleLab.textColor = model.user_id == currentUserId ? selfColor : otherColor
daysLab.text = model.joinDaysDisplay
}
daysRow.isHidden = false

View File

@ -15,7 +15,7 @@ final class MineView: UIView {
private let vipCardHeight: CGFloat = 90
private let settingsRowHeight: CGFloat = 52
private let quickItemTitles = ["权限检测", "数据安全", "状态设置", "账户设置"]
private let quickItemTitles = ["权限检测", "隐私设置", "状态设置", "账户设置"]
var onQuickTap: ((String) -> Void)?
@ -257,7 +257,7 @@ final class MineView: UIView {
private func setupQuickActions() {
let icons = [
"Mine/quick_permission",
"Mine/quick_data_security",
"PrivacySettings/privacy_entry",
"Mine/quick_status",
"Mine/quick_account"
]

View File

@ -96,17 +96,21 @@ final class MineViewController: BaseViewController {
if !cached.isEmpty {
groupKey = cached
loadPhotoWallMembers(groupKey: cached)
return
}
GroupService.groupInfo().subscribe(onNext: { [weak self] response in
guard let self = self, let model = response.model else { return }
self.groupModel = model
self.groupKey = model.default_group_key
guard !model.default_group_key.isEmpty else {
self.reloadPhotoWall(members: [], groupKey: "")
let key = model.default_group_key
guard !key.isEmpty else {
if self.groupKey.isEmpty {
self.reloadPhotoWall(members: [], groupKey: "")
}
return
}
self.loadPhotoWallMembers(groupKey: model.default_group_key)
if self.groupKey != key {
self.groupKey = key
self.loadPhotoWallMembers(groupKey: key)
}
}).disposed(by: disposeBag)
}
@ -154,7 +158,8 @@ final class MineViewController: BaseViewController {
guard !members.isEmpty else { return }
var userInfo: [String: Any] = [
"members": members,
"userId": selectedUserId
"userId": selectedUserId,
"groupKey": groupKey
]
if let group = groupModel {
userInfo["groupJson"] = group.toJSON()

View File

@ -34,7 +34,7 @@ class MineViewModel {
///
let quickItems: [MineMenuItem] = [
MineMenuItem(icon: "Mine/quick_permission", title: "权限检测"),
MineMenuItem(icon: "Mine/quick_data_security", title: "数据安全"),
MineMenuItem(icon: "PrivacySettings/privacy_entry", title: "隐私设置"),
MineMenuItem(icon: "Mine/quick_status", title: "状态设置"),
MineMenuItem(icon: "Mine/quick_account", title: "账户设置")
]
@ -52,14 +52,14 @@ class MineViewModel {
AppRouter.push(Route.account)
case "紧急联系人":
AppRouter.push(Route.emergencyContact)
case "隐私与协议", "隐私设置":
case "隐私与协议":
AppRouter.push(Route.privacyPolicy)
case "隐私设置":
AppRouter.push(Route.privacySettings)
case "在线客服":
requestWechatService()
case "权限检测":
AppRouter.push(Route.checkPermission)
case "数据安全":
DLToast.show(text: "敬请期待")
case "状态设置":
AppRouter.push(Route.moodStatus)
case "关于我们":

View File

@ -0,0 +1,663 @@
//
// PrivacySettingsViewController.swift
// QuickLocation
//
import UIKit
import RxSwift
import Kingfisher
enum PrivacySettingItem: CaseIterable {
case phoneReport
case groupLock
case pigeonMessage
var title: String {
switch self {
case .phoneReport:
return "手机报告"
case .groupLock:
return "一键锁机"
case .pigeonMessage:
return "飞鸽传书"
}
}
var imageName: String {
switch self {
case .phoneReport:
return "PrivacySettings/phone_report"
case .groupLock:
return "PrivacySettings/group_lock"
case .pigeonMessage:
return "PrivacySettings/pigeon_message"
}
}
var apiKey: String {
switch self {
case .phoneReport:
return "phone_report"
case .groupLock:
return "app_lock"
case .pigeonMessage:
return "pigeon_mail"
}
}
}
final class PrivacySettingsViewController: BaseViewController {
private var rootView: PrivacySettingsView!
private var groups: [GroupInfoModel] = []
private var isSubmitting = false
override func loadView() {
rootView = PrivacySettingsView(frame: UIScreen.main.bounds)
view = rootView
}
override func viewDidLoad() {
super.viewDidLoad()
bind()
requestGroups()
}
private func bind() {
rootView.onGroupChanged = { [weak self] in
guard let self else { return }
self.loadPrivilege(for: self.rootView.selectedGroupKey)
}
rootView.onToggle = { [weak self] item, isOn in
self?.changePrivilege(item: item, isOn: isOn)
}
}
private func requestGroups() {
rootView.setSettingsEnabled(false)
GroupService.groupInfo()
.subscribe(onNext: { [weak self] response in
guard let self else { return }
guard let model = response.model else {
self.groups = []
self.rootView.configure(groups: [], selectedGroupKey: "")
DLToast.showError(text: response.message ?? "获取圈子信息失败")
return
}
self.groups = model.groups
self.rootView.configure(
groups: model.groups,
selectedGroupKey: model.default_group_key
)
self.loadPrivilege(for: self.rootView.selectedGroupKey)
}, onError: { [weak self] error in
self?.groups = []
self?.rootView.configure(groups: [], selectedGroupKey: "")
DLToast.showError(text: error.localizedDescription)
})
.disposed(by: disposeBag)
}
private func applySelectedPrivilege() {
let privilege = groups.first(where: { $0.group_key == rootView.selectedGroupKey })?.privilege
?? GroupPrivilege()
rootView.apply(privilege)
}
private func loadPrivilege(for groupKey: String) {
guard !groupKey.isEmpty else {
applySelectedPrivilege()
return
}
rootView.setSettingsEnabled(false)
GroupService.groupUsers(groupKey: groupKey)
.subscribe(onNext: { [weak self] response in
guard let self else { return }
self.rootView.setSettingsEnabled(!self.groups.isEmpty)
guard response.isValid(for: groupKey),
self.rootView.selectedGroupKey == groupKey else {
self.applySelectedPrivilege()
return
}
let mine = response.list.first { $0.user_id == AppContextManager.shared.userId }
if let index = self.groups.firstIndex(where: { $0.group_key == groupKey }) {
self.groups[index].privilege = mine?.extra.privilege ?? GroupPrivilege()
}
self.applySelectedPrivilege()
}, onError: { [weak self] error in
self?.rootView.setSettingsEnabled(!(self?.groups.isEmpty ?? true))
self?.applySelectedPrivilege()
DLToast.showError(text: error.localizedDescription)
})
.disposed(by: disposeBag)
}
private func changePrivilege(item: PrivacySettingItem, isOn: Bool) {
let groupKey = rootView.selectedGroupKey
guard !groupKey.isEmpty,
let index = groups.firstIndex(where: { $0.group_key == groupKey }) else {
applySelectedPrivilege()
return
}
guard !isSubmitting else {
applySelectedPrivilege()
return
}
if item == .groupLock, !isOn {
interceptTurnOffAppLock { [weak self] blocked in
guard let self else { return }
if blocked {
self.applySelectedPrivilege()
return
}
self.submitPrivilege(item: item, isOn: isOn, groupKey: groupKey, index: index)
}
return
}
submitPrivilege(item: item, isOn: isOn, groupKey: groupKey, index: index)
}
private func interceptTurnOffAppLock(completion: @escaping (Bool) -> Void) {
if let record = lockRecordForPopup(from: PhoneLockSession.currentLocks) {
LockedAppPopView.show(record)
completion(true)
return
}
let userId = AppContextManager.shared.userId.trimmed
let revision = AppUnlockCoordinator.shared.currentLockStateRevision()
isSubmitting = true
rootView.setSettingsEnabled(false)
UserService.phoneLocked(os: "ios")
.observe(on: MainScheduler.instance)
.subscribe(onNext: { [weak self] response in
guard let self else { return }
self.isSubmitting = false
self.rootView.setSettingsEnabled(!self.groups.isEmpty)
let locks = AppUnlockCoordinator.shared.locksForRestoration(
response.model?.locks ?? [],
startedAt: revision,
userId: userId
) ?? []
if !locks.isEmpty {
PhoneLockSession.currentLocks = locks
}
if let record = self.lockRecordForPopup(from: locks) {
LockedAppPopView.show(record)
completion(true)
} else {
completion(false)
}
}, onError: { [weak self] _ in
guard let self else { return }
self.isSubmitting = false
self.rootView.setSettingsEnabled(!self.groups.isEmpty)
completion(false)
})
.disposed(by: disposeBag)
}
private func lockRecordForPopup(from locks: [PhoneLockRecord]) -> PhoneLockRecord? {
let groupKey = rootView.selectedGroupKey
return locks.first { $0.groupKey == groupKey } ?? locks.first
}
private func submitPrivilege(item: PrivacySettingItem, isOn: Bool, groupKey: String, index: Int) {
let previous = groups[index].privilege
var updated = previous
switch item {
case .phoneReport:
updated.phone_report = isOn
case .groupLock:
updated.app_lock = isOn
case .pigeonMessage:
updated.pigeon_mail = isOn
}
groups[index].privilege = updated
rootView.apply(updated)
isSubmitting = true
rootView.setSettingsEnabled(false)
GroupService.operate(
opType: "changeprivilege",
requestData: [
"group_key": groupKey,
"privilege_code": item.apiKey,
"enable": isOn
]
)
.subscribe(onNext: { [weak self] response in
guard let self else { return }
self.isSubmitting = false
self.rootView.setSettingsEnabled(!self.groups.isEmpty)
guard response.code == "0" else {
self.revertPrivilege(groupKey: groupKey, previous: previous)
DLToast.showError(text: response.message ?? "设置失败")
return
}
}, onError: { [weak self] error in
guard let self else { return }
self.isSubmitting = false
self.rootView.setSettingsEnabled(!self.groups.isEmpty)
self.revertPrivilege(groupKey: groupKey, previous: previous)
DLToast.showError(text: error.localizedDescription)
})
.disposed(by: disposeBag)
}
private func revertPrivilege(groupKey: String, previous: GroupPrivilege) {
if let index = groups.firstIndex(where: { $0.group_key == groupKey }) {
groups[index].privilege = previous
}
applySelectedPrivilege()
}
}
final class PrivacySettingsView: UIView {
var onGroupChanged: (() -> Void)?
var onToggle: ((PrivacySettingItem, Bool) -> Void)?
var selectedGroupKey: String { groupSelectorView.selectedGroupKey }
private let navBackgroundView = UIImageView(image: UIImage(named: "Common/navBar_bg_2"))
private let navView = BaseNavigationView(title: "隐私设置")
private let scrollView = UIScrollView()
private let contentView = UIView()
private let groupSelectorView = PrivacyGroupSelectorView()
private let phoneReportRow = PrivacySettingRow(item: .phoneReport)
private let groupLockRow = PrivacySettingRow(item: .groupLock)
private let pigeonMessageRow = PrivacySettingRow(item: .pigeonMessage)
private let footerLabel = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configure(groups: [GroupInfoModel], selectedGroupKey: String) {
groupSelectorView.configure(groups: groups, selectedGroupKey: selectedGroupKey)
setSettingsEnabled(!groups.isEmpty)
}
func apply(_ privilege: GroupPrivilege) {
phoneReportRow.setOn(privilege.phone_report)
groupLockRow.setOn(privilege.app_lock)
pigeonMessageRow.setOn(privilege.pigeon_mail)
}
func setSettingsEnabled(_ enabled: Bool) {
[phoneReportRow, groupLockRow, pigeonMessageRow].forEach {
$0.setSwitchEnabled(enabled)
}
}
private func setupUI() {
backgroundColor = UIColor(hexStr: "#FAFAFA")
navBackgroundView.contentMode = .scaleAspectFill
navBackgroundView.clipsToBounds = true
addSubview(navBackgroundView)
addSubview(navView)
scrollView.showsVerticalScrollIndicator = false
scrollView.alwaysBounceVertical = false
addSubview(scrollView)
scrollView.addSubview(contentView)
footerLabel.text = "ⓘ 仅对当前选中圈子生效哟~"
footerLabel.textColor = UIColor(hexStr: "#536177")
footerLabel.font = .systemFont(ofSize: 13, weight: .regular)
footerLabel.textAlignment = .center
addSubview(footerLabel)
contentView.addSubview(groupSelectorView)
let shareHeader = makeSectionHeader(title: "圈子中可以共享的数据")
contentView.addSubview(shareHeader)
contentView.addSubview(phoneReportRow)
let ownerHeader = makeSectionHeader(title: "圈主可以对你使用的功能")
contentView.addSubview(ownerHeader)
contentView.addSubview(groupLockRow)
contentView.addSubview(pigeonMessageRow)
navBackgroundView.layoutChain
.edges(excludingEdge: .bottom)
.height(kNaviHeight)
navView.layoutChain
.edges(excludingEdge: .bottom)
.height(kNaviHeight)
footerLabel.layoutChain
.left(20)
.right(20)
.bottom(kSafeBottomMargin + 12)
.height(24)
scrollView.layoutChain
.topToBottomOfView(navView)
.left()
.right()
.bottomToTopOfView(footerLabel, offset: -8)
contentView.layoutChain
.edges()
.widthToView(scrollView)
groupSelectorView.layoutChain
.top(12)
.left(15)
.right(15)
.height(100)
shareHeader.layoutChain
.topToBottomOfView(groupSelectorView, offset: 20)
.left(17)
.right(15)
.height(24)
phoneReportRow.layoutChain
.topToBottomOfView(shareHeader, offset: 12)
.left(15)
.right(15)
.height(64)
ownerHeader.layoutChain
.topToBottomOfView(phoneReportRow, offset: 18)
.left(17)
.right(15)
.height(24)
groupLockRow.layoutChain
.topToBottomOfView(ownerHeader, offset: 12)
.left(15)
.right(15)
.height(64)
pigeonMessageRow.layoutChain
.topToBottomOfView(groupLockRow, offset: 12)
.left(15)
.right(15)
.height(64)
.bottom(20)
groupSelectorView.onSelectionChanged = { [weak self] in
self?.onGroupChanged?()
}
phoneReportRow.onToggle = { [weak self] isOn in
self?.onToggle?(.phoneReport, isOn)
}
groupLockRow.onToggle = { [weak self] isOn in
self?.onToggle?(.groupLock, isOn)
}
pigeonMessageRow.onToggle = { [weak self] isOn in
self?.onToggle?(.pigeonMessage, isOn)
}
}
private func makeSectionHeader(title: String) -> UIView {
let view = UIView()
let sparkleLabel = UILabel()
sparkleLabel.text = ""
sparkleLabel.textColor = UIColor(hexStr: "#08ADEF")
sparkleLabel.font = .systemFont(ofSize: 18, weight: .bold)
sparkleLabel.textAlignment = .center
view.addSubview(sparkleLabel)
let titleLabel = UILabel()
titleLabel.text = title
titleLabel.textColor = UIColor(hexStr: "#28364B")
titleLabel.font = .systemFont(ofSize: 16, weight: .regular)
view.addSubview(titleLabel)
sparkleLabel.layoutChain
.left()
.centerY()
.width(18)
.height(24)
titleLabel.layoutChain
.leftToRightOfView(sparkleLabel, offset: 3)
.right()
.centerY()
.height(24)
return view
}
}
private final class PrivacyGroupSelectorView: UIView,
UICollectionViewDataSource,
UICollectionViewDelegateFlowLayout {
var onSelectionChanged: (() -> Void)?
private(set) var selectedGroupKey = ""
private let layout = UICollectionViewFlowLayout()
private lazy var collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
private let emptyLabel = UILabel()
private var groups: [GroupInfoModel] = []
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .white
layer.cornerRadius = 20
clipsToBounds = true
layout.scrollDirection = .horizontal
layout.minimumLineSpacing = 4
layout.minimumInteritemSpacing = 4
layout.sectionInset = UIEdgeInsets(top: 4, left: 6, bottom: 4, right: 6)
collectionView.backgroundColor = .clear
collectionView.showsHorizontalScrollIndicator = false
collectionView.alwaysBounceHorizontal = true
collectionView.dataSource = self
collectionView.delegate = self
collectionView.register(PrivacyGroupCell.self)
addSubview(collectionView)
collectionView.layoutChain.edges()
emptyLabel.text = "暂无圈子"
emptyLabel.textColor = UIColor(hexStr: "#9BA3AF")
emptyLabel.font = .systemFont(ofSize: 15, weight: .regular)
emptyLabel.textAlignment = .center
emptyLabel.isHidden = true
addSubview(emptyLabel)
emptyLabel.layoutChain.edges()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configure(groups: [GroupInfoModel], selectedGroupKey: String) {
self.groups = groups
if groups.contains(where: { $0.group_key == selectedGroupKey }) {
self.selectedGroupKey = selectedGroupKey
} else {
self.selectedGroupKey = groups.first?.group_key ?? ""
}
emptyLabel.isHidden = !groups.isEmpty
collectionView.isHidden = groups.isEmpty
collectionView.reloadData()
guard let index = groups.firstIndex(where: { $0.group_key == self.selectedGroupKey }) else {
return
}
collectionView.layoutIfNeeded()
collectionView.scrollToItem(
at: IndexPath(item: index, section: 0),
at: .centeredHorizontally,
animated: false
)
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
groups.count
}
func collectionView(_ collectionView: UICollectionView,
cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell: PrivacyGroupCell = collectionView.dequeueReusableCell(for: indexPath)
let group = groups[indexPath.item]
cell.configure(group: group, selected: group.group_key == selectedGroupKey)
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let groupKey = groups[indexPath.item].group_key
guard selectedGroupKey != groupKey else { return }
selectedGroupKey = groupKey
collectionView.reloadData()
collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
onSelectionChanged?()
}
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath) -> CGSize {
CGSize(width: 76, height: 92)
}
}
private final class PrivacyGroupCell: UICollectionViewCell {
private let selectionView = UIView()
private let selectionPointer = UIView()
private let iconView = UIImageView()
private let nameLabel = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
selectionView.backgroundColor = UIColor(hexStr: "#C9F4FF")
selectionView.layer.cornerRadius = 13
selectionView.isHidden = true
contentView.addSubview(selectionView)
selectionPointer.backgroundColor = UIColor(hexStr: "#C9F4FF")
selectionPointer.transform = CGAffineTransform(rotationAngle: .pi / 4)
selectionPointer.isHidden = true
contentView.addSubview(selectionPointer)
iconView.contentMode = .scaleAspectFill
iconView.clipsToBounds = true
iconView.layer.cornerRadius = 25
contentView.addSubview(iconView)
nameLabel.textColor = UIColor(hexStr: "#26364B")
nameLabel.font = .systemFont(ofSize: 14, weight: .regular)
nameLabel.textAlignment = .center
nameLabel.lineBreakMode = .byTruncatingTail
contentView.addSubview(nameLabel)
selectionView.layoutChain
.top(1)
.left(2)
.right(2)
.bottom(4)
selectionPointer.layoutChain
.centerX()
.bottom(1)
.width(12)
.height(12)
iconView.layoutChain
.top(8)
.centerX()
.width(50)
.height(50)
nameLabel.layoutChain
.topToBottomOfView(iconView, offset: 5)
.left(5)
.right(5)
.height(20)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareForReuse() {
super.prepareForReuse()
iconView.kf.cancelDownloadTask()
iconView.image = nil
}
func configure(group: GroupInfoModel, selected: Bool) {
nameLabel.text = group.name
iconView.setGroupIcon(group)
selectionView.isHidden = !selected
selectionPointer.isHidden = !selected
}
}
private final class PrivacySettingRow: UIView {
var onToggle: ((Bool) -> Void)?
private let settingSwitch = UISwitch()
init(item: PrivacySettingItem) {
super.init(frame: .zero)
backgroundColor = .white
layer.cornerRadius = 20
let imageView = UIImageView(image: UIImage(named: item.imageName))
imageView.contentMode = .scaleAspectFit
addSubview(imageView)
let titleLabel = UILabel()
titleLabel.text = item.title
titleLabel.textColor = UIColor(hexStr: "#28364B")
titleLabel.font = .systemFont(ofSize: 17, weight: .regular)
addSubview(titleLabel)
settingSwitch.onTintColor = UIColor(hexStr: "#08ADEF")
settingSwitch.setOn(false, animated: false)
settingSwitch.addTarget(self, action: #selector(switchChanged), for: .valueChanged)
addSubview(settingSwitch)
imageView.layoutChain
.left(8)
.centerY()
.width(96)
.height(50)
settingSwitch.layoutChain
.right(13)
.centerY()
titleLabel.layoutChain
.leftToRightOfView(imageView, offset: 8)
.rightToLeftOfView(settingSwitch, offset: -8)
.centerY()
.height(26)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setSwitchEnabled(_ enabled: Bool) {
settingSwitch.isEnabled = enabled
}
func setOn(_ on: Bool) {
settingSwitch.setOn(on, animated: false)
}
@objc private func switchChanged() {
onToggle?(settingSwitch.isOn)
}
}

View File

@ -6,6 +6,7 @@
import UIKit
import AVFoundation
import RxSwift
import SwiftyUserDefaults
final class PigeonMessageVC: BaseViewController {
@ -479,6 +480,10 @@ final class PigeonMessageVC: BaseViewController {
}
@objc private func sendMessage() {
if let wait = pigeonSendWaitSeconds() {
DLToast.show(text: "发送过于频繁,请\(Self.formatWaitDuration(wait))后再试")
return
}
guard !selectedMembers.isEmpty else {
DLToast.show(text: "请先选择收件人")
return
@ -574,6 +579,7 @@ final class PigeonMessageVC: BaseViewController {
self.rootView.clearImageDraft()
self.cleanupAudio(deleteFile: true)
self.rootView.clearVoiceDraft()
self.recordPigeonSendSuccess()
DLToast.show(text: "发送成功")
}, onError: { error in
DLToast.dismiss()
@ -582,6 +588,44 @@ final class PigeonMessageVC: BaseViewController {
.disposed(by: disposeBag)
}
private func pigeonSendWaitSeconds() -> Int? {
guard let config = AppContextManager.shared.systemConfig?.pigeonConfig,
config.isEnabled else { return nil }
let userId = AppContextManager.shared.userId
guard !userId.isEmpty else { return nil }
let window = TimeInterval(config.times)
let now = Date().timeIntervalSince1970
let recent = (Defaults[\.pigeonSendAtByUser][userId] ?? []).filter { now - $0 < window }
guard recent.count >= config.nums else { return nil }
let oldest = recent.min() ?? now
return max(1, Int(ceil(window - (now - oldest))))
}
static func formatWaitDuration(_ seconds: Int) -> String {
let total = max(0, seconds)
let hours = total / 3600
let minutes = (total % 3600) / 60
let secs = total % 60
var parts: [String] = []
if hours > 0 { parts.append("\(hours)小时") }
if minutes > 0 { parts.append("\(minutes)分钟") }
if secs > 0 || parts.isEmpty { parts.append("\(secs)") }
return parts.joined()
}
private func recordPigeonSendSuccess() {
guard let config = AppContextManager.shared.systemConfig?.pigeonConfig,
config.isEnabled else { return }
let userId = AppContextManager.shared.userId
guard !userId.isEmpty else { return }
let now = Date().timeIntervalSince1970
let window = TimeInterval(config.times)
var store = Defaults[\.pigeonSendAtByUser]
let recent = (store[userId] ?? []).filter { now - $0 < window }
store[userId] = recent + [now]
Defaults[\.pigeonSendAtByUser] = store
}
private func compressedPigeonJPEG(from image: UIImage) -> Data? {
let maxEdge: CGFloat = 1280
let quality: CGFloat = 0.55

View File

@ -674,9 +674,13 @@ extension PigeonMessageView: UICollectionViewDataSource, UICollectionViewDelegat
private extension PigeonMessageView {
func toggleMember(at index: Int) {
guard members.indices.contains(index) else { return }
let id = members[index].user_id
let member = members[index]
let id = member.user_id
if selectedMemberIds.contains(id) {
selectedMemberIds.remove(id)
} else if !member.extra.privilege.pigeon_mail {
DLToast.show(text: "该用户未开启设置,无法接收飞鸽传书")
return
} else {
selectedMemberIds.insert(id)
}

View File

@ -168,9 +168,8 @@ class FullscreenWebView: WKWebView {
super.viewDidLoad()
fd_interactivePopDisabled = true
if style == "default" {
let btn = UIButton(type: .system)
let btn = UIButton(type: .custom)
btn.setImage(UIImage(named: "Common/back"), for: .normal)
btn.tintColor = UIColor(hexStr: "#0F2846")
btn.addTarget(self, action: #selector(leftBackClicked(_:)), for: .touchUpInside)
navigationItem.leftBarButtonItem = UIBarButtonItem(customView: btn)
}
@ -315,17 +314,19 @@ class FullscreenWebView: WKWebView {
// backItem.tintColor = iconColor=="white" ? .white : .black
// backItem.tag = 1
let backBtn = UIButton(frame: CGRect(x: 0, y: 0, width: 19, height: 19))
let backBtn = UIButton(type: .custom)
backBtn.frame = CGRect(x: 0, y: 0, width: 19, height: 19)
backBtn.setImage(UIImage(named: "Common/back"), for: .normal)
backBtn.addTarget(self, action: #selector(leftBackClicked(_:)), for: .touchUpInside)
backBtn.tintColor = .black
// backBtn.tintColor = .black
backBtn.tag = 1
let backItem = UIBarButtonItem(customView: backBtn)
let popBtn = UIButton(frame: CGRect(x: 0, y: 0, width: 19, height: 19))
let popBtn = UIButton(type: .custom)
popBtn.frame = CGRect(x: 0, y: 0, width: 19, height: 19)
popBtn.setImage(UIImage(named: "Common/x_black"), for: .normal)
popBtn.addTarget(self, action: #selector(leftBackClicked(_:)), for: .touchUpInside)
popBtn.tintColor = .black
// popBtn.tintColor = .black
popBtn.tag = 2
let popItem = UIBarButtonItem(customView: popBtn)

View File

@ -13,7 +13,7 @@ struct GroupService {
static let disposeBag = DisposeBag()
private static let notifyingOperations: Set<String> = [
"create", "join", "setdefault", "editmateinfo"
"create", "join", "setdefault", "editmateinfo", "changeprivilege"
]
private static func postGroupDataChange(operation: String,

View File

@ -41,6 +41,13 @@ struct SystemService {
.map(ResponseModel.self)
.asObservable()
}
static func sosList() -> Observable<SOSListResponse> {
let api = SystemAPI.sosList.multiTarget
return APIProvider.request(token: api, handle: false)
.map(SOSListResponse.self)
.asObservable()
}
///
/// - Parameters:

View File

@ -371,6 +371,27 @@ struct UserService {
.asObservable()
}
static func mailboxList(page: Int, size: Int = 10) -> Observable<MailboxListResponse> {
let api = UserAPI.mailboxList(page: page, size: size).multiTarget
return APIProvider.request(token: api, handle: false)
.map(MailboxListResponse.self)
.asObservable()
}
static func mailboxDetail(id: Int) -> Observable<MailboxDetailResponse> {
let api = UserAPI.mailboxOperate(opType: "info", id: id).multiTarget
return APIProvider.request(token: api, handle: false)
.map(MailboxDetailResponse.self)
.asObservable()
}
static func clearMailbox() -> Observable<ResponseModel> {
let api = UserAPI.mailboxOperate(opType: "alldelete", id: nil).multiTarget
return APIProvider.request(token: api, handle: false)
.map(ResponseModel.self)
.asObservable()
}
///
static func feedback(
type: String,

View File

@ -22,7 +22,7 @@ public class DLToastConfig {
///
var containerBgColor: UIColor = .black.withAlphaComponent(0.7)
///
var containerRadius: CGFloat = 12.0
var containerRadius: CGFloat = 20.0
///
var textColor: UIColor = .white
///