import Foundation import UIKit import Display import LegacyComponents import TelegramCore import Postbox import SwiftSignalKit import TelegramPresentationData import AccountContext import AppBundle import CoreLocation import PresentationDataUtils import DeviceAccess public enum LocationPickerMode { case share(peer: Peer?, selfPeer: Peer?, hasLiveLocation: Bool) case pick } class LocationPickerInteraction { let sendLocation: (CLLocationCoordinate2D) -> Void let sendLiveLocation: (CLLocationCoordinate2D) -> Void let sendVenue: (TelegramMediaMap) -> Void let toggleMapModeSelection: () -> Void let updateMapMode: (LocationMapMode) -> Void let goToUserLocation: () -> Void let goToCoordinate: (CLLocationCoordinate2D) -> Void let openSearch: () -> Void let updateSearchQuery: (String) -> Void let dismissSearch: () -> Void let dismissInput: () -> Void let updateSendActionHighlight: (Bool) -> Void let openHomeWorkInfo: () -> Void let showPlacesInThisArea: () -> Void init(sendLocation: @escaping (CLLocationCoordinate2D) -> Void, sendLiveLocation: @escaping (CLLocationCoordinate2D) -> Void, sendVenue: @escaping (TelegramMediaMap) -> Void, toggleMapModeSelection: @escaping () -> Void, updateMapMode: @escaping (LocationMapMode) -> Void, goToUserLocation: @escaping () -> Void, goToCoordinate: @escaping (CLLocationCoordinate2D) -> Void, openSearch: @escaping () -> Void, updateSearchQuery: @escaping (String) -> Void, dismissSearch: @escaping () -> Void, dismissInput: @escaping () -> Void, updateSendActionHighlight: @escaping (Bool) -> 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.updateSendActionHighlight = updateSendActionHighlight self.openHomeWorkInfo = openHomeWorkInfo self.showPlacesInThisArea = showPlacesInThisArea } } public final class LocationPickerController: ViewController { private var controllerNode: LocationPickerControllerNode { return self.displayNode as! LocationPickerControllerNode } private let context: AccountContext private let mode: LocationPickerMode private let completion: (TelegramMediaMap, String?) -> Void private var presentationData: PresentationData private var presentationDataDisposable: Disposable? private var searchNavigationContentNode: LocationSearchNavigationContentNode? private var isSearchingDisposable = MetaDisposable() private let locationManager = LocationManager() private var permissionDisposable: Disposable? private var interaction: LocationPickerInteraction? public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, mode: LocationPickerMode, completion: @escaping (TelegramMediaMap, String?) -> Void) { self.context = context self.mode = mode self.completion = completion self.presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: NavigationBarTheme(rootControllerTheme: self.presentationData.theme).withUpdatedSeparatorColor(.clear), strings: NavigationBarStrings(presentationStrings: self.presentationData.strings))) self.navigationPresentation = .modal self.title = self.presentationData.strings.Map_ChooseLocationTitle self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)) 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 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 strongSelf.navigationBar?.updatePresentationData(NavigationBarPresentationData(theme: NavigationBarTheme(rootControllerTheme: strongSelf.presentationData.theme).withUpdatedSeparatorColor(.clear), strings: NavigationBarStrings(presentationStrings: strongSelf.presentationData.strings))) strongSelf.searchNavigationContentNode?.updatePresentationData(strongSelf.presentationData) strongSelf.navigationItem.rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationCompactSearchIcon(strongSelf.presentationData.theme), style: .plain, target: strongSelf, action: #selector(strongSelf.searchPressed)) if strongSelf.isNodeLoaded { strongSelf.controllerNode.updatePresentationData(presentationData) } }) let locationWithTimeout: (CLLocationCoordinate2D, Int32?) -> TelegramMediaMap = { coordinate, timeout in return TelegramMediaMap(latitude: coordinate.latitude, longitude: coordinate.longitude, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: nil, liveBroadcastingTimeout: timeout, liveProximityNotificationRadius: nil) } self.interaction = LocationPickerInteraction(sendLocation: { [weak self] coordinate in guard let strongSelf = self else { return } strongSelf.completion(locationWithTimeout(coordinate, nil), nil) 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 } let controller = ActionSheetController(presentationData: strongSelf.presentationData) var title = strongSelf.presentationData.strings.Map_LiveLocationGroupDescription if case let .share(peer, _, _) = strongSelf.mode, let receiver = peer as? TelegramUser { title = strongSelf.presentationData.strings.Map_LiveLocationPrivateDescription(receiver.compactDisplayTitle).string } controller.setItemGroups([ ActionSheetItemGroup(items: [ ActionSheetTextItem(title: title), ActionSheetButtonItem(title: strongSelf.presentationData.strings.Map_LiveLocationFor15Minutes, color: .accent, action: { [weak self, weak controller] in controller?.dismissAnimated() if let strongSelf = self { strongSelf.completion(TelegramMediaMap(coordinate: coordinate, liveBroadcastingTimeout: 15 * 60), nil) strongSelf.dismiss() } }), ActionSheetButtonItem(title: strongSelf.presentationData.strings.Map_LiveLocationFor1Hour, color: .accent, action: { [weak self, weak controller] in controller?.dismissAnimated() if let strongSelf = self { strongSelf.completion(TelegramMediaMap(coordinate: coordinate, liveBroadcastingTimeout: 60 * 60 - 1), nil) strongSelf.dismiss() } }), ActionSheetButtonItem(title: strongSelf.presentationData.strings.Map_LiveLocationFor8Hours, color: .accent, action: { [weak self, weak controller] in controller?.dismissAnimated() if let strongSelf = self { strongSelf.completion(TelegramMediaMap(coordinate: coordinate, liveBroadcastingTimeout: 8 * 60 * 60), nil) strongSelf.dismiss() } }) ]), ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak controller] in controller?.dismissAnimated() }) ]) ]) strongSelf.present(controller, in: .window(.root)) }) }, sendVenue: { [weak self] venue 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, geoPlace: nil, venue: nil, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil), nil) } else { completion(venue, 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 in guard let strongSelf = self else { return } strongSelf.controllerNode.updateState { state in var state = state state.displayingMapModeOptions = false state.selectedLocation = .location(coordinate, nil) state.searchingVenuesAround = false return state } }, openSearch: { [weak self] in guard let strongSelf = self, let interaction = strongSelf.interaction, let navigationBar = strongSelf.navigationBar else { return } strongSelf.controllerNode.updateState { state in var state = state state.displayingMapModeOptions = false return state } let contentNode = LocationSearchNavigationContentNode(presentationData: strongSelf.presentationData, interaction: interaction) strongSelf.searchNavigationContentNode = contentNode navigationBar.setContentNode(contentNode, animated: true) let isSearching = strongSelf.controllerNode.activateSearch(navigationBar: navigationBar) contentNode.activate() strongSelf.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 strongSelf = self, let navigationBar = strongSelf.navigationBar else { return } strongSelf.isSearchingDisposable.set(nil) strongSelf.searchNavigationContentNode?.deactivate() strongSelf.searchNavigationContentNode = nil navigationBar.setContentNode(nil, animated: true) strongSelf.controllerNode.deactivateSearch() }, dismissInput: { [weak self] in guard let strongSelf = self else { return } strongSelf.searchNavigationContentNode?.deactivate() }, updateSendActionHighlight: { [weak self] highlighted in guard let strongSelf = self else { return } strongSelf.controllerNode.updateSendActionHighlight(highlighted) }, openHomeWorkInfo: { [weak self] in guard let strongSelf = self else { return } let controller = textAlertController(context: strongSelf.context, 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() } override public func loadDisplayNode() { super.loadDisplayNode() guard let interaction = self.interaction else { return } self.displayNode = LocationPickerControllerNode(context: self.context, presentationData: self.presentationData, mode: self.mode, interaction: interaction, locationManager: self.locationManager) self.displayNodeDidLoad() 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 } }) } 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 private func searchPressed() { self.interaction?.openSearch() } }