parent
f8e0d98acf
commit
2fb18d85db
|
|
@ -103,7 +103,6 @@
|
||||||
305A76E22FCA8C7000227D26 /* GroupViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A76252FCA8C7000227D26 /* GroupViewModel.swift */; };
|
305A76E22FCA8C7000227D26 /* GroupViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A76252FCA8C7000227D26 /* GroupViewModel.swift */; };
|
||||||
305A76E52FCA8C7000227D26 /* HomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A76292FCA8C7000227D26 /* HomeViewController.swift */; };
|
305A76E52FCA8C7000227D26 /* HomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A76292FCA8C7000227D26 /* HomeViewController.swift */; };
|
||||||
305A76E62FCA8C7000227D26 /* LaunchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A762B2FCA8C7000227D26 /* LaunchViewController.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 */; };
|
305A76E72FCA8C7000227D26 /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A762D2FCA8C7000227D26 /* LoginView.swift */; };
|
||||||
305A76E82FCA8C7000227D26 /* LoginViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A762E2FCA8C7000227D26 /* LoginViewController.swift */; };
|
305A76E82FCA8C7000227D26 /* LoginViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A762E2FCA8C7000227D26 /* LoginViewController.swift */; };
|
||||||
305A76E92FCA8C7000227D26 /* LoginViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A762F2FCA8C7000227D26 /* LoginViewModel.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 */; };
|
55B250083038000100786508 /* MailboxDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B250073038000100786507 /* MailboxDetailViewController.swift */; };
|
||||||
55B26001304B000100786601 /* VersionUpgradePopVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B26000304B000100786600 /* VersionUpgradePopVC.swift */; };
|
55B26001304B000100786601 /* VersionUpgradePopVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B26000304B000100786600 /* VersionUpgradePopVC.swift */; };
|
||||||
55B261010000000100786601 /* PrivacySettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B261000000000100786600 /* PrivacySettingsViewController.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 */; };
|
55BF752D2FFE53F70055DA57 /* LocationPermissionPopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55BF752C2FFE53F70055DA57 /* LocationPermissionPopView.swift */; };
|
||||||
55C1A0113034101000786011 /* PhoneStatusCollector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55C1A0103034101000786010 /* PhoneStatusCollector.swift */; };
|
55C1A0113034101000786011 /* PhoneStatusCollector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55C1A0103034101000786010 /* PhoneStatusCollector.swift */; };
|
||||||
55C1A0213034102000786021 /* PrivilegeDeniedOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55C1A0203034102000786020 /* PrivilegeDeniedOverlay.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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
55C1A0203034102000786020 /* PrivilegeDeniedOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivilegeDeniedOverlay.swift; sourceTree = "<group>"; };
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,18 @@
|
||||||
<string>wx1e38bba788d55cde</string>
|
<string>wx1e38bba788d55cde</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Editor</string>
|
||||||
|
<key>CFBundleURLSchemes</key>
|
||||||
|
<array>
|
||||||
|
<string>quciklocation</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
<key>LSApplicationQueriesSchemes</key>
|
<key>LSApplicationQueriesSchemes</key>
|
||||||
<array>
|
<array>
|
||||||
|
<string>quciklocation</string>
|
||||||
<string>baidumap</string>
|
<string>baidumap</string>
|
||||||
<string>qqmap</string>
|
<string>qqmap</string>
|
||||||
<string>iosamap</string>
|
<string>iosamap</string>
|
||||||
|
|
@ -30,6 +39,13 @@
|
||||||
<false/>
|
<false/>
|
||||||
<key>NSExceptionDomains</key>
|
<key>NSExceptionDomains</key>
|
||||||
<dict>
|
<dict>
|
||||||
|
<key>cdn.u8t.cn</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
||||||
|
<true/>
|
||||||
|
<key>NSIncludesSubdomains</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
<key>cdn2.batiao8.com</key>
|
<key>cdn2.batiao8.com</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
||||||
|
|
@ -51,13 +67,6 @@
|
||||||
<key>NSIncludesSubdomains</key>
|
<key>NSIncludesSubdomains</key>
|
||||||
<true/>
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
<key>cdn.u8t.cn</key>
|
|
||||||
<dict>
|
|
||||||
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
|
||||||
<true/>
|
|
||||||
<key>NSIncludesSubdomains</key>
|
|
||||||
<true/>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
<key>UIAppFonts</key>
|
<key>UIAppFonts</key>
|
||||||
|
|
|
||||||
|
|
@ -56,13 +56,13 @@ class BaseViewController: UIViewController {
|
||||||
print(className)
|
print(className)
|
||||||
MobClick.beginLogPageView(className)
|
MobClick.beginLogPageView(className)
|
||||||
|
|
||||||
// 返回根页面时恢复 tabBar 可见性(UINavigationControllerDelegate 在 pop 时可能漏触发)
|
if navigationController?.transitionCoordinator == nil {
|
||||||
(tabBarController as? MainTabBarController)?.updateTabBarVisibility()
|
(tabBarController as? MainTabBarController)?.updateTabBarVisibility()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewDidAppear(_ animated: Bool) {
|
override func viewDidAppear(_ animated: Bool) {
|
||||||
super.viewDidAppear(animated)
|
super.viewDidAppear(animated)
|
||||||
// popToRoot 动画完成后 navigation stack 才减到 1,此时再恢复 tabBar
|
|
||||||
(tabBarController as? MainTabBarController)?.updateTabBarVisibility()
|
(tabBarController as? MainTabBarController)?.updateTabBarVisibility()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -113,6 +113,21 @@ extension MainTabBarController: QuickLocationTabBarDelegate {
|
||||||
// MARK: - UINavigationControllerDelegate
|
// MARK: - UINavigationControllerDelegate
|
||||||
extension MainTabBarController: UINavigationControllerDelegate {
|
extension MainTabBarController: UINavigationControllerDelegate {
|
||||||
func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
|
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
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,7 @@ final class GroupChatVC: BaseViewController {
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
viewModel.isChatActive = true
|
||||||
fd_interactivePopDisabled = true
|
fd_interactivePopDisabled = true
|
||||||
bindViewModel()
|
bindViewModel()
|
||||||
reactiveAction()
|
reactiveAction()
|
||||||
|
|
@ -93,6 +94,7 @@ final class GroupChatVC: BaseViewController {
|
||||||
|
|
||||||
override func viewWillAppear(_ animated: Bool) {
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
super.viewWillAppear(animated)
|
super.viewWillAppear(animated)
|
||||||
|
viewModel.isChatActive = true
|
||||||
setupKeyboard()
|
setupKeyboard()
|
||||||
IQKeyboardManager.shared.isEnabled = false
|
IQKeyboardManager.shared.isEnabled = false
|
||||||
IQKeyboardManager.shared.resignOnTouchOutside = false
|
IQKeyboardManager.shared.resignOnTouchOutside = false
|
||||||
|
|
@ -101,6 +103,7 @@ final class GroupChatVC: BaseViewController {
|
||||||
override func viewWillDisappear(_ animated: Bool) {
|
override func viewWillDisappear(_ animated: Bool) {
|
||||||
super.viewWillDisappear(animated)
|
super.viewWillDisappear(animated)
|
||||||
viewModel.markAsRead()
|
viewModel.markAsRead()
|
||||||
|
viewModel.isChatActive = false
|
||||||
VoicePlayerManager.shared.stop()
|
VoicePlayerManager.shared.stop()
|
||||||
if isVoiceRecording {
|
if isVoiceRecording {
|
||||||
finishVoiceRecording(action: .discard)
|
finishVoiceRecording(action: .discard)
|
||||||
|
|
@ -114,6 +117,9 @@ final class GroupChatVC: BaseViewController {
|
||||||
override func viewDidDisappear(_ animated: Bool) {
|
override func viewDidDisappear(_ animated: Bool) {
|
||||||
super.viewDidDisappear(animated)
|
super.viewDidDisappear(animated)
|
||||||
NotificationCenter.default.removeObserver(self)
|
NotificationCenter.default.removeObserver(self)
|
||||||
|
if isMovingFromParent || isBeingDismissed {
|
||||||
|
removeMessageListener()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Keyboard
|
// MARK: - Keyboard
|
||||||
|
|
@ -428,6 +434,12 @@ final class GroupChatVC: BaseViewController {
|
||||||
OIMManager.callbacker.addAdvancedMsgListener(listener: msgListener!)
|
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 @)
|
// MARK: - Long press (quote / revoke / avatar @)
|
||||||
private func setupChatLongPress() {
|
private func setupChatLongPress() {
|
||||||
let longPress = UILongPressGestureRecognizer(target: self, action: #selector(handleChatLongPress(_:)))
|
let longPress = UILongPressGestureRecognizer(target: self, action: #selector(handleChatLongPress(_:)))
|
||||||
|
|
|
||||||
|
|
@ -710,8 +710,11 @@ final class GroupChatViewModel {
|
||||||
messagesSubject.accept([])
|
messagesSubject.accept([])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 仅当前聊天页在前台时为 true,避免退出后仍把新消息标成已读。
|
||||||
|
var isChatActive = false
|
||||||
|
|
||||||
func markAsRead() {
|
func markAsRead() {
|
||||||
guard !conversationID.isEmpty else { return }
|
guard isChatActive, !conversationID.isEmpty else { return }
|
||||||
OIMManager.manager.markConversationMessage(asRead: conversationID,
|
OIMManager.manager.markConversationMessage(asRead: conversationID,
|
||||||
onSuccess: nil,
|
onSuccess: nil,
|
||||||
onFailure: nil)
|
onFailure: nil)
|
||||||
|
|
@ -735,7 +738,7 @@ final class GroupChatViewModel {
|
||||||
// MARK: - Receive
|
// MARK: - Receive
|
||||||
func onReceiveMessage(_ msg: OIMMessageInfo) {
|
func onReceiveMessage(_ msg: OIMMessageInfo) {
|
||||||
cacheMessage(msg)
|
cacheMessage(msg)
|
||||||
if msg.sendID != AppContextManager.shared.userId {
|
if isChatActive, msg.sendID != AppContextManager.shared.userId {
|
||||||
markAsRead()
|
markAsRead()
|
||||||
}
|
}
|
||||||
guard let item = toSectionItem(msg) else { return }
|
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 minutes = max(1, (end.expected_timestamp - start.expected_timestamp) / 60_000)
|
||||||
let hours = minutes / 60
|
let hours = minutes / 60
|
||||||
let remainder = 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 hasSetupIMListeners = false
|
||||||
private var hasRequestedIMData = false
|
private var hasRequestedIMData = false
|
||||||
private var isRequestingIMData = false
|
private var isRequestingIMData = false
|
||||||
|
private var conversationListRequestID = UUID()
|
||||||
|
|
||||||
/// 进入页面加载 IM 会话:圈子集合已由业务接口给出,这里只补未读/最后一条
|
/// 进入页面加载 IM 会话:圈子集合已由业务接口给出,这里只补未读/最后一条
|
||||||
private func loadIMData(showsLoading: Bool = false) {
|
private func loadIMData(showsLoading: Bool = false) {
|
||||||
|
|
@ -92,6 +93,7 @@ final class GroupViewController: BaseViewController {
|
||||||
|
|
||||||
GroupIMService.shared.setConversationListener({ [weak self] conversations in
|
GroupIMService.shared.setConversationListener({ [weak self] conversations in
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
|
self?.conversationListRequestID = UUID()
|
||||||
self?.viewModel.updateConversations(conversations)
|
self?.viewModel.updateConversations(conversations)
|
||||||
}
|
}
|
||||||
}, onNeedsRefresh: { [weak self] in
|
}, onNeedsRefresh: { [weak self] in
|
||||||
|
|
@ -110,12 +112,15 @@ final class GroupViewController: BaseViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func refreshIMData(showsLoading: Bool = false) {
|
private func refreshIMData(showsLoading: Bool = false) {
|
||||||
|
let requestID = UUID()
|
||||||
|
conversationListRequestID = requestID
|
||||||
GroupIMService.shared.getConversationList { [weak self] conversations in
|
GroupIMService.shared.getConversationList { [weak self] conversations in
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
if showsLoading {
|
if showsLoading {
|
||||||
DLToast.dismiss()
|
DLToast.dismiss()
|
||||||
}
|
}
|
||||||
self?.viewModel.loadConversations(conversations)
|
guard let self, self.conversationListRequestID == requestID else { return }
|
||||||
|
self.viewModel.loadConversations(conversations)
|
||||||
}
|
}
|
||||||
} onFailure: {
|
} onFailure: {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
|
|
|
||||||
|
|
@ -72,17 +72,32 @@ final class GroupViewModel {
|
||||||
/// 仅更新会话元数据(未读、时间)- 消息监听回调(只返回变更的会话,需合并)
|
/// 仅更新会话元数据(未读、时间)- 消息监听回调(只返回变更的会话,需合并)
|
||||||
func updateConversations(_ conversations: [OIMConversationInfo]) {
|
func updateConversations(_ conversations: [OIMConversationInfo]) {
|
||||||
var merged: [String: OIMConversationInfo] = [:]
|
var merged: [String: OIMConversationInfo] = [:]
|
||||||
for c in cachedConversations { if let gid = c.groupID { merged[gid] = c } }
|
for c in cachedConversations {
|
||||||
for c in conversations { if let gid = c.groupID { merged[gid] = c } }
|
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)
|
cachedConversations = Array(merged.values)
|
||||||
reloadData()
|
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() {
|
private func reloadData() {
|
||||||
let convMap: [String: OIMConversationInfo] = {
|
let convMap: [String: OIMConversationInfo] = {
|
||||||
var m: [String: OIMConversationInfo] = [:]
|
var m: [String: OIMConversationInfo] = [:]
|
||||||
for c in cachedConversations where c.groupID != nil && !(c.groupID?.isEmpty ?? true) {
|
for c in cachedConversations {
|
||||||
m[c.groupID!] = c
|
if let key = Self.conversationGroupKey(c) {
|
||||||
|
m[key] = c
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return m
|
return m
|
||||||
}()
|
}()
|
||||||
|
|
|
||||||
|
|
@ -214,11 +214,27 @@ class CreateScheduleVC: BaseViewController, MAMapViewDelegate {
|
||||||
|
|
||||||
private func showTimePicker(for pointIndex: Int) {
|
private func showTimePicker(for pointIndex: Int) {
|
||||||
view.endEditing(true)
|
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)
|
let picker = BRDatePickerView(pickerMode: .HM)
|
||||||
picker.title = "选择到达时间"
|
picker.title = "选择到达时间"
|
||||||
if pointIndex < viewModel.pointsRelay.value.count,
|
picker.minDate = bounds.min
|
||||||
let existing = viewModel.pointsRelay.value[pointIndex].expectedTime {
|
picker.maxDate = bounds.max
|
||||||
picker.selectDate = existing
|
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.pickerStyle = makeSchedulePickerStyle()
|
||||||
picker.pickerFooterView = makeSchedulePickerFooter { [weak picker] in
|
picker.pickerFooterView = makeSchedulePickerFooter { [weak picker] in
|
||||||
|
|
@ -242,6 +258,52 @@ class CreateScheduleVC: BaseViewController, MAMapViewDelegate {
|
||||||
showSchedulePicker(picker)
|
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) {
|
private func showLocationPicker(for pointIndex: Int, item: SchedulePointItem) {
|
||||||
let coord = item.latitude != 0 || item.longitude != 0
|
let coord = item.latitude != 0 || item.longitude != 0
|
||||||
? CLLocationCoordinate2D(latitude: item.latitude, longitude: item.longitude)
|
? 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 points.count >= 2 else { DLToast.show(text: "至少需要两个行程点"); return }
|
||||||
guard hasLocation.count == points.count 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 }
|
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 }
|
guard !viewModel.selectedGroupKeys.isEmpty else { DLToast.show(text: "请选择分享的圈子"); return }
|
||||||
|
|
||||||
// 生成 points 数组
|
// 生成 points 数组
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue