import Foundation import UIKit import Display import LegacyComponents import TelegramCore import SwiftSignalKit import TelegramPresentationData import AccountContext import AppBundle import CoreLocation import PresentationDataUtils import DeviceAccess import AttachmentUI import CounterControllerTitleView public enum LocationPickerMode { case share(peer: EnginePeer?, selfPeer: EnginePeer?, hasLiveLocation: Bool) case pick } class LocationPickerInteraction { let sendLocation: (CLLocationCoordinate2D, String?, MapGeoAddress?) -> Void let sendLiveLocation: (CLLocationCoordinate2D) -> Void let sendVenue: (TelegramMediaMap, Int64?, String?) -> Void let toggleMapModeSelection: () -> Void let updateMapMode: (LocationMapMode) -> Void let goToUserLocation: () -> Void let goToCoordinate: (CLLocationCoordinate2D, Bool) -> Void let openSearch: () -> Void let updateSearchQuery: (String) -> Void let dismissSearch: () -> Void let dismissInput: () -> Void let openHomeWorkInfo: () -> Void let showPlacesInThisArea: () -> Void init(sendLocation: @escaping (CLLocationCoordinate2D, String?, MapGeoAddress?) -> Void, sendLiveLocation: @escaping (CLLocationCoordinate2D) -> Void, sendVenue: @escaping (TelegramMediaMap, Int64?, String?) -> Void, toggleMapModeSelection: @escaping () -> Void, updateMapMode: @escaping (LocationMapMode) -> Void, goToUserLocation: @escaping () -> Void, goToCoordinate: @escaping (CLLocationCoordinate2D, Bool) -> Void, openSearch: @escaping () -> Void, updateSearchQuery: @escaping (String) -> Void, dismissSearch: @escaping () -> Void, dismissInput: @escaping () -> Void, openHomeWorkInfo: @escaping () -> Void, showPlacesInThisArea: @escaping ()-> Void) { self.sendLocation = sendLocation self.sendLiveLocation = sendLiveLocation self.sendVenue = sendVenue self.toggleMapModeSelection = toggleMapModeSelection self.updateMapMode = updateMapMode self.goToUserLocation = goToUserLocation self.goToCoordinate = goToCoordinate self.openSearch = openSearch self.updateSearchQuery = updateSearchQuery self.dismissSearch = dismissSearch self.dismissInput = dismissInput self.openHomeWorkInfo = openHomeWorkInfo self.showPlacesInThisArea = showPlacesInThisArea } } public final class LocationPickerController: ViewController, AttachmentContainable { public enum Source: Equatable { public enum PollMode: Equatable { case description case quizAnswer case option } case generic case story case poll(PollMode) } private var controllerNode: LocationPickerControllerNode { return self.displayNode as! LocationPickerControllerNode } private let context: AccountContext private let mode: LocationPickerMode let source: Source let initialLocation: CLLocationCoordinate2D? private let completion: (TelegramMediaMap, Int64?, String?, String?, String?) -> Void private var presentationData: PresentationData private var presentationDataDisposable: Disposable? let updatedPresentationData: (initial: PresentationData, signal: Signal)? private var searchNavigationContentNode: LocationSearchNavigationContentNode? private var isSearchingDisposable = MetaDisposable() private let locationManager = LocationManager() private var permissionDisposable: Disposable? private var interaction: LocationPickerInteraction? public var requestAttachmentMenuExpansion: () -> Void = {} public var updateNavigationStack: (@escaping ([AttachmentContainable]) -> ([AttachmentContainable], AttachmentMediaPickerContext?)) -> Void = { _ in } public var parentController: () -> ViewController? = { return nil } public var updateTabBarAlpha: (CGFloat, ContainedViewLayoutTransition) -> Void = { _, _ in } public var updateTabBarVisibility: (Bool, ContainedViewLayoutTransition) -> Void = { _, _ in } public var cancelPanGesture: () -> Void = { } public var isContainerPanning: () -> Bool = { return false } public var isContainerExpanded: () -> Bool = { return false } public var isMinimized: Bool = false private let style: Style public enum Style { case glass case legacy } public init( context: AccountContext, style: Style = .legacy, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, mode: LocationPickerMode, source: Source = .generic, initialLocation: CLLocationCoordinate2D? = nil, completion: @escaping (TelegramMediaMap, Int64?, String?, String?, String?) -> Void ) { self.context = context self.style = style self.mode = mode self.source = source self.initialLocation = initialLocation self.completion = completion self.presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } self.updatedPresentationData = updatedPresentationData let navigationBarPresentationData = NavigationBarPresentationData(theme: NavigationBarTheme(rootControllerTheme: self.presentationData.theme, hideBackground: style == .glass, hideSeparator: true), strings: NavigationBarStrings(presentationStrings: self.presentationData.strings)) super.init(navigationBarPresentationData: navigationBarPresentationData) self.navigationPresentation = .modal self._hasGlassStyle = true if case .share = mode { self.navigationItem.leftBarButtonItem = UIBarButtonItem(customView: UIView()) } else { self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "___close", style: .plain, target: self, action: #selector(self.cancelPressed)) } self.updateBarButtons() self.presentationDataDisposable = ((updatedPresentationData?.signal ?? context.sharedContext.presentationData) |> deliverOnMainQueue).start(next: { [weak self] presentationData in guard let strongSelf = self, strongSelf.presentationData.theme !== presentationData.theme else { return } strongSelf.presentationData = presentationData let navigationBarPresentationData = NavigationBarPresentationData(theme: NavigationBarTheme(rootControllerTheme: strongSelf.presentationData.theme, hideBackground: style == .glass, hideSeparator: true), strings: NavigationBarStrings(presentationStrings: strongSelf.presentationData.strings)) strongSelf.navigationBar?.updatePresentationData(navigationBarPresentationData, transition: .immediate) strongSelf.searchNavigationContentNode?.updatePresentationData(strongSelf.presentationData) strongSelf.updateBarButtons() if strongSelf.isNodeLoaded { strongSelf.controllerNode.updatePresentationData(presentationData) } }) self.interaction = LocationPickerInteraction(sendLocation: { [weak self] coordinate, name, geoAddress in guard let strongSelf = self else { return } strongSelf.completion( TelegramMediaMap( latitude: coordinate.latitude, longitude: coordinate.longitude, heading: nil, accuracyRadius: nil, venue: nil, address: geoAddress, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil ), nil, nil, name, geoAddress?.country ) strongSelf.dismiss() }, sendLiveLocation: { [weak self] coordinate in guard let strongSelf = self else { return } DeviceAccess.authorizeAccess(to: .location(.live), locationManager: strongSelf.locationManager, presentationData: strongSelf.presentationData, present: { c, a in strongSelf.present(c, in: .window(.root), with: a) }, openSettings: { strongSelf.context.sharedContext.applicationBindings.openSettings() }, { [weak self] authorized in guard let strongSelf = self, authorized else { return } var title = strongSelf.presentationData.strings.Map_LiveLocationGroupNewDescription if case let .share(peer, _, _) = strongSelf.mode, let peer = peer, case .user = peer { title = strongSelf.presentationData.strings.Map_LiveLocationPrivateNewDescription(peer.compactDisplayTitle).string } let sourceView = strongSelf.controllerNode.liveLocationActionSourceView(extend: false) ?? strongSelf.view let controller = makeLiveLocationDurationContextController( presentationData: strongSelf.presentationData, sourceView: sourceView!, title: title, selectPeriod: { [weak self] period in guard let self else { return } self.completion(TelegramMediaMap(coordinate: coordinate, liveBroadcastingTimeout: period), nil, nil, nil, nil) self.dismiss() } ) strongSelf.presentInGlobalOverlay(controller) }) }, sendVenue: { [weak self] venue, queryId, resultId in guard let strongSelf = self else { return } let venueType = venue.venue?.type ?? "" if ["home", "work"].contains(venueType) { completion(TelegramMediaMap(latitude: venue.latitude, longitude: venue.longitude, heading: nil, accuracyRadius: nil, venue: nil, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil), nil, nil, nil, nil) } else { completion(venue, queryId, resultId, venue.venue?.address, nil) } strongSelf.dismiss() }, toggleMapModeSelection: { [weak self] in guard let strongSelf = self else { return } strongSelf.controllerNode.updateState { state in var state = state state.displayingMapModeOptions = !state.displayingMapModeOptions return state } }, updateMapMode: { [weak self] mode in guard let strongSelf = self else { return } strongSelf.controllerNode.updateState { state in var state = state state.mapMode = mode state.displayingMapModeOptions = false return state } }, goToUserLocation: { [weak self] in guard let strongSelf = self else { return } strongSelf.controllerNode.goToUserLocation() }, goToCoordinate: { [weak self] coordinate, zoomOut in guard let strongSelf = self else { return } strongSelf.controllerNode.updateState { state in var state = state state.displayingMapModeOptions = false state.selectedLocation = .location(coordinate, nil, zoomOut) state.searchingVenuesAround = false return state } }, openSearch: { [weak self] in guard let self, let interaction = self.interaction, let navigationBar = self.navigationBar else { return } self.controllerNode.updateState { state in var state = state state.displayingMapModeOptions = false return state } switch style { case .glass: let _ = self.controllerNode.activateSearch(navigationBar: navigationBar) self.updateTabBarVisibility(false, .animated(duration: 0.4, curve: .spring)) case .legacy: let contentNode = LocationSearchNavigationContentNode(presentationData: self.presentationData, interaction: interaction) self.searchNavigationContentNode = contentNode navigationBar.setContentNode(contentNode, animated: true) let isSearching = self.controllerNode.activateSearch(navigationBar: navigationBar) contentNode.activate() self.isSearchingDisposable.set((isSearching |> deliverOnMainQueue).start(next: { [weak self] value in if let strongSelf = self, let searchNavigationContentNode = strongSelf.searchNavigationContentNode { searchNavigationContentNode.updateActivity(value) } })) } }, updateSearchQuery: { [weak self] query in guard let strongSelf = self else { return } strongSelf.controllerNode.searchContainerNode?.searchTextUpdated(text: query) }, dismissSearch: { [weak self] in guard let self, let navigationBar = self.navigationBar else { return } self.isSearchingDisposable.set(nil) self.searchNavigationContentNode?.deactivate() self.searchNavigationContentNode = nil navigationBar.setContentNode(nil, animated: true) self.controllerNode.deactivateSearch() if !self.controllerNode.isPickingLocation { self.updateTabBarVisibility(true, .animated(duration: 0.4, curve: .spring)) } }, dismissInput: { [weak self] in guard let self else { return } self.searchNavigationContentNode?.deactivate() self.controllerNode.deactivateInput() }, openHomeWorkInfo: { [weak self] in guard let strongSelf = self else { return } let controller = textAlertController(context: strongSelf.context, updatedPresentationData: updatedPresentationData, title: strongSelf.presentationData.strings.Map_HomeAndWorkTitle, text: strongSelf.presentationData.strings.Map_HomeAndWorkInfo, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]) strongSelf.present(controller, in: .window(.root)) }, showPlacesInThisArea: { [weak self] in guard let strongSelf = self else { return } strongSelf.controllerNode.requestPlacesAtSelectedLocation() }) self.scrollToTop = { [weak self] in if let strongSelf = self { strongSelf.controllerNode.scrollToTop() } } } required public init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } deinit { self.presentationDataDisposable?.dispose() self.permissionDisposable?.dispose() self.isSearchingDisposable.dispose() } private var locationAccessDenied = false private func updateBarButtons() { if self.locationAccessDenied { self.navigationItem.rightBarButtonItem = nil } else { if case .glass = self.style { } else { self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationCompactSearchIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.searchPressed)) self.navigationItem.rightBarButtonItem?.accessibilityLabel = self.presentationData.strings.Common_Search } } } override public func loadDisplayNode() { super.loadDisplayNode() guard let interaction = self.interaction else { return } self.displayNode = LocationPickerControllerNode(controller: self, context: self.context, presentationData: self.presentationData, mode: self.mode, source: self.source, interaction: interaction, locationManager: self.locationManager) self.displayNodeDidLoad() self.controllerNode.beganInteractiveDragging = { [weak self] in self?.requestAttachmentMenuExpansion() } self.controllerNode.locationAccessDeniedUpdated = { [weak self] denied in self?.locationAccessDenied = denied self?.updateBarButtons() } self.permissionDisposable = (DeviceAccess.authorizationStatus(subject: .location(.send)) |> deliverOnMainQueue).start(next: { [weak self] next in guard let strongSelf = self else { return } switch next { case .notDetermined: DeviceAccess.authorizeAccess(to: .location(.send), locationManager: strongSelf.locationManager, presentationData: strongSelf.presentationData, present: { c, a in strongSelf.present(c, in: .window(.root), with: a) }, openSettings: { strongSelf.context.sharedContext.applicationBindings.openSettings() }) case .denied: strongSelf.controllerNode.updateState { state in var state = state state.forceSelection = true return state } default: break } }) self.navigationBar?.passthroughTouches = false } override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) self.controllerNode.containerLayoutUpdated(layout, navigationHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) } @objc private func cancelPressed() { self.dismiss() } @objc func searchPressed() { self.requestAttachmentMenuExpansion() self.interaction?.openSearch() } func dismissSearchPressed() { self.interaction?.dismissSearch() } func updateSearchQuery(_ query: String) { self.interaction?.updateSearchQuery(query) } public func resetForReuse() { self.interaction?.updateMapMode(.map) self.interaction?.dismissSearch() self.scrollToTop?() } public var mediaPickerContext: AttachmentMediaPickerContext? { return LocationPickerContext() } } private final class LocationPickerContext: AttachmentMediaPickerContext { } public func storyLocationPickerController( context: AccountContext, location: CLLocationCoordinate2D?, dismissed: @escaping () -> Void, completion: @escaping (TelegramMediaMap, Int64?, String?, String?, String?) -> Void ) -> ViewController { let presentationData = context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkColorPresentationTheme) let updatedPresentationData: (PresentationData, Signal) = (presentationData, .single(presentationData)) let controller = AttachmentController(context: context, updatedPresentationData: updatedPresentationData, style: .glass, chatLocation: nil, buttons: [.standalone], initialButton: .standalone, fromMenu: false, hasTextInput: false) controller.requestController = { _, present in let locationPickerController = LocationPickerController(context: context, style: .glass, updatedPresentationData: updatedPresentationData, mode: .share(peer: nil, selfPeer: nil, hasLiveLocation: false), source: .story, initialLocation: location, completion: { location, queryId, resultId, address, countryCode in completion(location, queryId, resultId, address, countryCode) }) present(locationPickerController, locationPickerController.mediaPickerContext) return true } controller.navigationPresentation = .flatModal controller.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) controller.didDismiss = { dismissed() } return controller }