ソースを参照

feat: 用户定位改为每 3 小时获取一次,增加通用官方消息逻辑

陈文艺 3 ヶ月 前
コミット
acc448f046

+ 7 - 2
Lanu.xcodeproj/project.pbxproj

@@ -196,6 +196,7 @@
 				Views/IM/Chat/ViewModel/LNIMChatViewModel.swift,
 				Views/IM/ConversationList/LNIMConversationCell.swift,
 				Views/IM/ConversationList/LNIMConversationListController.swift,
+				Views/IM/ConversationList/LNIMNotificationPermissionView.swift,
 				Views/IM/Notify/Cell/LNIMOfficialMessageCell.swift,
 				Views/IM/Notify/LNIMOfficialMessageViewController.swift,
 				Views/Login/LNLoginPanel.swift,
@@ -518,12 +519,13 @@
 				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
 				CLANG_ENABLE_MODULES = YES;
 				CODE_SIGN_STYLE = Automatic;
-				CURRENT_PROJECT_VERSION = 7;
+				CURRENT_PROJECT_VERSION = 9;
 				DEVELOPMENT_TEAM = 5H8D98R72W;
 				ENABLE_USER_SCRIPT_SANDBOXING = NO;
 				GENERATE_INFOPLIST_FILE = YES;
 				INFOPLIST_FILE = Lanu/Info.plist;
 				INFOPLIST_KEY_CFBundleDisplayName = "Gami(Debug)";
+				INFOPLIST_KEY_NSAccessoryTrackingUsageDescription = "";
 				INFOPLIST_KEY_NSCameraUsageDescription = "need permission";
 				INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = 12;
 				INFOPLIST_KEY_NSMicrophoneUsageDescription = "need permission";
@@ -560,12 +562,13 @@
 				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
 				CLANG_ENABLE_MODULES = YES;
 				CODE_SIGN_STYLE = Automatic;
-				CURRENT_PROJECT_VERSION = 7;
+				CURRENT_PROJECT_VERSION = 9;
 				DEVELOPMENT_TEAM = 5H8D98R72W;
 				ENABLE_USER_SCRIPT_SANDBOXING = NO;
 				GENERATE_INFOPLIST_FILE = YES;
 				INFOPLIST_FILE = Lanu/Info.plist;
 				INFOPLIST_KEY_CFBundleDisplayName = "Gami(Debug)";
+				INFOPLIST_KEY_NSAccessoryTrackingUsageDescription = "";
 				INFOPLIST_KEY_NSCameraUsageDescription = "need permission";
 				INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = 12;
 				INFOPLIST_KEY_NSMicrophoneUsageDescription = "need permission";
@@ -648,6 +651,7 @@
 				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
 				GCC_WARN_UNUSED_FUNCTION = YES;
 				GCC_WARN_UNUSED_VARIABLE = YES;
+				INFOPLIST_KEY_NSAccessoryTrackingUsageDescription = "";
 				IPHONEOS_DEPLOYMENT_TARGET = 26.0;
 				LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
 				MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
@@ -709,6 +713,7 @@
 				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
 				GCC_WARN_UNUSED_FUNCTION = YES;
 				GCC_WARN_UNUSED_VARIABLE = YES;
+				INFOPLIST_KEY_NSAccessoryTrackingUsageDescription = "";
 				IPHONEOS_DEPLOYMENT_TARGET = 26.0;
 				LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
 				MTL_ENABLE_DEBUG_INFO = NO;

+ 22 - 0
Lanu/Assets.xcassets/IM/ic_im_unknown_official.imageset/Contents.json

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

BIN
Lanu/Assets.xcassets/IM/ic_im_unknown_official.imageset/ic_im_unknown_official@2x.png


BIN
Lanu/Assets.xcassets/IM/ic_im_unknown_official.imageset/ic_im_unknown_official@3x.png


+ 3 - 0
Lanu/Common/Storage/LNUserDefaultsKey.swift

@@ -21,4 +21,7 @@ enum LNUserDefaultsKey: String {
     case imRecentEmojis
     
     case purchaseOrderId
+    
+    case location
+    case locationTime
 }

+ 5 - 8
Lanu/Manager/IM/LNIMManager.swift

@@ -20,7 +20,7 @@ extension LNIMManagerNotify {
 
 extension String {
     var isImOfficialId: Bool {
-        self == LNIMManager.officialId
+        (Int(self) ?? -1) <= (Int(LNIMManager.officialId) ?? 0)
     }
 }
 
@@ -52,14 +52,11 @@ extension LNIMManager {
                     handler?(false)
                     return
                 }
-                // 1. 按照时间排序
-                list.sort { $0.orderKey > $1.orderKey }
+                // 按照时间排序
+                list.sort { $0.userID?.isImOfficialId == true && $0.orderKey > $1.orderKey }
                 
-                // 2. 官方消息插入首位
-                if let index = list.firstIndex(where: { $0.userID?.isImOfficialId == true }) {
-                    let item = list.remove(at: index)
-                    list.insert(item, at: 0)
-                } else {
+                if list.firstIndex(where: { $0.userID?.isImOfficialId == true }) == nil {
+                    // 插入官方消息
                     V2TIMManager.sharedInstance().setConversationDraft(conversationID: "c2c_" + Self.officialId, draftText: " ") {
                         V2TIMManager.sharedInstance().setConversationDraft(conversationID: "c2c_" + Self.officialId, draftText: nil, succ: nil)
                     }

+ 27 - 3
Lanu/Manager/Location/LNLocationManager.swift

@@ -7,13 +7,35 @@
 
 import Foundation
 import CoreLocation
+import AutoCodable
+
+
+@AutoCodable
+private class LNUserLocation: Codable {
+    var latitude: CLLocationDegrees = 0
+    var longitude: CLLocationDegrees = 0
+    
+    init(latitude: CLLocationDegrees, longitude: CLLocationDegrees) {
+        self.latitude = latitude
+        self.longitude = longitude
+    }
+}
 
 
 class LNLocationManager: NSObject {
     static let shared = LNLocationManager()
     private let locationManager = CLLocationManager()
     
-    private var curLocation: CLLocationCoordinate2D?
+    private var curLocation: LNUserLocation? = LNUserDefaults[.location] {
+        didSet {
+            LNUserDefaults[.location] = curLocation
+        }
+    }
+    private var lastTime: TimeInterval? = LNUserDefaults[.locationTime] {
+        didSet {
+            LNUserDefaults[.locationTime] = lastTime
+        }
+    }
     
     private override init() {
         super.init()
@@ -42,7 +64,8 @@ extension LNLocationManager: CLLocationManagerDelegate {
         
         manager.stopUpdatingLocation()
         
-        curLocation = userLocation.coordinate
+        curLocation = .init(latitude: userLocation.coordinate.latitude, longitude: userLocation.coordinate.longitude)
+        lastTime = curTime
         uploadLocation(coordinate: userLocation.coordinate)
     }
     
@@ -57,7 +80,8 @@ extension LNLocationManager: LNAccountManagerNotify {
 
 extension LNLocationManager {
     private func getLocationIfNeed() {
-        if curLocation == nil {
+        if curLocation == nil
+            || curTime - (lastTime ?? 0) > 3 * 60 * 60 { // 3 小时内不拉取
             locationManager.requestWhenInUseAuthorization()  // 请求使用时的定位权限
             locationManager.startUpdatingLocation()  // 开始更新位置
         }

+ 7 - 2
Lanu/Views/IM/ConversationList/LNIMConversationCell.swift

@@ -27,8 +27,13 @@ class LNIMConversationCell: UITableViewCell {
     
     func update(_ item: V2TIMConversation) {
         if item.userID?.isImOfficialId == true {
-            avatar.image = .init(named: "ic_im_official")
-            titleLabel.text = .init(key: "System Message")
+            if item.userID == LNIMManager.officialId {
+                avatar.image = .init(named: "ic_im_official")
+                titleLabel.text = .init(key: "System Message")
+            } else {
+                avatar.image = .init(named: "ic_im_unknown_official")
+                titleLabel.text = item.showName
+            }
             titleLabel.textColor = .text_6
         } else {
             titleLabel.text = item.showName

+ 12 - 4
Lanu/Views/IM/ConversationList/LNIMConversationListController.swift

@@ -12,6 +12,7 @@ import MJRefresh
 
 
 class LNIMConversationListController: UIViewController {
+    private let notifyView = LNIMNotificationPermissionView()
     private let tableView = UITableView()
     
     override func viewDidLoad() {
@@ -60,7 +61,7 @@ extension LNIMConversationListController: UITableViewDataSource, UITableViewDele
         guard let uid = data.userID else { return }
         
         if uid.isImOfficialId {
-            view.pushToOfficialMessage()
+            view.pushToOfficialMessage(uid: uid)
         } else {
             view.pushToChat(uid: uid)
         }
@@ -88,13 +89,20 @@ extension LNIMConversationListController {
             make.top.equalToSuperview()
         }
         
-        let tableView = buildTableView()
-        view.addSubview(tableView)
-        tableView.snp.makeConstraints { make in
+        let stackView = UIStackView()
+        stackView.axis = .vertical
+        stackView.spacing = 16
+        view.addSubview(stackView)
+        stackView.snp.makeConstraints { make in
             make.directionalHorizontalEdges.equalToSuperview()
             make.bottom.equalToSuperview()
             make.top.equalTo(topMenu.snp.bottom)
         }
+        
+        stackView.addArrangedSubview(notifyView)
+        
+        let tableView = buildTableView()
+        stackView.addArrangedSubview(tableView)
     }
     
     private func buildTopMenu() -> UIView {

+ 147 - 0
Lanu/Views/IM/ConversationList/LNIMNotificationPermissionView.swift

@@ -0,0 +1,147 @@
+//
+//  LNIMNotificationPermissionView.swift
+//  Lanu
+//
+//  Created by OneeChan on 2025/12/30.
+//
+
+import Foundation
+import UIKit
+import SnapKit
+
+
+class LNIMNotificationPermissionView: UIView {
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        
+        setupViews()
+        checkPermission()
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+}
+
+extension LNIMNotificationPermissionView {
+    private func requestPermission() {
+        let center = UNUserNotificationCenter.current()
+        center.requestAuthorization(options: [.alert, .sound, .badge])
+        { [weak self] granted, error in
+            guard let self else { return }
+            guard granted else { return }
+            DispatchQueue.main.async {
+                UIApplication.shared.registerForRemoteNotifications()
+            }
+            checkPermission()
+        }
+    }
+    
+    private func checkPermission() {
+        UNUserNotificationCenter.current().getNotificationSettings { [weak self] settings in
+            guard let self else { return }
+            DispatchQueue.main.async { [weak self] in
+                guard let self else { return }
+                switch settings.authorizationStatus {
+                case .authorized, .provisional, .ephemeral:
+                    isHidden = true
+                    break
+                case .notDetermined:
+                    requestPermission()
+                    isHidden = false
+                default:
+                    isHidden = false
+                    break
+                }
+            }
+        }
+    }
+    
+    private func openNotificationSettings() {
+        let settingsURL: URL? =
+        if #available(iOS 16.0, *) {
+            URL(string: UIApplication.openNotificationSettingsURLString)
+        } else {
+            URL(string: UIApplication.openSettingsURLString)
+        }
+        guard let settingsURL else { return }
+        guard UIApplication.shared.canOpenURL(settingsURL) else { return }
+        
+        UIApplication.shared.open(settingsURL) { _ in }
+    }
+}
+
+extension LNIMNotificationPermissionView {
+    private func setupViews() {
+        let container = UIView()
+        container.backgroundColor = .fill
+        container.layer.cornerRadius = 12
+        addSubview(container)
+        container.snp.makeConstraints { make in
+            make.directionalHorizontalEdges.equalToSuperview().inset(16)
+            make.verticalEdges.equalToSuperview()
+            make.height.equalTo(50)
+        }
+        
+        let close = UIButton()
+        close.setImage(.init(systemName: "xmark")?.withRenderingMode(.alwaysTemplate), for: .normal)
+        close.tintColor = .text_2
+        close.addAction(UIAction(handler: { [weak self] _ in
+            guard let self else { return }
+            isHidden = true
+        }), for: .touchUpInside)
+        container.addSubview(close)
+        close.snp.makeConstraints { make in
+            make.leading.equalToSuperview().offset(10)
+            make.centerY.equalToSuperview()
+            make.width.height.equalTo(20)
+        }
+        
+        let open = UIButton()
+        open.setTitle(.init(key: "Turn on"), for: .normal)
+        open.titleLabel?.font = .body_s
+        open.setTitleColor(.text_1, for: .normal)
+        open.setBackgroundImage(.primary_7, for: .normal)
+        open.contentEdgeInsets = .init(top: 5, left: 10, bottom: 5, right: 10)
+        open.clipsToBounds = true
+        open.layer.cornerRadius = 12
+        open.addAction(UIAction(handler: { [weak self] _ in
+            guard let self else { return }
+            openNotificationSettings()
+        }), for: .touchUpInside)
+        container.addSubview(open)
+        open.snp.makeConstraints { make in
+            make.trailing.equalToSuperview().offset(-10)
+            make.centerY.equalToSuperview()
+            make.height.equalTo(24)
+        }
+        
+        let textView = UIView()
+        container.addSubview(textView)
+        textView.snp.makeConstraints { make in
+            make.centerY.equalToSuperview()
+            make.leading.equalTo(close.snp.trailing).offset(12)
+            make.trailing.lessThanOrEqualTo(open.snp.leading).offset(-12)
+        }
+        
+        let titleLabel = UILabel()
+        titleLabel.text = .init(key: "Open notifications")
+        titleLabel.font = .heading_h5
+        titleLabel.textColor = .text_5
+        textView.addSubview(titleLabel)
+        titleLabel.snp.makeConstraints { make in
+            make.leading.top.equalToSuperview()
+        }
+        
+        let textLabel = UILabel()
+        textLabel.text = .init(key: "Receive important information")
+        textLabel.font = .body_xs
+        textLabel.textColor = .text_5
+        textView.addSubview(textLabel)
+        textLabel.snp.makeConstraints { make in
+            make.directionalHorizontalEdges.equalToSuperview()
+            make.bottom.equalToSuperview()
+            make.top.equalTo(titleLabel.snp.bottom).offset(2)
+        }
+    }
+}

+ 13 - 4
Lanu/Views/IM/Notify/LNIMOfficialMessageViewController.swift

@@ -11,18 +11,23 @@ import SnapKit
 
 
 extension UIView {
-    func pushToOfficialMessage() {
-        let vc = LNIMOfficialMessageViewController()
+    func pushToOfficialMessage(uid: String) {
+        let vc = LNIMOfficialMessageViewController(uid: uid)
         navigationController?.pushViewController(vc, animated: true)
     }
 }
 
 
 class LNIMOfficialMessageViewController: LNViewController {
-    private let viewModel = LNIMChatViewModel(userId: LNIMManager.officialId)
+    private let viewModel: LNIMChatViewModel
     
     private let tableView = UITableView()
     
+    init(uid: String) {
+        viewModel = LNIMChatViewModel(userId: LNIMManager.officialId)
+        super.init(nibName: nil, bundle: nil)
+    }
+    
     override func viewDidLoad() {
         super.viewDidLoad()
         
@@ -37,6 +42,10 @@ class LNIMOfficialMessageViewController: LNViewController {
         
         viewModel.cleanUnread()
     }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
 }
 
 extension LNIMOfficialMessageViewController {
@@ -134,7 +143,7 @@ extension LNIMOfficialMessageViewController {
     private func setupViews() {
         view.backgroundColor = .primary_1
         
-        title = .init(key: "System Message")
+        title = viewModel.userId == LNIMManager.officialId ? .init(key: "System Message") : .init(key: "Official Message")
         
         tableView.backgroundColor = .clear
         tableView.separatorStyle = .none

+ 1 - 0
Lanu/Views/Search/LNUserSearchViewController.swift

@@ -8,6 +8,7 @@
 import Foundation
 import UIKit
 import SnapKit
+import MJRefresh
 
 
 extension UIView {