mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-11-28 19:05:49 +00:00
Location view improvements
This commit is contained in:
parent
86b181d390
commit
1b264ac3a2
@ -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 {
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
264
submodules/LocationUI/Sources/LocationInfoListItem.swift
Normal file
264
submodules/LocationUI/Sources/LocationInfoListItem.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
})
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
188
submodules/LocationUI/Sources/LocationViewController.swift
Normal file
188
submodules/LocationUI/Sources/LocationViewController.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
|
||||
431
submodules/LocationUI/Sources/LocationViewControllerNode.swift
Normal file
431
submodules/LocationUI/Sources/LocationViewControllerNode.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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):
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user