import Foundation import UIKit import Display import LegacyComponents import TelegramCore import SyncCore import Postbox import SwiftSignalKit import MergeLists import ItemListUI import ItemListVenueItem import ActivityIndicator import TelegramPresentationData import AccountContext import AppBundle import CoreLocation import Geocoding import PhoneNumberFormat private struct LocationPickerTransaction { let deletions: [ListViewDeleteItem] let insertions: [ListViewInsertItem] let updates: [ListViewUpdateItem] let isLoading: Bool let crossFade: Bool } private enum LocationPickerEntryId: Hashable { case location case liveLocation case header case venue(String) case attribution } private enum LocationPickerEntry: Comparable, Identifiable { case location(PresentationTheme, String, String, TelegramMediaMap?, CLLocationCoordinate2D?) case liveLocation(PresentationTheme, String, String, CLLocationCoordinate2D?) case header(PresentationTheme, String) case venue(PresentationTheme, TelegramMediaMap, Int) case attribution(PresentationTheme) var stableId: LocationPickerEntryId { switch self { case .location: return .location case .liveLocation: return .liveLocation case .header: return .header case let .venue(_, venue, _): return .venue(venue.venue?.id ?? "") case .attribution: return .attribution } } static func ==(lhs: LocationPickerEntry, rhs: LocationPickerEntry) -> Bool { switch lhs { case let .location(lhsTheme, lhsTitle, lhsSubtitle, lhsVenue, lhsCoordinate): if case let .location(rhsTheme, rhsTitle, rhsSubtitle, rhsVenue, rhsCoordinate) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsSubtitle == rhsSubtitle, lhsVenue?.venue?.id == rhsVenue?.venue?.id, lhsCoordinate == rhsCoordinate { 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, lhsIndex): if case let .venue(rhsTheme, rhsVenue, rhsIndex) = rhs, lhsTheme === rhsTheme, lhsVenue.venue?.id == rhsVenue.venue?.id, lhsIndex == rhsIndex { return true } else { return false } case let .attribution(lhsTheme): if case let .attribution(rhsTheme) = rhs, lhsTheme === rhsTheme { return true } else { return false } } } static func <(lhs: LocationPickerEntry, rhs: LocationPickerEntry) -> Bool { switch lhs { case .location: switch rhs { case .location: return false case .liveLocation, .header, .venue, .attribution: return true } case .liveLocation: switch rhs { case .location, .liveLocation: return false case .header, .venue, .attribution: return true } case .header: switch rhs { case .location, .liveLocation, .header: return false case .venue, .attribution: return true } case let .venue(_, _, lhsIndex): switch rhs { case .location, .liveLocation, .header: return false case let .venue(_, _, rhsIndex): return lhsIndex < rhsIndex case .attribution: return true } case .attribution: return false } } func item(account: Account, presentationData: PresentationData, interaction: LocationPickerInteraction?) -> ListViewItem { switch self { case let .location(theme, title, subtitle, venue, coordinate): let icon: LocationActionListItemIcon if let venue = venue { icon = .venue(venue) } else { icon = .location } return LocationActionListItem(presentationData: ItemListPresentationData(presentationData), account: account, title: title, subtitle: subtitle, icon: icon, action: { if let venue = venue { interaction?.sendVenue(venue) } else if let coordinate = coordinate { interaction?.sendLocation(coordinate) } }, highlighted: { highlighted in interaction?.updateSendActionHighlight(highlighted) }) case let .liveLocation(theme, title, subtitle, coordinate): return LocationActionListItem(presentationData: ItemListPresentationData(presentationData), account: account, title: title, subtitle: subtitle, icon: .liveLocation, action: { if let coordinate = coordinate { interaction?.sendLiveLocation(coordinate) } }) case let .header(theme, title): return LocationSectionHeaderItem(presentationData: ItemListPresentationData(presentationData), title: title) case let .venue(theme, venue, _): let venueType = venue.venue?.type ?? "" return ItemListVenueItem(presentationData: ItemListPresentationData(presentationData), account: account, venue: venue, style: .plain, action: { interaction?.sendVenue(venue) }, infoAction: ["home", "work"].contains(venueType) ? { interaction?.openHomeWorkInfo() } : nil) case let .attribution(theme): return LocationAttributionItem(presentationData: ItemListPresentationData(presentationData)) } } } private func preparedTransition(from fromEntries: [LocationPickerEntry], to toEntries: [LocationPickerEntry], isLoading: Bool, crossFade: Bool, account: Account, 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(account: account, presentationData: presentationData, interaction: interaction), directionHint: nil) } let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, presentationData: presentationData, interaction: interaction), directionHint: nil) } return LocationPickerTransaction(deletions: deletions, insertions: insertions, updates: updates, isLoading: isLoading, crossFade: crossFade) } enum LocationPickerLocation: Equatable { case none case selecting case location(CLLocationCoordinate2D, String?) case venue(TelegramMediaMap) 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): if case let .venue(rhsVenue) = rhs, lhsVenue.venue?.id == rhsVenue.venue?.id { return true } else { return false } } } } struct LocationPickerState { var mapMode: LocationMapMode var displayingMapModeOptions: Bool var selectedLocation: LocationPickerLocation var forceSelection: Bool init() { self.mapMode = .map self.displayingMapModeOptions = false self.selectedLocation = .none self.forceSelection = false } } final class LocationPickerControllerNode: ViewControllerTracingNode { private let context: AccountContext private var presentationData: PresentationData private let presentationDataPromise: Promise private let mode: LocationPickerMode private let interaction: LocationPickerInteraction private let listNode: ListView private let headerNode: LocationMapHeaderNode private let activityIndicator: ActivityIndicator private let shadeNode: ASDisplayNode private let innerShadeNode: ASDisplayNode private let optionsNode: LocationOptionsNode private(set) var searchContainerNode: LocationSearchContainerNode? private var enqueuedTransitions: [LocationPickerTransaction] = [] private var disposable: Disposable? private var state: LocationPickerState private let statePromise: Promise private var geocodingDisposable = MetaDisposable() private var validLayout: (layout: ContainerViewLayout, navigationHeight: CGFloat)? private var listOffset: CGFloat? init(context: AccountContext, presentationData: PresentationData, mode: LocationPickerMode, interaction: LocationPickerInteraction) { self.context = context self.presentationData = presentationData self.presentationDataPromise = Promise(presentationData) self.mode = mode self.interaction = interaction 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.headerNode = LocationMapHeaderNode(presentationData: presentationData, toggleMapModeSelection: interaction.toggleMapModeSelection, goToUserLocation: interaction.goToUserLocation) self.headerNode.mapNode.isRotateEnabled = false self.optionsNode = LocationOptionsNode(presentationData: presentationData, updateMapMode: interaction.updateMapMode) self.activityIndicator = ActivityIndicator(type: .custom(self.presentationData.theme.list.itemSecondaryTextColor, 22.0, 1.0, false)) 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.activityIndicator) self.shadeNode.addSubnode(self.innerShadeNode) self.addSubnode(self.shadeNode) let userLocation: Signal = 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 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 == "_$!!$_" { homeAddress = address } else if address.label == "_$!!$_" { 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(postbox: context.account.postbox, address: address) } else { home = .single(nil) } if let address = workAddress { work = geocodeAddress(postbox: context.account.postbox, 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, geoPlace: nil, venue: MapVenue(title: presentationData.strings.Map_Home, address: address.displayString, provider: nil, id: "home", type: "home"), liveBroadcastingTimeout: nil)) } if let (latitude, longitude) = workCoordinate, let address = workAddress { venues.append(TelegramMediaMap(latitude: latitude, longitude: longitude, geoPlace: nil, venue: MapVenue(title: presentationData.strings.Map_Work, address: address.displayString, provider: nil, id: "work", type: "work"), liveBroadcastingTimeout: nil)) } return venues } } else { return .single(nil) } } ) let venues: Signal<[TelegramMediaMap]?, NoError> = .single(nil) |> then( throttledUserLocation(userLocation) |> mapToSignal { location -> Signal<[TelegramMediaMap]?, NoError> in if let location = location, location.horizontalAccuracy > 0 { return combineLatest(nearbyVenues(account: context.account, latitude: location.coordinate.latitude, longitude: location.coordinate.longitude), personalVenues) |> map { nearbyVenues, personalVenues -> [TelegramMediaMap]? in var resultVenues: [TelegramMediaMap] = [] 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) } } } resultVenues.append(contentsOf: nearbyVenues) return resultVenues } } else { return .single(nil) } } ) let previousState = Atomic(value: self.state) let previousUserLocation = Atomic(value: nil) let previousAnnotations = Atomic<[LocationPinAnnotation]>(value: []) let previousEntries = Atomic<[LocationPickerEntry]?>(value: nil) self.disposable = (combineLatest(self.presentationDataPromise.get(), self.statePromise.get(), userLocation, venues) |> deliverOnMainQueue).start(next: { [weak self] presentationData, state, userLocation, venues in if let strongSelf = self { var entries: [LocationPickerEntry] = [] switch state.selectedLocation { case let .location(coordinate, address): let title: String switch strongSelf.mode { case .share: title = presentationData.strings.Map_SendThisLocation case .pick: title = presentationData.strings.Map_SetThisLocation } entries.append(.location(presentationData.theme, title, address ?? presentationData.strings.Map_Locating, nil, coordinate)) case .selecting: let title: String switch strongSelf.mode { case .share: title = presentationData.strings.Map_SendThisLocation case .pick: title = presentationData.strings.Map_SetThisLocation } entries.append(.location(presentationData.theme, title, presentationData.strings.Map_Locating, nil, nil)) case let .venue(venue): 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, venue.coordinate)) case .none: let title: String switch strongSelf.mode { case .share: title = presentationData.strings.Map_SendMyCurrentLocation case .pick: title = presentationData.strings.Map_SetThisLocation } entries.append(.location(presentationData.theme, title, (userLocation?.horizontalAccuracy).flatMap { presentationData.strings.Map_AccurateTo(stringForDistance(strings: presentationData.strings, distance: $0)).0 } ?? presentationData.strings.Map_Locating, nil, userLocation?.coordinate)) } 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())) if let venues = venues { var index: Int = 0 for venue in venues { entries.append(.venue(presentationData.theme, venue, index)) index += 1 } if !venues.isEmpty { entries.append(.attribution(presentationData.theme)) } } 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: venues == nil, crossFade: crossFade, account: context.account, presentationData: presentationData, interaction: strongSelf.interaction) strongSelf.enqueueTransition(transition) strongSelf.headerNode.updateState(mapMode: state.mapMode, displayingMapModeOptions: state.displayingMapModeOptions) let previousUserLocation = previousUserLocation.swap(userLocation) switch state.selectedLocation { case .none: 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, _): var updateMap = false switch previousState.selectedLocation { case .none, .venue: updateMap = true case let .location(previousCoordinate, address): if previousCoordinate != coordinate { updateMap = true } default: break } if updateMap { strongSelf.headerNode.mapNode.setMapCenter(coordinate: coordinate, isUserLocation: false, animated: true) strongSelf.headerNode.mapNode.switchToPicking(animated: false) } case let .venue(venue): strongSelf.headerNode.mapNode.setMapCenter(coordinate: venue.coordinate, animated: true) } let annotations: [LocationPinAnnotation] if let venues = venues { annotations = venues.compactMap { LocationPinAnnotation(context: context, theme: presentationData.theme, location: $0) } } else { annotations = [] } let previousAnnotations = previousAnnotations.swap(annotations) if annotations != previousAnnotations { strongSelf.headerNode.mapNode.annotations = annotations } if let (layout, navigationBarHeight) = strongSelf.validLayout { var updateLayout = false var transition: ContainedViewLayoutTransition = .animated(duration: 0.45, curve: .spring) if previousState.displayingMapModeOptions != state.displayingMapModeOptions { updateLayout = true } else if previousState.selectedLocation.isCustom != state.selectedLocation.isCustom { updateLayout = true } if updateLayout { strongSelf.containerLayoutUpdated(layout, navigationHeight: navigationBarHeight, transition: transition) } } if case let .location(coordinate, address) = state.selectedLocation, address == nil { strongSelf.geocodingDisposable.set((reverseGeocodeLocation(latitude: coordinate.latitude, longitude: coordinate.longitude) |> deliverOnMainQueue).start(next: { [weak self] placemark in if let strongSelf = self { var address = placemark?.fullAddress ?? "" if address.isEmpty { address = presentationData.strings.Map_Unknown } strongSelf.updateState { state in var state = state state.selectedLocation = .location(coordinate, address) return state } } })) } else { strongSelf.geocodingDisposable.set(nil) } } }) if case let .share(_, selfPeer, _) = self.mode, let peer = selfPeer { self.headerNode.mapNode.userLocationAnnotation = LocationPinAnnotation(context: context, theme: self.presentationData.theme, peer: peer) 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.layoutActivityIndicator(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.updateState { state in var state = state state.displayingMapModeOptions = false state.selectedLocation = .selecting 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) } 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 state.selectedLocation = annotation?.location.flatMap { .venue($0) } ?? .none return state } } self.headerNode.mapNode.userLocationAnnotationSelected = { [weak self] in guard let strongSelf = self else { return } strongSelf.updateState { state in var state = state state.displayingMapModeOptions = false state.selectedLocation = .none return state } } } deinit { self.disposable?.dispose() self.geocodingDisposable.dispose() } 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 layout = 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.activityIndicator.isHidden = !transition.isLoading } }) } func activateSearch(navigationBar: NavigationBar) -> Signal { guard let (layout, navigationBarHeight) = self.validLayout, self.searchContainerNode == nil, let coordinate = self.headerNode.mapNode.mapCenterCoordinate else { return .complete() } let searchContainerNode = LocationSearchContainerNode(context: self.context, coordinate: coordinate, interaction: self.interaction) 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 layoutActivityIndicator(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 indicatorSize = self.activityIndicator.measure(CGSize(width: 100.0, height: 100.0)) let actionsInset: CGFloat = 148.0 transition.updateFrame(node: self.activityIndicator, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - indicatorSize.width) / 2.0), y: headerHeight + actionsInset + floor((layout.size.height - headerHeight - actionsInset - indicatorSize.height - layout.intrinsicInsets.bottom) / 2.0)), size: indicatorSize)) } 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 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 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.layoutActivityIndicator(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: layout.safeInsets.left, rightInset: layout.safeInsets.right, transition: transition) 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, statusBarHeight: nil, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver), navigationBarHeight: navigationHeight, transition: transition) } } func updateSendActionHighlight(_ highlighted: Bool) { self.headerNode.updateHighlight(highlighted) self.shadeNode.backgroundColor = highlighted ? self.presentationData.theme.list.itemHighlightedBackgroundColor : self.presentationData.theme.list.plainBackgroundColor } }