mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-09-17 09:10:30 +00:00
Location picker fixes
This commit is contained in:
parent
3b931cfd96
commit
f4dcc9cb92
@ -5114,6 +5114,7 @@ Any member of this group will be able to see messages in the channel.";
|
||||
"Theme.Colors.ColorWallpaperWarning" = "Are you sure you want to change your chat wallpaper to a color?";
|
||||
"Theme.Colors.ColorWallpaperWarningProceed" = "Proceed";
|
||||
|
||||
"ChatSettings.IntentsSettings" = "Share Sheet";
|
||||
"IntentsSettings.Title" = "Share Sheet";
|
||||
"IntentsSettings.MainAccount" = "Main Account";
|
||||
"IntentsSettings.MainAccountInfo" = "Choose an account for Siri and share suggestions.";
|
||||
@ -5146,3 +5147,9 @@ Any member of this group will be able to see messages in the channel.";
|
||||
|
||||
"Map.SendThisPlace" = "Send This Place";
|
||||
"Map.SetThisPlace" = "Set This Place";
|
||||
"Map.AddressOnMap" = "Address On Map";
|
||||
"Map.PlacesNearby" = "Places Nearby";
|
||||
"Map.Home" = "Home";
|
||||
"Map.Work" = "Work";
|
||||
"Map.HomeAndWorkTitle" = "Home & Work Addresses";
|
||||
"Map.HomeAndWorkInfo" = "Telegram uses the Home and Work addresses from your Contact Card.\n\nKeep your Contact Card up to date for quick access to sending Home and Work addresses.";
|
||||
|
@ -17,6 +17,8 @@ public enum ChatListSearchItemHeaderType: Int32 {
|
||||
case phoneNumber
|
||||
case exceptions
|
||||
case addToExceptions
|
||||
case mapAddress
|
||||
case nearbyVenues
|
||||
}
|
||||
|
||||
public final class ChatListSearchItemHeader: ListViewItemHeader {
|
||||
@ -30,7 +32,7 @@ public final class ChatListSearchItemHeader: ListViewItemHeader {
|
||||
|
||||
public let height: CGFloat = 28.0
|
||||
|
||||
public init(type: ChatListSearchItemHeaderType, theme: PresentationTheme, strings: PresentationStrings, actionTitle: String?, action: (() -> Void)?) {
|
||||
public init(type: ChatListSearchItemHeaderType, theme: PresentationTheme, strings: PresentationStrings, actionTitle: String? = nil, action: (() -> Void)? = nil) {
|
||||
self.type = type
|
||||
self.id = Int64(self.type.rawValue)
|
||||
self.theme = theme
|
||||
@ -93,6 +95,10 @@ public final class ChatListSearchItemHeaderNode: ListViewItemHeaderNode {
|
||||
self.sectionHeaderNode.title = strings.GroupInfo_Permissions_Exceptions.uppercased()
|
||||
case .addToExceptions:
|
||||
self.sectionHeaderNode.title = strings.Exceptions_AddToExceptions.uppercased()
|
||||
case .mapAddress:
|
||||
self.sectionHeaderNode.title = strings.Map_AddressOnMap.uppercased()
|
||||
case .nearbyVenues:
|
||||
self.sectionHeaderNode.title = strings.Map_PlacesNearby.uppercased()
|
||||
}
|
||||
|
||||
self.sectionHeaderNode.action = actionTitle
|
||||
|
@ -244,7 +244,7 @@ public final class DeviceAccess {
|
||||
}
|
||||
}
|
||||
|
||||
public static func authorizeAccess(to subject: DeviceAccessSubject, registerForNotifications: ((@escaping (Bool) -> Void) -> Void)? = nil, requestSiriAuthorization: ((@escaping (Bool) -> Void) -> Void)? = nil, presentationData: PresentationData? = nil, present: @escaping (ViewController, Any?) -> Void = { _, _ in }, openSettings: @escaping () -> Void = { }, displayNotificationFromBackground: @escaping (String) -> Void = { _ in }, _ completion: @escaping (Bool) -> Void = { _ in }) {
|
||||
public static func authorizeAccess(to subject: DeviceAccessSubject, registerForNotifications: ((@escaping (Bool) -> Void) -> Void)? = nil, requestSiriAuthorization: ((@escaping (Bool) -> Void) -> Void)? = nil, locationManager: CLLocationManager? = nil, presentationData: PresentationData? = nil, present: @escaping (ViewController, Any?) -> Void = { _, _ in }, openSettings: @escaping () -> Void = { }, displayNotificationFromBackground: @escaping (String) -> Void = { _ in }, _ completion: @escaping (Bool) -> Void = { _ in }) {
|
||||
switch subject {
|
||||
case .camera:
|
||||
let status = PGCamera.cameraAuthorizationStatus()
|
||||
@ -382,7 +382,14 @@ public final class DeviceAccess {
|
||||
})]), nil)
|
||||
}
|
||||
case .notDetermined:
|
||||
completion(true)
|
||||
switch locationSubject {
|
||||
case .send:
|
||||
locationManager?.requestWhenInUseAuthorization()
|
||||
case .live:
|
||||
locationManager?.requestAlwaysAuthorization()
|
||||
default:
|
||||
break
|
||||
}
|
||||
@unknown default:
|
||||
fatalError()
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ public class ImmediateTextNode: TextNode {
|
||||
public var lineSpacing: CGFloat = 0.0
|
||||
public var insets: UIEdgeInsets = UIEdgeInsets()
|
||||
public var textShadowColor: UIColor?
|
||||
public var textStroke: (UIColor, CGFloat)?
|
||||
|
||||
private var tapRecognizer: TapLongTapOrDoubleTapGestureRecognizer?
|
||||
private var linkHighlightingNode: LinkHighlightingNode?
|
||||
@ -35,7 +36,7 @@ public class ImmediateTextNode: TextNode {
|
||||
|
||||
public func updateLayout(_ constrainedSize: CGSize) -> CGSize {
|
||||
let makeLayout = TextNode.asyncLayout(self)
|
||||
let (layout, apply) = makeLayout(TextNodeLayoutArguments(attributedString: self.attributedText, backgroundColor: nil, maximumNumberOfLines: self.maximumNumberOfLines, truncationType: self.truncationType, constrainedSize: constrainedSize, alignment: self.textAlignment, lineSpacing: self.lineSpacing, cutout: nil, insets: self.insets, textShadowColor: self.textShadowColor))
|
||||
let (layout, apply) = makeLayout(TextNodeLayoutArguments(attributedString: self.attributedText, backgroundColor: nil, maximumNumberOfLines: self.maximumNumberOfLines, truncationType: self.truncationType, constrainedSize: constrainedSize, alignment: self.textAlignment, lineSpacing: self.lineSpacing, cutout: nil, insets: self.insets, textShadowColor: self.textShadowColor, textStroke: self.textStroke))
|
||||
let _ = apply()
|
||||
if layout.numberOfLines > 1 {
|
||||
self.trailingLineWidth = layout.trailingLineWidth
|
||||
|
@ -95,8 +95,9 @@ public final class TextNodeLayoutArguments {
|
||||
public let insets: UIEdgeInsets
|
||||
public let lineColor: UIColor?
|
||||
public let textShadowColor: UIColor?
|
||||
public let textStroke: (UIColor, CGFloat)?
|
||||
|
||||
public init(attributedString: NSAttributedString?, backgroundColor: UIColor? = nil, minimumNumberOfLines: Int = 0, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, constrainedSize: CGSize, alignment: NSTextAlignment = .natural, lineSpacing: CGFloat = 0.12, cutout: TextNodeCutout? = nil, insets: UIEdgeInsets = UIEdgeInsets(), lineColor: UIColor? = nil, textShadowColor: UIColor? = nil) {
|
||||
public init(attributedString: NSAttributedString?, backgroundColor: UIColor? = nil, minimumNumberOfLines: Int = 0, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, constrainedSize: CGSize, alignment: NSTextAlignment = .natural, lineSpacing: CGFloat = 0.12, cutout: TextNodeCutout? = nil, insets: UIEdgeInsets = UIEdgeInsets(), lineColor: UIColor? = nil, textShadowColor: UIColor? = nil, textStroke: (UIColor, CGFloat)? = nil) {
|
||||
self.attributedString = attributedString
|
||||
self.backgroundColor = backgroundColor
|
||||
self.minimumNumberOfLines = minimumNumberOfLines
|
||||
@ -109,6 +110,7 @@ public final class TextNodeLayoutArguments {
|
||||
self.insets = insets
|
||||
self.lineColor = lineColor
|
||||
self.textShadowColor = textShadowColor
|
||||
self.textStroke = textStroke
|
||||
}
|
||||
}
|
||||
|
||||
@ -130,9 +132,10 @@ public final class TextNodeLayout: NSObject {
|
||||
fileprivate let blockQuotes: [TextNodeBlockQuote]
|
||||
fileprivate let lineColor: UIColor?
|
||||
fileprivate let textShadowColor: UIColor?
|
||||
fileprivate let textStroke: (UIColor, CGFloat)?
|
||||
public let hasRTL: Bool
|
||||
|
||||
fileprivate init(attributedString: NSAttributedString?, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, constrainedSize: CGSize, alignment: NSTextAlignment, lineSpacing: CGFloat, cutout: TextNodeCutout?, insets: UIEdgeInsets, size: CGSize, rawTextSize: CGSize, truncated: Bool, firstLineOffset: CGFloat, lines: [TextNodeLine], blockQuotes: [TextNodeBlockQuote], backgroundColor: UIColor?, lineColor: UIColor?, textShadowColor: UIColor?) {
|
||||
fileprivate init(attributedString: NSAttributedString?, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, constrainedSize: CGSize, alignment: NSTextAlignment, lineSpacing: CGFloat, cutout: TextNodeCutout?, insets: UIEdgeInsets, size: CGSize, rawTextSize: CGSize, truncated: Bool, firstLineOffset: CGFloat, lines: [TextNodeLine], blockQuotes: [TextNodeBlockQuote], backgroundColor: UIColor?, lineColor: UIColor?, textShadowColor: UIColor?, textStroke: (UIColor, CGFloat)?) {
|
||||
self.attributedString = attributedString
|
||||
self.maximumNumberOfLines = maximumNumberOfLines
|
||||
self.truncationType = truncationType
|
||||
@ -150,6 +153,7 @@ public final class TextNodeLayout: NSObject {
|
||||
self.backgroundColor = backgroundColor
|
||||
self.lineColor = lineColor
|
||||
self.textShadowColor = textShadowColor
|
||||
self.textStroke = textStroke
|
||||
var hasRTL = false
|
||||
for line in lines {
|
||||
if line.isRTL {
|
||||
@ -780,7 +784,7 @@ public class TextNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
private class func calculateLayout(attributedString: NSAttributedString?, minimumNumberOfLines: Int, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, backgroundColor: UIColor?, constrainedSize: CGSize, alignment: NSTextAlignment, lineSpacingFactor: CGFloat, cutout: TextNodeCutout?, insets: UIEdgeInsets, lineColor: UIColor?, textShadowColor: UIColor?) -> TextNodeLayout {
|
||||
private class func calculateLayout(attributedString: NSAttributedString?, minimumNumberOfLines: Int, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, backgroundColor: UIColor?, constrainedSize: CGSize, alignment: NSTextAlignment, lineSpacingFactor: CGFloat, cutout: TextNodeCutout?, insets: UIEdgeInsets, lineColor: UIColor?, textShadowColor: UIColor?, textStroke: (UIColor, CGFloat)?) -> TextNodeLayout {
|
||||
if let attributedString = attributedString {
|
||||
let stringLength = attributedString.length
|
||||
|
||||
@ -806,7 +810,7 @@ public class TextNode: ASDisplayNode {
|
||||
var maybeTypesetter: CTTypesetter?
|
||||
maybeTypesetter = CTTypesetterCreateWithAttributedString(attributedString as CFAttributedString)
|
||||
if maybeTypesetter == nil {
|
||||
return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, alignment: alignment, lineSpacing: lineSpacingFactor, cutout: cutout, insets: insets, size: CGSize(), rawTextSize: CGSize(), truncated: false, firstLineOffset: 0.0, lines: [], blockQuotes: [], backgroundColor: backgroundColor, lineColor: lineColor, textShadowColor: textShadowColor)
|
||||
return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, alignment: alignment, lineSpacing: lineSpacingFactor, cutout: cutout, insets: insets, size: CGSize(), rawTextSize: CGSize(), truncated: false, firstLineOffset: 0.0, lines: [], blockQuotes: [], backgroundColor: backgroundColor, lineColor: lineColor, textShadowColor: textShadowColor, textStroke: textStroke)
|
||||
}
|
||||
|
||||
let typesetter = maybeTypesetter!
|
||||
@ -1010,9 +1014,9 @@ public class TextNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, alignment: alignment, lineSpacing: lineSpacingFactor, cutout: cutout, insets: insets, size: CGSize(width: ceil(layoutSize.width) + insets.left + insets.right, height: ceil(layoutSize.height) + insets.top + insets.bottom), rawTextSize: CGSize(width: ceil(rawLayoutSize.width) + insets.left + insets.right, height: ceil(rawLayoutSize.height) + insets.top + insets.bottom), truncated: truncated, firstLineOffset: firstLineOffset, lines: lines, blockQuotes: blockQuotes, backgroundColor: backgroundColor, lineColor: lineColor, textShadowColor: textShadowColor)
|
||||
return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, alignment: alignment, lineSpacing: lineSpacingFactor, cutout: cutout, insets: insets, size: CGSize(width: ceil(layoutSize.width) + insets.left + insets.right, height: ceil(layoutSize.height) + insets.top + insets.bottom), rawTextSize: CGSize(width: ceil(rawLayoutSize.width) + insets.left + insets.right, height: ceil(rawLayoutSize.height) + insets.top + insets.bottom), truncated: truncated, firstLineOffset: firstLineOffset, lines: lines, blockQuotes: blockQuotes, backgroundColor: backgroundColor, lineColor: lineColor, textShadowColor: textShadowColor, textStroke: textStroke)
|
||||
} else {
|
||||
return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, alignment: alignment, lineSpacing: lineSpacingFactor, cutout: cutout, insets: insets, size: CGSize(), rawTextSize: CGSize(), truncated: false, firstLineOffset: 0.0, lines: [], blockQuotes: [], backgroundColor: backgroundColor, lineColor: lineColor, textShadowColor: textShadowColor)
|
||||
return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, alignment: alignment, lineSpacing: lineSpacingFactor, cutout: cutout, insets: insets, size: CGSize(), rawTextSize: CGSize(), truncated: false, firstLineOffset: 0.0, lines: [], blockQuotes: [], backgroundColor: backgroundColor, lineColor: lineColor, textShadowColor: textShadowColor, textStroke: textStroke)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1050,6 +1054,14 @@ public class TextNode: ASDisplayNode {
|
||||
context.setShadow(offset: CGSize(width: 0.0, height: 1.0), blur: 0.0, color: textShadowColor.cgColor)
|
||||
}
|
||||
|
||||
// if let (textStrokeColor, textStrokeWidth) = layout.textStroke {
|
||||
// context.setBlendMode(.normal)
|
||||
// context.setLineCap(.round)
|
||||
// context.setLineJoin(.round)
|
||||
// context.setStrokeColor(textStrokeColor.cgColor)
|
||||
// context.setLineWidth(textStrokeWidth)
|
||||
// }
|
||||
|
||||
let textMatrix = context.textMatrix
|
||||
let textPosition = context.textPosition
|
||||
context.textMatrix = CGAffineTransform(scaleX: 1.0, y: -1.0)
|
||||
@ -1156,11 +1168,11 @@ public class TextNode: ASDisplayNode {
|
||||
if stringMatch {
|
||||
layout = existingLayout
|
||||
} else {
|
||||
layout = TextNode.calculateLayout(attributedString: arguments.attributedString, minimumNumberOfLines: arguments.minimumNumberOfLines, maximumNumberOfLines: arguments.maximumNumberOfLines, truncationType: arguments.truncationType, backgroundColor: arguments.backgroundColor, constrainedSize: arguments.constrainedSize, alignment: arguments.alignment, lineSpacingFactor: arguments.lineSpacing, cutout: arguments.cutout, insets: arguments.insets, lineColor: arguments.lineColor, textShadowColor: arguments.textShadowColor)
|
||||
layout = TextNode.calculateLayout(attributedString: arguments.attributedString, minimumNumberOfLines: arguments.minimumNumberOfLines, maximumNumberOfLines: arguments.maximumNumberOfLines, truncationType: arguments.truncationType, backgroundColor: arguments.backgroundColor, constrainedSize: arguments.constrainedSize, alignment: arguments.alignment, lineSpacingFactor: arguments.lineSpacing, cutout: arguments.cutout, insets: arguments.insets, lineColor: arguments.lineColor, textShadowColor: arguments.textShadowColor, textStroke: arguments.textStroke)
|
||||
updated = true
|
||||
}
|
||||
} else {
|
||||
layout = TextNode.calculateLayout(attributedString: arguments.attributedString, minimumNumberOfLines: arguments.minimumNumberOfLines, maximumNumberOfLines: arguments.maximumNumberOfLines, truncationType: arguments.truncationType, backgroundColor: arguments.backgroundColor, constrainedSize: arguments.constrainedSize, alignment: arguments.alignment, lineSpacingFactor: arguments.lineSpacing, cutout: arguments.cutout, insets: arguments.insets, lineColor: arguments.lineColor, textShadowColor: arguments.textShadowColor)
|
||||
layout = TextNode.calculateLayout(attributedString: arguments.attributedString, minimumNumberOfLines: arguments.minimumNumberOfLines, maximumNumberOfLines: arguments.maximumNumberOfLines, truncationType: arguments.truncationType, backgroundColor: arguments.backgroundColor, constrainedSize: arguments.constrainedSize, alignment: arguments.alignment, lineSpacingFactor: arguments.lineSpacing, cutout: arguments.cutout, insets: arguments.insets, lineColor: arguments.lineColor, textShadowColor: arguments.textShadowColor, textStroke: arguments.textStroke)
|
||||
updated = true
|
||||
}
|
||||
|
||||
|
@ -2,6 +2,19 @@ import Foundation
|
||||
import CoreLocation
|
||||
import SwiftSignalKit
|
||||
|
||||
public func geocodeLocation(address: String) -> Signal<[CLPlacemark]?, NoError> {
|
||||
return Signal { subscriber in
|
||||
let geocoder = CLGeocoder()
|
||||
geocoder.geocodeAddressString(address) { (placemarks, _) in
|
||||
subscriber.putNext(placemarks)
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
return ActionDisposable {
|
||||
geocoder.cancelGeocode()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func geocodeLocation(dictionary: [String: String]) -> Signal<(Double, Double)?, NoError> {
|
||||
return Signal { subscriber in
|
||||
let geocoder = CLGeocoder()
|
||||
|
@ -14,24 +14,46 @@ public final class ItemListVenueItem: ListViewItem, ItemListItem {
|
||||
let presentationData: ItemListPresentationData
|
||||
let account: Account
|
||||
let venue: TelegramMediaMap
|
||||
|
||||
public let sectionId: ItemListSectionId
|
||||
let title: String?
|
||||
let subtitle: String?
|
||||
let style: ItemListStyle
|
||||
let action: (() -> Void)?
|
||||
let infoAction: (() -> Void)?
|
||||
|
||||
public init(presentationData: ItemListPresentationData, account: Account, venue: TelegramMediaMap, sectionId: ItemListSectionId, style: ItemListStyle, action: (() -> Void)?) {
|
||||
public let sectionId: ItemListSectionId
|
||||
let header: ListViewItemHeader?
|
||||
|
||||
public init(presentationData: ItemListPresentationData, account: Account, venue: TelegramMediaMap, title: String? = nil, subtitle: String? = nil, sectionId: ItemListSectionId = 0, style: ItemListStyle, action: (() -> Void)?, infoAction: (() -> Void)? = nil, header: ListViewItemHeader? = nil) {
|
||||
self.presentationData = presentationData
|
||||
self.account = account
|
||||
self.venue = venue
|
||||
self.title = title
|
||||
self.subtitle = subtitle
|
||||
self.sectionId = sectionId
|
||||
self.style = style
|
||||
self.action = action
|
||||
self.infoAction = infoAction
|
||||
self.header = header
|
||||
}
|
||||
|
||||
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 {
|
||||
var firstWithHeader = false
|
||||
var last = false
|
||||
if self.style == .plain {
|
||||
if previousItem == nil {
|
||||
firstWithHeader = true
|
||||
} else if let previousItem = previousItem as? ItemListVenueItem, self.header != nil && previousItem.header?.id != self.header?.id {
|
||||
firstWithHeader = true
|
||||
}
|
||||
if nextItem == nil {
|
||||
last = true
|
||||
} else if let nextItem = nextItem as? ItemListVenueItem, self.header != nil && nextItem.header?.id != self.header?.id {
|
||||
last = true
|
||||
}
|
||||
}
|
||||
let node = ItemListVenueItemNode()
|
||||
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
|
||||
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem), firstWithHeader, last)
|
||||
|
||||
node.contentSize = layout.contentSize
|
||||
node.insets = layout.insets
|
||||
@ -55,7 +77,21 @@ public final class ItemListVenueItem: ListViewItem, ItemListItem {
|
||||
}
|
||||
|
||||
async {
|
||||
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
|
||||
var firstWithHeader = false
|
||||
var last = false
|
||||
if self.style == .plain {
|
||||
if previousItem == nil {
|
||||
firstWithHeader = true
|
||||
} else if let previousItem = previousItem as? ItemListVenueItem, self.header != nil && previousItem.header?.id != self.header?.id {
|
||||
firstWithHeader = true
|
||||
}
|
||||
if nextItem == nil {
|
||||
last = true
|
||||
} else if let nextItem = nextItem as? ItemListVenueItem, self.header != nil && nextItem.header?.id != self.header?.id {
|
||||
last = true
|
||||
}
|
||||
}
|
||||
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem), firstWithHeader, last)
|
||||
Queue.mainQueue().async {
|
||||
completion(layout, { _ in
|
||||
apply()
|
||||
@ -84,8 +120,10 @@ public class ItemListVenueItemNode: ListViewItemNode, ItemListItemNode {
|
||||
private let iconNode: TransformImageNode
|
||||
private let titleNode: TextNode
|
||||
private let addressNode: TextNode
|
||||
private let infoButton: HighlightableButtonNode
|
||||
|
||||
private var layoutParams: (ItemListVenueItem, ListViewItemLayoutParams, ItemListNeighbors)?
|
||||
private var item: ItemListVenueItem?
|
||||
private var layoutParams: (ItemListVenueItem, ListViewItemLayoutParams, ItemListNeighbors, Bool, Bool)?
|
||||
|
||||
public var tag: ItemListItemTag?
|
||||
|
||||
@ -119,6 +157,8 @@ public class ItemListVenueItemNode: ListViewItemNode, ItemListItemNode {
|
||||
self.addressNode.isUserInteractionEnabled = false
|
||||
self.addressNode.contentMode = .left
|
||||
self.addressNode.contentsScale = UIScreen.main.scale
|
||||
|
||||
self.infoButton = HighlightableButtonNode()
|
||||
|
||||
self.highlightedBackgroundNode = ASDisplayNode()
|
||||
self.highlightedBackgroundNode.isLayerBacked = true
|
||||
@ -130,16 +170,19 @@ public class ItemListVenueItemNode: ListViewItemNode, ItemListItemNode {
|
||||
self.addSubnode(self.iconNode)
|
||||
self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.addressNode)
|
||||
self.addSubnode(self.infoButton)
|
||||
|
||||
self.infoButton.addTarget(self, action: #selector(self.infoPressed), forControlEvents: .touchUpInside)
|
||||
}
|
||||
|
||||
public func asyncLayout() -> (_ item: ItemListVenueItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
|
||||
public func asyncLayout() -> (_ item: ItemListVenueItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors, _ firstWithHeader: Bool, _ last: Bool) -> (ListViewItemNodeLayout, () -> Void) {
|
||||
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
|
||||
let makeAddressLayout = TextNode.asyncLayout(self.addressNode)
|
||||
let iconLayout = self.iconNode.asyncLayout()
|
||||
|
||||
let currentItem = self.layoutParams?.0
|
||||
|
||||
return { item, params, neighbors in
|
||||
return { item, params, neighbors, firstWithHeader, last in
|
||||
var updatedTheme: PresentationTheme?
|
||||
var updatedVenueType: String?
|
||||
|
||||
@ -155,11 +198,29 @@ public class ItemListVenueItemNode: ListViewItemNode, ItemListItemNode {
|
||||
updatedVenueType = venueType
|
||||
}
|
||||
|
||||
let titleAttributedString = NSAttributedString(string: item.venue.venue?.title ?? "", font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor)
|
||||
let addressAttributedString = NSAttributedString(string: item.venue.venue?.address ?? "", font: addressFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor)
|
||||
let title: String
|
||||
if let venueTitle = item.venue.venue?.title {
|
||||
title = venueTitle
|
||||
} else if let customTitle = item.title {
|
||||
title = customTitle
|
||||
} else {
|
||||
title = ""
|
||||
}
|
||||
|
||||
let subtitle: String
|
||||
if let address = item.venue.venue?.address {
|
||||
subtitle = address
|
||||
} else if let customSubtitle = item.subtitle {
|
||||
subtitle = customSubtitle
|
||||
} else {
|
||||
subtitle = ""
|
||||
}
|
||||
|
||||
let titleAttributedString = NSAttributedString(string: title, font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor)
|
||||
let addressAttributedString = NSAttributedString(string: subtitle, font: addressFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor)
|
||||
|
||||
let leftInset: CGFloat = 65.0 + params.leftInset
|
||||
let rightInset: CGFloat = 16.0 + params.rightInset
|
||||
let rightInset: CGFloat = 16.0 + params.rightInset + (item.infoAction != nil ? 48.0 : 0.0)
|
||||
let verticalInset: CGFloat = addressAttributedString.string.isEmpty ? 14.0 : 8.0
|
||||
let iconSize: CGFloat = 40.0
|
||||
|
||||
@ -179,6 +240,7 @@ public class ItemListVenueItemNode: ListViewItemNode, ItemListItemNode {
|
||||
itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor
|
||||
itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor
|
||||
insets = itemListNeighborsPlainInsets(neighbors)
|
||||
insets.top = firstWithHeader ? 29.0 : 0.0
|
||||
insets.bottom = 0.0
|
||||
case .blocks:
|
||||
itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor
|
||||
@ -193,7 +255,8 @@ public class ItemListVenueItemNode: ListViewItemNode, ItemListItemNode {
|
||||
|
||||
return (layout, { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.layoutParams = (item, params, neighbors)
|
||||
strongSelf.item = item
|
||||
strongSelf.layoutParams = (item, params, neighbors, firstWithHeader, last)
|
||||
|
||||
strongSelf.accessibilityLabel = titleAttributedString.string
|
||||
strongSelf.accessibilityValue = addressAttributedString.string
|
||||
@ -203,6 +266,7 @@ public class ItemListVenueItemNode: ListViewItemNode, ItemListItemNode {
|
||||
strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor
|
||||
strongSelf.backgroundNode.backgroundColor = itemBackgroundColor
|
||||
strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor
|
||||
strongSelf.infoButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Location/InfoIcon"), color: item.presentationData.theme.list.itemAccentColor), for: .normal)
|
||||
}
|
||||
|
||||
let transition = ContainedViewLayoutTransition.immediate
|
||||
@ -239,7 +303,7 @@ public class ItemListVenueItemNode: ListViewItemNode, ItemListItemNode {
|
||||
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
|
||||
strongSelf.bottomStripeNode.isHidden = last
|
||||
case .blocks:
|
||||
if strongSelf.backgroundNode.supernode == nil {
|
||||
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
|
||||
@ -287,6 +351,9 @@ public class ItemListVenueItemNode: ListViewItemNode, ItemListItemNode {
|
||||
|
||||
transition.updateFrame(node: strongSelf.iconNode, frame: CGRect(origin: CGPoint(x: params.leftInset + 15.0, y: floorToScreenPixels((layout.contentSize.height - iconSize) / 2.0)), size: CGSize(width: iconSize, height: iconSize)))
|
||||
|
||||
transition.updateFrame(node: strongSelf.infoButton, frame: CGRect(x: layout.contentSize.width - params.rightInset - 60.0, y: 0.0, width: 60.0, height: layout.contentSize.height))
|
||||
strongSelf.infoButton.isHidden = item.infoAction == nil
|
||||
|
||||
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: contentSize.height + UIScreenPixel + UIScreenPixel))
|
||||
}
|
||||
})
|
||||
@ -338,4 +405,12 @@ public class ItemListVenueItemNode: ListViewItemNode, ItemListItemNode {
|
||||
override public func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
||||
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
|
||||
}
|
||||
|
||||
@objc private func infoPressed() {
|
||||
self.item?.infoAction?()
|
||||
}
|
||||
|
||||
override public func header() -> ListViewItemHeader? {
|
||||
return self.item?.header
|
||||
}
|
||||
}
|
||||
|
@ -167,12 +167,14 @@ const CGFloat TGLocationInfoCellHeight = 134.0f;
|
||||
|
||||
_titleLabel.text = location.venue.title.length > 0 ? location.venue.title : TGLocalized(@"Map.Location");
|
||||
|
||||
UIColor *pinColor = _pallete != nil ? _pallete.iconColor : [UIColor whiteColor];
|
||||
if (color != nil) {
|
||||
[_circleView setImage:TGTintedImage([TGLocationVenueCell circleImage], color)];
|
||||
pinColor = [UIColor whiteColor];
|
||||
}
|
||||
|
||||
if (location.venue.type.length > 0 && [location.venue.provider isEqualToString:@"foursquare"])
|
||||
[_iconView loadUri:[NSString stringWithFormat:@"location-venue-icon://type=%@&width=%d&height=%d&color=%d", location.venue.type, 48, 48, TGColorHexCode(_pallete != nil ? _pallete.iconColor : [UIColor whiteColor])] withOptions:nil];
|
||||
[_iconView loadUri:[NSString stringWithFormat:@"location-venue-icon://type=%@&width=%d&height=%d&color=%d", location.venue.type, 48, 48, TGColorHexCode(pinColor)] withOptions:nil];
|
||||
|
||||
SSignal *addressSignal = [SSignal single:@""];
|
||||
if (location.venue.address.length > 0)
|
||||
|
@ -266,14 +266,16 @@ NSString *const TGLocationPinAnnotationKind = @"TGLocationPinAnnotation";
|
||||
_iconView.hidden = false;
|
||||
|
||||
UIColor *color = _pallete != nil ? _pallete.locationColor : UIColorRGB(0x008df2);
|
||||
UIColor *pinColor = _pallete != nil ? _pallete.iconColor : [UIColor whiteColor];
|
||||
if (locationAnnotation.color != nil) {
|
||||
color = locationAnnotation.color;
|
||||
pinColor = [UIColor whiteColor];
|
||||
}
|
||||
|
||||
_backgroundView.image = TGTintedImage(TGComponentsImageNamed(@"LocationPinBackground"), color);
|
||||
if (location.venue.type.length > 0)
|
||||
{
|
||||
[_iconView loadUri:[NSString stringWithFormat:@"location-venue-icon://type=%@&width=%d&height=%d&color=%d", location.venue.type, 64, 64, TGColorHexCode(_pallete != nil ? _pallete.iconColor : [UIColor whiteColor])] withOptions:nil];
|
||||
[_iconView loadUri:[NSString stringWithFormat:@"location-venue-icon://type=%@&width=%d&height=%d&color=%d", location.venue.type, 64, 64, TGColorHexCode(pinColor)] withOptions:nil];
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -134,9 +134,14 @@ private let venueColors: [String: UIColor] = [
|
||||
"parks_outdoors": UIColor(rgb: 0x6cc039),
|
||||
"shops": UIColor(rgb: 0xffb300),
|
||||
"travel": UIColor(rgb: 0x1c9fff),
|
||||
"work": UIColor(rgb: 0xad7854),
|
||||
"home": UIColor(rgb: 0x00aeef)
|
||||
]
|
||||
|
||||
public func venueIconColor(type: String) -> UIColor {
|
||||
if type.isEmpty {
|
||||
return UIColor(rgb: 0x008df2)
|
||||
}
|
||||
if let color = venueColors[type] {
|
||||
return color
|
||||
}
|
||||
@ -150,7 +155,8 @@ public func venueIconColor(type: String) -> UIColor {
|
||||
}
|
||||
|
||||
public func venueIcon(postbox: Postbox, type: String, background: Bool) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> {
|
||||
let data: Signal<Data?, NoError> = type.isEmpty ? .single(nil) : venueIconData(postbox: postbox, resource: VenueIconResource(type: type))
|
||||
let isBuiltinIcon = ["", "home", "work"].contains(type)
|
||||
let data: Signal<Data?, NoError> = isBuiltinIcon ? .single(nil) : venueIconData(postbox: postbox, resource: VenueIconResource(type: type))
|
||||
return data |> map { data in
|
||||
return { arguments in
|
||||
let context = DrawingContext(size: arguments.drawingSize, clear: true)
|
||||
@ -172,8 +178,21 @@ public func venueIcon(postbox: Postbox, type: String, background: Bool) -> Signa
|
||||
let boundsSize = CGSize(width: arguments.drawingRect.size.width - 4.0 * 2.0, height: arguments.drawingRect.size.height - 4.0 * 2.0)
|
||||
let fittedSize = image.size.aspectFitted(boundsSize)
|
||||
c.draw(cgImage, in: CGRect(origin: CGPoint(x: floor((arguments.drawingRect.width - fittedSize.width) / 2.0), y: floor((arguments.drawingRect.height - fittedSize.height) / 2.0)), size: fittedSize))
|
||||
} else if type.isEmpty, let pinImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/LocationPinForeground"), color: foregroundColor), let cgImage = pinImage.cgImage {
|
||||
c.draw(cgImage, in: CGRect(origin: CGPoint(x: floor((arguments.drawingRect.width - pinImage.size.width) / 2.0), y: floor((arguments.drawingRect.height - pinImage.size.height) / 2.0)), size: pinImage.size))
|
||||
} else if isBuiltinIcon {
|
||||
let image: UIImage?
|
||||
switch type {
|
||||
case "":
|
||||
image = UIImage(bundleImageName: "Chat/Message/LocationPinForeground")
|
||||
case "home":
|
||||
image = UIImage(bundleImageName: "Location/HomeIcon")
|
||||
case "work":
|
||||
image = UIImage(bundleImageName: "Location/WorkIcon")
|
||||
default:
|
||||
image = nil
|
||||
}
|
||||
if let image = image, let pinImage = generateTintedImage(image: image, color: foregroundColor), let cgImage = pinImage.cgImage {
|
||||
c.draw(cgImage, in: CGRect(origin: CGPoint(x: floor((arguments.drawingRect.width - pinImage.size.width) / 2.0), y: floor((arguments.drawingRect.height - pinImage.size.height) / 2.0)), size: pinImage.size))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -29,6 +29,8 @@ static_library(
|
||||
"//submodules/MergeLists:MergeLists",
|
||||
"//submodules/TelegramUIPreferences:TelegramUIPreferences",
|
||||
"//submodules/SearchBarNode:SearchBarNode",
|
||||
"//submodules/PresentationDataUtils:PresentationDataUtils",
|
||||
"//submodules/DeviceAccess:DeviceAccess",
|
||||
],
|
||||
frameworks = [
|
||||
"$SDKROOT/System/Library/Frameworks/Foundation.framework",
|
||||
|
@ -80,6 +80,7 @@ class LocationPinAnnotationView: MKAnnotationView {
|
||||
let smallIconNode: TransformImageNode
|
||||
let dotNode: ASImageNode
|
||||
var avatarNode: AvatarNode?
|
||||
var labelNode: ImmediateTextNode?
|
||||
|
||||
var appeared = false
|
||||
var animating = false
|
||||
@ -226,6 +227,29 @@ class LocationPinAnnotationView: MKAnnotationView {
|
||||
|
||||
self.dotNode.alpha = 1.0
|
||||
self.dotNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
|
||||
|
||||
if let annotation = self.annotation as? LocationPinAnnotation, let venue = annotation.location?.venue {
|
||||
let labelNode = ImmediateTextNode()
|
||||
labelNode.displaysAsynchronously = false
|
||||
labelNode.isUserInteractionEnabled = false
|
||||
labelNode.attributedText = NSAttributedString(string: venue.title, font: Font.medium(10), textColor: .black)
|
||||
labelNode.maximumNumberOfLines = 2
|
||||
labelNode.textAlignment = .center
|
||||
labelNode.truncationType = .end
|
||||
labelNode.textStroke = (UIColor.white, 1.0)
|
||||
self.labelNode = labelNode
|
||||
self.addSubnode(labelNode)
|
||||
|
||||
let size = labelNode.updateLayout(CGSize(width: 120.0, height: CGFloat.greatestFiniteMagnitude))
|
||||
labelNode.bounds = CGRect(origin: CGPoint(), size: size)
|
||||
labelNode.position = CGPoint(x: 0.0, y: 10.0)
|
||||
|
||||
labelNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
} else {
|
||||
self.labelNode?.removeFromSupernode()
|
||||
self.labelNode = nil
|
||||
}
|
||||
} else {
|
||||
let avatarSnapshot = self.avatarNode?.view.snapshotContentTree()
|
||||
if let avatarSnapshot = avatarSnapshot, let avatarNode = self.avatarNode {
|
||||
@ -268,6 +292,13 @@ class LocationPinAnnotationView: MKAnnotationView {
|
||||
let previousAlpha = self.dotNode.alpha
|
||||
self.dotNode.alpha = 0.0
|
||||
self.dotNode.layer.animateAlpha(from: previousAlpha, to: 0.0, duration: 0.2)
|
||||
|
||||
if let labelNode = self.labelNode {
|
||||
self.labelNode = nil
|
||||
labelNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in
|
||||
labelNode.removeFromSupernode()
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.smallNode.isHidden = selected
|
||||
@ -279,50 +310,13 @@ class LocationPinAnnotationView: MKAnnotationView {
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
avatarNode.view.superview?.bringSubviewToFront(avatarNode.view)
|
||||
}
|
||||
|
||||
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.isLayerBacked = false
|
||||
avatarNode.bounds = CGRect(origin: CGPoint(), size: CGSize(width: 55.0, height: 55.0))
|
||||
avatarNode.position = CGPoint()
|
||||
self.avatarNode = avatarNode
|
||||
@ -332,19 +326,23 @@ class LocationPinAnnotationView: MKAnnotationView {
|
||||
avatarNode.setPeer(account: account, theme: theme, peer: peer)
|
||||
}
|
||||
|
||||
private var raised = false
|
||||
var isRaised = false
|
||||
func setRaised(_ raised: Bool, animated: Bool, completion: @escaping () -> Void = {}) {
|
||||
guard raised != self.raised else {
|
||||
guard raised != self.isRaised else {
|
||||
return
|
||||
}
|
||||
|
||||
self.raised = raised
|
||||
self.isRaised = raised
|
||||
self.shadowNode.layer.removeAllAnimations()
|
||||
|
||||
if animated {
|
||||
self.animating = true
|
||||
|
||||
if raised {
|
||||
let previousPosition = self.shadowNode.position
|
||||
self.shadowNode.layer.animatePosition(from: previousPosition, to: CGPoint(x: UIScreenPixel, y: -66.0), duration: 0.2, delay: 0.0, timingFunction: kCAMediaTimingFunctionSpring) { finished in
|
||||
self.shadowNode.position = CGPoint(x: UIScreenPixel, y: -66.0)
|
||||
self.shadowNode.layer.animatePosition(from: previousPosition, to: self.shadowNode.position, duration: 0.2, delay: 0.0, timingFunction: kCAMediaTimingFunctionSpring) { finished in
|
||||
self.animating = false
|
||||
if finished {
|
||||
completion()
|
||||
}
|
||||
@ -353,6 +351,7 @@ class LocationPinAnnotationView: MKAnnotationView {
|
||||
UIView.animate(withDuration: 0.2, delay: 0.0, usingSpringWithDamping: 0.6, initialSpringVelocity: 0.0, options: [.allowAnimatedContent], animations: {
|
||||
self.shadowNode.position = CGPoint(x: UIScreenPixel, y: -36.0)
|
||||
}) { finished in
|
||||
self.animating = false
|
||||
if finished {
|
||||
completion()
|
||||
}
|
||||
@ -396,7 +395,6 @@ class LocationPinAnnotationView: MKAnnotationView {
|
||||
self.addSubnode(avatarNode)
|
||||
}
|
||||
self.animating = false
|
||||
self.setNeedsLayout()
|
||||
}
|
||||
}
|
||||
|
||||
@ -405,4 +403,42 @@ class LocationPinAnnotationView: MKAnnotationView {
|
||||
|
||||
self.dotNode.isHidden = !custom
|
||||
}
|
||||
|
||||
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: self.isRaised ? -66.0 : -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)
|
||||
avatarNode.view.superview?.bringSubviewToFront(avatarNode.view)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -100,19 +100,21 @@ final class LocationMapHeaderNode: ASDisplayNode {
|
||||
self.shadowNode.image = generateShadowImage(theme: presentationData.theme, highlighted: false)
|
||||
}
|
||||
|
||||
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))
|
||||
func updateLayout(layout: ContainerViewLayout, navigationBarHeight: CGFloat, topPadding: CGFloat, offset: CGFloat, size: CGSize, transition: ContainedViewLayoutTransition) {
|
||||
let mapHeight: CGFloat = floor(layout.size.height * 1.5)
|
||||
let mapFrame = CGRect(x: 0.0, y: floorToScreenPixels((size.height - mapHeight + navigationBarHeight) / 2.0) + offset, width: size.width, height: mapHeight)
|
||||
transition.updateFrame(node: self.mapNode, frame: mapFrame)
|
||||
self.mapNode.updateLayout(size: mapFrame.size)
|
||||
|
||||
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.optionsBackgroundNode, frame: CGRect(x: size.width - inset - panelSize.width - panelInset * 2.0, y: navigationBarHeight + topPadding + 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 > 124.0 + navigationBarHeight ? 1.0 : 0.0
|
||||
let optionsAlpha: CGFloat = size.height > 160.0 + navigationBarHeight ? 1.0 : 0.0
|
||||
alphaTransition.updateAlpha(node: self.optionsBackgroundNode, alpha: optionsAlpha)
|
||||
}
|
||||
|
||||
|
@ -39,10 +39,13 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate {
|
||||
private let pickerAnnotationContainerView: PickerAnnotationContainerView
|
||||
private weak var userLocationAnnotationView: MKAnnotationView?
|
||||
|
||||
private let pinDisposable = MetaDisposable()
|
||||
|
||||
private var mapView: MKMapView? {
|
||||
return self.view as? MKMapView
|
||||
}
|
||||
|
||||
var returnedToUserLocation = true
|
||||
var ignoreRegionChanges = false
|
||||
var isDragging = false
|
||||
var beganInteractiveDragging: (() -> Void)?
|
||||
@ -101,6 +104,11 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate {
|
||||
self.mapView?.setVisibleMapRect(mapRect, edgePadding: UIEdgeInsets(top: offset.y, left: offset.x, bottom: 0.0, right: 0.0), animated: animated)
|
||||
}
|
||||
self.ignoreRegionChanges = false
|
||||
|
||||
if isUserLocation && !self.returnedToUserLocation {
|
||||
self.returnedToUserLocation = true
|
||||
self.pickerAnnotationView?.setRaised(true, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
func mapView(_ mapView: MKMapView, regionWillChangeAnimated animated: Bool) {
|
||||
@ -111,13 +119,18 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate {
|
||||
for gestureRecognizer in gestureRecognizers {
|
||||
if gestureRecognizer.state == .began || gestureRecognizer.state == .ended {
|
||||
self.isDragging = true
|
||||
self.returnedToUserLocation = false
|
||||
self.beganInteractiveDragging?()
|
||||
|
||||
if self.hasPickerAnnotation {
|
||||
self.customUserLocationAnnotationView?.isHidden = true
|
||||
self.pickerAnnotationContainerView.isHidden = false
|
||||
self.pickerAnnotationView?.setCustom(true, animated: true)
|
||||
if let pickerAnnotationView = self.pickerAnnotationView, !pickerAnnotationView.isRaised {
|
||||
pickerAnnotationView.setCustom(true, animated: true)
|
||||
pickerAnnotationView.setRaised(true, animated: true)
|
||||
}
|
||||
self.resetAnnotationSelection()
|
||||
self.resetScheduledPin()
|
||||
}
|
||||
break
|
||||
}
|
||||
@ -125,9 +138,18 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate {
|
||||
}
|
||||
|
||||
func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
|
||||
if self.isDragging, let coordinate = self.mapCenterCoordinate {
|
||||
let wasDragging = self.isDragging
|
||||
if self.isDragging {
|
||||
self.isDragging = false
|
||||
self.endedInteractiveDragging?(coordinate)
|
||||
if let coordinate = self.mapCenterCoordinate {
|
||||
self.endedInteractiveDragging?(coordinate)
|
||||
}
|
||||
}
|
||||
|
||||
if let pickerAnnotationView = self.pickerAnnotationView {
|
||||
if pickerAnnotationView.isRaised && (wasDragging || self.returnedToUserLocation) {
|
||||
self.schedulePin(wasDragging: wasDragging)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -293,12 +315,41 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
override func layout() {
|
||||
super.layout()
|
||||
|
||||
self.pickerAnnotationContainerView.frame = CGRect(x: 0.0, y: floorToScreenPixels((self.frame.size.height - self.frame.size.width) / 2.0), width: self.frame.size.width, height: self.frame.size.width)
|
||||
private func schedulePin(wasDragging: Bool) {
|
||||
let timeout: Double = wasDragging ? 0.38 : 0.05
|
||||
|
||||
let signal: Signal<Never, NoError> = .complete()
|
||||
|> delay(timeout, queue: Queue.mainQueue())
|
||||
self.pinDisposable.set(signal.start(completed: { [weak self] in
|
||||
guard let strongSelf = self, let pickerAnnotationView = strongSelf.pickerAnnotationView else {
|
||||
return
|
||||
}
|
||||
|
||||
pickerAnnotationView.setRaised(false, animated: true) { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
if strongSelf.returnedToUserLocation {
|
||||
strongSelf.pickerAnnotationContainerView.isHidden = true
|
||||
strongSelf.customUserLocationAnnotationView?.isHidden = false
|
||||
}
|
||||
}
|
||||
|
||||
if strongSelf.returnedToUserLocation {
|
||||
pickerAnnotationView.setCustom(false, animated: true)
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
private func resetScheduledPin() {
|
||||
self.pinDisposable.set(nil)
|
||||
}
|
||||
|
||||
func updateLayout(size: CGSize) {
|
||||
self.pickerAnnotationContainerView.frame = CGRect(x: 0.0, y: floorToScreenPixels((size.height - size.width) / 2.0), width: size.width, height: size.width)
|
||||
if let pickerAnnotationView = self.pickerAnnotationView {
|
||||
pickerAnnotationView.center = CGPoint(x: self.pickerAnnotationContainerView.frame.width / 2.0, y: self.pickerAnnotationContainerView.frame.height / 2.0 + 16.0)
|
||||
pickerAnnotationView.center = CGPoint(x: self.pickerAnnotationContainerView.frame.width / 2.0, y: self.pickerAnnotationContainerView.frame.height / 2.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,8 @@ import TelegramPresentationData
|
||||
import AccountContext
|
||||
import AppBundle
|
||||
import CoreLocation
|
||||
import PresentationDataUtils
|
||||
import DeviceAccess
|
||||
|
||||
public enum LocationPickerMode {
|
||||
case share(peer: Peer?, selfPeer: Peer?, hasLiveLocation: Bool)
|
||||
@ -23,24 +25,28 @@ class LocationPickerInteraction {
|
||||
let toggleMapModeSelection: () -> Void
|
||||
let updateMapMode: (LocationMapMode) -> Void
|
||||
let goToUserLocation: () -> Void
|
||||
let goToCoordinate: (CLLocationCoordinate2D) -> Void
|
||||
let openSearch: () -> Void
|
||||
let updateSearchQuery: (String) -> Void
|
||||
let dismissSearch: () -> Void
|
||||
let dismissInput: () -> Void
|
||||
let updateSendActionHighlight: (Bool) -> Void
|
||||
let openHomeWorkInfo: () -> 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, updateSendActionHighlight: @escaping (Bool) -> Void) {
|
||||
init(sendLocation: @escaping (CLLocationCoordinate2D) -> Void, sendLiveLocation: @escaping (CLLocationCoordinate2D) -> Void, sendVenue: @escaping (TelegramMediaMap) -> Void, toggleMapModeSelection: @escaping () -> Void, updateMapMode: @escaping (LocationMapMode) -> Void, goToUserLocation: @escaping () -> Void, goToCoordinate: @escaping (CLLocationCoordinate2D) -> Void, openSearch: @escaping () -> Void, updateSearchQuery: @escaping (String) -> Void, dismissSearch: @escaping () -> Void, dismissInput: @escaping () -> Void, updateSendActionHighlight: @escaping (Bool) -> Void, openHomeWorkInfo: @escaping () -> Void) {
|
||||
self.sendLocation = sendLocation
|
||||
self.sendLiveLocation = sendLiveLocation
|
||||
self.sendVenue = sendVenue
|
||||
self.toggleMapModeSelection = toggleMapModeSelection
|
||||
self.updateMapMode = updateMapMode
|
||||
self.goToUserLocation = goToUserLocation
|
||||
self.goToCoordinate = goToCoordinate
|
||||
self.openSearch = openSearch
|
||||
self.updateSearchQuery = updateSearchQuery
|
||||
self.dismissSearch = dismissSearch
|
||||
self.dismissInput = dismissInput
|
||||
self.updateSendActionHighlight = updateSendActionHighlight
|
||||
self.openHomeWorkInfo = openHomeWorkInfo
|
||||
}
|
||||
}
|
||||
|
||||
@ -56,6 +62,10 @@ public final class LocationPickerController: ViewController {
|
||||
private var presentationDataDisposable: Disposable?
|
||||
|
||||
private var searchNavigationContentNode: LocationSearchNavigationContentNode?
|
||||
private var isSearchingDisposable = MetaDisposable()
|
||||
|
||||
private let locationManager = CLLocationManager()
|
||||
private var permissionDisposable: Disposable?
|
||||
|
||||
private var interaction: LocationPickerInteraction?
|
||||
|
||||
@ -88,6 +98,29 @@ public final class LocationPickerController: ViewController {
|
||||
strongSelf.controllerNode.updatePresentationData(presentationData)
|
||||
})
|
||||
|
||||
self.permissionDisposable = (DeviceAccess.authorizationStatus(subject: .location(.send))
|
||||
|> deliverOnMainQueue).start(next: { [weak self] next in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
switch next {
|
||||
case .notDetermined:
|
||||
DeviceAccess.authorizeAccess(to: .location(.send), locationManager: strongSelf.locationManager, presentationData: strongSelf.presentationData, present: { c, a in
|
||||
strongSelf.present(c, in: .window(.root), with: a)
|
||||
}, openSettings: {
|
||||
strongSelf.context.sharedContext.applicationBindings.openSettings()
|
||||
})
|
||||
case .denied:
|
||||
strongSelf.controllerNode.updateState { state in
|
||||
var state = state
|
||||
state.forceSelection = true
|
||||
return state
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
let locationWithTimeout: (CLLocationCoordinate2D, Int32?) -> TelegramMediaMap = { coordinate, timeout in
|
||||
return TelegramMediaMap(latitude: coordinate.latitude, longitude: coordinate.longitude, geoPlace: nil, venue: nil, liveBroadcastingTimeout: timeout)
|
||||
}
|
||||
@ -143,7 +176,12 @@ public final class LocationPickerController: ViewController {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
completion(venue, nil)
|
||||
let venueType = venue.venue?.type ?? ""
|
||||
if ["home", "work"].contains(venueType) {
|
||||
completion(TelegramMediaMap(latitude: venue.latitude, longitude: venue.longitude, geoPlace: nil, venue: nil, liveBroadcastingTimeout: nil), nil)
|
||||
} else {
|
||||
completion(venue, nil)
|
||||
}
|
||||
strongSelf.dismiss()
|
||||
}, toggleMapModeSelection: { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
@ -174,6 +212,16 @@ public final class LocationPickerController: ViewController {
|
||||
state.selectedLocation = .none
|
||||
return state
|
||||
}
|
||||
}, goToCoordinate: { [weak self] coordinate in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.controllerNode.updateState { state in
|
||||
var state = state
|
||||
state.displayingMapModeOptions = false
|
||||
state.selectedLocation = .location(coordinate, nil)
|
||||
return state
|
||||
}
|
||||
}, openSearch: { [weak self] in
|
||||
guard let strongSelf = self, let interaction = strongSelf.interaction, let navigationBar = strongSelf.navigationBar else {
|
||||
return
|
||||
@ -186,8 +234,15 @@ public final class LocationPickerController: ViewController {
|
||||
let contentNode = LocationSearchNavigationContentNode(presentationData: strongSelf.presentationData, interaction: interaction)
|
||||
strongSelf.searchNavigationContentNode = contentNode
|
||||
navigationBar.setContentNode(contentNode, animated: true)
|
||||
strongSelf.controllerNode.activateSearch(navigationBar: navigationBar)
|
||||
let isSearching = strongSelf.controllerNode.activateSearch(navigationBar: navigationBar)
|
||||
contentNode.activate()
|
||||
|
||||
strongSelf.isSearchingDisposable.set((isSearching
|
||||
|> deliverOnMainQueue).start(next: { [weak self] value in
|
||||
if let strongSelf = self, let searchNavigationContentNode = strongSelf.searchNavigationContentNode {
|
||||
searchNavigationContentNode.updateActivity(value)
|
||||
}
|
||||
}))
|
||||
}, updateSearchQuery: { [weak self] query in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
@ -197,6 +252,7 @@ public final class LocationPickerController: ViewController {
|
||||
guard let strongSelf = self, let navigationBar = strongSelf.navigationBar else {
|
||||
return
|
||||
}
|
||||
strongSelf.isSearchingDisposable.set(nil)
|
||||
strongSelf.searchNavigationContentNode?.deactivate()
|
||||
strongSelf.searchNavigationContentNode = nil
|
||||
navigationBar.setContentNode(nil, animated: true)
|
||||
@ -211,6 +267,13 @@ public final class LocationPickerController: ViewController {
|
||||
return
|
||||
}
|
||||
strongSelf.controllerNode.updateSendActionHighlight(highlighted)
|
||||
}, openHomeWorkInfo: { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
let controller = textAlertController(context: strongSelf.context, title: strongSelf.presentationData.strings.Map_HomeAndWorkTitle, text: strongSelf.presentationData.strings.Map_HomeAndWorkInfo, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})])
|
||||
strongSelf.present(controller, in: .window(.root))
|
||||
})
|
||||
|
||||
self.scrollToTop = { [weak self] in
|
||||
@ -228,6 +291,8 @@ public final class LocationPickerController: ViewController {
|
||||
|
||||
deinit {
|
||||
self.presentationDataDisposable?.dispose()
|
||||
self.permissionDisposable?.dispose()
|
||||
self.isSearchingDisposable.dispose()
|
||||
}
|
||||
|
||||
override public func loadDisplayNode() {
|
||||
|
@ -151,9 +151,12 @@ private enum LocationPickerEntry: Comparable, Identifiable {
|
||||
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: {
|
||||
let venueType = venue.venue?.type ?? ""
|
||||
return ItemListVenueItem(presentationData: ItemListPresentationData(presentationData), account: account, venue: venue, style: .plain, action: {
|
||||
interaction?.sendVenue(venue)
|
||||
})
|
||||
}, infoAction: ["home", "work"].contains(venueType) ? {
|
||||
interaction?.openHomeWorkInfo()
|
||||
} : nil)
|
||||
case let .attribution(theme):
|
||||
return LocationAttributionItem(presentationData: ItemListPresentationData(presentationData))
|
||||
}
|
||||
@ -220,11 +223,13 @@ struct LocationPickerState {
|
||||
var mapMode: LocationMapMode
|
||||
var displayingMapModeOptions: Bool
|
||||
var selectedLocation: LocationPickerLocation
|
||||
var forceSelection: Bool
|
||||
|
||||
init() {
|
||||
self.mapMode = .map
|
||||
self.displayingMapModeOptions = false
|
||||
self.selectedLocation = .none
|
||||
self.forceSelection = false
|
||||
}
|
||||
}
|
||||
|
||||
@ -415,10 +420,23 @@ final class LocationPickerControllerNode: ViewControllerTracingNode {
|
||||
strongSelf.headerNode.mapNode.resetAnnotationSelection()
|
||||
case .selecting:
|
||||
strongSelf.headerNode.mapNode.resetAnnotationSelection()
|
||||
case let .location(coordinate, _):
|
||||
var updateMap = false
|
||||
switch previousState.selectedLocation {
|
||||
case .none, .venue:
|
||||
updateMap = true
|
||||
case let .location(previousCoordinate, address):
|
||||
if previousCoordinate != coordinate {
|
||||
updateMap = true
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
if updateMap {
|
||||
strongSelf.headerNode.mapNode.setMapCenter(coordinate: coordinate, isUserLocation: false, animated: true)
|
||||
}
|
||||
case let .venue(venue):
|
||||
strongSelf.headerNode.mapNode.setMapCenter(coordinate: venue.coordinate, animated: true)
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
let annotations: [LocationPinAnnotation]
|
||||
@ -482,7 +500,7 @@ final class LocationPickerControllerNode: ViewControllerTracingNode {
|
||||
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.headerNode.updateLayout(layout: layout, navigationBarHeight: navigationBarHeight, topPadding: strongSelf.state.displayingMapModeOptions ? 38.0 : 0.0, offset: 0.0, size: headerFrame.size, transition: listTransition)
|
||||
strongSelf.layoutActivityIndicator(transition: listTransition)
|
||||
}
|
||||
|
||||
@ -590,9 +608,9 @@ final class LocationPickerControllerNode: ViewControllerTracingNode {
|
||||
})
|
||||
}
|
||||
|
||||
func activateSearch(navigationBar: NavigationBar) {
|
||||
func activateSearch(navigationBar: NavigationBar) -> Signal<Bool, NoError> {
|
||||
guard let (layout, navigationBarHeight) = self.validLayout, self.searchContainerNode == nil, let coordinate = self.headerNode.mapNode.mapCenterCoordinate else {
|
||||
return
|
||||
return .complete()
|
||||
}
|
||||
|
||||
let searchContainerNode = LocationSearchContainerNode(context: self.context, coordinate: coordinate, interaction: self.interaction)
|
||||
@ -602,6 +620,8 @@ final class LocationPickerControllerNode: ViewControllerTracingNode {
|
||||
searchContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
|
||||
self.containerLayoutUpdated(layout, navigationHeight: navigationBarHeight, transition: .immediate)
|
||||
|
||||
return searchContainerNode.isSearching
|
||||
}
|
||||
|
||||
func deactivateSearch() {
|
||||
@ -643,10 +663,8 @@ final class LocationPickerControllerNode: ViewControllerTracingNode {
|
||||
let isFirstLayout = self.validLayout == nil
|
||||
self.validLayout = (layout, navigationHeight)
|
||||
|
||||
let isPickingLocation = self.state.selectedLocation.isCustom || self.state.forceSelection
|
||||
let optionsHeight: CGFloat = 38.0
|
||||
|
||||
let pickingCustomLocation = self.state.selectedLocation.isCustom
|
||||
|
||||
var actionHeight: CGFloat?
|
||||
self.listNode.forEachItemNode { itemNode in
|
||||
if let itemNode = itemNode as? LocationActionListItemNode {
|
||||
@ -659,7 +677,7 @@ final class LocationPickerControllerNode: ViewControllerTracingNode {
|
||||
let topInset: CGFloat = floor((layout.size.height - navigationHeight) / 2.0 + navigationHeight)
|
||||
let overlap: CGFloat = 6.0
|
||||
let headerHeight: CGFloat
|
||||
if pickingCustomLocation, let actionHeight = actionHeight {
|
||||
if isPickingLocation, let actionHeight = actionHeight {
|
||||
self.listOffset = topInset
|
||||
headerHeight = layout.size.height - actionHeight - layout.intrinsicInsets.bottom + overlap - 2.0
|
||||
} else if let listOffset = self.listOffset {
|
||||
@ -669,26 +687,29 @@ final class LocationPickerControllerNode: ViewControllerTracingNode {
|
||||
}
|
||||
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)
|
||||
|
||||
self.headerNode.updateLayout(layout: layout, navigationBarHeight: navigationHeight, topPadding: self.state.displayingMapModeOptions ? optionsHeight : 0.0, offset: 0.0, size: headerFrame.size, transition: transition)
|
||||
|
||||
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
|
||||
let scrollToItem: ListViewScrollToItem?
|
||||
if pickingCustomLocation {
|
||||
if isPickingLocation {
|
||||
scrollToItem = ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: curve, directionHint: .Up)
|
||||
} else {
|
||||
scrollToItem = nil
|
||||
}
|
||||
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: scrollToItem, 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 = !pickingCustomLocation
|
||||
|
||||
let insets = UIEdgeInsets(top: topInset, left: layout.safeInsets.left, bottom: layout.intrinsicInsets.bottom, right: layout.safeInsets.right)
|
||||
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: scrollToItem, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, headerInsets: UIEdgeInsets(top: navigationHeight, left: 0.0, bottom: layout.intrinsicInsets.bottom, right: 0.0), scrollIndicatorInsets: UIEdgeInsets(top: topInset + 3.0, left: 0.0, bottom: layout.intrinsicInsets.bottom, right: 0.0), duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
|
||||
self.listNode.scrollEnabled = !isPickingLocation
|
||||
|
||||
var listFrame: CGRect = CGRect(origin: CGPoint(), size: layout.size)
|
||||
if pickingCustomLocation {
|
||||
if isPickingLocation {
|
||||
listFrame.origin.y = headerHeight - topInset - overlap
|
||||
}
|
||||
transition.updateFrame(node: self.listNode, frame: listFrame)
|
||||
transition.updateAlpha(node: self.shadeNode, alpha: pickingCustomLocation ? 1.0 : 0.0)
|
||||
transition.updateAlpha(node: self.shadeNode, alpha: isPickingLocation ? 1.0 : 0.0)
|
||||
transition.updateFrame(node: self.shadeNode, frame: CGRect(x: 0.0, y: listFrame.minY + topInset + (actionHeight ?? 0.0) - 3.0, width: layout.size.width, height: 10000.0))
|
||||
self.shadeNode.isUserInteractionEnabled = pickingCustomLocation
|
||||
self.shadeNode.isUserInteractionEnabled = isPickingLocation
|
||||
self.innerShadeNode.frame = CGRect(x: 0.0, y: 4.0, width: layout.size.width, height: 10000.0)
|
||||
self.innerShadeNode.alpha = layout.intrinsicInsets.bottom > 0.0 ? 1.0 : 0.0
|
||||
|
||||
|
@ -13,14 +13,18 @@ import AccountContext
|
||||
import ItemListVenueItem
|
||||
import ItemListUI
|
||||
import MapKit
|
||||
import Geocoding
|
||||
import ChatListSearchItemHeader
|
||||
|
||||
private struct LocationSearchEntry: Identifiable, Comparable {
|
||||
let index: Int
|
||||
let theme: PresentationTheme
|
||||
let venue: TelegramMediaMap
|
||||
|
||||
let location: TelegramMediaMap
|
||||
let title: String?
|
||||
let distance: Double
|
||||
|
||||
var stableId: String {
|
||||
return self.venue.venue?.id ?? ""
|
||||
return self.location.venue?.id ?? ""
|
||||
}
|
||||
|
||||
static func ==(lhs: LocationSearchEntry, rhs: LocationSearchEntry) -> Bool {
|
||||
@ -30,7 +34,13 @@ private struct LocationSearchEntry: Identifiable, Comparable {
|
||||
if lhs.theme !== rhs.theme {
|
||||
return false
|
||||
}
|
||||
if lhs.venue.venue?.id != rhs.venue.venue?.id {
|
||||
if lhs.location.venue?.id != rhs.location.venue?.id {
|
||||
return false
|
||||
}
|
||||
if lhs.title != rhs.title {
|
||||
return false
|
||||
}
|
||||
if lhs.distance != rhs.distance {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
@ -41,10 +51,19 @@ private struct LocationSearchEntry: Identifiable, Comparable {
|
||||
}
|
||||
|
||||
func item(account: Account, presentationData: PresentationData, sendVenue: @escaping (TelegramMediaMap) -> Void) -> ListViewItem {
|
||||
let venue = self.venue
|
||||
return ItemListVenueItem(presentationData: ItemListPresentationData(presentationData), account: account, venue: self.venue, sectionId: 0, style: .plain, action: {
|
||||
let venue = self.location
|
||||
let header: ChatListSearchItemHeader
|
||||
let subtitle: String?
|
||||
if let _ = venue.venue {
|
||||
header = ChatListSearchItemHeader(type: .nearbyVenues, theme: presentationData.theme, strings: presentationData.strings)
|
||||
subtitle = nil
|
||||
} else {
|
||||
header = ChatListSearchItemHeader(type: .mapAddress, theme: presentationData.theme, strings: presentationData.strings)
|
||||
subtitle = presentationData.strings.Map_DistanceAway(stringForDistance(strings: presentationData.strings, distance: self.distance)).0
|
||||
}
|
||||
return ItemListVenueItem(presentationData: ItemListPresentationData(presentationData), account: account, venue: self.location, title: self.title, subtitle: subtitle, style: .plain, action: {
|
||||
sendVenue(venue)
|
||||
})
|
||||
}, header: header)
|
||||
}
|
||||
}
|
||||
|
||||
@ -81,10 +100,17 @@ final class LocationSearchContainerNode: ASDisplayNode {
|
||||
private var containerViewLayout: (ContainerViewLayout, CGFloat)?
|
||||
private var enqueuedTransitions: [LocationSearchContainerTransition] = []
|
||||
|
||||
private let _isSearching = ValuePromise<Bool>(false, ignoreRepeated: true)
|
||||
var isSearching: Signal<Bool, NoError> {
|
||||
return self._isSearching.get()
|
||||
}
|
||||
|
||||
public init(context: AccountContext, coordinate: CLLocationCoordinate2D, interaction: LocationPickerInteraction) {
|
||||
self.context = context
|
||||
self.interaction = interaction
|
||||
|
||||
let currentLocation = CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude)
|
||||
|
||||
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
self.themeAndStringsPromise = Promise((self.presentationData.theme, self.presentationData.strings))
|
||||
@ -107,6 +133,7 @@ final class LocationSearchContainerNode: ASDisplayNode {
|
||||
|
||||
let themeAndStringsPromise = self.themeAndStringsPromise
|
||||
|
||||
let isSearching = self._isSearching
|
||||
let searchItems = self.searchQuery.get()
|
||||
|> mapToSignal { query -> Signal<String?, NoError> in
|
||||
if let query = query, !query.isEmpty {
|
||||
@ -119,19 +146,43 @@ final class LocationSearchContainerNode: ASDisplayNode {
|
||||
|> 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())
|
||||
|> afterCompleted {
|
||||
isSearching.set(false)
|
||||
}
|
||||
let foundPlacemarks = geocodeLocation(address: query)
|
||||
return combineLatest(foundVenues, foundPlacemarks, themeAndStringsPromise.get())
|
||||
|> delay(0.1, queue: Queue.concurrentDefaultQueue())
|
||||
|> map { venues, themeAndStrings -> [LocationSearchEntry] in
|
||||
|> beforeStarted {
|
||||
isSearching.set(true)
|
||||
}
|
||||
|> map { venues, placemarks, themeAndStrings -> [LocationSearchEntry] in
|
||||
var entries: [LocationSearchEntry] = []
|
||||
var index: Int = 0
|
||||
|
||||
if let placemarks = placemarks {
|
||||
for placemark in placemarks {
|
||||
guard let placemarkLocation = placemark.location else {
|
||||
continue
|
||||
}
|
||||
let location = TelegramMediaMap(latitude: placemarkLocation.coordinate.latitude, longitude: placemarkLocation.coordinate.longitude, geoPlace: nil, venue: nil, liveBroadcastingTimeout: nil)
|
||||
|
||||
entries.append(LocationSearchEntry(index: index, theme: themeAndStrings.0, location: location, title: placemark.name ?? "Name", distance: placemarkLocation.distance(from: currentLocation)))
|
||||
|
||||
index += 1
|
||||
}
|
||||
}
|
||||
|
||||
for venue in venues {
|
||||
entries.append(LocationSearchEntry(index: index, theme: themeAndStrings.0, venue: venue))
|
||||
entries.append(LocationSearchEntry(index: index, theme: themeAndStrings.0, location: venue, title: nil, distance: 0.0))
|
||||
index += 1
|
||||
}
|
||||
return entries
|
||||
}
|
||||
} else {
|
||||
return .single(nil)
|
||||
|> afterCompleted {
|
||||
isSearching.set(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -141,7 +192,12 @@ final class LocationSearchContainerNode: ASDisplayNode {
|
||||
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)
|
||||
if let _ = venue.venue {
|
||||
self?.interaction.sendVenue(venue)
|
||||
} else {
|
||||
self?.interaction.goToCoordinate(venue.coordinate)
|
||||
self?.interaction.dismissSearch()
|
||||
}
|
||||
})
|
||||
strongSelf.enqueueTransition(transition)
|
||||
}
|
||||
|
@ -54,6 +54,10 @@ final class LocationSearchNavigationContentNode: NavigationBarContentNode {
|
||||
self.searchBar.deactivate(clear: false)
|
||||
}
|
||||
|
||||
func updateActivity(_ activity: Bool) {
|
||||
self.searchBar.activity = activity
|
||||
}
|
||||
|
||||
func updatePresentationData(_ presentationData: PresentationData) {
|
||||
self.presentationData = presentationData
|
||||
self.searchBar.updateThemeAndStrings(theme: SearchBarNodeTheme(theme: presentationData.theme, hasSeparator: false), strings: presentationData.strings)
|
||||
|
@ -468,7 +468,9 @@ private func dataAndStorageControllerEntries(state: DataAndStorageControllerStat
|
||||
entries.append(.useLessVoiceData(presentationData.theme, presentationData.strings.CallSettings_UseLessData, stringForUseLessDataSetting(dataSaving, strings: presentationData.strings)))
|
||||
|
||||
entries.append(.otherHeader(presentationData.theme, presentationData.strings.ChatSettings_Other))
|
||||
entries.append(.shareSheet(presentationData.theme, "Share Sheet"))
|
||||
if #available(iOSApplicationExtension 13.2, iOS 13.2, *) {
|
||||
entries.append(.shareSheet(presentationData.theme, presentationData.strings.ChatSettings_IntentsSettings))
|
||||
}
|
||||
entries.append(.saveIncomingPhotos(presentationData.theme, presentationData.strings.Settings_SaveIncomingPhotos))
|
||||
entries.append(.saveEditedPhotos(presentationData.theme, presentationData.strings.Settings_SaveEditedPhotos, data.generatedMediaStoreSettings.storeEditedPhotos))
|
||||
entries.append(.openLinksIn(presentationData.theme, presentationData.strings.ChatSettings_OpenLinksIn, defaultWebBrowser))
|
||||
|
@ -150,7 +150,6 @@ private enum IntentsSettingsControllerEntry: ItemListNodeEntry {
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
|
||||
case let .suggestHeader(lhsTheme, lhsText):
|
||||
if case let .suggestHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
|
||||
return true
|
||||
|
File diff suppressed because it is too large
Load Diff
12
submodules/TelegramUI/Images.xcassets/Location/HomeIcon.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Location/HomeIcon.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "ic_maphome.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
BIN
submodules/TelegramUI/Images.xcassets/Location/HomeIcon.imageset/ic_maphome.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Location/HomeIcon.imageset/ic_maphome.pdf
vendored
Normal file
Binary file not shown.
12
submodules/TelegramUI/Images.xcassets/Location/WorkIcon.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Location/WorkIcon.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "ic_mapwork.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
BIN
submodules/TelegramUI/Images.xcassets/Location/WorkIcon.imageset/ic_mapwork.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Location/WorkIcon.imageset/ic_mapwork.pdf
vendored
Normal file
Binary file not shown.
Binary file not shown.
@ -82,7 +82,7 @@ public struct IntentsSettings: PreferencesEntry, Equatable {
|
||||
return IntentsSettings(initiallyReset: self.initiallyReset, account: self.account, contacts: self.contacts, privateChats: self.privateChats, savedMessages: self.savedMessages, groups: groups, onlyShared: self.onlyShared)
|
||||
}
|
||||
|
||||
public func withUpdatedOnlyShared(_ savedMessages: Bool) -> IntentsSettings {
|
||||
public func withUpdatedOnlyShared(_ onlyShared: Bool) -> IntentsSettings {
|
||||
return IntentsSettings(initiallyReset: self.initiallyReset, account: self.account, contacts: self.contacts, privateChats: self.privateChats, savedMessages: self.savedMessages, groups: self.groups, onlyShared: onlyShared)
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user