Upgrade location picker
@ -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";
|
||||
|
@ -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))
|
||||
|
@ -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",
|
||||
|
322
submodules/LocationUI/Sources/LocationActionListItem.swift
Normal 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)
|
||||
}
|
||||
}
|
284
submodules/LocationUI/Sources/LocationAnnotation.swift
Normal 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 = {}) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
116
submodules/LocationUI/Sources/LocationAttributionItem.swift
Normal 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)
|
||||
}
|
||||
}
|
122
submodules/LocationUI/Sources/LocationMapHeaderNode.swift
Normal 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()
|
||||
}
|
||||
}
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
67
submodules/LocationUI/Sources/LocationOptionsNode.swift
Normal 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)
|
||||
}
|
||||
}
|
259
submodules/LocationUI/Sources/LocationPickerController.swift
Normal 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()
|
||||
}
|
||||
}
|
609
submodules/LocationUI/Sources/LocationPickerControllerNode.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
//
|
||||
// LocationPickerScreen.swift
|
||||
// LocationUI
|
||||
//
|
||||
// Created by Ilya Laktyushin on 13.11.2019.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class LocationPickerScreen: NSObject {
|
||||
|
||||
}
|
237
submodules/LocationUI/Sources/LocationSearchContainerNode.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
120
submodules/LocationUI/Sources/LocationSectionHeaderItem.swift
Normal 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)
|
||||
}
|
||||
}
|
78
submodules/LocationUI/Sources/LocationUtils.swift
Normal 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)
|
||||
}
|
@ -2,7 +2,7 @@
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "Information Icon.pdf"
|
||||
"filename" : "ic_info.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
|
BIN
submodules/TelegramUI/Images.xcassets/Chat List/InfoIcon.imageset/ic_info.pdf
vendored
Normal file
21
submodules/TelegramUI/Images.xcassets/Location/FoursquareAttribution.imageset/Contents.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 4.8 KiB |
12
submodules/TelegramUI/Images.xcassets/Location/InfoActiveIcon.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "ic_infofilled_svg.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
BIN
submodules/TelegramUI/Images.xcassets/Location/InfoActiveIcon.imageset/ic_infofilled_svg.pdf
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Location/InfoIcon.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "ic_info.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
BIN
submodules/TelegramUI/Images.xcassets/Location/InfoIcon.imageset/ic_info.pdf
vendored
Normal file
22
submodules/TelegramUI/Images.xcassets/Location/LiveLocationIcon.imageset/Contents.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 2.5 KiB |
22
submodules/TelegramUI/Images.xcassets/Location/PinBackground.imageset/Contents.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
BIN
submodules/TelegramUI/Images.xcassets/Location/PinBackground.imageset/LocationPinBackground@2x.png
vendored
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
submodules/TelegramUI/Images.xcassets/Location/PinBackground.imageset/LocationPinBackground@3x.png
vendored
Normal file
After Width: | Height: | Size: 4.6 KiB |
22
submodules/TelegramUI/Images.xcassets/Location/PinIcon.imageset/Contents.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
BIN
submodules/TelegramUI/Images.xcassets/Location/PinIcon.imageset/LocationPinIcon@2x.png
vendored
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
submodules/TelegramUI/Images.xcassets/Location/PinIcon.imageset/LocationPinIcon@3x.png
vendored
Normal file
After Width: | Height: | Size: 1.7 KiB |
22
submodules/TelegramUI/Images.xcassets/Location/PinShadow.imageset/Contents.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
BIN
submodules/TelegramUI/Images.xcassets/Location/PinShadow.imageset/LocationPinShadow@2x.png
vendored
Normal file
After Width: | Height: | Size: 7.3 KiB |
BIN
submodules/TelegramUI/Images.xcassets/Location/PinShadow.imageset/LocationPinShadow@3x.png
vendored
Normal file
After Width: | Height: | Size: 13 KiB |
22
submodules/TelegramUI/Images.xcassets/Location/PinSmallBackground.imageset/Contents.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 4.6 KiB |
After Width: | Height: | Size: 6.3 KiB |
12
submodules/TelegramUI/Images.xcassets/Location/TrackIcon.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "ic_gps (2).pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
@ -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() {
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|