Upgrade location picker

This commit is contained in:
Ilya Laktyushin 2019-11-25 14:09:43 +04:00
parent f9f8788e65
commit 6e3969cd46
46 changed files with 5676 additions and 3043 deletions

View File

@ -5143,3 +5143,5 @@ Any member of this group will be able to see messages in the channel.";
"Settings.AddDevice" = "Add";
"AuthSessions.DevicesTitle" = "Devices";
"AuthSessions.AddDevice" = "Add Device";
"Map.SendThisPlace" = "Send This Place";

View File

@ -143,7 +143,7 @@ public class ItemListVenueItemNode: ListViewItemNode, ItemListItemNode {
var updatedTheme: PresentationTheme?
var updatedVenueType: String?
let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize)
let titleFont = Font.medium(item.presentationData.fontSize.itemListBaseFontSize)
let addressFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 14.0 / 17.0))
if currentItem?.presentationData.theme !== item.presentationData.theme {
@ -159,7 +159,7 @@ public class ItemListVenueItemNode: ListViewItemNode, ItemListItemNode {
let addressAttributedString = NSAttributedString(string: item.venue.venue?.address ?? "", font: addressFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor)
let leftInset: CGFloat = 65.0 + params.leftInset
let rightInset: CGFloat = params.rightInset
let rightInset: CGFloat = 16.0 + params.rightInset
let verticalInset: CGFloat = addressAttributedString.string.isEmpty ? 14.0 : 8.0
let iconSize: CGFloat = 40.0
@ -171,12 +171,25 @@ public class ItemListVenueItemNode: ListViewItemNode, ItemListItemNode {
let minHeight: CGFloat = titleLayout.size.height + verticalInset * 2.0
let rawHeight: CGFloat = verticalInset * 2.0 + titleLayout.size.height + titleSpacing + addressLayout.size.height
let insets = itemListNeighborsGroupedInsets(neighbors)
var insets: UIEdgeInsets
let itemBackgroundColor: UIColor
let itemSeparatorColor: UIColor
switch item.style {
case .plain:
itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor
itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor
insets = itemListNeighborsPlainInsets(neighbors)
insets.bottom = 0.0
case .blocks:
itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor
itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor
insets = itemListNeighborsGroupedInsets(neighbors)
}
let contentSize = CGSize(width: params.width, height: max(minHeight, rawHeight))
let separatorHeight = UIScreenPixel
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let layoutSize = layout.size
return (layout, { [weak self] in
if let strongSelf = self {
@ -186,9 +199,9 @@ public class ItemListVenueItemNode: ListViewItemNode, ItemListItemNode {
strongSelf.accessibilityValue = addressAttributedString.string
if let _ = updatedTheme {
strongSelf.topStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = item.presentationData.theme.list.itemBlocksSeparatorColor
strongSelf.backgroundNode.backgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor
strongSelf.topStripeNode.backgroundColor = itemSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor
strongSelf.backgroundNode.backgroundColor = itemBackgroundColor
strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor
}
@ -203,49 +216,71 @@ public class ItemListVenueItemNode: ListViewItemNode, ItemListItemNode {
let iconApply = iconLayout(TransformImageArguments(corners: ImageCorners(), imageSize: CGSize(width: iconSize, height: iconSize), boundingSize: CGSize(width: iconSize, height: iconSize), intrinsicInsets: UIEdgeInsets()))
iconApply()
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
if strongSelf.maskNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
}
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners
switch item.style {
case .plain:
if strongSelf.backgroundNode.supernode != nil {
strongSelf.backgroundNode.removeFromSupernode()
}
if strongSelf.topStripeNode.supernode != nil {
strongSelf.topStripeNode.removeFromSupernode()
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0)
}
if strongSelf.maskNode.supernode != nil {
strongSelf.maskNode.removeFromSupernode()
}
let stripeInset: CGFloat
if case .none = neighbors.bottom {
stripeInset = 0.0
} else {
stripeInset = leftInset
}
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: stripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - stripeInset, height: separatorHeight))
strongSelf.bottomStripeNode.isHidden = false
case .blocks:
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
if strongSelf.maskNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
}
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners
}
let bottomStripeInset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = leftInset
default:
bottomStripeInset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - bottomStripeInset, height: separatorHeight))
}
let bottomStripeInset: CGFloat
let bottomStripeOffset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = leftInset
bottomStripeOffset = -separatorHeight
default:
bottomStripeInset = 0.0
bottomStripeOffset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
transition.updateFrame(node: strongSelf.topStripeNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)))
transition.updateFrame(node: strongSelf.bottomStripeNode, frame: CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)))
transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftInset, y: verticalInset), size: titleLayout.size))
transition.updateFrame(node: strongSelf.addressNode, frame: CGRect(origin: CGPoint(x: leftInset, y: verticalInset + titleLayout.size.height + titleSpacing), size: addressLayout.size))

View File

@ -17,8 +17,14 @@ static_library(
"//submodules/ShareController:ShareController",
"//submodules/AccountContext:AccountContext",
"//submodules/OpenInExternalAppUI:OpenInExternalAppUI",
"//submodules/ItemListUI:ItemListUI",
"//submodules/LegacyUI:LegacyUI",
"//submodules/AppBundle:AppBundle",
"//submodules/TelegramStringFormatting:TelegramStringFormatting",
"//submodules/LocationResources:LocationResources",
"//submodules/ListSectionHeaderNode:ListSectionHeaderNode",
"//submodules/SegmentedControlNode:SegmentedControlNode",
"//submodules/Geocoding:Geocoding",
],
frameworks = [
"$SDKROOT/System/Library/Frameworks/Foundation.framework",

View File

@ -0,0 +1,322 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Postbox
import Display
import SwiftSignalKit
import TelegramCore
import SyncCore
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import LocationResources
import AppBundle
public enum LocationActionListItemIcon: Equatable {
case location
case liveLocation
case stopLiveLocation
case venue(TelegramMediaMap)
public static func ==(lhs: LocationActionListItemIcon, rhs: LocationActionListItemIcon) -> Bool {
switch lhs {
case .location:
if case .location = rhs {
return true
} else {
return false
}
case .liveLocation:
if case .liveLocation = rhs {
return true
} else {
return false
}
case .stopLiveLocation:
if case .stopLiveLocation = rhs {
return true
} else {
return false
}
case let .venue(lhsVenue):
if case let .venue(rhsVenue) = rhs, lhsVenue.venue?.id == rhsVenue.venue?.id {
return true
} else {
return false
}
}
}
}
private func generateLocationIcon(theme: PresentationTheme) -> UIImage {
return generateImage(CGSize(width: 40.0, height: 40.0)) { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(theme.chat.inputPanel.actionControlFillColor.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
context.scaleBy(x: 1.0, y: -1.0)
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
if let image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/LocationPinForeground"), color: theme.chat.inputPanel.actionControlForegroundColor) {
context.draw(image.cgImage!, in: CGRect(origin: CGPoint(x: floor((size.width - image.size.width) / 2.0), y: floor((size.height - image.size.height) / 2.0)), size: image.size))
}
}!
}
private func generateLiveLocationIcon(theme: PresentationTheme) -> UIImage {
return generateImage(CGSize(width: 40.0, height: 40.0)) { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(UIColor(rgb: 0x6cc139).cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
context.scaleBy(x: 1.0, y: -1.0)
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
if let image = generateTintedImage(image: UIImage(bundleImageName: "Location/LiveLocationIcon"), color: theme.chat.inputPanel.actionControlForegroundColor) {
context.draw(image.cgImage!, in: CGRect(origin: CGPoint(x: floor((size.width - image.size.width) / 2.0), y: floor((size.height - image.size.height) / 2.0)), size: image.size))
}
}!
}
public class LocationActionListItem: ListViewItem {
let presentationData: ItemListPresentationData
let account: Account
let title: String
let subtitle: String
let icon: LocationActionListItemIcon
let action: () -> Void
public init(presentationData: ItemListPresentationData, account: Account, title: String, subtitle: String, icon: LocationActionListItemIcon, action: @escaping () -> Void) {
self.presentationData = presentationData
self.account = account
self.title = title
self.subtitle = subtitle
self.icon = icon
self.action = action
}
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 = LocationActionListItemNode()
let makeLayout = node.asyncLayout()
let (nodeLayout, nodeApply) = makeLayout(self, params, nextItem is LocationActionListItem)
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? LocationActionListItemNode {
let layout = nodeValue.asyncLayout()
async {
let (nodeLayout, apply) = layout(self, params, nextItem is LocationActionListItem)
Queue.mainQueue().async {
completion(nodeLayout, { info in
apply().1(info)
})
}
}
}
}
}
public var selectable: Bool {
return true
}
public func selected(listView: ListView) {
listView.clearHighlightAnimated(true)
self.action()
}
}
class LocationActionListItemNode: ListViewItemNode {
private let backgroundNode: ASDisplayNode
private let separatorNode: ASDisplayNode
private let highlightedBackgroundNode: ASDisplayNode
private var titleNode: TextNode?
private var subtitleNode: TextNode?
private let iconNode: ASImageNode
private let venueIconNode: TransformImageNode
private var item: LocationActionListItem?
private var layoutParams: ListViewItemLayoutParams?
required init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.separatorNode = ASDisplayNode()
self.separatorNode.isLayerBacked = true
self.highlightedBackgroundNode = ASDisplayNode()
self.highlightedBackgroundNode.isLayerBacked = true
self.iconNode = ASImageNode()
self.iconNode.displaysAsynchronously = false
self.iconNode.displayWithoutProcessing = true
self.venueIconNode = TransformImageNode()
super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false)
self.addSubnode(self.backgroundNode)
self.addSubnode(self.separatorNode)
self.addSubnode(self.iconNode)
self.addSubnode(self.venueIconNode)
}
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, nextItem is LocationActionListItem)
self.contentSize = nodeLayout.contentSize
self.insets = nodeLayout.insets
let _ = nodeApply()
}
}
override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
super.setHighlighted(highlighted, at: point, animated: animated)
if highlighted {
self.highlightedBackgroundNode.alpha = 1.0
if self.highlightedBackgroundNode.supernode == nil {
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: self.separatorNode)
}
} else {
if self.highlightedBackgroundNode.supernode != nil {
if animated {
self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in
if let strongSelf = self {
if completed {
strongSelf.highlightedBackgroundNode.removeFromSupernode()
}
}
})
self.highlightedBackgroundNode.alpha = 0.0
} else {
self.highlightedBackgroundNode.removeFromSupernode()
}
}
}
}
func asyncLayout() -> (_ item: LocationActionListItem, _ params: ListViewItemLayoutParams, _ hasSeparator: Bool) -> (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, hasSeparator in
let leftInset: CGFloat = 65.0 + params.leftInset
let rightInset: CGFloat = params.rightInset
let verticalInset: CGFloat = 8.0
let iconSize: CGFloat = 40.0
let titleFont = Font.medium(item.presentationData.fontSize.itemListBaseFontSize)
let subtitleFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 14.0 / 17.0))
let titleAttributedString = NSAttributedString(string: item.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: item.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 = hasSeparator ? 0.0 : 4.0
let contentSize = CGSize(width: params.width, height: 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 updatedIcon: LocationActionListItemIcon?
if currentItem?.icon != item.icon || updatedTheme != nil {
updatedIcon = item.icon
}
return (nil, { _ in
if let strongSelf = self {
strongSelf.item = item
strongSelf.layoutParams = params
if let _ = updatedTheme {
strongSelf.separatorNode.backgroundColor = item.presentationData.theme.list.itemPlainSeparatorColor
strongSelf.backgroundNode.backgroundColor = item.presentationData.theme.list.plainBackgroundColor
strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor
}
if let updatedIcon = updatedIcon {
switch updatedIcon {
case .location:
strongSelf.iconNode.isHidden = false
strongSelf.venueIconNode.isHidden = true
strongSelf.iconNode.image = generateLocationIcon(theme: item.presentationData.theme)
case .liveLocation, .stopLiveLocation:
strongSelf.iconNode.isHidden = false
strongSelf.venueIconNode.isHidden = true
strongSelf.iconNode.image = generateLiveLocationIcon(theme: item.presentationData.theme)
case let .venue(venue):
strongSelf.iconNode.isHidden = true
strongSelf.venueIconNode.isHidden = false
strongSelf.venueIconNode.setSignal(venueIcon(postbox: item.account.postbox, type: venue.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 {
strongSelf.titleNode = titleNode
strongSelf.addSubnode(titleNode)
}
let subtitleNode = subtitleApply()
if strongSelf.subtitleNode == nil {
strongSelf.subtitleNode = subtitleNode
strongSelf.addSubnode(subtitleNode)
}
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 + 15.0, y: floorToScreenPixels((contentSize.height - bottomInset - iconSize) / 2.0)), size: CGSize(width: iconSize, height: iconSize))
strongSelf.iconNode.frame = iconNodeFrame
strongSelf.venueIconNode.frame = iconNodeFrame
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: contentSize.width, height: contentSize.height))
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -nodeLayout.insets.top - topHighlightInset), size: CGSize(width: contentSize.width, height: contentSize.height + topHighlightInset))
strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: leftInset, y: nodeLayout.contentSize.height - separatorHeight), size: CGSize(width: nodeLayout.size.width, height: separatorHeight))
strongSelf.separatorNode.isHidden = !hasSeparator
}
})
})
}
}
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)
}
}

View File

@ -0,0 +1,284 @@
import Foundation
import UIKit
import MapKit
import Display
import Postbox
import SyncCore
import TelegramCore
import AvatarNode
import AppBundle
import TelegramPresentationData
import LocationResources
let locationPinReuseIdentifier = "locationPin"
private func generateSmallBackgroundImage(color: UIColor) -> UIImage? {
return generateImage(CGSize(width: 56.0, height: 56.0)) { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setShadow(offset: CGSize(), blur: 4.0, color: UIColor(rgb: 0x000000, alpha: 0.5).cgColor)
context.setFillColor(UIColor.white.cgColor)
context.fillEllipse(in: CGRect(x: 16.0, y: 16.0, width: 24.0, height: 24.0))
context.setShadow(offset: CGSize(), blur: 0.0, color: nil)
context.setFillColor(color.cgColor)
context.fillEllipse(in: CGRect(x: 17.0 + UIScreenPixel, y: 17.0 + UIScreenPixel, width: 22.0 - 2.0 * UIScreenPixel, height: 22.0 - 2.0 * UIScreenPixel))
}
}
class LocationPinAnnotation: NSObject, MKAnnotation {
let account: Account
let theme: PresentationTheme
var coordinate: CLLocationCoordinate2D
let location: TelegramMediaMap
var title: String? = ""
var subtitle: String? = ""
init(account: Account, theme: PresentationTheme, location: TelegramMediaMap) {
self.account = account
self.theme = theme
self.location = location
self.coordinate = location.coordinate
super.init()
}
}
class LocationPinAnnotationLayer: CALayer {
var customZPosition: CGFloat?
override var zPosition: CGFloat {
get {
if let zPosition = self.customZPosition {
return zPosition
} else {
return super.zPosition
}
} set {
super.zPosition = newValue
}
}
}
class LocationPinAnnotationView: MKAnnotationView {
let shadowNode: ASImageNode
let backgroundNode: ASImageNode
let smallNode: ASImageNode
let iconNode: TransformImageNode
let smallIconNode: TransformImageNode
let dotNode: ASImageNode
var avatarNode: AvatarNode?
var appeared = false
var animating = false
override class var layerClass: AnyClass {
return LocationPinAnnotationLayer.self
}
func setZPosition(_ zPosition: CGFloat?) {
if let layer = self.layer as? LocationPinAnnotationLayer {
layer.customZPosition = zPosition
}
}
init(annotation: LocationPinAnnotation) {
self.shadowNode = ASImageNode()
self.shadowNode.image = UIImage(bundleImageName: "Location/PinShadow")
if let image = self.shadowNode.image {
self.shadowNode.bounds = CGRect(origin: CGPoint(), size: image.size)
}
self.backgroundNode = ASImageNode()
self.backgroundNode.image = UIImage(bundleImageName: "Location/PinBackground")
if let image = self.backgroundNode.image {
self.backgroundNode.bounds = CGRect(origin: CGPoint(), size: image.size)
}
self.smallNode = ASImageNode()
self.smallNode.image = UIImage(bundleImageName: "Location/PinSmallBackground")
if let image = self.smallNode.image {
self.smallNode.bounds = CGRect(origin: CGPoint(), size: image.size)
}
self.iconNode = TransformImageNode()
self.iconNode.bounds = CGRect(origin: CGPoint(), size: CGSize(width: 64.0, height: 64.0))
self.smallIconNode = TransformImageNode()
self.smallIconNode.frame = CGRect(origin: CGPoint(x: 15.0, y: 15.0), size: CGSize(width: 26.0, height: 26.0))
self.dotNode = ASImageNode()
self.dotNode.image = generateFilledCircleImage(diameter: 6.0, color: annotation.theme.list.itemAccentColor)
if let image = self.dotNode.image {
self.dotNode.bounds = CGRect(origin: CGPoint(), size: image.size)
}
super.init(annotation: annotation, reuseIdentifier: locationPinReuseIdentifier)
self.addSubnode(self.smallNode)
self.smallNode.addSubnode(self.smallIconNode)
self.addSubnode(self.shadowNode)
self.shadowNode.addSubnode(self.backgroundNode)
self.backgroundNode.addSubnode(self.iconNode)
self.addSubnode(self.dotNode)
self.annotation = annotation
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override var annotation: MKAnnotation? {
didSet {
if let annotation = self.annotation as? LocationPinAnnotation {
let venueType = annotation.location.venue?.type ?? ""
let color = venueType.isEmpty ? annotation.theme.list.itemAccentColor : venueIconColor(type: venueType)
self.backgroundNode.image = generateTintedImage(image: UIImage(bundleImageName: "Location/PinBackground"), color: color)
self.iconNode.setSignal(venueIcon(postbox: annotation.account.postbox, type: annotation.location.venue?.type ?? "", background: false))
self.smallIconNode.setSignal(venueIcon(postbox: annotation.account.postbox, type: annotation.location.venue?.type ?? "", background: false))
self.smallNode.image = generateSmallBackgroundImage(color: color)
self.dotNode.image = generateFilledCircleImage(diameter: 6.0, color: color)
self.dotNode.isHidden = false
if !self.isSelected {
self.dotNode.alpha = 0.0
self.shadowNode.isHidden = true
self.smallNode.isHidden = false
}
}
}
}
override func prepareForReuse() {
self.smallNode.isHidden = true
self.backgroundNode.isHidden = false
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
if animated {
self.layoutSubviews()
self.animating = true
if selected {
self.shadowNode.position = CGPoint(x: self.shadowNode.position.x, y: self.shadowNode.position.y + self.shadowNode.frame.height / 2.0)
self.shadowNode.anchorPoint = CGPoint(x: 0.5, y: 1.0)
self.shadowNode.isHidden = false
self.shadowNode.transform = CATransform3DMakeScale(0.1, 0.1, 1.0)
UIView.animate(withDuration: 0.35, delay: 0.0, usingSpringWithDamping: 0.6, initialSpringVelocity: 0.5, options: [], animations: {
self.smallNode.transform = CATransform3DMakeScale(0.001, 0.001, 1.0)
self.shadowNode.transform = CATransform3DIdentity
if self.dotNode.isHidden {
self.smallNode.alpha = 0.0
}
}) { _ in
self.animating = false
self.shadowNode.anchorPoint = CGPoint(x: 0.5, y: 0.5)
self.smallNode.isHidden = true
self.smallNode.transform = CATransform3DIdentity
}
self.dotNode.alpha = 1.0
self.dotNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
} else {
self.smallNode.isHidden = false
self.smallNode.transform = CATransform3DMakeScale(0.01, 0.01, 1.0)
self.shadowNode.position = CGPoint(x: self.shadowNode.position.x, y: self.shadowNode.position.y + self.shadowNode.frame.height / 2.0)
self.shadowNode.anchorPoint = CGPoint(x: 0.5, y: 1.0)
UIView.animate(withDuration: 0.35, delay: 0.0, usingSpringWithDamping: 0.6, initialSpringVelocity: 0.5, options: [], animations: {
self.smallNode.transform = CATransform3DIdentity
self.shadowNode.transform = CATransform3DMakeScale(0.1, 0.1, 1.0)
if self.dotNode.isHidden {
self.smallNode.alpha = 1.0
}
}) { _ in
self.animating = false
self.shadowNode.anchorPoint = CGPoint(x: 0.5, y: 0.5)
self.shadowNode.isHidden = true
self.shadowNode.transform = CATransform3DIdentity
}
let previousAlpha = self.dotNode.alpha
self.dotNode.alpha = 0.0
self.dotNode.layer.animateAlpha(from: previousAlpha, to: 0.0, duration: 0.2)
}
} else {
self.smallNode.isHidden = selected
self.shadowNode.isHidden = !selected
self.dotNode.alpha = selected ? 1.0 : 0.0
self.smallNode.alpha = 1.0
self.layoutSubviews()
}
}
override func layoutSubviews() {
super.layoutSubviews()
guard !self.animating else {
return
}
self.dotNode.position = CGPoint()
self.smallNode.position = CGPoint()
self.shadowNode.position = CGPoint(x: UIScreenPixel, y: -36.0)
self.backgroundNode.position = CGPoint(x: self.shadowNode.frame.width / 2.0, y: self.shadowNode.frame.height / 2.0)
self.iconNode.position = CGPoint(x: self.shadowNode.frame.width / 2.0, y: self.shadowNode.frame.height / 2.0 - 5.0)
let smallIconLayout = self.smallIconNode.asyncLayout()
let smallIconApply = smallIconLayout(TransformImageArguments(corners: ImageCorners(), imageSize: self.smallIconNode.bounds.size, boundingSize: self.smallIconNode.bounds.size, intrinsicInsets: UIEdgeInsets()))
smallIconApply()
let iconLayout = self.iconNode.asyncLayout()
let iconApply = iconLayout(TransformImageArguments(corners: ImageCorners(), imageSize: self.iconNode.bounds.size, boundingSize: self.iconNode.bounds.size, intrinsicInsets: UIEdgeInsets()))
iconApply()
if let avatarNode = self.avatarNode {
avatarNode.position = self.isSelected ? CGPoint(x: UIScreenPixel, y: -41.0) : CGPoint()
avatarNode.transform = self.isSelected ? CATransform3DIdentity : CATransform3DMakeScale(0.64, 0.64, 1.0)
}
if !self.appeared {
self.appeared = true
self.smallNode.transform = CATransform3DMakeScale(0.1, 0.1, 1.0)
UIView.animate(withDuration: 0.55, delay: 0.0, usingSpringWithDamping: 0.6, initialSpringVelocity: 0.5, options: [], animations: {
self.smallNode.transform = CATransform3DIdentity
}) { _ in
}
}
}
func setPeer(account: Account, theme: PresentationTheme, peer: Peer) {
let avatarNode: AvatarNode
if let currentAvatarNode = self.avatarNode {
avatarNode = currentAvatarNode
} else {
avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 24.0))
avatarNode.bounds = CGRect(origin: CGPoint(), size: CGSize(width: 55.0, height: 55.0))
avatarNode.position = CGPoint()
self.avatarNode = avatarNode
self.addSubnode(avatarNode)
}
avatarNode.setPeer(account: account, theme: theme, peer: peer)
}
func setRaised(_ raised: Bool, avatar: Bool, animated: Bool, completion: @escaping () -> Void = {}) {
}
}

View File

@ -0,0 +1,116 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Postbox
import Display
import SwiftSignalKit
import TelegramPresentationData
import ListSectionHeaderNode
import ItemListUI
import AppBundle
class LocationAttributionItem: ListViewItem {
let presentationData: ItemListPresentationData
public init(presentationData: ItemListPresentationData) {
self.presentationData = presentationData
}
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 = LocationAttributionItemNode()
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? LocationAttributionItemNode {
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
}
}
private class LocationAttributionItemNode: ListViewItemNode {
private var imageNode: ASImageNode
private var item: LocationAttributionItem?
private var layoutParams: ListViewItemLayoutParams?
required init() {
self.imageNode = ASImageNode()
self.imageNode.displaysAsynchronously = false
self.imageNode.displayWithoutProcessing = true
super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false)
self.addSubnode(self.imageNode)
}
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: LocationAttributionItem, _ params: ListViewItemLayoutParams) -> (ListViewItemNodeLayout, () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) {
let currentItem = self.item
return { [weak self] item, params in
let contentSize = CGSize(width: params.width, height: 55.0)
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
}
return (nil, { _ in
if let strongSelf = self {
strongSelf.item = item
strongSelf.layoutParams = params
if let _ = updatedTheme {
strongSelf.imageNode.image = generateTintedImage(image: UIImage(bundleImageName: "Location/FoursquareAttribution"), color: item.presentationData.theme.list.itemSecondaryTextColor)
}
if let image = strongSelf.imageNode.image {
strongSelf.imageNode.frame = CGRect(x: floor((params.width - image.size.width) / 2.0), y: 0.0, width: image.size.width, height: image.size.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)
}
}

View File

@ -0,0 +1,122 @@
import Foundation
import Display
import TelegramPresentationData
import AppBundle
private let panelInset: CGFloat = 4.0
private let panelSize = CGSize(width: 46.0, height: 90.0)
private func generateBackgroundImage(theme: PresentationTheme) -> UIImage? {
return generateImage(CGSize(width: panelSize.width + panelInset * 2.0, height: panelSize.height + panelInset * 2.0)) { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setShadow(offset: CGSize(), blur: 10.0, color: UIColor(rgb: 0x000000, alpha: 0.2).cgColor)
context.setFillColor(theme.rootController.navigationBar.backgroundColor.cgColor)
let path = UIBezierPath(roundedRect: CGRect(origin: CGPoint(x: panelInset, y: panelInset), size: panelSize), cornerRadius: 9.0)
context.addPath(path.cgPath)
context.fillPath()
context.setShadow(offset: CGSize(), blur: 0.0, color: nil)
context.setFillColor(theme.rootController.navigationBar.separatorColor.cgColor)
context.fill(CGRect(x: panelInset, y: panelInset + floorToScreenPixels(panelSize.height / 2.0), width: panelSize.width, height: UIScreenPixel))
}
}
private func generateShadowImage(theme: PresentationTheme) -> UIImage? {
return generateImage(CGSize(width: 26.0, height: 14.0)) { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setShadow(offset: CGSize(), blur: 10.0, color: UIColor(rgb: 0x000000, alpha: 0.2).cgColor)
context.setFillColor(theme.list.plainBackgroundColor.cgColor)
let path = UIBezierPath(roundedRect: CGRect(origin: CGPoint(x: 0.0, y: 4.0), size: CGSize(width: 26.0, height: 20.0)), cornerRadius: 9.0)
context.addPath(path.cgPath)
context.fillPath()
}?.stretchableImage(withLeftCapWidth: 13, topCapHeight: 0)
}
final class LocationMapHeaderNode: ASDisplayNode {
private let interaction: LocationPickerInteraction
let mapNode: LocationMapNode
private let optionsBackgroundNode: ASImageNode
private let infoButtonNode: HighlightableButtonNode
private let locationButtonNode: HighlightableButtonNode
private let shadowNode: ASImageNode
init(presentationData: PresentationData, interaction: LocationPickerInteraction) {
self.interaction = interaction
self.mapNode = LocationMapNode()
self.optionsBackgroundNode = ASImageNode()
self.optionsBackgroundNode.displaysAsynchronously = false
self.optionsBackgroundNode.displayWithoutProcessing = true
self.optionsBackgroundNode.image = generateBackgroundImage(theme: presentationData.theme)
self.optionsBackgroundNode.isUserInteractionEnabled = true
self.infoButtonNode = HighlightableButtonNode()
self.infoButtonNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "Location/InfoIcon"), color: presentationData.theme.rootController.navigationBar.buttonColor), for: .normal)
self.infoButtonNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "Location/InfoActiveIcon"), color: presentationData.theme.rootController.navigationBar.buttonColor), for: .selected)
self.infoButtonNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "Location/InfoActiveIcon"), color: presentationData.theme.rootController.navigationBar.buttonColor), for: [.selected, .highlighted])
self.locationButtonNode = HighlightableButtonNode()
self.locationButtonNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "Location/TrackIcon"), color: presentationData.theme.rootController.navigationBar.buttonColor), for: .normal)
self.shadowNode = ASImageNode()
self.shadowNode.contentMode = .scaleToFill
self.shadowNode.displaysAsynchronously = false
self.shadowNode.displayWithoutProcessing = true
self.shadowNode.image = generateShadowImage(theme: presentationData.theme)
super.init()
self.clipsToBounds = true
self.addSubnode(self.mapNode)
self.addSubnode(self.optionsBackgroundNode)
self.optionsBackgroundNode.addSubnode(self.infoButtonNode)
self.optionsBackgroundNode.addSubnode(self.locationButtonNode)
self.addSubnode(self.shadowNode)
self.infoButtonNode.addTarget(self, action: #selector(self.infoPressed), forControlEvents: .touchUpInside)
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 updatePresentationData(_ presentationData: PresentationData) {
self.optionsBackgroundNode.image = generateBackgroundImage(theme: presentationData.theme)
self.infoButtonNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "Location/InfoIcon"), color: presentationData.theme.rootController.navigationBar.buttonColor), for: .normal)
self.infoButtonNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "Location/InfoActiveIcon"), color: presentationData.theme.rootController.navigationBar.buttonColor), for: .selected)
self.infoButtonNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "Location/InfoActiveIcon"), color: presentationData.theme.rootController.navigationBar.buttonColor), for: [.selected, .highlighted])
self.locationButtonNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "Location/TrackIcon"), color: presentationData.theme.rootController.navigationBar.buttonColor), for: .normal)
self.shadowNode.image = generateShadowImage(theme: presentationData.theme)
}
func updateLayout(layout: ContainerViewLayout, navigationBarHeight: CGFloat, padding: CGFloat, size: CGSize, transition: ContainedViewLayoutTransition) {
transition.updateFrame(node: self.mapNode, frame: CGRect(x: 0.0, y: floorToScreenPixels((size.height - layout.size.height + navigationBarHeight) / 2.0), width: size.width, height: layout.size.height))
transition.updateFrame(node: self.shadowNode, frame: CGRect(x: 0.0, y: size.height - 14.0, width: size.width, height: 14.0))
let inset: CGFloat = 6.0
transition.updateFrame(node: self.optionsBackgroundNode, frame: CGRect(x: size.width - inset - panelSize.width - panelInset * 2.0, y: navigationBarHeight + padding + inset, width: panelSize.width + panelInset * 2.0, height: panelSize.height + panelInset * 2.0))
transition.updateFrame(node: self.infoButtonNode, frame: CGRect(x: panelInset, y: panelInset, width: panelSize.width, height: panelSize.height / 2.0))
transition.updateFrame(node: self.locationButtonNode, frame: CGRect(x: panelInset, y: panelInset + panelSize.height / 2.0, width: panelSize.width, height: panelSize.height / 2.0))
let alphaTransition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut)
let optionsAlpha: CGFloat = size.height > 110.0 + navigationBarHeight ? 1.0 : 0.0
alphaTransition.updateAlpha(node: self.optionsBackgroundNode, alpha: optionsAlpha)
}
@objc private func infoPressed() {
self.interaction.toggleMapModeSelection()
}
@objc private func locationPressed() {
self.interaction.goToUserLocation()
}
}

View File

@ -1,12 +1,222 @@
//
// LocationMapNode.swift
// LocationUI
//
// Created by Ilya Laktyushin on 13.11.2019.
//
import Foundation
import Display
import SwiftSignalKit
import MapKit
import UIKit
class LocationMapNode: NSObject {
let defaultMapSpan = MKCoordinateSpan(latitudeDelta: 0.016, longitudeDelta: 0.016)
private let pinOffset = CGPoint(x: 0.0, y: 33.0)
public enum LocationMapMode {
case map
case sattelite
case hybrid
var mapType: MKMapType {
switch self {
case .sattelite:
return .satellite
case .hybrid:
return .hybrid
default:
return .standard
}
}
}
class LocationMapNode: ASDisplayNode, MKMapViewDelegate {
private let locationPromise = Promise<CLLocation?>(nil)
private weak var userLocationAnnotationView: MKAnnotationView?
private var mapView: MKMapView? {
return self.view as? MKMapView
}
var ignoreRegionChanges = false
var beganInteractiveDragging: (() -> Void)?
var endedInteractiveDragging: ((CLLocationCoordinate2D) -> Void)?
var annotationSelected: ((LocationPinAnnotation?) -> Void)?
override init() {
super.init()
self.setViewBlock({
return MKMapView()
})
}
override func didLoad() {
super.didLoad()
self.mapView?.interactiveTransitionGestureRecognizerTest = { p in
if p.x > 44.0 {
return true
} else {
return false
}
}
self.mapView?.delegate = self
self.mapView?.mapType = self.mapMode.mapType
self.mapView?.isRotateEnabled = self.isRotateEnabled
self.mapView?.showsUserLocation = true
self.mapView?.showsPointsOfInterest = false
}
var isRotateEnabled: Bool = true {
didSet {
self.mapView?.isRotateEnabled = self.isRotateEnabled
}
}
var mapMode: LocationMapMode = .map {
didSet {
self.mapView?.mapType = self.mapMode.mapType
}
}
func setMapCenter(coordinate: CLLocationCoordinate2D, span: MKCoordinateSpan = defaultMapSpan, offset: CGPoint = CGPoint(), animated: Bool = false) {
let region = MKCoordinateRegion(center: coordinate, span: span)
self.ignoreRegionChanges = true
if offset == CGPoint() {
self.mapView?.setRegion(region, animated: animated)
} else {
let mapRect = MKMapRect(region: region)
self.mapView?.setVisibleMapRect(mapRect, edgePadding: UIEdgeInsets(top: offset.y, left: offset.x, bottom: 0.0, right: 0.0), animated: animated)
}
self.ignoreRegionChanges = false
}
func mapView(_ mapView: MKMapView, regionWillChangeAnimated animated: Bool) {
guard !self.ignoreRegionChanges, let scrollView = mapView.subviews.first, let gestureRecognizers = scrollView.gestureRecognizers else {
return
}
for gestureRecognizer in gestureRecognizers {
if gestureRecognizer.state == .began || gestureRecognizer.state == .ended {
self.beganInteractiveDragging?()
break
}
}
}
func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
if !self.ignoreRegionChanges, let coordinate = self.mapCenterCoordinate {
self.endedInteractiveDragging?(coordinate)
}
}
func mapView(_ mapView: MKMapView, didUpdate userLocation: MKUserLocation) {
guard let location = userLocation.location else {
return
}
userLocation.title = ""
self.locationPromise.set(.single(location))
}
func mapView(_ mapView: MKMapView, didFailToLocateUserWithError error: Error) {
self.locationPromise.set(.single(nil))
}
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
if annotation === mapView.userLocation {
return nil
}
if let annotation = annotation as? LocationPinAnnotation {
var view = mapView.dequeueReusableAnnotationView(withIdentifier: locationPinReuseIdentifier)
if view == nil {
view = LocationPinAnnotationView(annotation: annotation)
}
return view
}
return nil
}
func mapView(_ mapView: MKMapView, didAdd views: [MKAnnotationView]) {
for view in views {
if view.annotation is MKUserLocation {
self.userLocationAnnotationView = view
} else if let view = view as? LocationPinAnnotationView {
view.setZPosition(-1.0)
}
}
}
func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
guard let annotation = view.annotation as? LocationPinAnnotation else {
return
}
if let view = view as? LocationPinAnnotationView {
view.setZPosition(nil)
}
self.annotationSelected?(annotation)
}
func mapView(_ mapView: MKMapView, didDeselect view: MKAnnotationView) {
Queue.mainQueue().async {
if mapView.selectedAnnotations.isEmpty {
if let view = view as? LocationPinAnnotationView {
view.setZPosition(-1.0)
}
self.annotationSelected?(nil)
}
}
}
var userLocation: Signal<CLLocation?, NoError> {
return self.locationPromise.get()
}
var mapCenterCoordinate: CLLocationCoordinate2D? {
guard let mapView = self.mapView else {
return nil
}
return mapView.convert(CGPoint(x: (mapView.frame.width + pinOffset.x) / 2.0, y: (mapView.frame.height + pinOffset.y) / 2.0), toCoordinateFrom: mapView)
}
func resetAnnotationSelection() {
guard let mapView = self.mapView else {
return
}
for annotation in mapView.selectedAnnotations {
mapView.deselectAnnotation(annotation, animated: true)
}
}
var annotations: [LocationPinAnnotation] = [] {
didSet {
guard let mapView = self.mapView else {
return
}
var dict: [String: LocationPinAnnotation] = [:]
for annotation in self.annotations {
if let identifier = annotation.location.venue?.id {
dict[identifier] = annotation
}
}
var annotationsToRemove = Set<LocationPinAnnotation>()
for annotation in mapView.annotations {
guard let annotation = annotation as? LocationPinAnnotation else {
continue
}
if let identifier = annotation.location.venue?.id, let updatedAnnotation = dict[identifier] {
annotation.coordinate = updatedAnnotation.coordinate
dict[identifier] = nil
} else {
annotationsToRemove.insert(annotation)
}
}
mapView.removeAnnotations(Array(annotationsToRemove))
mapView.addAnnotations(Array(dict.values))
}
}
}

View File

@ -0,0 +1,67 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import Postbox
import TelegramCore
import SyncCore
import TelegramPresentationData
import SegmentedControlNode
final class LocationOptionsNode: ASDisplayNode {
private var presentationData: PresentationData
private let backgroundNode: ASDisplayNode
private let separatorNode: ASDisplayNode
private let segmentedControlNode: SegmentedControlNode
private let interaction: LocationPickerInteraction
init(presentationData: PresentationData, interaction: LocationPickerInteraction) {
self.presentationData = presentationData
self.interaction = interaction
self.backgroundNode = ASDisplayNode()
self.backgroundNode.backgroundColor = self.presentationData.theme.rootController.navigationBar.backgroundColor
self.separatorNode = ASDisplayNode()
self.separatorNode.backgroundColor = self.presentationData.theme.rootController.navigationBar.separatorColor
self.segmentedControlNode = SegmentedControlNode(theme: SegmentedControlTheme(theme: self.presentationData.theme), items: [SegmentedControlItem(title: self.presentationData.strings.Map_Map), SegmentedControlItem(title: self.presentationData.strings.Map_Satellite), SegmentedControlItem(title: self.presentationData.strings.Map_Hybrid)], selectedIndex: 0)
super.init()
self.addSubnode(self.backgroundNode)
self.addSubnode(self.separatorNode)
self.addSubnode(self.segmentedControlNode)
self.segmentedControlNode.selectedIndexChanged = { [weak self] index in
guard let strongSelf = self else {
return
}
switch index {
case 0:
strongSelf.interaction.updateMapMode(.map)
case 1:
strongSelf.interaction.updateMapMode(.sattelite)
case 2:
strongSelf.interaction.updateMapMode(.hybrid)
default:
break
}
}
}
func updatePresentationData(_ presentationData: PresentationData) {
self.presentationData = presentationData
self.backgroundNode.backgroundColor = self.presentationData.theme.rootController.navigationBar.backgroundColor
self.separatorNode.backgroundColor = self.presentationData.theme.rootController.navigationBar.separatorColor
self.segmentedControlNode.updateTheme(SegmentedControlTheme(theme: self.presentationData.theme))
}
func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) {
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: size))
transition.updateFrame(node: self.separatorNode, frame: CGRect(x: 0.0, y: size.height, width: size.width, height: UIScreenPixel))
let controlSize = self.segmentedControlNode.updateLayout(.stretchToFill(width: size.width - 16.0), transition: .immediate)
self.segmentedControlNode.frame = CGRect(origin: CGPoint(x: floor((size.width - controlSize.width) / 2.0), y: 0.0), size: controlSize)
}
}

View File

@ -0,0 +1,259 @@
import Foundation
import UIKit
import Display
import LegacyComponents
import TelegramCore
import SyncCore
import Postbox
import SwiftSignalKit
import TelegramPresentationData
import AccountContext
import AppBundle
import CoreLocation
public enum LocationPickerMode {
case share(peer: Peer?, selfPeer: Peer?, hasLiveLocation: Bool)
case pick
}
class LocationPickerInteraction {
let sendLocation: (CLLocationCoordinate2D) -> Void
let sendLiveLocation: (CLLocationCoordinate2D) -> Void
let sendVenue: (TelegramMediaMap) -> Void
let toggleMapModeSelection: () -> Void
let updateMapMode: (LocationMapMode) -> Void
let goToUserLocation: () -> Void
let openSearch: () -> Void
let updateSearchQuery: (String) -> Void
let dismissSearch: () -> Void
let dismissInput: () -> Void
init(sendLocation: @escaping (CLLocationCoordinate2D) -> Void, sendLiveLocation: @escaping (CLLocationCoordinate2D) -> Void, sendVenue: @escaping (TelegramMediaMap) -> Void, toggleMapModeSelection: @escaping () -> Void, updateMapMode: @escaping (LocationMapMode) -> Void, goToUserLocation: @escaping () -> Void, openSearch: @escaping () -> Void, updateSearchQuery: @escaping (String) -> Void, dismissSearch: @escaping () -> Void, dismissInput: @escaping () -> Void) {
self.sendLocation = sendLocation
self.sendLiveLocation = sendLiveLocation
self.sendVenue = sendVenue
self.toggleMapModeSelection = toggleMapModeSelection
self.updateMapMode = updateMapMode
self.goToUserLocation = goToUserLocation
self.openSearch = openSearch
self.updateSearchQuery = updateSearchQuery
self.dismissSearch = dismissSearch
self.dismissInput = dismissInput
}
}
public final class LocationPickerController: ViewController {
private var controllerNode: LocationPickerControllerNode {
return self.displayNode as! LocationPickerControllerNode
}
private let context: AccountContext
private let mode: LocationPickerMode
private let completion: (TelegramMediaMap, String?) -> Void
private var presentationData: PresentationData
private var presentationDataDisposable: Disposable?
private var searchNavigationContentNode: LocationSearchNavigationContentNode?
private var interaction: LocationPickerInteraction?
private let _ready = Promise<Bool>()
override public var ready: Promise<Bool> {
return self._ready
}
public init(context: AccountContext, mode: LocationPickerMode, completion: @escaping (TelegramMediaMap, String?) -> Void) {
self.context = context
self.mode = mode
self.completion = completion
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_ChooseLocationTitle
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed))
self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationCompactSearchIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.searchPressed))
self.navigationItem.rightBarButtonItem?.accessibilityLabel = self.presentationData.strings.Common_Search
self.presentationDataDisposable = (context.sharedContext.presentationData
|> deliverOnMainQueue).start(next: { [weak self] presentationData in
guard let strongSelf = self, strongSelf.presentationData.theme !== presentationData.theme else {
return
}
strongSelf.presentationData = presentationData
strongSelf.navigationBar?.updatePresentationData(NavigationBarPresentationData(theme: NavigationBarTheme(rootControllerTheme: strongSelf.presentationData.theme).withUpdatedSeparatorColor(.clear), strings: NavigationBarStrings(presentationStrings: strongSelf.presentationData.strings)))
strongSelf.searchNavigationContentNode?.updatePresentationData(strongSelf.presentationData)
strongSelf.navigationItem.rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationCompactSearchIcon(strongSelf.presentationData.theme), style: .plain, target: strongSelf, action: #selector(strongSelf.searchPressed))
strongSelf.controllerNode.updatePresentationData(presentationData)
})
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
}
strongSelf.completion(locationWithTimeout(coordinate, nil), nil)
strongSelf.dismiss()
}, sendLiveLocation: { [weak self] coordinate in
guard let strongSelf = self else {
return
}
let controller = ActionSheetController(presentationTheme: strongSelf.presentationData.theme)
var title = strongSelf.presentationData.strings.Map_LiveLocationGroupDescription
if case let .share(peer, _, _) = strongSelf.mode, let receiver = peer {
title = strongSelf.presentationData.strings.Map_LiveLocationPrivateDescription(receiver.compactDisplayTitle).0
}
controller.setItemGroups([
ActionSheetItemGroup(items: [
ActionSheetTextItem(title: title),
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Map_LiveLocationFor15Minutes, color: .accent, action: { [weak self, weak controller] in
controller?.dismissAnimated()
if let strongSelf = self {
strongSelf.completion(locationWithTimeout(coordinate, 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.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.dismiss()
}
})
]),
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak controller] in
controller?.dismissAnimated()
})
])
])
strongSelf.present(controller, in: .window(.root))
}, sendVenue: { [weak self] venue in
guard let strongSelf = self else {
return
}
completion(venue, nil)
strongSelf.dismiss()
}, toggleMapModeSelection: { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.controllerNode.updateState { state in
var state = state
state.displayingMapModeOptions = !state.displayingMapModeOptions
return state
}
}, updateMapMode: { [weak self] mode in
guard let strongSelf = self else {
return
}
strongSelf.controllerNode.updateState { state in
var state = state
state.mapMode = mode
state.displayingMapModeOptions = false
return state
}
}, goToUserLocation: { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.controllerNode.updateState { state in
var state = state
state.displayingMapModeOptions = false
state.selectedLocation = .none
return state
}
}, openSearch: { [weak self] in
guard let strongSelf = self, let interaction = strongSelf.interaction, let navigationBar = strongSelf.navigationBar else {
return
}
strongSelf.controllerNode.updateState { state in
var state = state
state.displayingMapModeOptions = false
return state
}
let contentNode = LocationSearchNavigationContentNode(presentationData: strongSelf.presentationData, interaction: interaction)
strongSelf.searchNavigationContentNode = contentNode
navigationBar.setContentNode(contentNode, animated: true)
strongSelf.controllerNode.activateSearch(navigationBar: navigationBar)
contentNode.activate()
}, updateSearchQuery: { [weak self] query in
guard let strongSelf = self else {
return
}
strongSelf.controllerNode.searchContainerNode?.searchTextUpdated(text: query)
}, dismissSearch: { [weak self] in
guard let strongSelf = self, let navigationBar = strongSelf.navigationBar else {
return
}
strongSelf.searchNavigationContentNode?.deactivate()
strongSelf.searchNavigationContentNode = nil
navigationBar.setContentNode(nil, animated: true)
strongSelf.controllerNode.deactivateSearch()
}, dismissInput: { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.searchNavigationContentNode?.deactivate()
})
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() {
guard let interaction = self.interaction else {
return
}
self.displayNode = LocationPickerControllerNode(context: self.context, presentationData: self.presentationData, mode: self.mode, interaction: interaction)
self.controllerNode.present = { [weak self] c, a in
self?.present(c, in: .window(.root), with: a)
}
self.displayNodeDidLoad()
self._ready.set(.single(true))
}
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 searchPressed() {
self.interaction?.openSearch()
}
}

View File

@ -0,0 +1,609 @@
import Foundation
import UIKit
import Display
import LegacyComponents
import TelegramCore
import SyncCore
import Postbox
import SwiftSignalKit
import MergeLists
import ItemListUI
import ItemListVenueItem
import ActivityIndicator
import TelegramPresentationData
import AccountContext
import AppBundle
import CoreLocation
import Geocoding
private struct LocationPickerTransaction {
let deletions: [ListViewDeleteItem]
let insertions: [ListViewInsertItem]
let updates: [ListViewUpdateItem]
let isLoading: Bool
}
private enum LocationPickerEntryId: Hashable {
case location
case liveLocation
case header
case venue(String)
case attribution
}
private enum LocationPickerEntry: Comparable, Identifiable {
case location(PresentationTheme, String, String, TelegramMediaMap?, CLLocationCoordinate2D?)
case liveLocation(PresentationTheme, String, String, CLLocationCoordinate2D?)
case header(PresentationTheme, String)
case venue(PresentationTheme, TelegramMediaMap, Int)
case attribution(PresentationTheme)
var stableId: LocationPickerEntryId {
switch self {
case .location:
return .location
case .liveLocation:
return .liveLocation
case .header:
return .header
case let .venue(_, venue, _):
return .venue(venue.venue?.id ?? "")
case .attribution:
return .attribution
}
}
static func ==(lhs: LocationPickerEntry, rhs: LocationPickerEntry) -> Bool {
switch lhs {
case let .location(lhsTheme, lhsTitle, lhsSubtitle, lhsVenue, lhsCoordinate):
if case let .location(rhsTheme, rhsTitle, rhsSubtitle, rhsVenue, rhsCoordinate) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsSubtitle == rhsSubtitle, lhsVenue?.venue?.id == rhsVenue?.venue?.id, lhsCoordinate == rhsCoordinate {
return true
} else {
return false
}
case let .liveLocation(lhsTheme, lhsTitle, lhsSubtitle, lhsCoordinate):
if case let .liveLocation(rhsTheme, rhsTitle, rhsSubtitle, rhsCoordinate) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsSubtitle == rhsSubtitle, lhsCoordinate == rhsCoordinate {
return true
} else {
return false
}
case let .header(lhsTheme, lhsTitle):
if case let .header(rhsTheme, rhsTitle) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle {
return true
} else {
return false
}
case let .venue(lhsTheme, lhsVenue, lhsIndex):
if case let .venue(rhsTheme, rhsVenue, rhsIndex) = rhs, lhsTheme === rhsTheme, lhsVenue.venue?.id == rhsVenue.venue?.id, lhsIndex == rhsIndex {
return true
} else {
return false
}
case let .attribution(lhsTheme):
if case let .attribution(rhsTheme) = rhs, lhsTheme === rhsTheme {
return true
} else {
return false
}
}
}
static func <(lhs: LocationPickerEntry, rhs: LocationPickerEntry) -> Bool {
switch lhs {
case .location:
switch rhs {
case .location:
return false
case .liveLocation, .header, .venue, .attribution:
return true
}
case .liveLocation:
switch rhs {
case .location, .liveLocation:
return false
case .header, .venue, .attribution:
return true
}
case .header:
switch rhs {
case .location, .liveLocation, .header:
return false
case .venue, .attribution:
return true
}
case let .venue(_, _, lhsIndex):
switch rhs {
case .location, .liveLocation, .header:
return false
case let .venue(_, _, rhsIndex):
return lhsIndex < rhsIndex
case .attribution:
return true
}
case .attribution:
return false
}
}
func item(account: Account, presentationData: PresentationData, interaction: LocationPickerInteraction?) -> ListViewItem {
switch self {
case let .location(theme, title, subtitle, venue, coordinate):
let icon: LocationActionListItemIcon
if let venue = venue {
icon = .venue(venue)
} else {
icon = .location
}
return LocationActionListItem(presentationData: ItemListPresentationData(presentationData), account: account, title: title, subtitle: subtitle, icon: icon, action: {
if let coordinate = coordinate {
interaction?.sendLocation(coordinate)
}
})
case let .liveLocation(theme, title, subtitle, coordinate):
return LocationActionListItem(presentationData: ItemListPresentationData(presentationData), account: account, title: title, subtitle: subtitle, icon: .liveLocation, action: {
if let coordinate = coordinate {
interaction?.sendLiveLocation(coordinate)
}
})
case let .header(theme, title):
return LocationSectionHeaderItem(presentationData: ItemListPresentationData(presentationData), title: title)
case let .venue(theme, venue, _):
return ItemListVenueItem(presentationData: ItemListPresentationData(presentationData), account: account, venue: venue, sectionId: 0, style: .plain, action: {
interaction?.sendVenue(venue)
})
case let .attribution(theme):
return LocationAttributionItem(presentationData: ItemListPresentationData(presentationData))
}
}
}
private func preparedTransition(from fromEntries: [LocationPickerEntry], to toEntries: [LocationPickerEntry], isLoading: Bool, account: Account, presentationData: PresentationData, interaction: LocationPickerInteraction?) -> LocationPickerTransaction {
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, presentationData: presentationData, interaction: interaction), directionHint: nil) }
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, presentationData: presentationData, interaction: interaction), directionHint: nil) }
return LocationPickerTransaction(deletions: deletions, insertions: insertions, updates: updates, isLoading: isLoading)
}
enum LocationPickerLocation {
case none
case selecting
case location(CLLocationCoordinate2D, String?)
case venue(TelegramMediaMap)
var isCustom: Bool {
switch self {
case .none:
return false
default:
return true
}
}
}
struct LocationPickerState {
var mapMode: LocationMapMode
var displayingMapModeOptions: Bool
var selectedLocation: LocationPickerLocation
init() {
self.mapMode = .map
self.displayingMapModeOptions = false
self.selectedLocation = .none
}
}
final class LocationPickerControllerNode: ViewControllerTracingNode {
private let context: AccountContext
private var presentationData: PresentationData
private let presentationDataPromise: Promise<PresentationData>
private let mode: LocationPickerMode
private let interaction: LocationPickerInteraction
private let listNode: ListView
private let headerNode: LocationMapHeaderNode
private let activityIndicator: ActivityIndicator
private let optionsNode: LocationOptionsNode
private(set) var searchContainerNode: LocationSearchContainerNode?
private var enqueuedTransitions: [(LocationPickerTransaction, Bool)] = []
private var disposable: Disposable?
private var state: LocationPickerState
private let statePromise: Promise<LocationPickerState>
private var geocodingDisposable = MetaDisposable()
private var validLayout: (layout: ContainerViewLayout, navigationHeight: CGFloat)?
private var listOffset: CGFloat?
var present: ((ViewController, Any?) -> Void)?
init(context: AccountContext, presentationData: PresentationData, mode: LocationPickerMode, interaction: LocationPickerInteraction) {
self.context = context
self.presentationData = presentationData
self.presentationDataPromise = Promise(presentationData)
self.mode = mode
self.interaction = interaction
self.state = LocationPickerState()
self.statePromise = Promise(self.state)
self.listNode = ListView()
self.listNode.verticalScrollIndicatorColor = UIColor(white: 0.0, alpha: 0.3)
self.listNode.verticalScrollIndicatorFollowsOverscroll = true
self.headerNode = LocationMapHeaderNode(presentationData: presentationData, interaction: interaction)
self.headerNode.mapNode.isRotateEnabled = false
self.optionsNode = LocationOptionsNode(presentationData: presentationData, interaction: interaction)
self.activityIndicator = ActivityIndicator(type: .custom(self.presentationData.theme.list.itemSecondaryTextColor, 22.0, 1.0, false))
super.init()
self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
self.addSubnode(self.listNode)
self.addSubnode(self.headerNode)
self.addSubnode(self.optionsNode)
self.listNode.addSubnode(self.activityIndicator)
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 {
emit(updated)
return updated
} else {
return current
}
} else {
return current
}
} else {
if let updated = updated, updated.horizontalAccuracy > 0.0 && updated.horizontalAccuracy < 50.0 {
emit(updated)
return updated
} else {
return nil
}
}
}
let venues: Signal<[TelegramMediaMap]?, NoError> = .single(nil)
|> then(
filteredUserLocation
|> mapToSignal { location -> Signal<[TelegramMediaMap]?, NoError> in
if let location = location, location.horizontalAccuracy > 0 {
return nearbyVenues(account: context.account, latitude: location.coordinate.latitude, longitude: location.coordinate.longitude)
|> map(Optional.init)
} else {
return .single(nil)
}
}
)
let previousState = Atomic<LocationPickerState>(value: self.state)
let previousUserLocation = Atomic<CLLocation?>(value: nil)
let previousAnnotations = Atomic<[LocationPinAnnotation]>(value: [])
let previousEntries = Atomic<[LocationPickerEntry]?>(value: nil)
self.disposable = (combineLatest(self.presentationDataPromise.get(), self.statePromise.get(), userLocation, venues)
|> deliverOnMainQueue).start(next: { [weak self] presentationData, state, userLocation, venues in
if let strongSelf = self {
var entries: [LocationPickerEntry] = []
switch state.selectedLocation {
case let .location(coordinate, address):
entries.append(.location(presentationData.theme, presentationData.strings.Map_SendThisLocation, address ?? presentationData.strings.Map_Locating, nil, coordinate))
case .selecting:
entries.append(.location(presentationData.theme, presentationData.strings.Map_SendThisLocation, presentationData.strings.Map_Locating, nil, nil))
case let .venue(venue):
entries.append(.location(presentationData.theme, presentationData.strings.Map_SendThisPlace, venue.venue?.title ?? "", venue, venue.coordinate))
case .none:
let title: String
switch strongSelf.mode {
case .share:
title = presentationData.strings.Map_SendMyCurrentLocation
case .pick:
title = presentationData.strings.Map_SetThisLocation
}
entries.append(.location(presentationData.theme, title, (userLocation?.horizontalAccuracy).flatMap { presentationData.strings.Map_AccurateTo(stringForDistance(strings: presentationData.strings, distance: $0)).0 } ?? presentationData.strings.Map_Locating, nil, userLocation?.coordinate))
}
if case .share(_, _, true) = mode {
entries.append(.liveLocation(presentationData.theme, presentationData.strings.Map_ShareLiveLocation, presentationData.strings.Map_ShareLiveLocationHelp, userLocation?.coordinate))
}
entries.append(.header(presentationData.theme, presentationData.strings.Map_ChooseAPlace.uppercased()))
if let venues = venues {
var index: Int = 0
for venue in venues {
entries.append(.venue(presentationData.theme, venue, index))
index += 1
}
if !venues.isEmpty {
entries.append(.attribution(presentationData.theme))
}
}
let previousEntries = previousEntries.swap(entries)
let transition = preparedTransition(from: previousEntries ?? [], to: entries, isLoading: venues == nil, account: context.account, presentationData: presentationData, interaction: strongSelf.interaction)
strongSelf.enqueueTransition(transition, firstTime: false)
strongSelf.headerNode.updateState(state)
let previousUserLocation = previousUserLocation.swap(userLocation)
switch state.selectedLocation {
case .none:
if let userLocation = userLocation {
strongSelf.headerNode.mapNode.setMapCenter(coordinate: userLocation.coordinate, animated: previousUserLocation != nil)
}
strongSelf.headerNode.mapNode.resetAnnotationSelection()
case .selecting:
strongSelf.headerNode.mapNode.resetAnnotationSelection()
case let .venue(venue):
strongSelf.headerNode.mapNode.setMapCenter(coordinate: venue.coordinate, animated: true)
default:
break
}
let annotations: [LocationPinAnnotation]
if let venues = venues {
annotations = venues.compactMap { LocationPinAnnotation(account: context.account, theme: presentationData.theme, location: $0) }
} else {
annotations = []
}
let previousAnnotations = previousAnnotations.swap(annotations)
if annotations != previousAnnotations {
strongSelf.headerNode.mapNode.annotations = annotations
}
let previousState = previousState.swap(state)
if let (layout, navigationBarHeight) = strongSelf.validLayout {
var updateLayout = false
var transition: ContainedViewLayoutTransition = .animated(duration: 0.45, curve: .spring)
if previousState.displayingMapModeOptions != state.displayingMapModeOptions {
updateLayout = true
} else if previousState.selectedLocation.isCustom != state.selectedLocation.isCustom {
updateLayout = true
}
if updateLayout {
strongSelf.containerLayoutUpdated(layout, navigationHeight: navigationBarHeight, transition: transition)
}
}
if case let .location(coordinate, address) = state.selectedLocation, address == nil {
strongSelf.geocodingDisposable.set((reverseGeocodeLocation(latitude: coordinate.latitude, longitude: coordinate.longitude)
|> deliverOnMainQueue).start(next: { [weak self] placemark in
if let strongSelf = self {
strongSelf.updateState { state in
var state = state
state.selectedLocation = .location(coordinate, placemark?.fullAddress)
return state
}
}
}))
} else {
strongSelf.geocodingDisposable.set(nil)
}
}
})
self.listNode.updateFloatingHeaderOffset = { [weak self] offset, listTransition in
guard let strongSelf = self, let (layout, navigationBarHeight) = strongSelf.validLayout 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, padding: strongSelf.state.displayingMapModeOptions ? 38.0 : 0.0, size: headerFrame.size, transition: listTransition)
strongSelf.layoutActivityIndicator(transition: listTransition)
}
self.listNode.beganInteractiveDragging = { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.updateState { state in
var state = state
state.displayingMapModeOptions = false
return state
}
}
self.headerNode.mapNode.beganInteractiveDragging = { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.updateState { state in
var state = state
state.displayingMapModeOptions = false
state.selectedLocation = .selecting
return state
}
}
self.headerNode.mapNode.endedInteractiveDragging = { [weak self] coordinate in
guard let strongSelf = self else {
return
}
strongSelf.updateState { state in
var state = state
if case .selecting = state.selectedLocation {
state.selectedLocation = .location(coordinate, nil)
}
return state
}
}
self.headerNode.mapNode.annotationSelected = { [weak self] annotation in
guard let strongSelf = self else {
return
}
strongSelf.updateState { state in
var state = state
state.displayingMapModeOptions = false
state.selectedLocation = annotation.flatMap { .venue($0.location) } ?? .none
return state
}
}
}
deinit {
self.disposable?.dispose()
self.geocodingDisposable.dispose()
}
func updatePresentationData(_ presentationData: PresentationData) {
self.presentationData = presentationData
self.presentationDataPromise.set(.single(presentationData))
self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
self.headerNode.updatePresentationData(self.presentationData)
self.optionsNode.updatePresentationData(self.presentationData)
self.searchContainerNode?.updatePresentationData(self.presentationData)
}
func updateState(_ f: (LocationPickerState) -> LocationPickerState) {
self.state = f(self.state)
self.statePromise.set(.single(self.state))
}
private func enqueueTransition(_ transition: LocationPickerTransaction, firstTime: Bool) {
self.enqueuedTransitions.append((transition, firstTime))
if let _ = self.validLayout {
while !self.enqueuedTransitions.isEmpty {
self.dequeueTransition()
}
}
}
private func dequeueTransition() {
guard let layout = self.validLayout, let (transition, firstTime) = self.enqueuedTransitions.first else {
return
}
self.enqueuedTransitions.remove(at: 0)
var options = ListViewDeleteAndInsertOptions()
if firstTime {
options.insert(.PreferSynchronousDrawing)
} else {
options.insert(.AnimateCrossfade)
}
self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { [weak self] _ in
if let strongSelf = self {
strongSelf.activityIndicator.isHidden = !transition.isLoading
}
})
}
func activateSearch(navigationBar: NavigationBar) {
guard let (layout, navigationBarHeight) = self.validLayout, self.searchContainerNode == nil, let coordinate = self.headerNode.mapNode.mapCenterCoordinate else {
return
}
let searchContainerNode = LocationSearchContainerNode(context: self.context, coordinate: coordinate, interaction: self.interaction)
self.insertSubnode(searchContainerNode, belowSubnode: navigationBar)
self.searchContainerNode = searchContainerNode
searchContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
self.containerLayoutUpdated(layout, navigationHeight: navigationBarHeight, transition: .immediate)
}
func deactivateSearch() {
guard let searchContainerNode = self.searchContainerNode else {
return
}
searchContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak searchContainerNode] _ in
searchContainerNode?.removeFromSupernode()
})
self.searchContainerNode = nil
}
func scrollToTop() {
if let searchContainerNode = self.searchContainerNode {
searchContainerNode.scrollToTop()
} else {
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
}
}
private func layoutActivityIndicator(transition: ContainedViewLayoutTransition) {
guard let (layout, navigationHeight) = self.validLayout else {
return
}
let topInset: CGFloat = floor((layout.size.height - navigationHeight) / 2.0 + navigationHeight)
let headerHeight: CGFloat
if let listOffset = self.listOffset {
headerHeight = max(0.0, listOffset)
} else {
headerHeight = topInset
}
let indicatorSize = self.activityIndicator.measure(CGSize(width: 100.0, height: 100.0))
transition.updateFrame(node: self.activityIndicator, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - indicatorSize.width) / 2.0), y: headerHeight + 140.0 + floor((layout.size.height - headerHeight - 140.0 - 50.0) / 2.0)), size: indicatorSize))
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ContainedViewLayoutTransition) {
let isFirstLayout = self.validLayout == nil
self.validLayout = (layout, navigationHeight)
let optionsHeight: CGFloat = 38.0
let topInset: CGFloat = floor((layout.size.height - navigationHeight) / 2.0 + navigationHeight)
let overlap: CGFloat = 6.0
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, padding: self.state.displayingMapModeOptions ? optionsHeight : 0.0, size: headerFrame.size, transition: transition)
transition.updateFrame(node: self.listNode, frame: CGRect(origin: CGPoint(), size: layout.size))
var insets = layout.insets(options: [.input])
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: UIEdgeInsets(top: topInset, left: 0.0, bottom: layout.intrinsicInsets.bottom, right: 0.0), headerInsets: UIEdgeInsets(top: navigationHeight, left: 0.0, bottom: layout.intrinsicInsets.bottom, right: 0.0), scrollIndicatorInsets: UIEdgeInsets(top: topInset + 3.0, left: 0.0, bottom: layout.intrinsicInsets.bottom, right: 0.0), duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
self.listNode.scrollEnabled = !self.state.selectedLocation.isCustom
self.layoutActivityIndicator(transition: transition)
if isFirstLayout {
while !self.enqueuedTransitions.isEmpty {
self.dequeueTransition()
}
}
let optionsOffset: CGFloat = self.state.displayingMapModeOptions ? navigationHeight : navigationHeight - optionsHeight
let optionsFrame = CGRect(x: 0.0, y: optionsOffset, width: layout.size.width, height: optionsHeight)
transition.updateFrame(node: self.optionsNode, frame: optionsFrame)
self.optionsNode.updateLayout(size: optionsFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, transition: transition)
if let searchContainerNode = self.searchContainerNode {
searchContainerNode.frame = CGRect(origin: CGPoint(), size: layout.size)
searchContainerNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: LayoutMetrics(), deviceMetrics: layout.deviceMetrics, intrinsicInsets: layout.intrinsicInsets, safeInsets: layout.safeInsets, statusBarHeight: nil, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver), navigationBarHeight: navigationHeight, transition: transition)
}
}
}

View File

@ -1,12 +0,0 @@
//
// LocationPickerScreen.swift
// LocationUI
//
// Created by Ilya Laktyushin on 13.11.2019.
//
import UIKit
class LocationPickerScreen: NSObject {
}

View File

@ -0,0 +1,237 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import SyncCore
import TelegramPresentationData
import TelegramUIPreferences
import MergeLists
import AccountContext
import SearchUI
import ChatListSearchItemHeader
import ItemListVenueItem
import ContextUI
import ItemListUI
import MapKit
private struct LocationSearchEntry: Identifiable, Comparable {
let index: Int
let theme: PresentationTheme
let venue: TelegramMediaMap
var stableId: String {
return self.venue.venue?.id ?? ""
}
static func ==(lhs: LocationSearchEntry, rhs: LocationSearchEntry) -> Bool {
if lhs.index != rhs.index {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.venue.venue?.id != rhs.venue.venue?.id {
return false
}
return true
}
static func <(lhs: LocationSearchEntry, rhs: LocationSearchEntry) -> Bool {
return lhs.index < rhs.index
}
func item(account: Account, presentationData: PresentationData, sendVenue: @escaping (TelegramMediaMap) -> Void) -> ListViewItem {
// let header = ChatListSearchItemHeader(type: .contacts, theme: self.theme, strings: self.strings, actionTitle: nil, action: nil)
let venue = self.venue
return ItemListVenueItem(presentationData: ItemListPresentationData(presentationData), account: account, venue: self.venue, sectionId: 0, style: .plain, action: {
sendVenue(venue)
})
}
}
struct LocationSearchContainerTransition {
let deletions: [ListViewDeleteItem]
let insertions: [ListViewInsertItem]
let updates: [ListViewUpdateItem]
let isSearching: Bool
}
private func locationSearchContainerPreparedTransition(from fromEntries: [LocationSearchEntry], to toEntries: [LocationSearchEntry], isSearching: Bool, account: Account, presentationData: PresentationData, sendVenue: @escaping (TelegramMediaMap) -> Void) -> LocationSearchContainerTransition {
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, sendVenue: sendVenue), directionHint: nil) }
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, presentationData: presentationData, sendVenue: sendVenue), directionHint: nil) }
return LocationSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, isSearching: isSearching)
}
final class LocationSearchContainerNode: ASDisplayNode {
private let context: AccountContext
private let interaction: LocationPickerInteraction
private let dimNode: ASDisplayNode
public let listNode: ListView
private let searchQuery = Promise<String?>()
private let searchDisposable = MetaDisposable()
private var presentationData: PresentationData
private let themeAndStringsPromise: Promise<(PresentationTheme, PresentationStrings)>
private var containerViewLayout: (ContainerViewLayout, CGFloat)?
private var enqueuedTransitions: [LocationSearchContainerTransition] = []
public init(context: AccountContext, coordinate: CLLocationCoordinate2D, interaction: LocationPickerInteraction) {
self.context = context
self.interaction = interaction
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.themeAndStringsPromise = Promise((self.presentationData.theme, self.presentationData.strings))
self.dimNode = ASDisplayNode()
self.dimNode.backgroundColor = UIColor.black.withAlphaComponent(0.5)
self.listNode = ListView()
self.listNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
self.listNode.isHidden = true
super.init()
self.backgroundColor = nil
self.isOpaque = false
self.addSubnode(self.dimNode)
self.addSubnode(self.listNode)
self.listNode.isHidden = true
let themeAndStringsPromise = self.themeAndStringsPromise
let searchItems = self.searchQuery.get()
|> mapToSignal { query -> Signal<String?, NoError> in
if let query = query, !query.isEmpty {
return (.complete() |> delay(0.6, queue: Queue.mainQueue()))
|> then(.single(query))
} else {
return .single(query)
}
}
|> mapToSignal { query -> Signal<[LocationSearchEntry]?, NoError> in
if let query = query, !query.isEmpty {
let foundVenues = nearbyVenues(account: context.account, latitude: coordinate.latitude, longitude: coordinate.longitude, query: query)
return combineLatest(foundVenues, themeAndStringsPromise.get())
|> delay(0.1, queue: Queue.concurrentDefaultQueue())
|> map { venues, themeAndStrings -> [LocationSearchEntry] in
var entries: [LocationSearchEntry] = []
var index: Int = 0
for venue in venues {
entries.append(LocationSearchEntry(index: index, theme: themeAndStrings.0, venue: venue))
index += 1
}
return entries
}
} else {
return .single(nil)
}
}
let previousSearchItems = Atomic<[LocationSearchEntry]>(value: [])
self.searchDisposable.set((searchItems
|> deliverOnMainQueue).start(next: { [weak self] items in
if let strongSelf = self {
let previousItems = previousSearchItems.swap(items ?? [])
let transition = locationSearchContainerPreparedTransition(from: previousItems, to: items ?? [], isSearching: items != nil, account: context.account, presentationData: strongSelf.presentationData, sendVenue: { venue in self?.listNode.clearHighlightAnimated(true)
self?.interaction.sendVenue(venue)
})
strongSelf.enqueueTransition(transition)
}
}))
self.listNode.beganInteractiveDragging = { [weak self] in
self?.interaction.dismissInput()
}
}
deinit {
self.searchDisposable.dispose()
}
func scrollToTop() {
if !self.listNode.isHidden {
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 })
}
}
override func didLoad() {
super.didLoad()
self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:))))
}
func updatePresentationData(_ presentationData: PresentationData) {
self.presentationData = presentationData
self.themeAndStringsPromise.set(.single((presentationData.theme, presentationData.strings)))
self.listNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
}
func searchTextUpdated(text: String) {
if text.isEmpty {
self.searchQuery.set(.single(nil))
} else {
self.searchQuery.set(.single(text))
}
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
let hadValidLayout = self.containerViewLayout != nil
self.containerViewLayout = (layout, navigationBarHeight)
let topInset = navigationBarHeight
transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset), size: CGSize(width: layout.size.width, height: layout.size.height - topInset)))
self.listNode.frame = CGRect(origin: CGPoint(), size: layout.size)
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: UIEdgeInsets(top: topInset, left: 0.0, bottom: layout.intrinsicInsets.bottom, right: 0.0), duration: 0.0, curve: .Default(duration: nil)), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
if !hadValidLayout {
while !self.enqueuedTransitions.isEmpty {
self.dequeueTransition()
}
}
}
private func enqueueTransition(_ transition: LocationSearchContainerTransition) {
self.enqueuedTransitions.append(transition)
if self.containerViewLayout != nil {
while !self.enqueuedTransitions.isEmpty {
self.dequeueTransition()
}
}
}
private func dequeueTransition() {
if let transition = self.enqueuedTransitions.first {
self.enqueuedTransitions.remove(at: 0)
var options = ListViewDeleteAndInsertOptions()
options.insert(.PreferSynchronousDrawing)
options.insert(.PreferSynchronousResourceLoading)
let isSearching = transition.isSearching
self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { [weak self] _ in
self?.listNode.isHidden = !isSearching
self?.dimNode.isHidden = isSearching
})
}
}
@objc private func dimTapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
self.interaction.dismissSearch()
}
}
}

View File

@ -0,0 +1,62 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import Postbox
import TelegramCore
import SyncCore
import TelegramPresentationData
import SearchBarNode
import LocalizedPeerData
private let searchBarFont = Font.regular(17.0)
final class LocationSearchNavigationContentNode: NavigationBarContentNode {
private var presentationData: PresentationData
private let searchBar: SearchBarNode
private let interaction: LocationPickerInteraction
init(presentationData: PresentationData, interaction: LocationPickerInteraction) {
self.presentationData = presentationData
self.interaction = interaction
self.searchBar = SearchBarNode(theme: SearchBarNodeTheme(theme: presentationData.theme, hasSeparator: false), strings: presentationData.strings, fieldStyle: .modern)
self.searchBar.placeholderString = NSAttributedString(string: presentationData.strings.Map_Search, font: searchBarFont, textColor: presentationData.theme.rootController.navigationSearchBar.inputPlaceholderTextColor)
super.init()
self.addSubnode(self.searchBar)
self.searchBar.cancel = { [weak self] in
self?.searchBar.deactivate(clear: false)
self?.interaction.dismissSearch()
}
self.searchBar.textUpdated = { [weak self] query in
self?.interaction.updateSearchQuery(query)
}
}
override var nominalHeight: CGFloat {
return 56.0
}
override func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) {
let searchBarFrame = CGRect(origin: CGPoint(x: 0.0, y: size.height - self.nominalHeight), size: CGSize(width: size.width, height: 56.0))
self.searchBar.frame = searchBarFrame
self.searchBar.updateLayout(boundingSize: searchBarFrame.size, leftInset: leftInset, rightInset: rightInset, transition: transition)
}
func activate() {
self.searchBar.activate()
}
func deactivate() {
self.searchBar.deactivate(clear: false)
}
func updatePresentationData(_ presentationData: PresentationData) {
self.presentationData = presentationData
self.searchBar.updateThemeAndStrings(theme: SearchBarNodeTheme(theme: presentationData.theme, hasSeparator: false), strings: presentationData.strings)
}
}

View File

@ -0,0 +1,120 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Postbox
import Display
import SwiftSignalKit
import TelegramPresentationData
import ListSectionHeaderNode
import ItemListUI
class LocationSectionHeaderItem: ListViewItem {
let presentationData: ItemListPresentationData
let title: String
public init(presentationData: ItemListPresentationData, title: String) {
self.presentationData = presentationData
self.title = title
}
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 = LocationSectionHeaderItemNode()
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? LocationSectionHeaderItemNode {
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
}
}
private class LocationSectionHeaderItemNode: ListViewItemNode {
private var headerNode: ListSectionHeaderNode?
private var item: LocationSectionHeaderItem?
private var layoutParams: ListViewItemLayoutParams?
required init() {
super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false)
}
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: LocationSectionHeaderItem, _ params: ListViewItemLayoutParams) -> (ListViewItemNodeLayout, () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) {
let currentItem = self.item
return { [weak self] item, params in
let contentSize = CGSize(width: params.width, height: 28.0)
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
}
return (nil, { _ in
if let strongSelf = self {
strongSelf.item = item
strongSelf.layoutParams = params
let headerNode: ListSectionHeaderNode
if let currentHeaderNode = strongSelf.headerNode {
headerNode = currentHeaderNode
if let _ = updatedTheme {
headerNode.updateTheme(theme: item.presentationData.theme)
}
} else {
headerNode = ListSectionHeaderNode(theme: item.presentationData.theme)
headerNode.title = item.title
strongSelf.addSubnode(headerNode)
strongSelf.headerNode = headerNode
}
headerNode.frame = CGRect(origin: CGPoint(), size: contentSize)
headerNode.updateLayout(size: contentSize, leftInset: params.leftInset, rightInset: params.rightInset)
}
})
})
}
}
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)
}
}

View File

@ -0,0 +1,78 @@
import Foundation
import SwiftSignalKit
import SyncCore
import TelegramCore
import TelegramPresentationData
import TelegramStringFormatting
import MapKit
extension TelegramMediaMap {
var coordinate: CLLocationCoordinate2D {
return CLLocationCoordinate2D(latitude: self.latitude, longitude: self.longitude)
}
}
extension MKMapRect {
init(region: MKCoordinateRegion) {
let point1 = MKMapPoint(CLLocationCoordinate2D(latitude: region.center.latitude + region.span.latitudeDelta / 2.0, longitude: region.center.longitude - region.span.longitudeDelta / 2.0))
let point2 = MKMapPoint(CLLocationCoordinate2D(latitude: region.center.latitude - region.span.latitudeDelta / 2.0, longitude: region.center.longitude + region.span.longitudeDelta / 2.0))
self = MKMapRect(x: min(point1.x, point2.x), y: min(point1.y, point2.y), width: abs(point1.x - point2.x), height: abs(point1.y - point2.y))
}
}
extension CLLocationCoordinate2D: Equatable {
}
public func ==(lhs: CLLocationCoordinate2D, rhs: CLLocationCoordinate2D) -> Bool {
return lhs.latitude == rhs.latitude && lhs.longitude == rhs.longitude
}
public func nearbyVenues(account: Account, latitude: Double, longitude: Double, query: String? = nil) -> Signal<[TelegramMediaMap], NoError> {
return resolvePeerByName(account: account, name: "foursquare")
|> take(1)
|> mapToSignal { peerId -> Signal<ChatContextResultCollection?, NoError> in
guard let peerId = peerId else {
return .single(nil)
}
return requestChatContextResults(account: account, botId: peerId, peerId: account.peerId, query: query ?? "", location: .single((latitude, longitude)), offset: "")
|> `catch` { error -> Signal<ChatContextResultCollection?, NoError> in
return .single(nil)
}
}
|> map { contextResult -> [TelegramMediaMap] in
guard let contextResult = contextResult else {
return []
}
var list: [TelegramMediaMap] = []
for result in contextResult.results {
switch result.message {
case let .mapLocation(mapMedia, _):
if let _ = mapMedia.venue {
list.append(mapMedia)
}
default:
break
}
}
return list
}
}
private var sharedDistanceFormatter: MKDistanceFormatter?
func stringForDistance(strings: PresentationStrings, distance: CLLocationDistance) -> String {
let distanceFormatter: MKDistanceFormatter
if let currentDistanceFormatter = sharedDistanceFormatter {
distanceFormatter = currentDistanceFormatter
} else {
distanceFormatter = MKDistanceFormatter()
distanceFormatter.unitStyle = .full
sharedDistanceFormatter = distanceFormatter
}
let locale = localeWithStrings(strings)
if distanceFormatter.locale != locale {
distanceFormatter.locale = locale
}
return distanceFormatter.string(fromDistance: distance)
}

View File

@ -2,7 +2,7 @@
"images" : [
{
"idiom" : "universal",
"filename" : "Information Icon.pdf"
"filename" : "ic_info.pdf"
}
],
"info" : {

View File

@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "FoursquareAttribution@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "ic_infofilled_svg.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "ic_info.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "LocationMessageLiveIcon@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "LocationMessageLiveIcon@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "LocationPinBackground@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "LocationPinBackground@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "LocationPinIcon@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "LocationPinIcon@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "LocationPinShadow@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "LocationPinShadow@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "LocationSmallCircle@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "LocationSmallCircle@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "ic_gps (2).pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@ -5425,7 +5425,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
items.append(ActionSheetTextItem(title: banDescription))
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Conversation_Location, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
self?.presentMapPicker(editingMessage: false)
self?.presentLocationPicker()
}))
if canSendPolls {
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.AttachmentMenu_Poll, color: .accent, action: { [weak actionSheet] in
@ -5525,7 +5525,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}, openWebSearch: {
self?.presentWebSearch(editingMessage : editMediaOptions != nil)
}, openMap: {
self?.presentMapPicker(editingMessage: editMediaOptions != nil)
self?.presentLocationPicker()
}, openContacts: {
self?.presentContactPicker()
}, openPoll: {
@ -5803,7 +5803,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
})
}
private func presentMapPicker(editingMessage: Bool) {
private func presentLocationPicker() {
guard let peer = self.presentationInterfaceState.renderedPeer?.peer else {
return
}
@ -5815,23 +5815,18 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
let _ = (self.context.account.postbox.transaction { transaction -> Peer? in
return transaction.getPeer(selfPeerId)
}
|> deliverOnMainQueue).start(next: { [weak self] selfPeer in
guard let strongSelf = self, let selfPeer = selfPeer else {
return
}
strongSelf.chatDisplayNode.dismissInput()
strongSelf.effectiveNavigationController?.pushViewController(legacyLocationPickerController(context: strongSelf.context, selfPeer: selfPeer, peer: peer, sendLocation: { coordinate, venue, _ in
guard let strongSelf = self else {
|> deliverOnMainQueue).start(next: { [weak self] selfPeer in
guard let strongSelf = self, let selfPeer = selfPeer else {
return
}
let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId
let message: EnqueueMessage = .message(text: "", attributes: [], mediaReference: .standalone(media: TelegramMediaMap(latitude: coordinate.latitude, longitude: coordinate.longitude, geoPlace: nil, venue: venue, liveBroadcastingTimeout: nil)), replyToMessageId: replyMessageId, localGroupingKey: nil)
if editingMessage {
strongSelf.editMessageMediaWithMessages([message])
} else {
let hasLiveLocation = peer.id.namespace != Namespaces.Peer.SecretChat && peer.id != strongSelf.context.account.peerId && !strongSelf.presentationInterfaceState.isScheduledMessages
let controller = LocationPickerController(context: strongSelf.context, mode: .share(peer: peer, selfPeer: peer, hasLiveLocation: hasLiveLocation), completion: { [weak self] location, _ in
guard let strongSelf = self else {
return
}
let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId
let message: EnqueueMessage = .message(text: "", attributes: [], mediaReference: .standalone(media: location), replyToMessageId: replyMessageId, localGroupingKey: nil)
strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({
if let strongSelf = self {
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
@ -5840,27 +5835,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
})
strongSelf.sendMessages([message])
}
}, sendLiveLocation: { [weak self] coordinate, period in
guard let strongSelf = self else {
return
}
let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId
let message: EnqueueMessage = .message(text: "", attributes: [], mediaReference: .standalone(media: TelegramMediaMap(latitude: coordinate.latitude, longitude: coordinate.longitude, geoPlace: nil, venue: nil, liveBroadcastingTimeout: period)), replyToMessageId: replyMessageId, localGroupingKey: nil)
if editingMessage {
strongSelf.editMessageMediaWithMessages([message])
} else {
strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({
if let strongSelf = self {
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
$0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) }
})
}
})
strongSelf.sendMessages([message])
}
}, theme: strongSelf.presentationData.theme, hasLiveLocation: !strongSelf.presentationInterfaceState.isScheduledMessages))
})
})
strongSelf.effectiveNavigationController?.pushViewController(controller)
strongSelf.chatDisplayNode.dismissInput()
})
}
private func presentContactPicker() {

View File

@ -252,7 +252,7 @@ private enum CreateGroupEntry: ItemListNodeEntry {
case let .changeLocation(theme, text):
return ItemListActionItem(presentationData: presentationData, title: text, kind: .generic, alignment: .natural, sectionId: ItemListSectionId(self.section), style: .blocks, action: {
arguments.changeLocation()
}, clearHighlightAutomatically: false)
})
case let .locationInfo(theme, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .venueHeader(theme, title):
@ -397,34 +397,8 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId]
})
}
venuesPromise.set(resolvePeerByName(account: context.account, name: "foursquare")
|> take(1)
|> mapToSignal { peerId -> Signal<ChatContextResultCollection?, NoError> in
guard let peerId = peerId else {
return .single(nil)
}
return requestChatContextResults(account: context.account, botId: peerId, peerId: context.account.peerId, query: "", location: .single((latitude, longitude)), offset: "")
|> `catch` { error -> Signal<ChatContextResultCollection?, NoError> in
return .single(nil)
}
}
|> map { contextResult -> [TelegramMediaMap] in
guard let contextResult = contextResult else {
return []
}
var list: [TelegramMediaMap] = []
for result in contextResult.results {
switch result.message {
case let .mapLocation(mapMedia, _):
if let _ = mapMedia.venue {
list.append(mapMedia)
}
default:
break
}
}
return list
} |> map(Optional.init))
venuesPromise.set(nearbyVenues(account: context.account, latitude: latitude, longitude: longitude)
|> map(Optional.init))
}
let arguments = CreateGroupArguments(account: context.account, updateEditingName: { editingName in
@ -645,38 +619,34 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId]
}
})
}, changeLocation: {
endEditingImpl?()
let peer = TelegramChannel(id: PeerId(0), accessHash: nil, title: "", username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .member, info: .group(TelegramChannelGroupInfo(flags: [])), flags: [], restrictionInfo: nil, adminRights: nil, bannedRights: nil, defaultBannedRights: nil)
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let controller = legacyLocationPickerController(context: context, selfPeer: peer, peer: peer, sendLocation: { coordinate, _, address in
let addressSignal: Signal<String, NoError>
if let address = address {
addressSignal = .single(address)
} else {
addressSignal = reverseGeocodeLocation(latitude: coordinate.latitude, longitude: coordinate.longitude)
|> map { placemark in
if let placemark = placemark {
return placemark.fullAddress
} else {
return "\(coordinate.latitude), \(coordinate.longitude)"
}
}
}
let _ = (addressSignal
|> deliverOnMainQueue).start(next: { address in
addressPromise.set(.single(address))
updateState { current in
var current = current
current.location = PeerGeoLocation(latitude: coordinate.latitude, longitude: coordinate.longitude, address: address)
return current
}
})
}, sendLiveLocation: { _, _ in }, theme: presentationData.theme, customLocationPicker: true, presentationCompleted: {
clearHighlightImpl?()
})
pushImpl?(controller)
endEditingImpl?()
let controller = LocationPickerController(context: context, mode: .pick, completion: { location, address in
let addressSignal: Signal<String, NoError>
if let address = address {
addressSignal = .single(address)
} else {
addressSignal = reverseGeocodeLocation(latitude: location.latitude, longitude: location.longitude)
|> map { placemark in
if let placemark = placemark {
return placemark.fullAddress
} else {
return "\(location.latitude), \(location.longitude)"
}
}
}
let _ = (addressSignal
|> deliverOnMainQueue).start(next: { address in
addressPromise.set(.single(address))
updateState { current in
var current = current
current.location = PeerGeoLocation(latitude: location.latitude, longitude: location.longitude, address: address)
return current
}
})
})
pushImpl?(controller)
}, updateWithVenue: { venue in
guard let venueData = venue.venue else {
return

View File

@ -279,12 +279,12 @@ class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode {
let height: CGFloat
switch item.labelStyle {
case .detailText:
height = 64.0
case .multilineDetailText:
height = 44.0 + labelLayout.size.height
default:
height = 44.0
case .detailText:
height = 64.0
case .multilineDetailText:
height = 44.0 + labelLayout.size.height
default:
height = 44.0
}
switch item.style {