parent
f8e0d98acf
commit
2fb18d85db
|
|
@ -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 = "<group>"; };
|
||||
305A76292FCA8C7000227D26 /* HomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewController.swift; sourceTree = "<group>"; };
|
||||
305A762B2FCA8C7000227D26 /* LaunchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchViewController.swift; sourceTree = "<group>"; };
|
||||
55B2A1003022F0A000784900 /* LaunchPrivacyConsentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchPrivacyConsentView.swift; sourceTree = "<group>"; };
|
||||
305A762D2FCA8C7000227D26 /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = "<group>"; };
|
||||
305A762E2FCA8C7000227D26 /* LoginViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginViewController.swift; sourceTree = "<group>"; };
|
||||
305A762F2FCA8C7000227D26 /* LoginViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginViewModel.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -779,6 +778,7 @@
|
|||
55B250073038000100786507 /* MailboxDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MailboxDetailViewController.swift; sourceTree = "<group>"; };
|
||||
55B26000304B000100786600 /* VersionUpgradePopVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionUpgradePopVC.swift; sourceTree = "<group>"; };
|
||||
55B261000000000100786600 /* PrivacySettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacySettingsViewController.swift; sourceTree = "<group>"; };
|
||||
55B2A1003022F0A000784900 /* LaunchPrivacyConsentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchPrivacyConsentView.swift; sourceTree = "<group>"; };
|
||||
55BF752C2FFE53F70055DA57 /* LocationPermissionPopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationPermissionPopView.swift; sourceTree = "<group>"; };
|
||||
55C1A0103034101000786010 /* PhoneStatusCollector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhoneStatusCollector.swift; sourceTree = "<group>"; };
|
||||
55C1A0203034102000786020 /* PrivilegeDeniedOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivilegeDeniedOverlay.swift; sourceTree = "<group>"; };
|
||||
|
|
|
|||
|
|
@ -14,9 +14,18 @@
|
|||
<string>wx1e38bba788d55cde</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>quciklocation</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>LSApplicationQueriesSchemes</key>
|
||||
<array>
|
||||
<string>quciklocation</string>
|
||||
<string>baidumap</string>
|
||||
<string>qqmap</string>
|
||||
<string>iosamap</string>
|
||||
|
|
@ -30,6 +39,13 @@
|
|||
<false/>
|
||||
<key>NSExceptionDomains</key>
|
||||
<dict>
|
||||
<key>cdn.u8t.cn</key>
|
||||
<dict>
|
||||
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
||||
<true/>
|
||||
<key>NSIncludesSubdomains</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>cdn2.batiao8.com</key>
|
||||
<dict>
|
||||
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
||||
|
|
@ -51,13 +67,6 @@
|
|||
<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>
|
||||
|
|
|
|||
|
|
@ -56,13 +56,13 @@ class BaseViewController: UIViewController {
|
|||
print(className)
|
||||
MobClick.beginLogPageView(className)
|
||||
|
||||
// 返回根页面时恢复 tabBar 可见性(UINavigationControllerDelegate 在 pop 时可能漏触发)
|
||||
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()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -113,6 +113,21 @@ extension MainTabBarController: QuickLocationTabBarDelegate {
|
|||
// MARK: - UINavigationControllerDelegate
|
||||
extension MainTabBarController: UINavigationControllerDelegate {
|
||||
func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
|
||||
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
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(_:)))
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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 "--"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}()
|
||||
|
|
|
|||
|
|
@ -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 数组
|
||||
|
|
|
|||
Loading…
Reference in New Issue