From 2fb18d85dbb2bc546cc14e15b31d683e7315e86a Mon Sep 17 00:00:00 2001 From: linshujie Date: Wed, 30 Sep 2026 16:08:13 +0800 Subject: [PATCH] =?UTF-8?q?-=20IM=E5=88=97=E8=A1=A8=E6=B2=A1=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=E6=9C=AA=E8=AF=BB=E6=B6=88=E6=81=AF=E6=95=B0=20-=20?= =?UTF-8?q?=E5=88=9B=E5=BB=BA=E8=A1=8C=E7=A8=8B=E7=9A=84=E6=97=B6=E9=97=B4?= =?UTF-8?q?=E9=99=90=E5=88=B6=20=20=E5=BC=80=E5=A7=8B=20<=20=E9=80=94?= =?UTF-8?q?=E5=BE=84=20<=20=E7=BB=93=E6=9D=9F=20-=20=E8=A1=8C=E7=A8=8B?= =?UTF-8?q?=E5=88=97=E8=A1=A8=E7=9A=84=E6=98=BE=E7=A4=BA=E6=A0=BC=E5=BC=8F?= =?UTF-8?q?=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- QuickLocation.xcodeproj/project.pbxproj | 4 +- QuickLocation/Info.plist | 23 ++++-- .../BaseViewController.swift | 6 +- .../Main/Tabbar/MainTabBarController.swift | 17 ++++- .../Section/Group/GroupChat/GroupChatVC.swift | 12 ++++ .../Group/GroupChat/GroupChatViewModel.swift | 7 +- .../Section/Group/GroupItineraryView.swift | 11 ++- .../Section/Group/GroupViewController.swift | 7 +- .../Section/Group/GroupViewModel.swift | 23 ++++-- .../CreateSchedule/CreateScheduleVC.swift | 71 ++++++++++++++++++- 10 files changed, 157 insertions(+), 24 deletions(-) diff --git a/QuickLocation.xcodeproj/project.pbxproj b/QuickLocation.xcodeproj/project.pbxproj index 07c421b7..17a56649 100644 --- a/QuickLocation.xcodeproj/project.pbxproj +++ b/QuickLocation.xcodeproj/project.pbxproj @@ -103,7 +103,6 @@ 305A76E22FCA8C7000227D26 /* GroupViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A76252FCA8C7000227D26 /* GroupViewModel.swift */; }; 305A76E52FCA8C7000227D26 /* HomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A76292FCA8C7000227D26 /* HomeViewController.swift */; }; 305A76E62FCA8C7000227D26 /* LaunchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A762B2FCA8C7000227D26 /* LaunchViewController.swift */; }; - 55B2A1013022F0A000784901 /* LaunchPrivacyConsentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B2A1003022F0A000784900 /* LaunchPrivacyConsentView.swift */; }; 305A76E72FCA8C7000227D26 /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A762D2FCA8C7000227D26 /* LoginView.swift */; }; 305A76E82FCA8C7000227D26 /* LoginViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A762E2FCA8C7000227D26 /* LoginViewController.swift */; }; 305A76E92FCA8C7000227D26 /* LoginViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A762F2FCA8C7000227D26 /* LoginViewModel.swift */; }; @@ -347,6 +346,7 @@ 55B250083038000100786508 /* MailboxDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B250073038000100786507 /* MailboxDetailViewController.swift */; }; 55B26001304B000100786601 /* VersionUpgradePopVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B26000304B000100786600 /* VersionUpgradePopVC.swift */; }; 55B261010000000100786601 /* PrivacySettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B261000000000100786600 /* PrivacySettingsViewController.swift */; }; + 55B2A1013022F0A000784901 /* LaunchPrivacyConsentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B2A1003022F0A000784900 /* LaunchPrivacyConsentView.swift */; }; 55BF752D2FFE53F70055DA57 /* LocationPermissionPopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55BF752C2FFE53F70055DA57 /* LocationPermissionPopView.swift */; }; 55C1A0113034101000786011 /* PhoneStatusCollector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55C1A0103034101000786010 /* PhoneStatusCollector.swift */; }; 55C1A0213034102000786021 /* PrivilegeDeniedOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55C1A0203034102000786020 /* PrivilegeDeniedOverlay.swift */; }; @@ -526,7 +526,6 @@ 305A76252FCA8C7000227D26 /* GroupViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupViewModel.swift; sourceTree = ""; }; 305A76292FCA8C7000227D26 /* HomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewController.swift; sourceTree = ""; }; 305A762B2FCA8C7000227D26 /* LaunchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchViewController.swift; sourceTree = ""; }; - 55B2A1003022F0A000784900 /* LaunchPrivacyConsentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchPrivacyConsentView.swift; sourceTree = ""; }; 305A762D2FCA8C7000227D26 /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = ""; }; 305A762E2FCA8C7000227D26 /* LoginViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginViewController.swift; sourceTree = ""; }; 305A762F2FCA8C7000227D26 /* LoginViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginViewModel.swift; sourceTree = ""; }; @@ -779,6 +778,7 @@ 55B250073038000100786507 /* MailboxDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MailboxDetailViewController.swift; sourceTree = ""; }; 55B26000304B000100786600 /* VersionUpgradePopVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionUpgradePopVC.swift; sourceTree = ""; }; 55B261000000000100786600 /* PrivacySettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacySettingsViewController.swift; sourceTree = ""; }; + 55B2A1003022F0A000784900 /* LaunchPrivacyConsentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchPrivacyConsentView.swift; sourceTree = ""; }; 55BF752C2FFE53F70055DA57 /* LocationPermissionPopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationPermissionPopView.swift; sourceTree = ""; }; 55C1A0103034101000786010 /* PhoneStatusCollector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhoneStatusCollector.swift; sourceTree = ""; }; 55C1A0203034102000786020 /* PrivilegeDeniedOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivilegeDeniedOverlay.swift; sourceTree = ""; }; diff --git a/QuickLocation/Info.plist b/QuickLocation/Info.plist index 923da73a..534be2ef 100644 --- a/QuickLocation/Info.plist +++ b/QuickLocation/Info.plist @@ -14,9 +14,18 @@ wx1e38bba788d55cde + + CFBundleTypeRole + Editor + CFBundleURLSchemes + + quciklocation + + LSApplicationQueriesSchemes + quciklocation baidumap qqmap iosamap @@ -30,6 +39,13 @@ NSExceptionDomains + cdn.u8t.cn + + NSExceptionAllowsInsecureHTTPLoads + + NSIncludesSubdomains + + cdn2.batiao8.com NSExceptionAllowsInsecureHTTPLoads @@ -51,13 +67,6 @@ NSIncludesSubdomains - cdn.u8t.cn - - NSExceptionAllowsInsecureHTTPLoads - - NSIncludesSubdomains - - UIAppFonts diff --git a/QuickLocation/Main/BaseViewController/BaseViewController.swift b/QuickLocation/Main/BaseViewController/BaseViewController.swift index 3f12989d..8d73cc45 100644 --- a/QuickLocation/Main/BaseViewController/BaseViewController.swift +++ b/QuickLocation/Main/BaseViewController/BaseViewController.swift @@ -56,13 +56,13 @@ class BaseViewController: UIViewController { print(className) MobClick.beginLogPageView(className) - // 返回根页面时恢复 tabBar 可见性(UINavigationControllerDelegate 在 pop 时可能漏触发) - (tabBarController as? MainTabBarController)?.updateTabBarVisibility() + if navigationController?.transitionCoordinator == nil { + (tabBarController as? MainTabBarController)?.updateTabBarVisibility() + } } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - // popToRoot 动画完成后 navigation stack 才减到 1,此时再恢复 tabBar (tabBarController as? MainTabBarController)?.updateTabBarVisibility() } diff --git a/QuickLocation/Main/Tabbar/MainTabBarController.swift b/QuickLocation/Main/Tabbar/MainTabBarController.swift index 9aa8a01b..dac62de1 100644 --- a/QuickLocation/Main/Tabbar/MainTabBarController.swift +++ b/QuickLocation/Main/Tabbar/MainTabBarController.swift @@ -113,6 +113,21 @@ extension MainTabBarController: QuickLocationTabBarDelegate { // MARK: - UINavigationControllerDelegate extension MainTabBarController: UINavigationControllerDelegate { func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) { - updateTabBarVisibility() + let hideTabBar = viewController !== navigationController.viewControllers.first + if hideTabBar { + customTabBar.isHidden = true + return + } + guard let coordinator = navigationController.transitionCoordinator else { + updateTabBarVisibility() + return + } + coordinator.animate(alongsideTransition: { _ in }, completion: { [weak self] context in + if context.isCancelled { + self?.updateTabBarVisibility() + } else { + self?.customTabBar.isHidden = false + } + }) } } diff --git a/QuickLocation/Section/Group/GroupChat/GroupChatVC.swift b/QuickLocation/Section/Group/GroupChat/GroupChatVC.swift index 7efa8b2c..39b4a2b6 100644 --- a/QuickLocation/Section/Group/GroupChat/GroupChatVC.swift +++ b/QuickLocation/Section/Group/GroupChat/GroupChatVC.swift @@ -68,6 +68,7 @@ final class GroupChatVC: BaseViewController { override func viewDidLoad() { super.viewDidLoad() + viewModel.isChatActive = true fd_interactivePopDisabled = true bindViewModel() reactiveAction() @@ -93,6 +94,7 @@ final class GroupChatVC: BaseViewController { override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) + viewModel.isChatActive = true setupKeyboard() IQKeyboardManager.shared.isEnabled = false IQKeyboardManager.shared.resignOnTouchOutside = false @@ -101,6 +103,7 @@ final class GroupChatVC: BaseViewController { override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) viewModel.markAsRead() + viewModel.isChatActive = false VoicePlayerManager.shared.stop() if isVoiceRecording { finishVoiceRecording(action: .discard) @@ -114,6 +117,9 @@ final class GroupChatVC: BaseViewController { override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) NotificationCenter.default.removeObserver(self) + if isMovingFromParent || isBeingDismissed { + removeMessageListener() + } } // MARK: - Keyboard @@ -428,6 +434,12 @@ final class GroupChatVC: BaseViewController { OIMManager.callbacker.addAdvancedMsgListener(listener: msgListener!) } + private func removeMessageListener() { + guard let listener = msgListener else { return } + OIMManager.callbacker.removeAdvancedMsgListener(listener: listener) + msgListener = nil + } + // MARK: - Long press (quote / revoke / avatar @) private func setupChatLongPress() { let longPress = UILongPressGestureRecognizer(target: self, action: #selector(handleChatLongPress(_:))) diff --git a/QuickLocation/Section/Group/GroupChat/GroupChatViewModel.swift b/QuickLocation/Section/Group/GroupChat/GroupChatViewModel.swift index bdc3a6e0..07e333a0 100644 --- a/QuickLocation/Section/Group/GroupChat/GroupChatViewModel.swift +++ b/QuickLocation/Section/Group/GroupChat/GroupChatViewModel.swift @@ -710,8 +710,11 @@ final class GroupChatViewModel { messagesSubject.accept([]) } + /// 仅当前聊天页在前台时为 true,避免退出后仍把新消息标成已读。 + var isChatActive = false + func markAsRead() { - guard !conversationID.isEmpty else { return } + guard isChatActive, !conversationID.isEmpty else { return } OIMManager.manager.markConversationMessage(asRead: conversationID, onSuccess: nil, onFailure: nil) @@ -735,7 +738,7 @@ final class GroupChatViewModel { // MARK: - Receive func onReceiveMessage(_ msg: OIMMessageInfo) { cacheMessage(msg) - if msg.sendID != AppContextManager.shared.userId { + if isChatActive, msg.sendID != AppContextManager.shared.userId { markAsRead() } guard let item = toSectionItem(msg) else { return } diff --git a/QuickLocation/Section/Group/GroupItineraryView.swift b/QuickLocation/Section/Group/GroupItineraryView.swift index d46a03b7..5bef3205 100644 --- a/QuickLocation/Section/Group/GroupItineraryView.swift +++ b/QuickLocation/Section/Group/GroupItineraryView.swift @@ -513,7 +513,16 @@ final class GroupItineraryCell: UITableViewCell { let minutes = max(1, (end.expected_timestamp - start.expected_timestamp) / 60_000) let hours = minutes / 60 let remainder = minutes % 60 - return hours > 0 ? "\(hours)小时\(remainder)分" : "\(minutes)分钟" + switch (hours > 0, remainder > 0) { + case (true, true): + return "\(hours)小时\(remainder)分" + case (true, false): + return "\(hours)小时" + case (false, true): + return "\(remainder)分钟" + case (false, false): + return "--" + } } } diff --git a/QuickLocation/Section/Group/GroupViewController.swift b/QuickLocation/Section/Group/GroupViewController.swift index 9eaa6ec3..a3534d9e 100644 --- a/QuickLocation/Section/Group/GroupViewController.swift +++ b/QuickLocation/Section/Group/GroupViewController.swift @@ -59,6 +59,7 @@ final class GroupViewController: BaseViewController { private var hasSetupIMListeners = false private var hasRequestedIMData = false private var isRequestingIMData = false + private var conversationListRequestID = UUID() /// 进入页面加载 IM 会话:圈子集合已由业务接口给出,这里只补未读/最后一条 private func loadIMData(showsLoading: Bool = false) { @@ -92,6 +93,7 @@ final class GroupViewController: BaseViewController { GroupIMService.shared.setConversationListener({ [weak self] conversations in DispatchQueue.main.async { + self?.conversationListRequestID = UUID() self?.viewModel.updateConversations(conversations) } }, onNeedsRefresh: { [weak self] in @@ -110,12 +112,15 @@ final class GroupViewController: BaseViewController { } private func refreshIMData(showsLoading: Bool = false) { + let requestID = UUID() + conversationListRequestID = requestID GroupIMService.shared.getConversationList { [weak self] conversations in DispatchQueue.main.async { if showsLoading { DLToast.dismiss() } - self?.viewModel.loadConversations(conversations) + guard let self, self.conversationListRequestID == requestID else { return } + self.viewModel.loadConversations(conversations) } } onFailure: { DispatchQueue.main.async { diff --git a/QuickLocation/Section/Group/GroupViewModel.swift b/QuickLocation/Section/Group/GroupViewModel.swift index dc7b86b5..b2d459ec 100644 --- a/QuickLocation/Section/Group/GroupViewModel.swift +++ b/QuickLocation/Section/Group/GroupViewModel.swift @@ -72,17 +72,32 @@ final class GroupViewModel { /// 仅更新会话元数据(未读、时间)- 消息监听回调(只返回变更的会话,需合并) func updateConversations(_ conversations: [OIMConversationInfo]) { var merged: [String: OIMConversationInfo] = [:] - for c in cachedConversations { if let gid = c.groupID { merged[gid] = c } } - for c in conversations { if let gid = c.groupID { merged[gid] = c } } + for c in cachedConversations { + if let key = Self.conversationGroupKey(c) { merged[key] = c } + } + for c in conversations { + if let key = Self.conversationGroupKey(c) { merged[key] = c } + } cachedConversations = Array(merged.values) reloadData() } + private static func conversationGroupKey(_ conversation: OIMConversationInfo) -> String? { + if let groupID = conversation.groupID, !groupID.isEmpty { + return groupID + } + guard let conversationID = conversation.conversationID, conversationID.hasPrefix("sg_"), + conversationID.count > 3 else { return nil } + return String(conversationID.dropFirst(3)) + } + private func reloadData() { let convMap: [String: OIMConversationInfo] = { var m: [String: OIMConversationInfo] = [:] - for c in cachedConversations where c.groupID != nil && !(c.groupID?.isEmpty ?? true) { - m[c.groupID!] = c + for c in cachedConversations { + if let key = Self.conversationGroupKey(c) { + m[key] = c + } } return m }() diff --git a/QuickLocation/Section/Schedule/CreateSchedule/CreateScheduleVC.swift b/QuickLocation/Section/Schedule/CreateSchedule/CreateScheduleVC.swift index 7b8a591e..25344334 100644 --- a/QuickLocation/Section/Schedule/CreateSchedule/CreateScheduleVC.swift +++ b/QuickLocation/Section/Schedule/CreateSchedule/CreateScheduleVC.swift @@ -214,11 +214,27 @@ class CreateScheduleVC: BaseViewController, MAMapViewDelegate { private func showTimePicker(for pointIndex: Int) { view.endEditing(true) + let points = viewModel.pointsRelay.value + guard pointIndex < points.count else { return } + + let day = viewModel.selectedDate.value + let bounds = timePickerBounds(for: pointIndex, in: points, on: day) + guard bounds.canSelect else { + DLToast.show(text: "行程时间需满足:开始 < 途径 < 结束") + return + } + let picker = BRDatePickerView(pickerMode: .HM) picker.title = "选择到达时间" - if pointIndex < viewModel.pointsRelay.value.count, - let existing = viewModel.pointsRelay.value[pointIndex].expectedTime { - picker.selectDate = existing + picker.minDate = bounds.min + picker.maxDate = bounds.max + if let existing = points[pointIndex].expectedTime, + let onDay = combining(timeFrom: existing, on: day) { + picker.selectDate = clamp(onDay, min: bounds.min, max: bounds.max) + } else if let minDate = bounds.min { + picker.selectDate = minDate + } else if let maxDate = bounds.max { + picker.selectDate = maxDate } picker.pickerStyle = makeSchedulePickerStyle() picker.pickerFooterView = makeSchedulePickerFooter { [weak picker] in @@ -242,6 +258,52 @@ class CreateScheduleVC: BaseViewController, MAMapViewDelegate { showSchedulePicker(picker) } + /// 当前点必须晚于前方已选时间、早于后方已选时间(开始 < 途径 < 结束)。 + private func timePickerBounds(for index: Int, in points: [SchedulePointItem], on day: Date) -> (min: Date?, max: Date?, canSelect: Bool) { + let cal = Calendar.current + let previous = points.prefix(index).reversed().compactMap(\.expectedTime).first + let next = points.dropFirst(index + 1).compactMap(\.expectedTime).first + + var minDate: Date? + if let previous, let onDay = combining(timeFrom: previous, on: day) { + guard let candidate = cal.date(byAdding: .minute, value: 1, to: onDay), + cal.isDate(candidate, inSameDayAs: day) else { + return (nil, nil, false) + } + minDate = candidate + } + + var maxDate: Date? + if let next, let onDay = combining(timeFrom: next, on: day) { + guard let candidate = cal.date(byAdding: .minute, value: -1, to: onDay), + cal.isDate(candidate, inSameDayAs: day) else { + return (nil, nil, false) + } + maxDate = candidate + } + + if let minDate, let maxDate, minDate > maxDate { + return (minDate, maxDate, false) + } + return (minDate, maxDate, true) + } + + private func combining(timeFrom source: Date, on day: Date) -> Date? { + let cal = Calendar.current + var comps = cal.dateComponents([.year, .month, .day], from: day) + let time = cal.dateComponents([.hour, .minute], from: source) + comps.hour = time.hour + comps.minute = time.minute + comps.second = 0 + return cal.date(from: comps) + } + + private func clamp(_ date: Date, min: Date?, max: Date?) -> Date { + if let min, date < min { return min } + if let max, date > max { return max } + return date + } + private func showLocationPicker(for pointIndex: Int, item: SchedulePointItem) { let coord = item.latitude != 0 || item.longitude != 0 ? CLLocationCoordinate2D(latitude: item.latitude, longitude: item.longitude) @@ -389,6 +451,9 @@ class CreateScheduleVC: BaseViewController, MAMapViewDelegate { guard points.count >= 2 else { DLToast.show(text: "至少需要两个行程点"); return } guard hasLocation.count == points.count else { DLToast.show(text: "请为每个行程点选择地点"); return } guard hasTime.count == points.count else { DLToast.show(text: "请为每个行程点选择到达时间"); return } + let times = points.compactMap(\.expectedTime) + let isIncreasing = zip(times, times.dropFirst()).allSatisfy { $0 < $1 } + guard isIncreasing else { DLToast.show(text: "行程时间需满足:开始 < 途径 < 结束"); return } guard !viewModel.selectedGroupKeys.isEmpty else { DLToast.show(text: "请选择分享的圈子"); return } // 生成 points 数组