Location view improvements

This commit is contained in:
Ilya Laktyushin 2019-12-04 05:05:28 +04:00
parent 86b181d390
commit 1b264ac3a2
14 changed files with 1089 additions and 74 deletions

View File

@ -37,6 +37,19 @@ public struct ReverseGeocodedPlacemark {
public let city: String?
public let country: String?
public var compactDisplayAddress: String? {
if let street = self.street {
return street
}
if let city = self.city {
return city
}
if let country = self.country {
return country
}
return nil
}
public var fullAddress: String {
var components: [String] = []
if let street = self.street {

View File

@ -34,6 +34,7 @@ static_library(
"//submodules/ChatListSearchItemHeader:ChatListSearchItemHeader",
"//submodules/PhoneNumberFormat:PhoneNumberFormat",
"//submodules/PersistentStringHash:PersistentStringHash",
"//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode",
],
frameworks = [
"$SDKROOT/System/Library/Frameworks/Foundation.framework",

View File

@ -33,6 +33,7 @@ class LocationPinAnnotation: NSObject, MKAnnotation {
var coordinate: CLLocationCoordinate2D
let location: TelegramMediaMap?
let peer: Peer?
let forcedSelection: Bool
var title: String? = ""
var subtitle: String? = ""
@ -43,17 +44,29 @@ class LocationPinAnnotation: NSObject, MKAnnotation {
self.location = nil
self.peer = peer
self.coordinate = kCLLocationCoordinate2DInvalid
self.forcedSelection = false
super.init()
}
init(account: Account, theme: PresentationTheme, location: TelegramMediaMap) {
init(account: Account, theme: PresentationTheme, location: TelegramMediaMap, forcedSelection: Bool = false) {
self.account = account
self.theme = theme
self.location = location
self.peer = nil
self.coordinate = location.coordinate
self.forcedSelection = forcedSelection
super.init()
}
var id: String {
if let peer = self.peer {
return "\(peer.id.toInt64())"
} else if let venueId = self.location?.venue?.id {
return venueId
} else {
return String(format: "%.5f_%.5f", self.coordinate.latitude, self.coordinate.longitude)
}
}
}
class LocationPinAnnotationLayer: CALayer {
@ -142,8 +155,14 @@ class LocationPinAnnotationView: MKAnnotationView {
}
var defaultZPosition: CGFloat {
if let annotation = self.annotation as? LocationPinAnnotation, let venueType = annotation.location?.venue?.type, ["home", "work"].contains(venueType) {
return -0.5
if let annotation = self.annotation as? LocationPinAnnotation {
if annotation.forcedSelection {
return 0.0
} else if let venueType = annotation.location?.venue?.type, ["home", "work"].contains(venueType) {
return -0.5
} else {
return -1.0
}
} else {
return -1.0
}
@ -179,6 +198,10 @@ class LocationPinAnnotationView: MKAnnotationView {
self.shadowNode.isHidden = true
self.smallNode.isHidden = false
}
if annotation.forcedSelection {
self.setSelected(true, animated: false)
}
}
}
}
@ -192,6 +215,12 @@ class LocationPinAnnotationView: MKAnnotationView {
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
if let annotation = self.annotation as? LocationPinAnnotation {
if annotation.forcedSelection && !selected {
return
}
}
if animated {
self.layoutSubviews()
@ -529,7 +558,7 @@ class LocationPinAnnotationView: MKAnnotationView {
if !self.appeared {
self.appeared = true
if let annotation = annotation as? LocationPinAnnotation, annotation.location != nil {
if let annotation = annotation as? LocationPinAnnotation, annotation.location != nil && !annotation.forcedSelection {
self.animateAppearance()
}
}

View File

@ -0,0 +1,264 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Postbox
import Display
import SwiftSignalKit
import TelegramCore
import SyncCore
import TelegramPresentationData
import ItemListUI
import LocationResources
import AppBundle
import SolidRoundedButtonNode
final class LocationInfoListItem: ListViewItem {
let presentationData: ItemListPresentationData
let account: Account
let location: TelegramMediaMap
let address: String?
let distance: String?
let eta: String?
let action: () -> Void
let getDirections: () -> Void
public init(presentationData: ItemListPresentationData, account: Account, location: TelegramMediaMap, address: String?, distance: String?, eta: String?, action: @escaping () -> Void, getDirections: @escaping () -> Void) {
self.presentationData = presentationData
self.account = account
self.location = location
self.address = address
self.distance = distance
self.eta = eta
self.action = action
self.getDirections = getDirections
}
public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = LocationInfoListItemNode()
let makeLayout = node.asyncLayout()
let (nodeLayout, nodeApply) = makeLayout(self, params)
node.contentSize = nodeLayout.contentSize
node.insets = nodeLayout.insets
completion(node, nodeApply)
}
}
public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? LocationInfoListItemNode {
let layout = nodeValue.asyncLayout()
async {
let (nodeLayout, apply) = layout(self, params)
Queue.mainQueue().async {
completion(nodeLayout, { info in
apply().1(info)
})
}
}
}
}
}
public var selectable: Bool {
return false
}
}
final class LocationInfoListItemNode: ListViewItemNode {
private let backgroundNode: ASDisplayNode
private var titleNode: TextNode?
private var subtitleNode: TextNode?
private let venueIconNode: TransformImageNode
private let buttonNode: HighlightableButtonNode
private var directionsButtonNode: SolidRoundedButtonNode?
private var item: LocationInfoListItem?
private var layoutParams: ListViewItemLayoutParams?
required init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.buttonNode = HighlightableButtonNode()
self.venueIconNode = TransformImageNode()
self.venueIconNode.isUserInteractionEnabled = false
super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false)
self.addSubnode(self.backgroundNode)
self.addSubnode(self.buttonNode)
self.addSubnode(self.venueIconNode)
self.buttonNode.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.titleNode?.layer.removeAnimation(forKey: "opacity")
strongSelf.titleNode?.alpha = 0.4
strongSelf.subtitleNode?.layer.removeAnimation(forKey: "opacity")
strongSelf.subtitleNode?.alpha = 0.4
strongSelf.venueIconNode.layer.removeAnimation(forKey: "opacity")
strongSelf.venueIconNode.alpha = 0.4
} else {
strongSelf.titleNode?.alpha = 1.0
strongSelf.titleNode?.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
strongSelf.subtitleNode?.alpha = 1.0
strongSelf.subtitleNode?.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
strongSelf.venueIconNode.alpha = 1.0
strongSelf.venueIconNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
}
}
}
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
}
override func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) {
if let item = self.item {
let makeLayout = self.asyncLayout()
let (nodeLayout, nodeApply) = makeLayout(item, params)
self.contentSize = nodeLayout.contentSize
self.insets = nodeLayout.insets
let _ = nodeApply()
}
}
func asyncLayout() -> (_ item: LocationInfoListItem, _ params: ListViewItemLayoutParams) -> (ListViewItemNodeLayout, () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) {
let currentItem = self.item
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeSubtitleLayout = TextNode.asyncLayout(self.subtitleNode)
let iconLayout = self.venueIconNode.asyncLayout()
return { [weak self] item, params in
let leftInset: CGFloat = 75.0 + params.leftInset
let rightInset: CGFloat = params.rightInset
let verticalInset: CGFloat = 14.0
let iconSize: CGFloat = 48.0
let inset: CGFloat = 15.0
let titleFont = Font.medium(item.presentationData.fontSize.itemListBaseFontSize)
let subtitleFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 14.0 / 17.0))
let title: String
let subtitle: String
var subtitleComponents: [String] = []
if let venue = item.location.venue {
title = venue.title
} else {
title = item.presentationData.strings.Map_Location
}
if let address = item.address {
subtitleComponents.append(address)
}
if let distance = item.distance {
subtitleComponents.append(distance)
}
subtitle = subtitleComponents.joined(separator: "")
let titleAttributedString = NSAttributedString(string: title, font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor)
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset - 15.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let subtitleAttributedString = NSAttributedString(string: subtitle, font: subtitleFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor)
let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: subtitleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset - 15.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let titleSpacing: CGFloat = 1.0
let bottomInset: CGFloat = 4.0
let contentSize = CGSize(width: params.width, height: max(126.0, verticalInset * 2.0 + titleLayout.size.height + titleSpacing + subtitleLayout.size.height + bottomInset))
let nodeLayout = ListViewItemNodeLayout(contentSize: contentSize, insets: UIEdgeInsets())
return (nodeLayout, { [weak self] in
var updatedTheme: PresentationTheme?
if currentItem?.presentationData.theme !== item.presentationData.theme {
updatedTheme = item.presentationData.theme
}
var updatedLocation: TelegramMediaMap?
if currentItem?.location.venue?.id != item.location.venue?.id || updatedTheme != nil {
updatedLocation = item.location
}
return (nil, { _ in
if let strongSelf = self {
strongSelf.item = item
strongSelf.layoutParams = params
if let _ = updatedTheme {
strongSelf.backgroundNode.backgroundColor = item.presentationData.theme.list.plainBackgroundColor
strongSelf.directionsButtonNode?.updateTheme(SolidRoundedButtonTheme(theme: item.presentationData.theme))
}
if let updatedLocation = updatedLocation {
strongSelf.venueIconNode.setSignal(venueIcon(postbox: item.account.postbox, type: updatedLocation.venue?.type ?? "", background: true))
}
let iconApply = iconLayout(TransformImageArguments(corners: ImageCorners(), imageSize: CGSize(width: iconSize, height: iconSize), boundingSize: CGSize(width: iconSize, height: iconSize), intrinsicInsets: UIEdgeInsets()))
iconApply()
let titleNode = titleApply()
if strongSelf.titleNode == nil {
titleNode.isUserInteractionEnabled = false
strongSelf.titleNode = titleNode
strongSelf.addSubnode(titleNode)
}
let subtitleNode = subtitleApply()
if strongSelf.subtitleNode == nil {
subtitleNode.isUserInteractionEnabled = false
strongSelf.subtitleNode = subtitleNode
strongSelf.addSubnode(subtitleNode)
}
let directionsButtonNode: SolidRoundedButtonNode
if let currentDirectionsButtonNode = strongSelf.directionsButtonNode {
directionsButtonNode = currentDirectionsButtonNode
} else {
directionsButtonNode = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(theme: item.presentationData.theme), height: 50.0, cornerRadius: 10.0)
directionsButtonNode.title = item.presentationData.strings.Map_Directions
directionsButtonNode.pressed = {
item.getDirections()
}
strongSelf.addSubnode(directionsButtonNode)
strongSelf.directionsButtonNode = directionsButtonNode
}
directionsButtonNode.subtitle = item.eta
let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset), size: titleLayout.size)
titleNode.frame = titleFrame
let subtitleFrame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset + titleLayout.size.height + titleSpacing), size: subtitleLayout.size)
subtitleNode.frame = subtitleFrame
let separatorHeight = UIScreenPixel
let topHighlightInset: CGFloat = separatorHeight
let iconNodeFrame = CGRect(origin: CGPoint(x: params.leftInset + inset, y: 10.0), size: CGSize(width: iconSize, height: iconSize))
strongSelf.venueIconNode.frame = iconNodeFrame
let directionsWidth = contentSize.width - inset * 2.0
let directionsHeight = directionsButtonNode.updateLayout(width: directionsWidth, transition: .immediate)
directionsButtonNode.frame = CGRect(x: inset, y: iconNodeFrame.maxY + 14.0, width: directionsWidth, height: directionsHeight)
strongSelf.buttonNode.frame = CGRect(x: 0.0, y: 0.0, width: contentSize.width, height: 72.0)
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: contentSize.width, height: contentSize.height))
}
})
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.5)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 0.5, removeOnCompletion: false)
}
@objc private func buttonPressed() {
self.item?.action()
}
}

View File

@ -36,7 +36,8 @@ private func generateShadowImage(theme: PresentationTheme, highlighted: Bool) ->
final class LocationMapHeaderNode: ASDisplayNode {
private var presentationData: PresentationData
private let interaction: LocationPickerInteraction
private let toggleMapModeSelection: () -> Void
private let goToUserLocation: () -> Void
let mapNode: LocationMapNode
private let optionsBackgroundNode: ASImageNode
@ -44,9 +45,10 @@ final class LocationMapHeaderNode: ASDisplayNode {
private let locationButtonNode: HighlightableButtonNode
private let shadowNode: ASImageNode
init(presentationData: PresentationData, interaction: LocationPickerInteraction) {
init(presentationData: PresentationData, toggleMapModeSelection: @escaping () -> Void, goToUserLocation: @escaping () -> Void) {
self.presentationData = presentationData
self.interaction = interaction
self.toggleMapModeSelection = toggleMapModeSelection
self.goToUserLocation = goToUserLocation
self.mapNode = LocationMapNode()
@ -84,9 +86,9 @@ final class LocationMapHeaderNode: ASDisplayNode {
self.locationButtonNode.addTarget(self, action: #selector(self.locationPressed), forControlEvents: .touchUpInside)
}
func updateState(_ state: LocationPickerState) {
self.mapNode.mapMode = state.mapMode
self.infoButtonNode.isSelected = state.displayingMapModeOptions
func updateState(mapMode: LocationMapMode, displayingMapModeOptions: Bool) {
self.mapNode.mapMode = mapMode
self.infoButtonNode.isSelected = displayingMapModeOptions
}
func updatePresentationData(_ presentationData: PresentationData) {
@ -123,10 +125,10 @@ final class LocationMapHeaderNode: ASDisplayNode {
}
@objc private func infoPressed() {
self.interaction.toggleMapModeSelection()
self.toggleMapModeSelection()
}
@objc private func locationPressed() {
self.interaction.goToUserLocation()
self.goToUserLocation()
}
}

View File

@ -4,6 +4,7 @@ import SwiftSignalKit
import MapKit
let defaultMapSpan = MKCoordinateSpan(latitudeDelta: 0.016, longitudeDelta: 0.016)
let viewMapSpan = MKCoordinateSpan(latitudeDelta: 0.008, longitudeDelta: 0.008)
private let pinOffset = CGPoint(x: 0.0, y: 33.0)
public enum LocationMapMode {
@ -331,9 +332,7 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate {
var dict: [String: LocationPinAnnotation] = [:]
for annotation in self.annotations {
if let identifier = annotation.location?.venue?.id {
dict[identifier] = annotation
}
dict[annotation.id] = annotation
}
var annotationsToRemove = Set<LocationPinAnnotation>()
@ -342,9 +341,9 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate {
continue
}
if let identifier = annotation.location?.venue?.id, let updatedAnnotation = dict[identifier] {
if let updatedAnnotation = dict[annotation.id] {
annotation.coordinate = updatedAnnotation.coordinate
dict[identifier] = nil
dict[annotation.id] = nil
} else {
annotationsToRemove.insert(annotation)
}

View File

@ -14,11 +14,9 @@ final class LocationOptionsNode: ASDisplayNode {
private let backgroundNode: ASDisplayNode
private let separatorNode: ASDisplayNode
private let segmentedControlNode: SegmentedControlNode
private let interaction: LocationPickerInteraction
init(presentationData: PresentationData, interaction: LocationPickerInteraction) {
init(presentationData: PresentationData, updateMapMode: @escaping (LocationMapMode) -> Void) {
self.presentationData = presentationData
self.interaction = interaction
self.backgroundNode = ASDisplayNode()
self.backgroundNode.backgroundColor = self.presentationData.theme.rootController.navigationBar.backgroundColor
@ -39,11 +37,11 @@ final class LocationOptionsNode: ASDisplayNode {
}
switch index {
case 0:
strongSelf.interaction.updateMapMode(.map)
updateMapMode(.map)
case 1:
strongSelf.interaction.updateMapMode(.sattelite)
updateMapMode(.sattelite)
case 2:
strongSelf.interaction.updateMapMode(.hybrid)
updateMapMode(.hybrid)
default:
break
}

View File

@ -103,7 +103,7 @@ public final class LocationPickerController: ViewController {
let locationWithTimeout: (CLLocationCoordinate2D, Int32?) -> TelegramMediaMap = { coordinate, timeout in
return TelegramMediaMap(latitude: coordinate.latitude, longitude: coordinate.longitude, geoPlace: nil, venue: nil, liveBroadcastingTimeout: timeout)
}
self.interaction = LocationPickerInteraction(sendLocation: { [weak self] coordinate in
guard let strongSelf = self else {
return
@ -133,21 +133,21 @@ public final class LocationPickerController: ViewController {
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Map_LiveLocationFor15Minutes, color: .accent, action: { [weak self, weak controller] in
controller?.dismissAnimated()
if let strongSelf = self {
strongSelf.completion(locationWithTimeout(coordinate, 15 * 60), nil)
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(locationWithTimeout(coordinate, 60 * 60 - 1), nil)
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(locationWithTimeout(coordinate, 8 * 60 * 60), nil)
strongSelf.completion(TelegramMediaMap(coordinate: coordinate, liveBroadcastingTimeout: 8 * 60 * 60), nil)
strongSelf.dismiss()
}
})

View File

@ -277,10 +277,10 @@ final class LocationPickerControllerNode: ViewControllerTracingNode {
self.listNode.verticalScrollIndicatorColor = UIColor(white: 0.0, alpha: 0.3)
self.listNode.verticalScrollIndicatorFollowsOverscroll = true
self.headerNode = LocationMapHeaderNode(presentationData: presentationData, interaction: interaction)
self.headerNode = LocationMapHeaderNode(presentationData: presentationData, toggleMapModeSelection: interaction.toggleMapModeSelection, goToUserLocation: interaction.goToUserLocation)
self.headerNode.mapNode.isRotateEnabled = false
self.optionsNode = LocationOptionsNode(presentationData: presentationData, interaction: interaction)
self.optionsNode = LocationOptionsNode(presentationData: presentationData, updateMapMode: interaction.updateMapMode)
self.activityIndicator = ActivityIndicator(type: .custom(self.presentationData.theme.list.itemSecondaryTextColor, 22.0, 1.0, false))
@ -302,28 +302,6 @@ final class LocationPickerControllerNode: ViewControllerTracingNode {
self.addSubnode(self.shadeNode)
let userLocation: Signal<CLLocation?, NoError> = self.headerNode.mapNode.userLocation
let filteredUserLocation: Signal<CLLocation?, NoError> = userLocation
|> reduceLeft(value: nil) { current, updated, emit -> CLLocation? in
if let current = current {
if let updated = updated {
if updated.distance(from: current) > 250 || (updated.horizontalAccuracy < 50.0 && updated.horizontalAccuracy < current.horizontalAccuracy) {
emit(updated)
return updated
} else {
return current
}
} else {
return current
}
} else {
if let updated = updated, updated.horizontalAccuracy > 0.0 {
emit(updated)
return updated
} else {
return nil
}
}
}
let personalAddresses = self.context.account.postbox.peerView(id: self.context.account.peerId)
|> mapToSignal { view -> Signal<(DeviceContactAddressData?, DeviceContactAddressData?)?, NoError> in
@ -407,7 +385,7 @@ final class LocationPickerControllerNode: ViewControllerTracingNode {
let venues: Signal<[TelegramMediaMap]?, NoError> = .single(nil)
|> then(
filteredUserLocation
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)
@ -505,7 +483,7 @@ final class LocationPickerControllerNode: ViewControllerTracingNode {
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(state)
strongSelf.headerNode.updateState(mapMode: state.mapMode, displayingMapModeOptions: state.displayingMapModeOptions)
let previousUserLocation = previousUserLocation.swap(userLocation)
switch state.selectedLocation {

View File

@ -7,6 +7,10 @@ import TelegramStringFormatting
import MapKit
extension TelegramMediaMap {
convenience init(coordinate: CLLocationCoordinate2D, liveBroadcastingTimeout: Int32? = nil) {
self.init(latitude: coordinate.latitude, longitude: coordinate.longitude, geoPlace: nil, venue: nil, liveBroadcastingTimeout: liveBroadcastingTimeout)
}
var coordinate: CLLocationCoordinate2D {
return CLLocationCoordinate2D(latitude: self.latitude, longitude: self.longitude)
}
@ -76,3 +80,72 @@ func stringForDistance(strings: PresentationStrings, distance: CLLocationDistanc
}
return distanceFormatter.string(fromDistance: distance)
}
func stringForEstimatedDuration(strings: PresentationStrings, eta: Double) -> String? {
if eta > 0.0 && eta < 60.0 * 60.0 * 10.0 {
var eta = max(eta, 60.0)
let minutes = Int32(eta / 60.0) % 60
let hours = Int32(eta / 3600.0)
let string: String
if hours > 1 {
if hours == 1 && minutes == 0 {
string = strings.Map_ETAHours(1)
} else {
string = strings.Map_ETAHours(9999).replacingOccurrences(of: "9999", with: String(format: "%d:%02d", arguments: [hours, minutes]))
}
} else {
string = strings.Map_ETAMinutes(minutes)
}
return strings.Map_DirectionsDriveEta(string).0
} else {
return nil
}
}
func throttledUserLocation(_ userLocation: Signal<CLLocation?, NoError>) -> Signal<CLLocation?, NoError> {
return userLocation
|> reduceLeft(value: nil) { current, updated, emit -> CLLocation? in
if let current = current {
if let updated = updated {
if updated.distance(from: current) > 250 || (updated.horizontalAccuracy < 50.0 && updated.horizontalAccuracy < current.horizontalAccuracy) {
emit(updated)
return updated
} else {
return current
}
} else {
return current
}
} else {
if let updated = updated, updated.horizontalAccuracy > 0.0 {
emit(updated)
return updated
} else {
return nil
}
}
}
}
func driveEta(coordinate: CLLocationCoordinate2D) -> Signal<Double?, NoError> {
return Signal { subscriber in
let destinationPlacemark = MKPlacemark(coordinate: coordinate, addressDictionary: nil)
let destination = MKMapItem(placemark: destinationPlacemark)
let request = MKDirections.Request()
request.source = MKMapItem.forCurrentLocation()
request.destination = destination
request.transportType = .automobile
request.requestsAlternateRoutes = false
let directions = MKDirections(request: request)
directions.calculateETA { response, error in
subscriber.putNext(response?.expectedTravelTime)
subscriber.putCompletion()
}
return ActionDisposable {
directions.cancel()
}
}
}

View File

@ -0,0 +1,188 @@
import Foundation
import UIKit
import Display
import LegacyComponents
import TelegramCore
import SyncCore
import Postbox
import SwiftSignalKit
import TelegramPresentationData
import AccountContext
import AppBundle
import CoreLocation
import PresentationDataUtils
import OpenInExternalAppUI
import ShareController
public class LocationViewParams {
let sendLiveLocation: (TelegramMediaMap) -> Void
let stopLiveLocation: () -> Void
let openUrl: (String) -> Void
let openPeer: (Peer) -> Void
public init(sendLiveLocation: @escaping (TelegramMediaMap) -> Void, stopLiveLocation: @escaping () -> Void, openUrl: @escaping (String) -> Void, openPeer: @escaping (Peer) -> Void) {
self.sendLiveLocation = sendLiveLocation
self.stopLiveLocation = stopLiveLocation
self.openUrl = openUrl
self.openPeer = openPeer
}
}
class LocationViewInteraction {
let toggleMapModeSelection: () -> Void
let updateMapMode: (LocationMapMode) -> Void
let goToUserLocation: () -> Void
let goToCoordinate: (CLLocationCoordinate2D) -> Void
let requestDirections: () -> Void
let share: () -> Void
init(toggleMapModeSelection: @escaping () -> Void, updateMapMode: @escaping (LocationMapMode) -> Void, goToUserLocation: @escaping () -> Void, goToCoordinate: @escaping (CLLocationCoordinate2D) -> Void, requestDirections: @escaping () -> Void, share: @escaping () -> Void) {
self.toggleMapModeSelection = toggleMapModeSelection
self.updateMapMode = updateMapMode
self.goToUserLocation = goToUserLocation
self.goToCoordinate = goToCoordinate
self.requestDirections = requestDirections
self.share = share
}
}
public final class LocationViewController: ViewController {
private var controllerNode: LocationViewControllerNode {
return self.displayNode as! LocationViewControllerNode
}
private let context: AccountContext
private var mapMedia: TelegramMediaMap
private var presentationData: PresentationData
private var presentationDataDisposable: Disposable?
private var interaction: LocationViewInteraction?
public init(context: AccountContext, mapMedia: TelegramMediaMap, params: LocationViewParams) {
self.context = context
self.mapMedia = mapMedia
self.presentationData = 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_LocationTitle
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Close, style: .plain, target: self, action: #selector(self.cancelPressed))
self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationShareIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.sharePressed))
self.navigationItem.rightBarButtonItem?.accessibilityLabel = self.presentationData.strings.VoiceOver_MessageContextShare
self.presentationDataDisposable = (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.navigationItem.rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationShareIcon(strongSelf.presentationData.theme), style: .plain, target: strongSelf, action: #selector(strongSelf.sharePressed))
if strongSelf.isNodeLoaded {
strongSelf.controllerNode.updatePresentationData(presentationData)
}
})
self.interaction = LocationViewInteraction(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.updateState { state in
var state = state
state.displayingMapModeOptions = false
state.selectedLocation = .user
return state
}
}, goToCoordinate: { [weak self] coordinate in
guard let strongSelf = self else {
return
}
strongSelf.controllerNode.updateState { state in
var state = state
state.displayingMapModeOptions = false
state.selectedLocation = .coordinate(coordinate)
return state
}
}, requestDirections: { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.present(OpenInActionSheetController(context: context, item: .location(location: mapMedia, withDirections: true), additionalAction: nil, openUrl: params.openUrl), in: .window(.root), with: nil)
}, share: { [weak self] in
guard let strongSelf = self else {
return
}
let shareAction = OpenInControllerAction(title: strongSelf.presentationData.strings.Conversation_ContextMenuShare, action: {
strongSelf.present(ShareController(context: context, subject: .mapMedia(mapMedia), externalShare: true), in: .window(.root), with: nil)
})
strongSelf.present(OpenInActionSheetController(context: context, item: .location(location: mapMedia, withDirections: false), additionalAction: shareAction, openUrl: params.openUrl), in: .window(.root), with: nil)
})
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()
}
override public func loadDisplayNode() {
super.loadDisplayNode()
guard let interaction = self.interaction else {
return
}
self.displayNode = LocationViewControllerNode(context: self.context, presentationData: self.presentationData, mapMedia: self.mapMedia, interaction: interaction)
self.displayNodeDidLoad()
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.controllerNode.containerLayoutUpdated(layout, navigationHeight: self.navigationHeight, transition: transition)
}
@objc private func cancelPressed() {
self.dismiss()
}
@objc private func sharePressed() {
self.interaction?.share()
}
@objc private func showAllPressed() {
self.dismiss()
}
}

View File

@ -0,0 +1,431 @@
import Foundation
import UIKit
import Display
import LegacyComponents
import TelegramCore
import SyncCore
import Postbox
import SwiftSignalKit
import MergeLists
import ItemListUI
import ItemListVenueItem
import TelegramPresentationData
import AccountContext
import AppBundle
import CoreLocation
import Geocoding
private func areMessagesEqual(_ lhsMessage: Message, _ rhsMessage: Message) -> Bool {
if lhsMessage.stableVersion != rhsMessage.stableVersion {
return false
}
if lhsMessage.id != rhsMessage.id || lhsMessage.flags != rhsMessage.flags {
return false
}
return true
}
private struct LocationViewTransaction {
let deletions: [ListViewDeleteItem]
let insertions: [ListViewInsertItem]
let updates: [ListViewUpdateItem]
}
private enum LocationViewEntryId: Hashable {
case info
case toggleLiveLocation
case liveLocation(PeerId)
}
private enum LocationViewEntry: Comparable, Identifiable {
case info(PresentationTheme, TelegramMediaMap, String?, Double?, Double?)
case toggleLiveLocation(PresentationTheme, String, String)
case liveLocation(PresentationTheme, Message, Int)
var stableId: LocationViewEntryId {
switch self {
case .info:
return .info
case .toggleLiveLocation:
return .toggleLiveLocation
case let .liveLocation(_, message, _):
return .liveLocation(message.id.peerId)
}
}
static func ==(lhs: LocationViewEntry, rhs: LocationViewEntry) -> Bool {
switch lhs {
case let .info(lhsTheme, lhsLocation, lhsAddress, lhsDistance, lhsTime):
if case let .info(rhsTheme, rhsLocation, rhsAddress, rhsDistance, rhsTime) = rhs, lhsTheme === rhsTheme, lhsLocation.venue?.id == rhsLocation.venue?.id, lhsAddress == rhsAddress, lhsDistance == rhsDistance, lhsTime == rhsTime {
return true
} else {
return false
}
case let .toggleLiveLocation(lhsTheme, lhsTitle, lhsSubtitle):
if case let .toggleLiveLocation(rhsTheme, rhsTitle, rhsSubtitle) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsSubtitle == rhsSubtitle {
return true
} else {
return false
}
case let .liveLocation(lhsTheme, lhsMessage, lhsIndex):
if case let .liveLocation(rhsTheme, rhsMessage, rhsIndex) = rhs, lhsTheme === rhsTheme, areMessagesEqual(lhsMessage, rhsMessage), lhsIndex == rhsIndex {
return true
} else {
return false
}
}
}
static func <(lhs: LocationViewEntry, rhs: LocationViewEntry) -> Bool {
switch lhs {
case .info:
switch rhs {
case .info:
return false
case .toggleLiveLocation, .liveLocation:
return true
}
case .toggleLiveLocation:
switch rhs {
case .info, .toggleLiveLocation:
return false
case .liveLocation:
return true
}
case let .liveLocation(_, _, lhsIndex):
switch rhs {
case .info, .toggleLiveLocation:
return false
case let .liveLocation(_, _, rhsIndex):
return lhsIndex < rhsIndex
}
}
}
func item(account: Account, presentationData: PresentationData, interaction: LocationViewInteraction?) -> ListViewItem {
switch self {
case let .info(theme, location, address, distance, time):
let addressString: String?
if let address = address {
addressString = address
} else {
addressString = presentationData.strings.Map_Locating
}
let distanceString: String?
if let distance = distance {
distanceString = distance < 10 ? presentationData.strings.Map_YouAreHere : presentationData.strings.Map_DistanceAway(stringForDistance(strings: presentationData.strings, distance: distance)).0
} else {
distanceString = nil
}
let eta = time.flatMap { stringForEstimatedDuration(strings: presentationData.strings, eta: $0) }
return LocationInfoListItem(presentationData: ItemListPresentationData(presentationData), account: account, location: location, address: addressString, distance: distanceString, eta: eta, action: {
interaction?.goToCoordinate(location.coordinate)
}, getDirections: {
interaction?.requestDirections()
})
case let .toggleLiveLocation(theme, title, subtitle):
return LocationActionListItem(presentationData: ItemListPresentationData(presentationData), account: account, title: title, subtitle: subtitle, icon: .liveLocation, action: {
// if let coordinate = coordinate {
// interaction?.sendLiveLocation(coordinate)
// }
})
case let .liveLocation(theme, message, _):
return ItemListTextItem(presentationData: ItemListPresentationData(presentationData), text: .plain(""), sectionId: 0)
}
}
}
private func preparedTransition(from fromEntries: [LocationViewEntry], to toEntries: [LocationViewEntry], account: Account, presentationData: PresentationData, interaction: LocationViewInteraction?) -> LocationViewTransaction {
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 LocationViewTransaction(deletions: deletions, insertions: insertions, updates: updates)
}
enum LocationViewLocation: Equatable {
case initial
case user
case coordinate(CLLocationCoordinate2D)
case custom
}
struct LocationViewState {
var mapMode: LocationMapMode
var displayingMapModeOptions: Bool
var selectedLocation: LocationViewLocation
init() {
self.mapMode = .map
self.displayingMapModeOptions = false
self.selectedLocation = .initial
}
}
final class LocationViewControllerNode: ViewControllerTracingNode {
private let context: AccountContext
private var presentationData: PresentationData
private let presentationDataPromise: Promise<PresentationData>
private var mapMedia: TelegramMediaMap
private let interaction: LocationViewInteraction
private let listNode: ListView
private let headerNode: LocationMapHeaderNode
private let optionsNode: LocationOptionsNode
private var enqueuedTransitions: [LocationViewTransaction] = []
private var disposable: Disposable?
private var state: LocationViewState
private let statePromise: Promise<LocationViewState>
private var geocodingDisposable = MetaDisposable()
private var validLayout: (layout: ContainerViewLayout, navigationHeight: CGFloat)?
private var listOffset: CGFloat?
init(context: AccountContext, presentationData: PresentationData, mapMedia: TelegramMediaMap, interaction: LocationViewInteraction) {
self.context = context
self.presentationData = presentationData
self.presentationDataPromise = Promise(presentationData)
self.mapMedia = mapMedia
self.interaction = interaction
self.state = LocationViewState()
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)
super.init()
self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
self.addSubnode(self.listNode)
self.addSubnode(self.headerNode)
self.addSubnode(self.optionsNode)
let distance: Signal<Double?, NoError> = .single(nil)
|> then(
throttledUserLocation(self.headerNode.mapNode.userLocation)
|> map { userLocation -> Double? in
let location = CLLocation(latitude: mapMedia.latitude, longitude: mapMedia.longitude)
return userLocation.flatMap { location.distance(from: $0) }
}
)
let address: Signal<String?, NoError>
var eta: Signal<Double?, NoError> = .single(nil)
|> then(
driveEta(coordinate: mapMedia.coordinate)
)
if let venue = mapMedia.venue, let venueAddress = venue.address, !venueAddress.isEmpty {
address = .single(venueAddress)
} else if mapMedia.liveBroadcastingTimeout == nil {
address = .single(nil)
|> then(
reverseGeocodeLocation(latitude: mapMedia.latitude, longitude: mapMedia.longitude)
|> map { placemark -> String? in
return placemark?.compactDisplayAddress ?? ""
}
)
} else {
address = .single(nil)
eta = .single(nil)
}
let previousState = Atomic<LocationViewState?>(value: nil)
let previousAnnotations = Atomic<[LocationPinAnnotation]>(value: [])
let previousEntries = Atomic<[LocationViewEntry]?>(value: nil)
self.disposable = (combineLatest(self.presentationDataPromise.get(), self.statePromise.get(), self.headerNode.mapNode.userLocation, distance, address, eta)
|> deliverOnMainQueue).start(next: { [weak self] presentationData, state, userLocation, distance, address, eta in
if let strongSelf = self {
var entries: [LocationViewEntry] = []
entries.append(.info(presentationData.theme, mapMedia, address, distance, eta))
let previousEntries = previousEntries.swap(entries)
let previousState = previousState.swap(state)
let transition = preparedTransition(from: previousEntries ?? [], to: entries, account: context.account, presentationData: presentationData, interaction: strongSelf.interaction)
strongSelf.enqueueTransition(transition)
strongSelf.headerNode.updateState(mapMode: state.mapMode, displayingMapModeOptions: state.displayingMapModeOptions)
switch state.selectedLocation {
case .initial:
if previousState?.selectedLocation != .initial {
strongSelf.headerNode.mapNode.setMapCenter(coordinate: mapMedia.coordinate, span: viewMapSpan, animated: previousState != nil)
}
case let .coordinate(coordinate):
if let previousState = previousState, case let .coordinate(previousCoordinate) = previousState.selectedLocation, previousCoordinate == coordinate {
} else {
strongSelf.headerNode.mapNode.setMapCenter(coordinate: coordinate, span: viewMapSpan, animated: true)
}
case .user:
if previousState?.selectedLocation != .user, let userLocation = userLocation {
strongSelf.headerNode.mapNode.setMapCenter(coordinate: userLocation.coordinate, isUserLocation: true, animated: true)
}
case .custom:
break
}
let annotations: [LocationPinAnnotation] = [LocationPinAnnotation(account: context.account, theme: presentationData.theme, location: mapMedia, forcedSelection: true)]
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
}
if updateLayout {
strongSelf.containerLayoutUpdated(layout, navigationHeight: navigationBarHeight, transition: transition)
}
}
}
})
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)
}
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 = .custom
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)
}
func updateState(_ f: (LocationViewState) -> LocationViewState) {
self.state = f(self.state)
self.statePromise.set(.single(self.state))
}
private func enqueueTransition(_ transition: LocationViewTransaction) {
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()
self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { [weak self] _ in
})
}
func scrollToTop() {
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 })
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ContainedViewLayoutTransition) {
let isFirstLayout = self.validLayout == nil
self.validLayout = (layout, navigationHeight)
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 overlap: CGFloat = 6.0
let topInset: CGFloat = layout.size.height - layout.intrinsicInsets.bottom - 126.0 - overlap
let headerHeight: CGFloat
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 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: nil, 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 })
let listFrame: CGRect = CGRect(origin: CGPoint(), size: layout.size)
transition.updateFrame(node: self.listNode, frame: listFrame)
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)
}
}

View File

@ -21,7 +21,8 @@ public final class SolidRoundedButtonNode: ASDisplayNode {
private let buttonBackgroundNode: ASImageNode
private let buttonGlossNode: SolidRoundedButtonGlossNode
private let buttonNode: HighlightTrackingButtonNode
private let labelNode: ImmediateTextNode
private let titleNode: ImmediateTextNode
private let subtitleNode: ImmediateTextNode
private let iconNode: ASImageNode
private let buttonHeight: CGFloat
@ -38,6 +39,14 @@ public final class SolidRoundedButtonNode: ASDisplayNode {
}
}
public var subtitle: String? {
didSet {
if let width = self.validLayout {
_ = self.updateLayout(width: width, previousSubtitle: oldValue, transition: .immediate)
}
}
}
public init(title: String? = nil, icon: UIImage? = nil, theme: SolidRoundedButtonTheme, height: CGFloat = 48.0, cornerRadius: CGFloat = 24.0, gloss: Bool = false) {
self.theme = theme
self.buttonHeight = height
@ -54,8 +63,11 @@ public final class SolidRoundedButtonNode: ASDisplayNode {
self.buttonNode = HighlightTrackingButtonNode()
self.labelNode = ImmediateTextNode()
self.labelNode.isUserInteractionEnabled = false
self.titleNode = ImmediateTextNode()
self.titleNode.isUserInteractionEnabled = false
self.subtitleNode = ImmediateTextNode()
self.subtitleNode.isUserInteractionEnabled = false
self.iconNode = ASImageNode()
self.iconNode.displayWithoutProcessing = true
@ -69,7 +81,8 @@ public final class SolidRoundedButtonNode: ASDisplayNode {
self.addSubnode(self.buttonGlossNode)
}
self.addSubnode(self.buttonNode)
self.addSubnode(self.labelNode)
self.addSubnode(self.titleNode)
self.addSubnode(self.subtitleNode)
self.addSubnode(self.iconNode)
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
@ -78,15 +91,19 @@ public final class SolidRoundedButtonNode: ASDisplayNode {
if highlighted {
strongSelf.buttonBackgroundNode.layer.removeAnimation(forKey: "opacity")
strongSelf.buttonBackgroundNode.alpha = 0.55
strongSelf.labelNode.layer.removeAnimation(forKey: "opacity")
strongSelf.labelNode.alpha = 0.55
strongSelf.titleNode.layer.removeAnimation(forKey: "opacity")
strongSelf.titleNode.alpha = 0.55
strongSelf.subtitleNode.layer.removeAnimation(forKey: "opacity")
strongSelf.subtitleNode.alpha = 0.55
strongSelf.iconNode.layer.removeAnimation(forKey: "opacity")
strongSelf.iconNode.alpha = 0.55
} else {
strongSelf.buttonBackgroundNode.alpha = 1.0
strongSelf.buttonBackgroundNode.layer.animateAlpha(from: 0.55, to: 1.0, duration: 0.2)
strongSelf.labelNode.alpha = 1.0
strongSelf.labelNode.layer.animateAlpha(from: 0.55, to: 1.0, duration: 0.2)
strongSelf.titleNode.alpha = 1.0
strongSelf.titleNode.layer.animateAlpha(from: 0.55, to: 1.0, duration: 0.2)
strongSelf.subtitleNode.alpha = 1.0
strongSelf.subtitleNode.layer.animateAlpha(from: 0.55, to: 1.0, duration: 0.2)
strongSelf.iconNode.alpha = 1.0
strongSelf.iconNode.layer.animateAlpha(from: 0.55, to: 1.0, duration: 0.2)
}
@ -102,10 +119,15 @@ public final class SolidRoundedButtonNode: ASDisplayNode {
self.buttonBackgroundNode.image = generateStretchableFilledCircleImage(radius: self.buttonCornerRadius, color: theme.backgroundColor)
self.buttonGlossNode.color = theme.foregroundColor
self.labelNode.attributedText = NSAttributedString(string: self.title ?? "", font: Font.semibold(17.0), textColor: theme.foregroundColor)
self.titleNode.attributedText = NSAttributedString(string: self.title ?? "", font: Font.semibold(17.0), textColor: theme.foregroundColor)
self.subtitleNode.attributedText = NSAttributedString(string: self.subtitle ?? "", font: Font.regular(14.0), textColor: theme.foregroundColor)
}
public func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
return self.updateLayout(width: width, previousSubtitle: nil, transition: transition)
}
private func updateLayout(width: CGFloat, previousSubtitle: String?, transition: ContainedViewLayoutTransition) -> CGFloat {
self.validLayout = width
let buttonSize = CGSize(width: width, height: self.buttonHeight)
@ -114,16 +136,16 @@ public final class SolidRoundedButtonNode: ASDisplayNode {
transition.updateFrame(node: self.buttonGlossNode, frame: buttonFrame)
transition.updateFrame(node: self.buttonNode, frame: buttonFrame)
if self.title != self.labelNode.attributedText?.string {
self.labelNode.attributedText = NSAttributedString(string: self.title ?? "", font: Font.semibold(17.0), textColor: self.theme.foregroundColor)
if self.title != self.titleNode.attributedText?.string {
self.titleNode.attributedText = NSAttributedString(string: self.title ?? "", font: Font.semibold(17.0), textColor: self.theme.foregroundColor)
}
let iconSize = self.iconNode.image?.size ?? CGSize()
let labelSize = self.labelNode.updateLayout(buttonSize)
let titleSize = self.titleNode.updateLayout(buttonSize)
let iconSpacing: CGFloat = 8.0
var contentWidth: CGFloat = labelSize.width
var contentWidth: CGFloat = titleSize.width
if !iconSize.width.isZero {
contentWidth += iconSize.width + iconSpacing
}
@ -133,8 +155,25 @@ public final class SolidRoundedButtonNode: ASDisplayNode {
nextContentOrigin += iconSize.width + iconSpacing
}
let labelFrame = CGRect(origin: CGPoint(x: buttonFrame.minX + nextContentOrigin, y: buttonFrame.minY + floor((buttonFrame.height - labelSize.height) / 2.0)), size: labelSize)
transition.updateFrame(node: self.labelNode, frame: labelFrame)
let spacingOffset: CGFloat = 9.0
var verticalInset: CGFloat = self.subtitle == nil ? floor((buttonFrame.height - titleSize.height) / 2.0) : floor((buttonFrame.height - titleSize.height) / 2.0) - spacingOffset
let titleFrame = CGRect(origin: CGPoint(x: buttonFrame.minX + nextContentOrigin, y: buttonFrame.minY + verticalInset), size: titleSize)
transition.updateFrame(node: self.titleNode, frame: titleFrame)
if self.subtitle != self.subtitleNode.attributedText?.string {
self.subtitleNode.attributedText = NSAttributedString(string: self.subtitle ?? "", font: Font.regular(14.0), textColor: self.theme.foregroundColor)
}
let subtitleSize = self.subtitleNode.updateLayout(buttonSize)
let subtitleFrame = CGRect(origin: CGPoint(x: buttonFrame.minX + floor((buttonFrame.width - subtitleSize.width) / 2.0), y: buttonFrame.minY + floor((buttonFrame.height - titleSize.height) / 2.0) + spacingOffset + 2.0), size: subtitleSize)
transition.updateFrame(node: self.subtitleNode, frame: subtitleFrame)
if previousSubtitle == nil && self.subtitle != nil {
self.titleNode.layer.animatePosition(from: CGPoint(x: 0.0, y: spacingOffset / 2.0), to: CGPoint(), duration: 0.3, additive: true)
self.subtitleNode.layer.animatePosition(from: CGPoint(x: 0.0, y: -spacingOffset / 2.0), to: CGPoint(), duration: 0.3, additive: true)
self.subtitleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
}
return buttonSize.height
}

View File

@ -283,15 +283,15 @@ func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool {
case let .map(mapMedia):
params.dismissInput()
let controller = legacyLocationController(message: params.message, mapMedia: mapMedia, context: params.context, openPeer: { peer in
params.openPeer(peer, .info)
}, sendLiveLocation: { coordinate, period in
let outMessage: EnqueueMessage = .message(text: "", attributes: [], mediaReference: .standalone(media: TelegramMediaMap(latitude: coordinate.latitude, longitude: coordinate.longitude, geoPlace: nil, venue: nil, liveBroadcastingTimeout: period)), replyToMessageId: nil, localGroupingKey: nil)
let controllerParams = LocationViewParams(sendLiveLocation: { location in
let outMessage: EnqueueMessage = .message(text: "", attributes: [], mediaReference: .standalone(media: location), replyToMessageId: nil, localGroupingKey: nil)
params.enqueueMessage(outMessage)
}, stopLiveLocation: {
params.context.liveLocationManager?.cancelLiveLocation(peerId: params.message.id.peerId)
}, openUrl: params.openUrl)
controller.navigationPresentation = .modal
}, openUrl: params.openUrl, openPeer: { peer in
params.openPeer(peer, .info)
})
let controller = LocationViewController(context: params.context, mapMedia: mapMedia, params: controllerParams)
params.navigationController?.pushViewController(controller)
return true
case let .stickerPack(reference):