mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
1231 lines
63 KiB
Swift
1231 lines
63 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import AsyncDisplayKit
|
|
import Display
|
|
import LegacyComponents
|
|
import TelegramCore
|
|
import SwiftSignalKit
|
|
import MergeLists
|
|
import ItemListUI
|
|
import ItemListVenueItem
|
|
import TelegramPresentationData
|
|
import TelegramStringFormatting
|
|
import AccountContext
|
|
import AppBundle
|
|
import CoreLocation
|
|
import Geocoding
|
|
import PhoneNumberFormat
|
|
import DeviceAccess
|
|
|
|
private struct LocationPickerTransaction {
|
|
let deletions: [ListViewDeleteItem]
|
|
let insertions: [ListViewInsertItem]
|
|
let updates: [ListViewUpdateItem]
|
|
let isLoading: Bool
|
|
let isEmpty: Bool
|
|
let crossFade: Bool
|
|
}
|
|
|
|
private enum LocationPickerEntryId: Hashable {
|
|
case city
|
|
case location
|
|
case liveLocation
|
|
case header
|
|
case venue(String)
|
|
case attribution
|
|
}
|
|
|
|
private enum LocationPickerEntry: Comparable, Identifiable {
|
|
case city(PresentationTheme, String, String, TelegramMediaMap?, Int64?, String?, CLLocationCoordinate2D?, String?, String?)
|
|
case location(PresentationTheme, String, String, TelegramMediaMap?, Int64?, String?, CLLocationCoordinate2D?, String?, String?, Bool)
|
|
case liveLocation(PresentationTheme, String, String, CLLocationCoordinate2D?)
|
|
case header(PresentationTheme, String)
|
|
case venue(PresentationTheme, TelegramMediaMap?, Int64?, String?, Int)
|
|
case attribution(PresentationTheme, LocationAttribution)
|
|
|
|
var stableId: LocationPickerEntryId {
|
|
switch self {
|
|
case .city:
|
|
return .city
|
|
case .location:
|
|
return .location
|
|
case .liveLocation:
|
|
return .liveLocation
|
|
case .header:
|
|
return .header
|
|
case let .venue(_, venue, _, _, index):
|
|
return .venue(venue?.venue?.id ?? "\(index)")
|
|
case .attribution:
|
|
return .attribution
|
|
}
|
|
}
|
|
|
|
static func ==(lhs: LocationPickerEntry, rhs: LocationPickerEntry) -> Bool {
|
|
switch lhs {
|
|
case let .city(lhsTheme, lhsTitle, lhsSubtitle, lhsVenue, lhsQueryId, lhsResultId, lhsCoordinate, lhsName, lhsCountryCode):
|
|
if case let .city(rhsTheme, rhsTitle, rhsSubtitle, rhsVenue, rhsQueryId, rhsResultId, rhsCoordinate, rhsName, rhsCountryCode) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsSubtitle == rhsSubtitle, lhsVenue?.venue?.id == rhsVenue?.venue?.id, lhsQueryId == rhsQueryId && lhsResultId == rhsResultId, lhsCoordinate == rhsCoordinate, lhsName == rhsName, lhsCountryCode == rhsCountryCode {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case let .location(lhsTheme, lhsTitle, lhsSubtitle, lhsVenue, lhsQueryId, lhsResultId, lhsCoordinate, lhsName, lhsCountryCode, lhsIsTop):
|
|
if case let .location(rhsTheme, rhsTitle, rhsSubtitle, rhsVenue, rhsQueryId, rhsResultId, rhsCoordinate, rhsName, rhsCountryCode, rhsIsTop) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsSubtitle == rhsSubtitle, lhsVenue?.venue?.id == rhsVenue?.venue?.id, lhsQueryId == rhsQueryId && lhsResultId == rhsResultId, lhsCoordinate == rhsCoordinate, lhsName == rhsName, lhsCountryCode == rhsCountryCode, lhsIsTop == rhsIsTop {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case let .liveLocation(lhsTheme, lhsTitle, lhsSubtitle, lhsCoordinate):
|
|
if case let .liveLocation(rhsTheme, rhsTitle, rhsSubtitle, rhsCoordinate) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsSubtitle == rhsSubtitle, lhsCoordinate == rhsCoordinate {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case let .header(lhsTheme, lhsTitle):
|
|
if case let .header(rhsTheme, rhsTitle) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case let .venue(lhsTheme, lhsVenue, lhsQueryId, lhsResultId, lhsIndex):
|
|
if case let .venue(rhsTheme, rhsVenue, rhsQueryId, rhsResultId, rhsIndex) = rhs, lhsTheme === rhsTheme, lhsVenue?.venue?.id == rhsVenue?.venue?.id, lhsQueryId == rhsQueryId && lhsResultId == rhsResultId, lhsIndex == rhsIndex {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case let .attribution(lhsTheme, lhsAttribution):
|
|
if case let .attribution(rhsTheme, rhsAttribution) = rhs, lhsTheme === rhsTheme, lhsAttribution == rhsAttribution {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
static func <(lhs: LocationPickerEntry, rhs: LocationPickerEntry) -> Bool {
|
|
switch lhs {
|
|
case .city:
|
|
switch rhs {
|
|
case .city:
|
|
return false
|
|
case .location, .liveLocation, .header, .venue, .attribution:
|
|
return true
|
|
}
|
|
case .location:
|
|
switch rhs {
|
|
case .city, .location:
|
|
return false
|
|
case .liveLocation, .header, .venue, .attribution:
|
|
return true
|
|
}
|
|
case .liveLocation:
|
|
switch rhs {
|
|
case .city, .location, .liveLocation:
|
|
return false
|
|
case .header, .venue, .attribution:
|
|
return true
|
|
}
|
|
case .header:
|
|
switch rhs {
|
|
case .city, .location, .liveLocation, .header:
|
|
return false
|
|
case .venue, .attribution:
|
|
return true
|
|
}
|
|
case let .venue(_, _, _, _, lhsIndex):
|
|
switch rhs {
|
|
case .city, .location, .liveLocation, .header:
|
|
return false
|
|
case let .venue(_, _, _, _, rhsIndex):
|
|
return lhsIndex < rhsIndex
|
|
case .attribution:
|
|
return true
|
|
}
|
|
case .attribution:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func item(engine: TelegramEngine, presentationData: PresentationData, interaction: LocationPickerInteraction?) -> ListViewItem {
|
|
switch self {
|
|
case let .city(_, title, subtitle, _, _, _, coordinate, name, countryCode):
|
|
let icon: LocationActionListItemIcon
|
|
if let name {
|
|
icon = .venue(TelegramMediaMap(latitude: 0, longitude: 0, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: MapVenue(title: name, address: "City", provider: nil, id: "city", type: "building/default"), liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil))
|
|
} else {
|
|
icon = .location
|
|
}
|
|
return LocationActionListItem(presentationData: ItemListPresentationData(presentationData), engine: engine, title: title, subtitle: subtitle, icon: icon, beginTimeAndTimeout: nil, action: {
|
|
if let coordinate = coordinate {
|
|
interaction?.sendLocation(coordinate, name, countryCode)
|
|
}
|
|
}, highlighted: { highlighted in
|
|
interaction?.updateSendActionHighlight(highlighted)
|
|
})
|
|
case let .location(_, title, subtitle, venue, queryId, resultId, coordinate, name, countryCode, isTop):
|
|
let icon: LocationActionListItemIcon
|
|
if let venue = venue {
|
|
icon = .venue(venue)
|
|
} else {
|
|
icon = .location
|
|
}
|
|
return LocationActionListItem(presentationData: ItemListPresentationData(presentationData), engine: engine, title: title, subtitle: subtitle, icon: icon, beginTimeAndTimeout: nil, action: {
|
|
if let venue = venue {
|
|
interaction?.sendVenue(venue, queryId, resultId)
|
|
} else if let coordinate = coordinate {
|
|
interaction?.sendLocation(coordinate, name, countryCode)
|
|
}
|
|
}, highlighted: { highlighted in
|
|
if isTop {
|
|
interaction?.updateSendActionHighlight(highlighted)
|
|
}
|
|
})
|
|
case let .liveLocation(_, title, subtitle, coordinate):
|
|
return LocationActionListItem(presentationData: ItemListPresentationData(presentationData), engine: engine, title: title, subtitle: subtitle, icon: .liveLocation, beginTimeAndTimeout: nil, action: {
|
|
if let coordinate = coordinate {
|
|
interaction?.sendLiveLocation(coordinate)
|
|
}
|
|
})
|
|
case let .header(_, title):
|
|
return LocationSectionHeaderItem(presentationData: ItemListPresentationData(presentationData), title: title)
|
|
case let .venue(_, venue, queryId, resultId, _):
|
|
let venueType = venue?.venue?.type ?? ""
|
|
return ItemListVenueItem(presentationData: ItemListPresentationData(presentationData), engine: engine, venue: venue, style: .plain, action: venue.flatMap { venue in
|
|
return { interaction?.sendVenue(venue, queryId, resultId) }
|
|
}, infoAction: ["home", "work"].contains(venueType) ? {
|
|
interaction?.openHomeWorkInfo()
|
|
} : nil)
|
|
case let .attribution(_, attribution):
|
|
return LocationAttributionItem(presentationData: ItemListPresentationData(presentationData), attribution: attribution)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func preparedTransition(from fromEntries: [LocationPickerEntry], to toEntries: [LocationPickerEntry], isLoading: Bool, isEmpty: Bool, crossFade: Bool, engine: TelegramEngine, presentationData: PresentationData, interaction: LocationPickerInteraction?) -> LocationPickerTransaction {
|
|
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
|
|
|
|
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
|
|
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(engine: engine, presentationData: presentationData, interaction: interaction), directionHint: nil) }
|
|
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(engine: engine, presentationData: presentationData, interaction: interaction), directionHint: nil) }
|
|
|
|
return LocationPickerTransaction(deletions: deletions, insertions: insertions, updates: updates, isLoading: isLoading, isEmpty: isEmpty, crossFade: crossFade)
|
|
}
|
|
|
|
enum LocationPickerLocation: Equatable {
|
|
case none
|
|
case selecting
|
|
case location(CLLocationCoordinate2D, String?)
|
|
case venue(TelegramMediaMap, Int64?, String?)
|
|
|
|
var isCustom: Bool {
|
|
switch self {
|
|
case .selecting, .location:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
public static func ==(lhs: LocationPickerLocation, rhs: LocationPickerLocation) -> Bool {
|
|
switch lhs {
|
|
case .none:
|
|
if case .none = rhs {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case .selecting:
|
|
if case .selecting = rhs {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case let .location(lhsCoordinate, lhsAddress):
|
|
if case let .location(rhsCoordinate, rhsAddress) = rhs, lhsCoordinate == rhsCoordinate, lhsAddress == rhsAddress {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case let .venue(lhsVenue, lhsQueryId, lhsResultId):
|
|
if case let .venue(rhsVenue, rhsQueryId, rhsResultId) = rhs, lhsVenue.venue?.id == rhsVenue.venue?.id, lhsQueryId == rhsQueryId, lhsResultId == rhsResultId {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
struct LocationPickerState {
|
|
var mapMode: LocationMapMode
|
|
var displayingMapModeOptions: Bool
|
|
var selectedLocation: LocationPickerLocation
|
|
var city: String?
|
|
var street: String?
|
|
var countryCode: String?
|
|
var isStreet: Bool
|
|
var forceSelection: Bool
|
|
var searchingVenuesAround: Bool
|
|
|
|
init() {
|
|
self.mapMode = .map
|
|
self.displayingMapModeOptions = false
|
|
self.selectedLocation = .none
|
|
self.city = nil
|
|
self.street = nil
|
|
self.isStreet = false
|
|
self.forceSelection = false
|
|
self.searchingVenuesAround = false
|
|
}
|
|
}
|
|
|
|
private class LocationContext: NSObject, CLLocationManagerDelegate {
|
|
private let locationManager: CLLocationManager
|
|
|
|
private let accessSink = ValuePipe<CLAuthorizationStatus>()
|
|
|
|
override init() {
|
|
self.locationManager = CLLocationManager()
|
|
|
|
super.init()
|
|
|
|
self.locationManager.delegate = self
|
|
}
|
|
|
|
func locationAccess() -> Signal<CLAuthorizationStatus, NoError> {
|
|
let initialStatus: CLAuthorizationStatus
|
|
if #available(iOS 14.0, *) {
|
|
initialStatus = self.locationManager.authorizationStatus
|
|
} else {
|
|
initialStatus = CLLocationManager.authorizationStatus()
|
|
}
|
|
return .single(initialStatus)
|
|
|> then(
|
|
self.accessSink.signal()
|
|
)
|
|
}
|
|
|
|
@available(iOS 14.0, *)
|
|
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
|
|
self.accessSink.putNext(manager.authorizationStatus)
|
|
}
|
|
|
|
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
|
|
self.accessSink.putNext(status)
|
|
}
|
|
}
|
|
|
|
final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationManagerDelegate {
|
|
private weak var controller: LocationPickerController?
|
|
private let context: AccountContext
|
|
private var presentationData: PresentationData
|
|
private let presentationDataPromise: Promise<PresentationData>
|
|
private let mode: LocationPickerMode
|
|
private let source: LocationPickerController.Source
|
|
private let interaction: LocationPickerInteraction
|
|
private let locationManager: LocationManager
|
|
|
|
private let locationContext: LocationContext
|
|
|
|
private let listNode: ListView
|
|
private let emptyResultsTextNode: ImmediateTextNode
|
|
private let headerNode: LocationMapHeaderNode
|
|
private let shadeNode: ASDisplayNode
|
|
private let innerShadeNode: ASDisplayNode
|
|
|
|
private let optionsNode: LocationOptionsNode
|
|
private(set) var searchContainerNode: LocationSearchContainerNode?
|
|
|
|
private var placeholderBackgroundNode: NavigationBackgroundNode?
|
|
private var placeholderNode: LocationPlaceholderNode?
|
|
private var locationAccessDenied = false
|
|
|
|
private var enqueuedTransitions: [LocationPickerTransaction] = []
|
|
|
|
private var disposable: Disposable?
|
|
private var state: LocationPickerState
|
|
private let statePromise: Promise<LocationPickerState>
|
|
private var geocodingDisposable = MetaDisposable()
|
|
|
|
private let searchVenuesPromise = Promise<CLLocationCoordinate2D?>()
|
|
|
|
private var validLayout: (layout: ContainerViewLayout, navigationHeight: CGFloat)?
|
|
private var listOffset: CGFloat?
|
|
|
|
var beganInteractiveDragging: () -> Void = {}
|
|
var locationAccessDeniedUpdated: (Bool) -> Void = { _ in }
|
|
|
|
init(controller: LocationPickerController, context: AccountContext, presentationData: PresentationData, mode: LocationPickerMode, source: LocationPickerController.Source, interaction: LocationPickerInteraction, locationManager: LocationManager) {
|
|
self.controller = controller
|
|
self.context = context
|
|
self.presentationData = presentationData
|
|
self.presentationDataPromise = Promise(presentationData)
|
|
self.mode = mode
|
|
self.source = source
|
|
self.interaction = interaction
|
|
self.locationManager = locationManager
|
|
|
|
self.locationContext = LocationContext()
|
|
|
|
self.state = LocationPickerState()
|
|
self.statePromise = Promise(self.state)
|
|
|
|
self.listNode = ListView()
|
|
self.listNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
|
|
self.listNode.verticalScrollIndicatorColor = UIColor(white: 0.0, alpha: 0.3)
|
|
self.listNode.verticalScrollIndicatorFollowsOverscroll = true
|
|
self.listNode.accessibilityPageScrolledString = { row, count in
|
|
return presentationData.strings.VoiceOver_ScrollStatus(row, count).string
|
|
}
|
|
|
|
self.emptyResultsTextNode = ImmediateTextNode()
|
|
self.emptyResultsTextNode.maximumNumberOfLines = 0
|
|
self.emptyResultsTextNode.textAlignment = .center
|
|
self.emptyResultsTextNode.isHidden = true
|
|
|
|
self.headerNode = LocationMapHeaderNode(presentationData: presentationData, toggleMapModeSelection: interaction.toggleMapModeSelection, goToUserLocation: interaction.goToUserLocation, showPlacesInThisArea: interaction.showPlacesInThisArea)
|
|
self.headerNode.mapNode.isRotateEnabled = false
|
|
|
|
self.optionsNode = LocationOptionsNode(presentationData: presentationData, updateMapMode: interaction.updateMapMode)
|
|
|
|
self.shadeNode = ASDisplayNode()
|
|
self.shadeNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
|
|
self.shadeNode.alpha = 0.0
|
|
self.innerShadeNode = ASDisplayNode()
|
|
self.innerShadeNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
|
|
|
|
super.init()
|
|
|
|
self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
|
|
|
|
self.addSubnode(self.listNode)
|
|
self.addSubnode(self.headerNode)
|
|
self.addSubnode(self.optionsNode)
|
|
self.listNode.addSubnode(self.emptyResultsTextNode)
|
|
self.shadeNode.addSubnode(self.innerShadeNode)
|
|
self.addSubnode(self.shadeNode)
|
|
|
|
let userLocation: Signal<CLLocation?, NoError> = self.headerNode.mapNode.userLocation
|
|
|
|
let personalAddresses = self.context.account.postbox.peerView(id: self.context.account.peerId)
|
|
|> mapToSignal { view -> Signal<(DeviceContactAddressData?, DeviceContactAddressData?)?, NoError> in
|
|
if let user = peerViewMainPeer(view) as? TelegramUser, let phoneNumber = user.phone {
|
|
return ((context.sharedContext.contactDataManager?.basicData() ?? .single([:])) |> take(1))
|
|
|> mapToSignal { basicData -> Signal<DeviceContactExtendedData?, NoError> in
|
|
var stableId: String?
|
|
let queryPhoneNumber = formatPhoneNumber(phoneNumber)
|
|
outer: for (id, data) in basicData {
|
|
for phoneNumber in data.phoneNumbers {
|
|
if formatPhoneNumber(phoneNumber.value) == queryPhoneNumber {
|
|
stableId = id
|
|
break outer
|
|
}
|
|
}
|
|
}
|
|
if let stableId = stableId {
|
|
return (context.sharedContext.contactDataManager?.extendedData(stableId: stableId) ?? .single(nil))
|
|
|> take(1)
|
|
|> map { extendedData -> DeviceContactExtendedData? in
|
|
return extendedData
|
|
}
|
|
} else {
|
|
return .single(nil)
|
|
}
|
|
}
|
|
|> map { extendedData -> (DeviceContactAddressData?, DeviceContactAddressData?)? in
|
|
if let extendedData = extendedData {
|
|
var homeAddress: DeviceContactAddressData?
|
|
var workAddress: DeviceContactAddressData?
|
|
for address in extendedData.addresses {
|
|
if address.label == "_$!<Home>!$_" {
|
|
homeAddress = address
|
|
} else if address.label == "_$!<Work>!$_" {
|
|
workAddress = address
|
|
}
|
|
}
|
|
return (homeAddress, workAddress)
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
} else {
|
|
return .single(nil)
|
|
}
|
|
}
|
|
|
|
let personalVenues: Signal<[TelegramMediaMap]?, NoError> = .single(nil)
|
|
|> then(
|
|
personalAddresses
|
|
|> mapToSignal { homeAndWorkAddresses -> Signal<[TelegramMediaMap]?, NoError> in
|
|
if let (homeAddress, workAddress) = homeAndWorkAddresses {
|
|
let home: Signal<(Double, Double)?, NoError>
|
|
let work: Signal<(Double, Double)?, NoError>
|
|
if let address = homeAddress {
|
|
home = geocodeAddress(engine: context.engine, address: address)
|
|
} else {
|
|
home = .single(nil)
|
|
}
|
|
if let address = workAddress {
|
|
work = geocodeAddress(engine: context.engine, address: address)
|
|
} else {
|
|
work = .single(nil)
|
|
}
|
|
return combineLatest(home, work)
|
|
|> map { homeCoordinate, workCoordinate -> [TelegramMediaMap]? in
|
|
var venues: [TelegramMediaMap] = []
|
|
if let (latitude, longitude) = homeCoordinate, let address = homeAddress {
|
|
venues.append(TelegramMediaMap(latitude: latitude, longitude: longitude, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: MapVenue(title: presentationData.strings.Map_Home, address: address.displayString, provider: nil, id: "home", type: "home"), liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil))
|
|
}
|
|
if let (latitude, longitude) = workCoordinate, let address = workAddress {
|
|
venues.append(TelegramMediaMap(latitude: latitude, longitude: longitude, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: MapVenue(title: presentationData.strings.Map_Work, address: address.displayString, provider: nil, id: "work", type: "work"), liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil))
|
|
}
|
|
return venues
|
|
}
|
|
} else {
|
|
return .single(nil)
|
|
}
|
|
}
|
|
)
|
|
|
|
let venuesLocation: Signal<CLLocation?, NoError>
|
|
if let initialLocation = controller.initialLocation {
|
|
venuesLocation = .single(CLLocation(coordinate: initialLocation, altitude: 0.0, horizontalAccuracy: 1.0, verticalAccuracy: 1.0, timestamp: Date()))
|
|
} else {
|
|
venuesLocation = throttledUserLocation(userLocation)
|
|
}
|
|
|
|
let venues: Signal<([(TelegramMediaMap, String)], Int64)?, NoError> = .single(nil)
|
|
|> then(
|
|
venuesLocation
|
|
|> mapToSignal { location -> Signal<([(TelegramMediaMap, String)], Int64)?, NoError> in
|
|
if let location = location, location.horizontalAccuracy > 0 {
|
|
return combineLatest(nearbyVenues(context: context, story: source == .story, latitude: location.coordinate.latitude, longitude: location.coordinate.longitude), personalVenues)
|
|
|> map { contextResult, personalVenues -> ([(TelegramMediaMap, String)], Int64)? in
|
|
var resultVenues: [(TelegramMediaMap, String)] = []
|
|
if let personalVenues = personalVenues {
|
|
for venue in personalVenues {
|
|
let venueLocation = CLLocation(latitude: venue.latitude, longitude: venue.longitude)
|
|
if venueLocation.distance(from: location) <= 1000 {
|
|
resultVenues.append((venue, ""))
|
|
}
|
|
}
|
|
}
|
|
if let contextResult {
|
|
for result in contextResult.results {
|
|
switch result.message {
|
|
case let .mapLocation(mapMedia, _):
|
|
if let _ = mapMedia.venue {
|
|
resultVenues.append((mapMedia, result.id))
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
return (resultVenues, contextResult.queryId)
|
|
} else {
|
|
return (resultVenues, 0)
|
|
}
|
|
}
|
|
} else {
|
|
return .single(nil)
|
|
}
|
|
}
|
|
)
|
|
|
|
let foundVenues: Signal<([(TelegramMediaMap, String)], Int64, CLLocation)?, NoError> = .single(nil)
|
|
|> then(
|
|
self.searchVenuesPromise.get()
|
|
|> distinctUntilChanged
|
|
|> mapToSignal { coordinate -> Signal<([(TelegramMediaMap, String)], Int64, CLLocation)?, NoError> in
|
|
if let coordinate = coordinate {
|
|
return (.single(nil)
|
|
|> then(
|
|
nearbyVenues(context: context, story: source == .story, latitude: coordinate.latitude, longitude: coordinate.longitude)
|
|
|> map { contextResult -> ([(TelegramMediaMap, String)], Int64, CLLocation)? in
|
|
if let contextResult {
|
|
var resultVenues: [(TelegramMediaMap, String)] = []
|
|
for result in contextResult.results {
|
|
switch result.message {
|
|
case let .mapLocation(mapMedia, _):
|
|
if let _ = mapMedia.venue {
|
|
resultVenues.append((mapMedia, result.id))
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
return (resultVenues, contextResult.queryId, CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude))
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
))
|
|
} else {
|
|
return .single(nil)
|
|
}
|
|
}
|
|
)
|
|
|
|
let previousState = Atomic<LocationPickerState>(value: self.state)
|
|
let previousUserLocation = Atomic<CLLocation?>(value: nil)
|
|
let previousAnnotations = Atomic<[LocationPinAnnotation]>(value: [])
|
|
let previousEntries = Atomic<[LocationPickerEntry]?>(value: nil)
|
|
|
|
self.disposable = (combineLatest(self.presentationDataPromise.get(), self.statePromise.get(), userLocation, venues, foundVenues, self.locationContext.locationAccess())
|
|
|> deliverOnMainQueue).start(next: { [weak self] presentationData, state, userLocation, venues, foundVenuesAndLocation, access in
|
|
if let strongSelf = self {
|
|
let (foundVenues, _, foundVenuesLocation) = foundVenuesAndLocation ?? (nil, nil, nil)
|
|
|
|
var entries: [LocationPickerEntry] = []
|
|
switch state.selectedLocation {
|
|
case let .location(coordinate, address):
|
|
let title: String
|
|
switch strongSelf.mode {
|
|
case .share:
|
|
if source == .story {
|
|
title = "Add This Location"
|
|
} else {
|
|
title = presentationData.strings.Map_SendThisLocation
|
|
}
|
|
case .pick:
|
|
title = presentationData.strings.Map_SetThisLocation
|
|
}
|
|
entries.append(.location(presentationData.theme, title, address ?? presentationData.strings.Map_Locating, nil, nil, nil, coordinate, state.street, state.countryCode, true))
|
|
case .selecting:
|
|
let title: String
|
|
switch strongSelf.mode {
|
|
case .share:
|
|
if source == .story {
|
|
title = "Add This Location"
|
|
} else {
|
|
title = presentationData.strings.Map_SendThisLocation
|
|
}
|
|
case .pick:
|
|
title = presentationData.strings.Map_SetThisLocation
|
|
}
|
|
entries.append(.location(presentationData.theme, title, presentationData.strings.Map_Locating, nil, nil, nil, nil, nil, nil, true))
|
|
case let .venue(venue, queryId, resultId):
|
|
let title: String
|
|
switch strongSelf.mode {
|
|
case .share:
|
|
title = presentationData.strings.Map_SendThisPlace
|
|
case .pick:
|
|
title = presentationData.strings.Map_SetThisPlace
|
|
}
|
|
entries.append(.location(presentationData.theme, title, venue.venue?.title ?? "", venue, queryId, resultId, venue.coordinate, nil, nil, true))
|
|
case .none:
|
|
let title: String
|
|
var coordinate = userLocation?.coordinate
|
|
switch strongSelf.mode {
|
|
case .share:
|
|
if source == .story {
|
|
if let initialLocation = strongSelf.controller?.initialLocation {
|
|
title = presentationData.strings.Location_AddThisLocation
|
|
coordinate = initialLocation
|
|
} else {
|
|
title = presentationData.strings.Location_AddMyLocation
|
|
}
|
|
} else {
|
|
title = presentationData.strings.Map_SendMyCurrentLocation
|
|
}
|
|
case .pick:
|
|
title = presentationData.strings.Map_SetThisLocation
|
|
}
|
|
if source == .story {
|
|
if state.city != "" {
|
|
entries.append(.city(presentationData.theme, state.city ?? presentationData.strings.Map_Locating, presentationData.strings.Location_TypeCity, nil, nil, nil, coordinate, state.city, state.countryCode))
|
|
}
|
|
if state.street != "" {
|
|
entries.append(.location(presentationData.theme, state.street ?? presentationData.strings.Map_Locating, state.isStreet ? presentationData.strings.Location_TypeStreet : presentationData.strings.Location_TypeLocation, nil, nil, nil, coordinate, state.street, nil, false))
|
|
}
|
|
} else {
|
|
entries.append(.location(presentationData.theme, title, (userLocation?.horizontalAccuracy).flatMap { presentationData.strings.Map_AccurateTo(stringForDistance(strings: presentationData.strings, distance: $0)).string } ?? presentationData.strings.Map_Locating, nil, nil, nil, coordinate, state.street, nil, true))
|
|
}
|
|
}
|
|
|
|
if case .share(_, _, true) = mode {
|
|
entries.append(.liveLocation(presentationData.theme, presentationData.strings.Map_ShareLiveLocation, presentationData.strings.Map_ShareLiveLocationHelp, userLocation?.coordinate))
|
|
}
|
|
|
|
entries.append(.header(presentationData.theme, presentationData.strings.Map_ChooseAPlace.uppercased()))
|
|
|
|
let displayedVenues: [(TelegramMediaMap, String)]?
|
|
let queryId: Int64?
|
|
if foundVenues != nil || state.searchingVenuesAround {
|
|
displayedVenues = foundVenues
|
|
queryId = foundVenuesAndLocation?.1
|
|
} else {
|
|
displayedVenues = venues?.0
|
|
queryId = venues?.1
|
|
}
|
|
|
|
var index: Int = 0
|
|
if let venues = displayedVenues, let queryId {
|
|
var attribution: LocationAttribution?
|
|
for (venue, resultId) in venues {
|
|
if venue.venue?.provider == "foursquare" {
|
|
attribution = .foursquare
|
|
} else if venue.venue?.provider == "gplaces" {
|
|
attribution = .google
|
|
}
|
|
entries.append(.venue(presentationData.theme, venue, queryId, resultId, index))
|
|
index += 1
|
|
}
|
|
if let attribution = attribution {
|
|
entries.append(.attribution(presentationData.theme, attribution))
|
|
}
|
|
} else {
|
|
for _ in 0 ..< 8 {
|
|
entries.append(.venue(presentationData.theme, nil, nil, nil, index))
|
|
index += 1
|
|
}
|
|
}
|
|
let previousEntries = previousEntries.swap(entries)
|
|
let previousState = previousState.swap(state)
|
|
|
|
var crossFade = false
|
|
if previousEntries?.count != entries.count || previousState.selectedLocation != state.selectedLocation {
|
|
crossFade = true
|
|
}
|
|
|
|
let transition = preparedTransition(from: previousEntries ?? [], to: entries, isLoading: displayedVenues == nil, isEmpty: displayedVenues?.isEmpty ?? false, crossFade: crossFade, engine: context.engine, presentationData: presentationData, interaction: strongSelf.interaction)
|
|
strongSelf.enqueueTransition(transition)
|
|
|
|
var displayingPlacesButton = false
|
|
let previousUserLocation = previousUserLocation.swap(userLocation)
|
|
switch state.selectedLocation {
|
|
case .none:
|
|
if let initialLocation = strongSelf.controller?.initialLocation {
|
|
strongSelf.headerNode.mapNode.setMapCenter(coordinate: initialLocation, animated: false)
|
|
} else if let userLocation = userLocation {
|
|
strongSelf.headerNode.mapNode.setMapCenter(coordinate: userLocation.coordinate, isUserLocation: true, animated: previousUserLocation != nil)
|
|
}
|
|
strongSelf.headerNode.mapNode.resetAnnotationSelection()
|
|
case .selecting:
|
|
strongSelf.headerNode.mapNode.resetAnnotationSelection()
|
|
case let .location(coordinate, address):
|
|
var updateMap = false
|
|
switch previousState.selectedLocation {
|
|
case .none, .venue:
|
|
updateMap = true
|
|
case let .location(previousCoordinate, _):
|
|
if previousCoordinate != coordinate {
|
|
updateMap = true
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
if updateMap {
|
|
strongSelf.headerNode.mapNode.setMapCenter(coordinate: coordinate, isUserLocation: false, hidePicker: false, animated: true)
|
|
strongSelf.headerNode.mapNode.switchToPicking(animated: false)
|
|
}
|
|
|
|
if address != nil {
|
|
if foundVenues == nil && !state.searchingVenuesAround {
|
|
displayingPlacesButton = true
|
|
} else if let previousLocation = foundVenuesLocation {
|
|
let currentLocation = CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude)
|
|
if currentLocation.distance(from: previousLocation) > 300 {
|
|
displayingPlacesButton = true
|
|
}
|
|
}
|
|
}
|
|
case let .venue(venue, _, _):
|
|
strongSelf.headerNode.mapNode.setMapCenter(coordinate: venue.coordinate, hidePicker: true, animated: true)
|
|
}
|
|
|
|
strongSelf.headerNode.updateState(mapMode: state.mapMode, trackingMode: .none, displayingMapModeOptions: state.displayingMapModeOptions, displayingPlacesButton: displayingPlacesButton, proximityNotification: nil, animated: true)
|
|
|
|
let annotations: [LocationPinAnnotation]
|
|
if let venues = displayedVenues, let queryId {
|
|
annotations = venues.compactMap { LocationPinAnnotation(context: context, theme: presentationData.theme, location: $0.0, queryId: queryId, resultId: $0.1) }
|
|
} else {
|
|
annotations = []
|
|
}
|
|
let previousAnnotations = previousAnnotations.swap(annotations)
|
|
if annotations != previousAnnotations {
|
|
strongSelf.headerNode.mapNode.annotations = annotations
|
|
}
|
|
|
|
if let (layout, navigationBarHeight) = strongSelf.validLayout {
|
|
var updateLayout = false
|
|
let transition: ContainedViewLayoutTransition = .animated(duration: 0.45, curve: .spring)
|
|
|
|
if [.denied, .restricted].contains(access) {
|
|
if !strongSelf.locationAccessDenied {
|
|
strongSelf.locationAccessDenied = true
|
|
strongSelf.locationAccessDeniedUpdated(true)
|
|
updateLayout = true
|
|
}
|
|
} else {
|
|
if strongSelf.locationAccessDenied {
|
|
strongSelf.locationAccessDenied = false
|
|
strongSelf.locationAccessDeniedUpdated(false)
|
|
updateLayout = true
|
|
}
|
|
}
|
|
|
|
if previousState.displayingMapModeOptions != state.displayingMapModeOptions {
|
|
updateLayout = true
|
|
} else if previousState.selectedLocation.isCustom != state.selectedLocation.isCustom {
|
|
updateLayout = true
|
|
} else if previousState.searchingVenuesAround != state.searchingVenuesAround {
|
|
updateLayout = true
|
|
}
|
|
|
|
if updateLayout {
|
|
strongSelf.containerLayoutUpdated(layout, navigationHeight: navigationBarHeight, transition: transition)
|
|
}
|
|
}
|
|
|
|
let locale = localeWithStrings(presentationData.strings)
|
|
if case let .location(coordinate, address) = state.selectedLocation, address == nil {
|
|
strongSelf.geocodingDisposable.set((reverseGeocodeLocation(latitude: coordinate.latitude, longitude: coordinate.longitude, locale: locale)
|
|
|> deliverOnMainQueue).start(next: { [weak self] placemark in
|
|
if let strongSelf = self {
|
|
var address = placemark?.fullAddress ?? ""
|
|
if address.isEmpty {
|
|
address = presentationData.strings.Map_Unknown
|
|
}
|
|
var cityName: String?
|
|
var streetName: String?
|
|
let countryCode = placemark?.countryCode
|
|
if let city = placemark?.city, let countryCode = placemark?.countryCode {
|
|
cityName = "\(city), \(displayCountryName(countryCode, locale: locale))"
|
|
} else {
|
|
cityName = ""
|
|
}
|
|
if let street = placemark?.street {
|
|
if let city = placemark?.city {
|
|
streetName = "\(street), \(city)"
|
|
} else {
|
|
streetName = street
|
|
}
|
|
} else if let name = placemark?.name {
|
|
streetName = name
|
|
} else if let country = placemark?.country, cityName == "" {
|
|
streetName = country
|
|
} else {
|
|
streetName = ""
|
|
}
|
|
if streetName == "" && cityName == "" {
|
|
streetName = presentationData.strings.Location_TypeLocation
|
|
}
|
|
strongSelf.updateState { state in
|
|
var state = state
|
|
state.selectedLocation = .location(coordinate, address)
|
|
state.city = cityName
|
|
state.street = streetName
|
|
state.countryCode = countryCode
|
|
state.isStreet = placemark?.street != nil
|
|
return state
|
|
}
|
|
}
|
|
}))
|
|
} else {
|
|
let coordinate = controller.initialLocation ?? userLocation?.coordinate
|
|
if case .none = state.selectedLocation, let coordinate, state.city == nil {
|
|
strongSelf.geocodingDisposable.set((reverseGeocodeLocation(latitude: coordinate.latitude, longitude: coordinate.longitude, locale: locale)
|
|
|> deliverOnMainQueue).start(next: { [weak self] placemark in
|
|
if let strongSelf = self {
|
|
var address = placemark?.fullAddress ?? ""
|
|
if address.isEmpty {
|
|
address = presentationData.strings.Map_Unknown
|
|
}
|
|
var cityName: String?
|
|
var streetName: String?
|
|
let countryCode = placemark?.countryCode
|
|
if let city = placemark?.city, let countryCode = placemark?.countryCode {
|
|
cityName = "\(city), \(displayCountryName(countryCode, locale: locale))"
|
|
} else {
|
|
cityName = ""
|
|
}
|
|
if let street = placemark?.street {
|
|
if let city = placemark?.city {
|
|
streetName = "\(street), \(city)"
|
|
} else {
|
|
streetName = street
|
|
}
|
|
} else if let name = placemark?.name {
|
|
streetName = name
|
|
} else if let country = placemark?.country, cityName == "" {
|
|
streetName = country
|
|
} else {
|
|
streetName = ""
|
|
}
|
|
if streetName == "" && cityName == "" {
|
|
streetName = presentationData.strings.Location_TypeLocation
|
|
}
|
|
strongSelf.updateState { state in
|
|
var state = state
|
|
state.city = cityName
|
|
state.street = streetName
|
|
state.countryCode = countryCode
|
|
state.isStreet = placemark?.street != nil
|
|
return state
|
|
}
|
|
}
|
|
}))
|
|
} else {
|
|
strongSelf.geocodingDisposable.set(nil)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
if case let .share(_, selfPeer, _) = self.mode {
|
|
if let selfPeer {
|
|
self.headerNode.mapNode.userLocationAnnotation = LocationPinAnnotation(context: context, theme: self.presentationData.theme, peer: selfPeer)
|
|
}
|
|
self.headerNode.mapNode.hasPickerAnnotation = true
|
|
}
|
|
|
|
self.listNode.updateFloatingHeaderOffset = { [weak self] offset, listTransition in
|
|
guard let strongSelf = self, let (layout, navigationBarHeight) = strongSelf.validLayout, strongSelf.listNode.scrollEnabled else {
|
|
return
|
|
}
|
|
let overlap: CGFloat = 6.0
|
|
strongSelf.listOffset = max(0.0, offset)
|
|
let headerFrame = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: max(0.0, offset + overlap)))
|
|
listTransition.updateFrame(node: strongSelf.headerNode, frame: headerFrame)
|
|
strongSelf.headerNode.updateLayout(layout: layout, navigationBarHeight: navigationBarHeight, topPadding: strongSelf.state.displayingMapModeOptions ? 38.0 : 0.0, offset: 0.0, size: headerFrame.size, transition: listTransition)
|
|
strongSelf.layoutEmptyResultsPlaceholder(transition: listTransition)
|
|
}
|
|
|
|
self.listNode.beganInteractiveDragging = { [weak self] _ in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.updateState { state in
|
|
var state = state
|
|
state.displayingMapModeOptions = false
|
|
return state
|
|
}
|
|
}
|
|
|
|
self.headerNode.mapNode.beganInteractiveDragging = { [weak self] in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.beganInteractiveDragging()
|
|
strongSelf.updateState { state in
|
|
var state = state
|
|
state.displayingMapModeOptions = false
|
|
state.selectedLocation = .selecting
|
|
state.searchingVenuesAround = false
|
|
return state
|
|
}
|
|
}
|
|
|
|
self.headerNode.mapNode.endedInteractiveDragging = { [weak self] coordinate in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.updateState { state in
|
|
var state = state
|
|
if case .selecting = state.selectedLocation {
|
|
state.selectedLocation = .location(coordinate, nil)
|
|
state.searchingVenuesAround = false
|
|
}
|
|
return state
|
|
}
|
|
}
|
|
|
|
self.headerNode.mapNode.annotationSelected = { [weak self] annotation in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.updateState { state in
|
|
var state = state
|
|
state.displayingMapModeOptions = false
|
|
if let annotation, let location = annotation.location {
|
|
state.selectedLocation = .venue(location, annotation.queryId, annotation.resultId)
|
|
}
|
|
if annotation == nil {
|
|
state.searchingVenuesAround = false
|
|
}
|
|
return state
|
|
}
|
|
}
|
|
|
|
self.headerNode.mapNode.userLocationAnnotationSelected = { [weak self] in
|
|
if let strongSelf = self {
|
|
strongSelf.goToUserLocation()
|
|
}
|
|
}
|
|
|
|
self.locationManager.manager.startUpdatingHeading()
|
|
self.locationManager.manager.delegate = self
|
|
}
|
|
|
|
deinit {
|
|
self.disposable?.dispose()
|
|
self.geocodingDisposable.dispose()
|
|
|
|
self.locationManager.manager.stopUpdatingHeading()
|
|
}
|
|
|
|
func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) {
|
|
self.headerNode.mapNode.userHeading = CGFloat(newHeading.magneticHeading)
|
|
}
|
|
|
|
func updatePresentationData(_ presentationData: PresentationData) {
|
|
self.presentationData = presentationData
|
|
self.presentationDataPromise.set(.single(presentationData))
|
|
|
|
self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
|
|
self.listNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
|
|
self.headerNode.updatePresentationData(self.presentationData)
|
|
self.optionsNode.updatePresentationData(self.presentationData)
|
|
self.shadeNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
|
|
self.innerShadeNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
|
|
self.searchContainerNode?.updatePresentationData(self.presentationData)
|
|
}
|
|
|
|
func updateState(_ f: (LocationPickerState) -> LocationPickerState) {
|
|
self.state = f(self.state)
|
|
self.statePromise.set(.single(self.state))
|
|
}
|
|
|
|
private func enqueueTransition(_ transition: LocationPickerTransaction) {
|
|
self.enqueuedTransitions.append(transition)
|
|
|
|
if let _ = self.validLayout {
|
|
while !self.enqueuedTransitions.isEmpty {
|
|
self.dequeueTransition()
|
|
}
|
|
}
|
|
}
|
|
|
|
private func dequeueTransition() {
|
|
guard let _ = self.validLayout, let transition = self.enqueuedTransitions.first else {
|
|
return
|
|
}
|
|
self.enqueuedTransitions.remove(at: 0)
|
|
|
|
var options = ListViewDeleteAndInsertOptions()
|
|
if transition.crossFade {
|
|
options.insert(.AnimateCrossfade)
|
|
}
|
|
|
|
self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { [weak self] _ in
|
|
if let strongSelf = self {
|
|
strongSelf.emptyResultsTextNode.isHidden = transition.isLoading || !transition.isEmpty
|
|
|
|
strongSelf.emptyResultsTextNode.attributedText = NSAttributedString(string: strongSelf.presentationData.strings.Map_NoPlacesNearby, font: Font.regular(15.0), textColor: strongSelf.presentationData.theme.list.freeTextColor)
|
|
|
|
strongSelf.layoutEmptyResultsPlaceholder(transition: .immediate)
|
|
}
|
|
})
|
|
}
|
|
|
|
func activateSearch(navigationBar: NavigationBar) -> Signal<Bool, NoError> {
|
|
guard let (layout, navigationBarHeight) = self.validLayout, self.searchContainerNode == nil, let coordinate = self.headerNode.mapNode.mapCenterCoordinate else {
|
|
return .complete()
|
|
}
|
|
|
|
let searchContainerNode = LocationSearchContainerNode(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, coordinate: coordinate, interaction: self.interaction, story: self.source == .story)
|
|
self.insertSubnode(searchContainerNode, belowSubnode: navigationBar)
|
|
self.searchContainerNode = searchContainerNode
|
|
|
|
searchContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
|
|
|
self.containerLayoutUpdated(layout, navigationHeight: navigationBarHeight, transition: .immediate)
|
|
|
|
return searchContainerNode.isSearching
|
|
}
|
|
|
|
func deactivateSearch() {
|
|
guard let searchContainerNode = self.searchContainerNode else {
|
|
return
|
|
}
|
|
searchContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak searchContainerNode] _ in
|
|
searchContainerNode?.removeFromSupernode()
|
|
})
|
|
self.searchContainerNode = nil
|
|
}
|
|
|
|
func scrollToTop() {
|
|
if let searchContainerNode = self.searchContainerNode {
|
|
searchContainerNode.scrollToTop()
|
|
} else {
|
|
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
|
|
}
|
|
}
|
|
|
|
private func layoutEmptyResultsPlaceholder(transition: ContainedViewLayoutTransition) {
|
|
guard let (layout, navigationHeight) = self.validLayout else {
|
|
return
|
|
}
|
|
|
|
let topInset: CGFloat = floor((layout.size.height - navigationHeight) / 2.0 + navigationHeight)
|
|
let headerHeight: CGFloat
|
|
if let listOffset = self.listOffset {
|
|
headerHeight = max(0.0, listOffset)
|
|
} else {
|
|
headerHeight = topInset
|
|
}
|
|
|
|
let actionsInset: CGFloat = 148.0
|
|
let padding: CGFloat = 16.0
|
|
let emptyTextSize = self.emptyResultsTextNode.updateLayout(CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - padding * 2.0, height: CGFloat.greatestFiniteMagnitude))
|
|
transition.updateFrame(node: self.emptyResultsTextNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - emptyTextSize.width) / 2.0), y: headerHeight + actionsInset + floor((layout.size.height - headerHeight - actionsInset - emptyTextSize.height - layout.intrinsicInsets.bottom - layout.additionalInsets.bottom) / 2.0)), size: emptyTextSize))
|
|
}
|
|
|
|
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
|
let isFirstLayout = self.validLayout == nil
|
|
self.validLayout = (layout, navigationHeight)
|
|
|
|
let isPickingLocation = (self.state.selectedLocation.isCustom || self.state.forceSelection) && !self.state.searchingVenuesAround
|
|
let optionsHeight: CGFloat = 38.0
|
|
var actionHeight: CGFloat?
|
|
self.listNode.forEachItemNode { itemNode in
|
|
if let itemNode = itemNode as? LocationActionListItemNode {
|
|
if actionHeight == nil {
|
|
actionHeight = itemNode.frame.height
|
|
}
|
|
}
|
|
}
|
|
|
|
// let topInset: CGFloat = floor((layout.size.height - navigationHeight) / 2.0 + navigationHeight)
|
|
let topInset: CGFloat = 240.0
|
|
let overlap: CGFloat = 6.0
|
|
let headerHeight: CGFloat
|
|
if isPickingLocation, let actionHeight = actionHeight {
|
|
self.listOffset = topInset
|
|
headerHeight = layout.size.height - actionHeight - layout.intrinsicInsets.bottom + overlap - 2.0
|
|
} else if let listOffset = self.listOffset {
|
|
headerHeight = max(0.0, listOffset + overlap)
|
|
} else {
|
|
headerHeight = topInset + overlap
|
|
}
|
|
let headerFrame = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: headerHeight))
|
|
transition.updateFrame(node: self.headerNode, frame: headerFrame)
|
|
|
|
self.headerNode.updateLayout(layout: layout, navigationBarHeight: navigationHeight, topPadding: self.state.displayingMapModeOptions ? optionsHeight : 0.0, offset: 0.0, size: headerFrame.size, transition: transition)
|
|
|
|
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
|
|
let scrollToItem: ListViewScrollToItem?
|
|
if isPickingLocation {
|
|
scrollToItem = ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: curve, directionHint: .Up)
|
|
} else {
|
|
scrollToItem = nil
|
|
}
|
|
|
|
let insets = UIEdgeInsets(top: topInset, left: layout.safeInsets.left, bottom: layout.intrinsicInsets.bottom, right: layout.safeInsets.right)
|
|
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: scrollToItem, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, headerInsets: UIEdgeInsets(top: navigationHeight, left: 0.0, bottom: layout.intrinsicInsets.bottom, right: 0.0), scrollIndicatorInsets: UIEdgeInsets(top: topInset + 3.0, left: 0.0, bottom: layout.intrinsicInsets.bottom, right: 0.0), duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
|
|
self.listNode.scrollEnabled = !isPickingLocation
|
|
|
|
var listFrame: CGRect = CGRect(origin: CGPoint(), size: layout.size)
|
|
if isPickingLocation {
|
|
listFrame.origin.y = headerHeight - topInset - overlap
|
|
}
|
|
transition.updateFrame(node: self.listNode, frame: listFrame)
|
|
transition.updateAlpha(node: self.shadeNode, alpha: isPickingLocation ? 1.0 : 0.0)
|
|
transition.updateFrame(node: self.shadeNode, frame: CGRect(x: 0.0, y: listFrame.minY + topInset + (actionHeight ?? 0.0) - 3.0, width: layout.size.width, height: 10000.0))
|
|
self.shadeNode.isUserInteractionEnabled = isPickingLocation
|
|
self.innerShadeNode.frame = CGRect(x: 0.0, y: 4.0, width: layout.size.width, height: 10000.0)
|
|
self.innerShadeNode.alpha = layout.intrinsicInsets.bottom > 0.0 ? 1.0 : 0.0
|
|
|
|
self.layoutEmptyResultsPlaceholder(transition: transition)
|
|
|
|
if isFirstLayout {
|
|
while !self.enqueuedTransitions.isEmpty {
|
|
self.dequeueTransition()
|
|
}
|
|
}
|
|
|
|
let optionsOffset: CGFloat = self.state.displayingMapModeOptions ? navigationHeight : navigationHeight - optionsHeight
|
|
let optionsFrame = CGRect(x: 0.0, y: optionsOffset, width: layout.size.width, height: optionsHeight)
|
|
transition.updateFrame(node: self.optionsNode, frame: optionsFrame)
|
|
self.optionsNode.updateLayout(size: optionsFrame.size, leftInset: insets.left, rightInset: insets.right, transition: transition)
|
|
self.optionsNode.isUserInteractionEnabled = self.state.displayingMapModeOptions
|
|
|
|
if let searchContainerNode = self.searchContainerNode {
|
|
searchContainerNode.frame = CGRect(origin: CGPoint(), size: layout.size)
|
|
searchContainerNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: LayoutMetrics(), deviceMetrics: layout.deviceMetrics, intrinsicInsets: layout.intrinsicInsets, safeInsets: layout.safeInsets, additionalInsets: layout.additionalInsets, statusBarHeight: nil, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver), navigationBarHeight: navigationHeight, transition: transition)
|
|
}
|
|
|
|
if self.locationAccessDenied {
|
|
self.controller?.navigationBar?.updateBackgroundAlpha(0.0, transition: .immediate)
|
|
Queue.mainQueue().after(0.25) {
|
|
self.controller?.updateTabBarAlpha(0.0, .immediate)
|
|
}
|
|
|
|
var placeholderTransition = transition
|
|
let placeholderNode: LocationPlaceholderNode
|
|
let backgroundNode: NavigationBackgroundNode
|
|
if let current = self.placeholderNode, let background = self.placeholderBackgroundNode {
|
|
placeholderNode = current
|
|
backgroundNode = background
|
|
|
|
backgroundNode.updateColor(color: self.presentationData.theme.rootController.tabBar.backgroundColor, transition: .immediate)
|
|
} else {
|
|
backgroundNode = NavigationBackgroundNode(color: self.presentationData.theme.rootController.tabBar.backgroundColor)
|
|
if let navigationBar = self.controller?.navigationBar {
|
|
self.insertSubnode(backgroundNode, belowSubnode: navigationBar)
|
|
} else {
|
|
self.addSubnode(backgroundNode)
|
|
}
|
|
self.placeholderBackgroundNode = backgroundNode
|
|
|
|
placeholderNode = LocationPlaceholderNode(content: .intro)
|
|
placeholderNode.settingsPressed = { [weak self] in
|
|
self?.context.sharedContext.applicationBindings.openSettings()
|
|
}
|
|
self.insertSubnode(placeholderNode, aboveSubnode: backgroundNode)
|
|
self.placeholderNode = placeholderNode
|
|
|
|
placeholderTransition = .immediate
|
|
}
|
|
placeholderNode.update(layout: layout, theme: self.presentationData.theme, strings: self.presentationData.strings, transition: placeholderTransition)
|
|
placeholderTransition.updateFrame(node: placeholderNode, frame: CGRect(origin: CGPoint(), size: layout.size))
|
|
|
|
let placeholderFrame = CGRect(origin: CGPoint(), size: layout.size)
|
|
backgroundNode.update(size: placeholderFrame.size, transition: placeholderTransition)
|
|
placeholderTransition.updateFrame(node: placeholderNode, frame: placeholderFrame)
|
|
} else {
|
|
if let placeholderNode = self.placeholderNode {
|
|
self.placeholderNode = nil
|
|
placeholderNode.removeFromSupernode()
|
|
}
|
|
if let placeholderBackgroundNode = self.placeholderBackgroundNode {
|
|
self.placeholderBackgroundNode = nil
|
|
placeholderBackgroundNode.removeFromSupernode()
|
|
}
|
|
|
|
self.controller?.navigationBar?.updateBackgroundAlpha(1.0, transition: .immediate)
|
|
self.controller?.updateTabBarAlpha(1.0, .immediate)
|
|
}
|
|
|
|
}
|
|
|
|
func updateSendActionHighlight(_ highlighted: Bool) {
|
|
self.headerNode.updateHighlight(highlighted)
|
|
self.shadeNode.backgroundColor = highlighted ? self.presentationData.theme.list.itemHighlightedBackgroundColor : self.presentationData.theme.list.plainBackgroundColor
|
|
}
|
|
|
|
func goToUserLocation() {
|
|
self.searchVenuesPromise.set(.single(nil))
|
|
self.updateState { state in
|
|
var state = state
|
|
state.displayingMapModeOptions = false
|
|
state.selectedLocation = .none
|
|
state.searchingVenuesAround = false
|
|
return state
|
|
}
|
|
}
|
|
|
|
func requestPlacesAtSelectedLocation() {
|
|
if case let .location(coordinate, _) = self.state.selectedLocation {
|
|
self.headerNode.mapNode.setMapCenter(coordinate: coordinate, animated: true)
|
|
self.searchVenuesPromise.set(.single(coordinate))
|
|
self.updateState { state in
|
|
var state = state
|
|
state.searchingVenuesAround = true
|
|
return state
|
|
}
|
|
}
|
|
}
|
|
}
|