mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
346 lines
17 KiB
Swift
346 lines
17 KiB
Swift
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
|
|
import AttachmentUI
|
|
|
|
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, AttachmentContainable {
|
|
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 var requestAttachmentMenuExpansion: () -> Void = {}
|
|
|
|
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = 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(EnginePeer(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, 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()
|
|
}
|
|
|
|
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.controllerNode.beganInteractiveDragging = { [weak self] in
|
|
self?.requestAttachmentMenuExpansion()
|
|
}
|
|
|
|
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.requestAttachmentMenuExpansion()
|
|
|
|
self.interaction?.openSearch()
|
|
}
|
|
|
|
public func resetForReuse() {
|
|
self.interaction?.updateMapMode(.map)
|
|
self.interaction?.dismissSearch()
|
|
self.scrollToTop?()
|
|
}
|
|
}
|