mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Live location improvements
This commit is contained in:
parent
f2ad10b937
commit
6299cd9a18
@ -5837,3 +5837,9 @@ Any member of this group will be able to see messages in the channel.";
|
||||
"Conversation.ContextMenuSelectAll_any" = "Select All %@ Items";
|
||||
|
||||
"Conversation.Dice.u1F3B0" = "Send a slot machine emoji to try your luck.";
|
||||
|
||||
"Notification.ProximityReached" = "%2$@ is now within %2$@ from you";
|
||||
|
||||
"Location.ProximityNotification.Title" = "Notification";
|
||||
"Location.ProximityNotification.Notify" = "Notify me within %@";
|
||||
"Location.ProximityNotification.AlreadyClose" = "You are already closer than %@";
|
||||
|
@ -35,6 +35,7 @@ static_library(
|
||||
"//submodules/PhoneNumberFormat:PhoneNumberFormat",
|
||||
"//submodules/PersistentStringHash:PersistentStringHash",
|
||||
"//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode",
|
||||
"//submodules/LiveLocationTimerNode:LiveLocationTimerNode",
|
||||
],
|
||||
frameworks = [
|
||||
"$SDKROOT/System/Library/Frameworks/Foundation.framework",
|
||||
|
@ -36,6 +36,7 @@ swift_library(
|
||||
"//submodules/PhoneNumberFormat:PhoneNumberFormat",
|
||||
"//submodules/PersistentStringHash:PersistentStringHash",
|
||||
"//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode",
|
||||
"//submodules/LiveLocationTimerNode:LiveLocationTimerNode",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -1,6 +1,7 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import LegacyComponents
|
||||
import TelegramCore
|
||||
import SyncCore
|
||||
@ -12,6 +13,7 @@ import LegacyUI
|
||||
import OpenInExternalAppUI
|
||||
import AppBundle
|
||||
import LocationResources
|
||||
import DeviceLocationManager
|
||||
|
||||
private func generateClearIcon(color: UIColor) -> UIImage? {
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Clear"), color: color)
|
||||
@ -234,7 +236,18 @@ public func legacyLocationController(message: Message?, mapMedia: TelegramMediaM
|
||||
legacyController?.dismiss()
|
||||
}
|
||||
controller.liveLocationStopped = { [weak legacyController] in
|
||||
stopLiveLocation()
|
||||
if let message = message, let locationManager = context.sharedContext.locationManager {
|
||||
let _ = (currentLocationManagerCoordinate(manager: locationManager, timeout: 5.0)
|
||||
|> deliverOnMainQueue).start(next: { coordinate in
|
||||
if let coordinate = coordinate {
|
||||
let _ = requestProximityNotification(postbox: context.account.postbox, network: context.account.network, messageId: message.id, distance: 500, coordinate: (coordinate.latitude, longitude: coordinate.longitude, accuracyRadius: nil)).start()
|
||||
} else {
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
// stopLiveLocation()
|
||||
legacyController?.dismiss()
|
||||
}
|
||||
|
||||
|
@ -10,6 +10,7 @@ import TelegramPresentationData
|
||||
import ItemListUI
|
||||
import LocationResources
|
||||
import AppBundle
|
||||
import LiveLocationTimerNode
|
||||
|
||||
public enum LocationActionListItemIcon: Equatable {
|
||||
case location
|
||||
@ -63,17 +64,17 @@ private func generateLocationIcon(theme: PresentationTheme) -> UIImage {
|
||||
})!
|
||||
}
|
||||
|
||||
private func generateLiveLocationIcon(theme: PresentationTheme) -> UIImage {
|
||||
private func generateLiveLocationIcon(theme: PresentationTheme, stop: Bool) -> UIImage {
|
||||
return generateImage(CGSize(width: 40.0, height: 40.0), rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
context.setFillColor(UIColor(rgb: 0x6cc139).cgColor)
|
||||
context.setFillColor(UIColor(rgb: stop ? 0xff6464 : 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/SendLiveLocationIcon"), color: .white) {
|
||||
if let image = generateTintedImage(image: UIImage(bundleImageName: "Location/SendLocationIcon"), color: .white) {
|
||||
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))
|
||||
}
|
||||
})!
|
||||
@ -85,15 +86,17 @@ final class LocationActionListItem: ListViewItem {
|
||||
let title: String
|
||||
let subtitle: String
|
||||
let icon: LocationActionListItemIcon
|
||||
let beginTimeAndTimeout: (Double, Double)?
|
||||
let action: () -> Void
|
||||
let highlighted: (Bool) -> Void
|
||||
|
||||
public init(presentationData: ItemListPresentationData, account: Account, title: String, subtitle: String, icon: LocationActionListItemIcon, action: @escaping () -> Void, highlighted: @escaping (Bool) -> Void = { _ in }) {
|
||||
public init(presentationData: ItemListPresentationData, account: Account, title: String, subtitle: String, icon: LocationActionListItemIcon, beginTimeAndTimeout: (Double, Double)?, action: @escaping () -> Void, highlighted: @escaping (Bool) -> Void = { _ in }) {
|
||||
self.presentationData = presentationData
|
||||
self.account = account
|
||||
self.title = title
|
||||
self.subtitle = subtitle
|
||||
self.icon = icon
|
||||
self.beginTimeAndTimeout = beginTimeAndTimeout
|
||||
self.action = action
|
||||
self.highlighted = highlighted
|
||||
}
|
||||
@ -144,6 +147,7 @@ final class LocationActionListItemNode: ListViewItemNode {
|
||||
private var subtitleNode: TextNode?
|
||||
private let iconNode: ASImageNode
|
||||
private let venueIconNode: TransformImageNode
|
||||
private var timerNode: ChatMessageLiveLocationTimerNode?
|
||||
|
||||
private var item: LocationActionListItem?
|
||||
private var layoutParams: ListViewItemLayoutParams?
|
||||
@ -268,7 +272,7 @@ final class LocationActionListItemNode: ListViewItemNode {
|
||||
case .liveLocation, .stopLiveLocation:
|
||||
strongSelf.iconNode.isHidden = false
|
||||
strongSelf.venueIconNode.isHidden = true
|
||||
strongSelf.iconNode.image = generateLiveLocationIcon(theme: item.presentationData.theme)
|
||||
strongSelf.iconNode.image = generateLiveLocationIcon(theme: item.presentationData.theme, stop: updatedIcon == .stopLiveLocation)
|
||||
case let .venue(venue):
|
||||
strongSelf.iconNode.isHidden = true
|
||||
strongSelf.venueIconNode.isHidden = false
|
||||
@ -308,6 +312,23 @@ final class LocationActionListItemNode: ListViewItemNode {
|
||||
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
|
||||
|
||||
if let (beginTimestamp, timeout) = item.beginTimeAndTimeout {
|
||||
let timerNode: ChatMessageLiveLocationTimerNode
|
||||
if let current = strongSelf.timerNode {
|
||||
timerNode = current
|
||||
} else {
|
||||
timerNode = ChatMessageLiveLocationTimerNode()
|
||||
strongSelf.addSubnode(timerNode)
|
||||
strongSelf.timerNode = timerNode
|
||||
}
|
||||
let timerSize = CGSize(width: 28.0, height: 28.0)
|
||||
timerNode.update(backgroundColor: item.presentationData.theme.list.itemAccentColor.withAlphaComponent(0.4), foregroundColor: item.presentationData.theme.list.itemAccentColor, textColor: item.presentationData.theme.list.itemAccentColor, beginTimestamp: beginTimestamp, timeout: timeout, strings: item.presentationData.strings)
|
||||
timerNode.frame = CGRect(origin: CGPoint(x: contentSize.width - 16.0 - timerSize.width, y: floorToScreenPixels((contentSize.height - timerSize.height) / 2.0) - 2.0), size: timerSize)
|
||||
} else if let timerNode = strongSelf.timerNode {
|
||||
strongSelf.timerNode = nil
|
||||
timerNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
@ -34,8 +34,11 @@ class LocationPinAnnotation: NSObject, MKAnnotation {
|
||||
var coordinate: CLLocationCoordinate2D
|
||||
let location: TelegramMediaMap?
|
||||
let peer: Peer?
|
||||
let message: Message?
|
||||
let forcedSelection: Bool
|
||||
var heading: Int32?
|
||||
|
||||
var selfPeer: Peer?
|
||||
var title: String? = ""
|
||||
var subtitle: String? = ""
|
||||
|
||||
@ -44,6 +47,7 @@ class LocationPinAnnotation: NSObject, MKAnnotation {
|
||||
self.theme = theme
|
||||
self.location = nil
|
||||
self.peer = peer
|
||||
self.message = nil
|
||||
self.coordinate = kCLLocationCoordinate2DInvalid
|
||||
self.forcedSelection = false
|
||||
super.init()
|
||||
@ -54,11 +58,29 @@ class LocationPinAnnotation: NSObject, MKAnnotation {
|
||||
self.theme = theme
|
||||
self.location = location
|
||||
self.peer = nil
|
||||
self.message = nil
|
||||
self.coordinate = location.coordinate
|
||||
self.forcedSelection = forcedSelection
|
||||
super.init()
|
||||
}
|
||||
|
||||
init(context: AccountContext, theme: PresentationTheme, message: Message, selfPeer: Peer?, heading: Int32?) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
self.location = nil
|
||||
self.peer = nil
|
||||
self.message = message
|
||||
if let location = getLocation(from: message) {
|
||||
self.coordinate = location.coordinate
|
||||
} else {
|
||||
self.coordinate = kCLLocationCoordinate2DInvalid
|
||||
}
|
||||
self.selfPeer = selfPeer
|
||||
self.forcedSelection = false
|
||||
self.heading = heading
|
||||
super.init()
|
||||
}
|
||||
|
||||
var id: String {
|
||||
if let peer = self.peer {
|
||||
return "\(peer.id.toInt64())"
|
||||
@ -89,6 +111,7 @@ class LocationPinAnnotationLayer: CALayer {
|
||||
class LocationPinAnnotationView: MKAnnotationView {
|
||||
let shadowNode: ASImageNode
|
||||
let backgroundNode: ASImageNode
|
||||
let arrowNode: ASImageNode
|
||||
let smallNode: ASImageNode
|
||||
let iconNode: TransformImageNode
|
||||
let smallIconNode: TransformImageNode
|
||||
@ -118,6 +141,11 @@ class LocationPinAnnotationView: MKAnnotationView {
|
||||
self.shadowNode.bounds = CGRect(origin: CGPoint(), size: image.size)
|
||||
}
|
||||
|
||||
self.arrowNode = ASImageNode()
|
||||
self.arrowNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 88.0, height: 88.0))
|
||||
self.arrowNode.image = generateHeadingArrowImage()
|
||||
self.arrowNode.isHidden = true
|
||||
|
||||
self.backgroundNode = ASImageNode()
|
||||
self.backgroundNode.image = UIImage(bundleImageName: "Location/PinBackground")
|
||||
if let image = self.backgroundNode.image {
|
||||
@ -147,6 +175,7 @@ class LocationPinAnnotationView: MKAnnotationView {
|
||||
self.addSubnode(self.dotNode)
|
||||
|
||||
self.addSubnode(self.shadowNode)
|
||||
self.addSubnode(self.arrowNode)
|
||||
self.shadowNode.addSubnode(self.backgroundNode)
|
||||
self.backgroundNode.addSubnode(self.iconNode)
|
||||
|
||||
@ -177,7 +206,24 @@ class LocationPinAnnotationView: MKAnnotationView {
|
||||
override var annotation: MKAnnotation? {
|
||||
didSet {
|
||||
if let annotation = self.annotation as? LocationPinAnnotation {
|
||||
if let peer = annotation.peer {
|
||||
if let message = annotation.message {
|
||||
self.iconNode.isHidden = true
|
||||
self.dotNode.isHidden = false
|
||||
self.backgroundNode.image = UIImage(bundleImageName: "Location/PinBackground")
|
||||
|
||||
if let author = message.author, let peer = message.peers[author.id] {
|
||||
self.setPeer(context: annotation.context, theme: annotation.theme, peer: peer)
|
||||
} else if let selfPeer = annotation.selfPeer {
|
||||
self.setPeer(context: annotation.context, theme: annotation.theme, peer: selfPeer)
|
||||
}
|
||||
|
||||
if !self.isSelected {
|
||||
self.dotNode.alpha = 0.0
|
||||
self.shadowNode.isHidden = true
|
||||
self.smallNode.isHidden = false
|
||||
}
|
||||
}
|
||||
else if let peer = annotation.peer {
|
||||
self.iconNode.isHidden = true
|
||||
self.dotNode.isHidden = true
|
||||
self.backgroundNode.image = UIImage(bundleImageName: "Location/PinBackground")
|
||||
@ -396,6 +442,7 @@ class LocationPinAnnotationView: MKAnnotationView {
|
||||
}
|
||||
}
|
||||
|
||||
var previousPeerId: PeerId?
|
||||
func setPeer(context: AccountContext, theme: PresentationTheme, peer: Peer) {
|
||||
let avatarNode: AvatarNode
|
||||
if let currentAvatarNode = self.avatarNode {
|
||||
@ -409,7 +456,10 @@ class LocationPinAnnotationView: MKAnnotationView {
|
||||
self.addSubnode(avatarNode)
|
||||
}
|
||||
|
||||
avatarNode.setPeer(context: context, theme: theme, peer: peer)
|
||||
if self.previousPeerId != peer.id {
|
||||
self.previousPeerId = peer.id
|
||||
avatarNode.setPeer(context: context, theme: theme, peer: peer)
|
||||
}
|
||||
}
|
||||
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
|
499
submodules/LocationUI/Sources/LocationDistancePickerScreen.swift
Normal file
499
submodules/LocationUI/Sources/LocationDistancePickerScreen.swift
Normal file
@ -0,0 +1,499 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import SyncCore
|
||||
import SwiftSignalKit
|
||||
import AccountContext
|
||||
import SolidRoundedButtonNode
|
||||
import TelegramPresentationData
|
||||
import PresentationDataUtils
|
||||
import CoreLocation
|
||||
|
||||
enum LocationDistancePickerScreenStyle {
|
||||
case `default`
|
||||
case media
|
||||
}
|
||||
|
||||
final class LocationDistancePickerScreen: ViewController {
|
||||
private var controllerNode: LocationDistancePickerScreenNode {
|
||||
return self.displayNode as! LocationDistancePickerScreenNode
|
||||
}
|
||||
|
||||
private var animatedIn = false
|
||||
|
||||
private let context: AccountContext
|
||||
private let style: LocationDistancePickerScreenStyle
|
||||
private let currentDistance: Double?
|
||||
private let updated: (Int32?) -> Void
|
||||
private let completion: (Int32?) -> Void
|
||||
|
||||
private var presentationDataDisposable: Disposable?
|
||||
|
||||
init(context: AccountContext, style: LocationDistancePickerScreenStyle, currentDistance: Double?, updated: @escaping (Int32?) -> Void, completion: @escaping (Int32?) -> Void) {
|
||||
self.context = context
|
||||
self.style = style
|
||||
self.currentDistance = currentDistance
|
||||
self.updated = updated
|
||||
self.completion = completion
|
||||
|
||||
super.init(navigationBarPresentationData: nil)
|
||||
|
||||
self.statusBar.statusBarStyle = .Ignore
|
||||
|
||||
self.blocksBackgroundWhenInOverlay = true
|
||||
|
||||
self.presentationDataDisposable = (context.sharedContext.presentationData
|
||||
|> deliverOnMainQueue).start(next: { [weak self] presentationData in
|
||||
if let strongSelf = self {
|
||||
strongSelf.controllerNode.updatePresentationData(presentationData)
|
||||
}
|
||||
})
|
||||
|
||||
self.statusBar.statusBarStyle = .Ignore
|
||||
}
|
||||
|
||||
required init(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.presentationDataDisposable?.dispose()
|
||||
}
|
||||
|
||||
override public func loadDisplayNode() {
|
||||
self.displayNode = LocationDistancePickerScreenNode(context: self.context, style: self.style, currentDistance: self.currentDistance)
|
||||
self.controllerNode.updated = { [weak self] distance in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.updated(distance)
|
||||
}
|
||||
self.controllerNode.completion = { [weak self] distance in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.completion(distance)
|
||||
strongSelf.dismiss()
|
||||
}
|
||||
self.controllerNode.dismiss = { [weak self] in
|
||||
self?.updated(nil)
|
||||
self?.presentingViewController?.dismiss(animated: false, completion: nil)
|
||||
}
|
||||
self.controllerNode.cancel = { [weak self] in
|
||||
self?.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
override public func loadView() {
|
||||
super.loadView()
|
||||
}
|
||||
|
||||
override public func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
if !self.animatedIn {
|
||||
self.animatedIn = true
|
||||
self.controllerNode.animateIn()
|
||||
}
|
||||
}
|
||||
|
||||
override public func dismiss(completion: (() -> Void)? = nil) {
|
||||
self.controllerNode.animateOut(completion: completion)
|
||||
}
|
||||
|
||||
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
super.containerLayoutUpdated(layout, transition: transition)
|
||||
|
||||
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
private class TimerPickerView: UIPickerView {
|
||||
var selectorColor: UIColor? = nil {
|
||||
didSet {
|
||||
for subview in self.subviews {
|
||||
if subview.bounds.height <= 1.0 {
|
||||
subview.backgroundColor = self.selectorColor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func didAddSubview(_ subview: UIView) {
|
||||
super.didAddSubview(subview)
|
||||
|
||||
if let selectorColor = self.selectorColor {
|
||||
if subview.bounds.height <= 1.0 {
|
||||
subview.backgroundColor = selectorColor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
|
||||
if let selectorColor = self.selectorColor {
|
||||
for subview in self.subviews {
|
||||
if subview.bounds.height <= 1.0 {
|
||||
subview.backgroundColor = selectorColor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var timerValues: [Int32] = {
|
||||
var values: [Int32] = []
|
||||
for i in 0 ..< 99 {
|
||||
values.append(Int32(i))
|
||||
}
|
||||
return values
|
||||
}()
|
||||
|
||||
private var smallerTimerValues: [Int32] = {
|
||||
var values: [Int32] = []
|
||||
for i in 0 ..< 100 {
|
||||
values.append(Int32(i))
|
||||
}
|
||||
return values
|
||||
}()
|
||||
|
||||
class LocationDistancePickerScreenNode: ViewControllerTracingNode, UIScrollViewDelegate, UIPickerViewDataSource, UIPickerViewDelegate {
|
||||
private let context: AccountContext
|
||||
private let controllerStyle: LocationDistancePickerScreenStyle
|
||||
private var presentationData: PresentationData
|
||||
private let currentDistance: Double?
|
||||
|
||||
private let dimNode: ASDisplayNode
|
||||
private let wrappingScrollNode: ASScrollNode
|
||||
private let contentContainerNode: ASDisplayNode
|
||||
private let effectNode: ASDisplayNode
|
||||
private let backgroundNode: ASDisplayNode
|
||||
private let contentBackgroundNode: ASDisplayNode
|
||||
private let titleNode: ASTextNode
|
||||
private let textNode: ImmediateTextNode
|
||||
private let cancelButton: HighlightableButtonNode
|
||||
private let doneButton: SolidRoundedButtonNode
|
||||
|
||||
private var pickerView: TimerPickerView?
|
||||
|
||||
private var containerLayout: (ContainerViewLayout, CGFloat)?
|
||||
|
||||
var updated: ((Int32) -> Void)?
|
||||
var completion: ((Int32) -> Void)?
|
||||
var dismiss: (() -> Void)?
|
||||
var cancel: (() -> Void)?
|
||||
|
||||
init(context: AccountContext, style: LocationDistancePickerScreenStyle, currentDistance: Double?) {
|
||||
self.context = context
|
||||
self.controllerStyle = style
|
||||
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
self.currentDistance = currentDistance
|
||||
|
||||
self.wrappingScrollNode = ASScrollNode()
|
||||
self.wrappingScrollNode.view.alwaysBounceVertical = true
|
||||
self.wrappingScrollNode.view.delaysContentTouches = false
|
||||
self.wrappingScrollNode.view.canCancelContentTouches = true
|
||||
|
||||
self.dimNode = ASDisplayNode()
|
||||
// self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
|
||||
|
||||
self.contentContainerNode = ASDisplayNode()
|
||||
self.contentContainerNode.isOpaque = false
|
||||
|
||||
self.backgroundNode = ASDisplayNode()
|
||||
self.backgroundNode.clipsToBounds = true
|
||||
self.backgroundNode.cornerRadius = 16.0
|
||||
|
||||
let backgroundColor: UIColor
|
||||
let textColor: UIColor
|
||||
let accentColor: UIColor
|
||||
let blurStyle: UIBlurEffect.Style
|
||||
switch style {
|
||||
case .default:
|
||||
backgroundColor = self.presentationData.theme.actionSheet.itemBackgroundColor
|
||||
textColor = self.presentationData.theme.actionSheet.primaryTextColor
|
||||
accentColor = self.presentationData.theme.actionSheet.controlAccentColor
|
||||
blurStyle = self.presentationData.theme.actionSheet.backgroundType == .light ? .light : .dark
|
||||
case .media:
|
||||
backgroundColor = UIColor(rgb: 0x1c1c1e)
|
||||
textColor = .white
|
||||
accentColor = self.presentationData.theme.actionSheet.controlAccentColor
|
||||
blurStyle = .dark
|
||||
}
|
||||
|
||||
self.effectNode = ASDisplayNode(viewBlock: {
|
||||
return UIVisualEffectView(effect: UIBlurEffect(style: blurStyle))
|
||||
})
|
||||
|
||||
self.contentBackgroundNode = ASDisplayNode()
|
||||
self.contentBackgroundNode.backgroundColor = backgroundColor
|
||||
|
||||
let title = "Notification"
|
||||
self.titleNode = ASTextNode()
|
||||
self.titleNode.attributedText = NSAttributedString(string: title, font: Font.bold(17.0), textColor: textColor)
|
||||
|
||||
self.textNode = ImmediateTextNode()
|
||||
|
||||
self.cancelButton = HighlightableButtonNode()
|
||||
self.cancelButton.setTitle(self.presentationData.strings.Common_Cancel, with: Font.regular(17.0), with: accentColor, for: .normal)
|
||||
|
||||
self.doneButton = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(theme: self.presentationData.theme), height: 52.0, cornerRadius: 11.0, gloss: false)
|
||||
self.doneButton.title = self.presentationData.strings.Conversation_Timer_Send
|
||||
|
||||
super.init()
|
||||
|
||||
self.backgroundColor = nil
|
||||
self.isOpaque = false
|
||||
|
||||
self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:))))
|
||||
self.addSubnode(self.dimNode)
|
||||
|
||||
self.wrappingScrollNode.view.delegate = self
|
||||
self.addSubnode(self.wrappingScrollNode)
|
||||
|
||||
self.wrappingScrollNode.addSubnode(self.backgroundNode)
|
||||
self.wrappingScrollNode.addSubnode(self.contentContainerNode)
|
||||
|
||||
self.backgroundNode.addSubnode(self.effectNode)
|
||||
self.backgroundNode.addSubnode(self.contentBackgroundNode)
|
||||
self.contentContainerNode.addSubnode(self.titleNode)
|
||||
self.contentContainerNode.addSubnode(self.textNode)
|
||||
self.contentContainerNode.addSubnode(self.cancelButton)
|
||||
self.contentContainerNode.addSubnode(self.doneButton)
|
||||
|
||||
self.cancelButton.addTarget(self, action: #selector(self.cancelButtonPressed), forControlEvents: .touchUpInside)
|
||||
self.doneButton.pressed = { [weak self] in
|
||||
if let strongSelf = self, let pickerView = strongSelf.pickerView {
|
||||
strongSelf.doneButton.isUserInteractionEnabled = false
|
||||
|
||||
let largeValue = timerValues[pickerView.selectedRow(inComponent: 0)]
|
||||
let smallValue = smallerTimerValues[pickerView.selectedRow(inComponent: 1)]
|
||||
var value = largeValue * 1000 + smallValue * 10
|
||||
value = Int32(Double(value) * 1.60934)
|
||||
strongSelf.completion?(value)
|
||||
}
|
||||
}
|
||||
|
||||
self.setupPickerView()
|
||||
|
||||
Queue.mainQueue().after(0.5) {
|
||||
self.updateDoneButtonTitle()
|
||||
}
|
||||
}
|
||||
|
||||
func setupPickerView() {
|
||||
if let pickerView = self.pickerView {
|
||||
pickerView.removeFromSuperview()
|
||||
}
|
||||
|
||||
let pickerView = TimerPickerView()
|
||||
pickerView.selectorColor = UIColor(rgb: 0xffffff, alpha: 0.18)
|
||||
pickerView.dataSource = self
|
||||
pickerView.delegate = self
|
||||
pickerView.selectRow(0, inComponent: 0, animated: false)
|
||||
pickerView.selectRow(30, inComponent: 1, animated: false)
|
||||
|
||||
self.contentContainerNode.view.addSubview(pickerView)
|
||||
self.pickerView = pickerView
|
||||
|
||||
self.updateDoneButtonTitle()
|
||||
}
|
||||
|
||||
func numberOfComponents(in pickerView: UIPickerView) -> Int {
|
||||
return 2
|
||||
}
|
||||
|
||||
private func updateDoneButtonTitle() {
|
||||
if let pickerView = self.pickerView {
|
||||
let largeValue = timerValues[pickerView.selectedRow(inComponent: 0)]
|
||||
let smallValue = smallerTimerValues[pickerView.selectedRow(inComponent: 1)]
|
||||
|
||||
var value = largeValue * 1000 + smallValue * 10
|
||||
value = Int32(Double(value) * 1.60934)
|
||||
let distance = stringForDistance(strings: context.sharedContext.currentPresentationData.with { $0 }.strings, distance: CLLocationDistance(value))
|
||||
|
||||
self.updated?(value)
|
||||
self.doneButton.title = "Notify me within \(distance)"
|
||||
|
||||
if let currentDistance = self.currentDistance, value > Int32(currentDistance) {
|
||||
self.doneButton.alpha = 0.4
|
||||
self.doneButton.isUserInteractionEnabled = false
|
||||
} else {
|
||||
self.doneButton.alpha = 1.0
|
||||
self.doneButton.isUserInteractionEnabled = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
|
||||
self.updateDoneButtonTitle()
|
||||
}
|
||||
|
||||
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
|
||||
if component == 0 {
|
||||
return timerValues.count
|
||||
} else if component == 1 {
|
||||
return smallerTimerValues.count
|
||||
} else {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
|
||||
if component == 0 {
|
||||
let value = timerValues[row]
|
||||
return "\(value)"
|
||||
} else if component == 1 {
|
||||
let value = String(format: "%.2d", smallerTimerValues[row])
|
||||
return ".\(value)"
|
||||
} else {
|
||||
return "MI"
|
||||
}
|
||||
}
|
||||
|
||||
func updatePresentationData(_ presentationData: PresentationData) {
|
||||
let previousTheme = self.presentationData.theme
|
||||
self.presentationData = presentationData
|
||||
|
||||
guard case .default = self.controllerStyle else {
|
||||
return
|
||||
}
|
||||
|
||||
if let effectView = self.effectNode.view as? UIVisualEffectView {
|
||||
effectView.effect = UIBlurEffect(style: presentationData.theme.actionSheet.backgroundType == .light ? .light : .dark)
|
||||
}
|
||||
|
||||
self.contentBackgroundNode.backgroundColor = self.presentationData.theme.actionSheet.itemBackgroundColor
|
||||
self.titleNode.attributedText = NSAttributedString(string: self.titleNode.attributedText?.string ?? "", font: Font.bold(17.0), textColor: self.presentationData.theme.actionSheet.primaryTextColor)
|
||||
|
||||
if previousTheme !== presentationData.theme, let (layout, navigationBarHeight) = self.containerLayout {
|
||||
self.setupPickerView()
|
||||
self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate)
|
||||
}
|
||||
|
||||
self.cancelButton.setTitle(self.presentationData.strings.Common_Cancel, with: Font.regular(17.0), with: self.presentationData.theme.actionSheet.controlAccentColor, for: .normal)
|
||||
self.doneButton.updateTheme(SolidRoundedButtonTheme(theme: self.presentationData.theme))
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
|
||||
self.wrappingScrollNode.view.contentInsetAdjustmentBehavior = .never
|
||||
}
|
||||
}
|
||||
|
||||
@objc func cancelButtonPressed() {
|
||||
self.cancel?()
|
||||
}
|
||||
|
||||
@objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||
if case .ended = recognizer.state {
|
||||
self.cancelButtonPressed()
|
||||
}
|
||||
}
|
||||
|
||||
func animateIn() {
|
||||
self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
|
||||
|
||||
let offset = self.bounds.size.height - self.contentBackgroundNode.frame.minY
|
||||
|
||||
let dimPosition = self.dimNode.layer.position
|
||||
self.dimNode.layer.animatePosition(from: CGPoint(x: dimPosition.x, y: dimPosition.y - offset), to: dimPosition, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
self.layer.animateBoundsOriginYAdditive(from: -offset, to: 0.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
}
|
||||
|
||||
func animateOut(completion: (() -> Void)? = nil) {
|
||||
var dimCompleted = false
|
||||
var offsetCompleted = false
|
||||
|
||||
let internalCompletion: () -> Void = { [weak self] in
|
||||
if let strongSelf = self, dimCompleted && offsetCompleted {
|
||||
strongSelf.dismiss?()
|
||||
}
|
||||
completion?()
|
||||
}
|
||||
|
||||
self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in
|
||||
dimCompleted = true
|
||||
internalCompletion()
|
||||
})
|
||||
|
||||
let offset = self.bounds.size.height - self.contentBackgroundNode.frame.minY
|
||||
let dimPosition = self.dimNode.layer.position
|
||||
self.dimNode.layer.animatePosition(from: dimPosition, to: CGPoint(x: dimPosition.x, y: dimPosition.y - offset), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
|
||||
self.layer.animateBoundsOriginYAdditive(from: 0.0, to: -offset, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in
|
||||
offsetCompleted = true
|
||||
internalCompletion()
|
||||
})
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
if self.bounds.contains(point) {
|
||||
if !self.contentBackgroundNode.bounds.contains(self.convert(point, to: self.contentBackgroundNode)) {
|
||||
return self.dimNode.view
|
||||
}
|
||||
}
|
||||
return super.hitTest(point, with: event)
|
||||
}
|
||||
|
||||
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
|
||||
let contentOffset = scrollView.contentOffset
|
||||
let additionalTopHeight = max(0.0, -contentOffset.y)
|
||||
|
||||
if additionalTopHeight >= 30.0 {
|
||||
self.cancelButtonPressed()
|
||||
}
|
||||
}
|
||||
|
||||
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
self.containerLayout = (layout, navigationBarHeight)
|
||||
|
||||
var insets = layout.insets(options: [.statusBar, .input])
|
||||
let cleanInsets = layout.insets(options: [.statusBar])
|
||||
insets.top = max(10.0, insets.top)
|
||||
|
||||
let buttonOffset: CGFloat = 0.0
|
||||
let bottomInset: CGFloat = 10.0 + cleanInsets.bottom
|
||||
let titleHeight: CGFloat = 54.0
|
||||
var contentHeight = titleHeight + bottomInset + 52.0 + 17.0
|
||||
let pickerHeight: CGFloat = min(216.0, layout.size.height - contentHeight)
|
||||
contentHeight = titleHeight + bottomInset + 52.0 + 17.0 + pickerHeight + buttonOffset
|
||||
|
||||
let width = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: layout.safeInsets.left)
|
||||
|
||||
let sideInset = floor((layout.size.width - width) / 2.0)
|
||||
let contentContainerFrame = CGRect(origin: CGPoint(x: sideInset, y: layout.size.height - contentHeight), size: CGSize(width: width, height: contentHeight))
|
||||
let contentFrame = contentContainerFrame
|
||||
|
||||
var backgroundFrame = CGRect(origin: CGPoint(x: contentFrame.minX, y: contentFrame.minY), size: CGSize(width: contentFrame.width, height: contentFrame.height + 2000.0))
|
||||
if backgroundFrame.minY < contentFrame.minY {
|
||||
backgroundFrame.origin.y = contentFrame.minY
|
||||
}
|
||||
transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame)
|
||||
transition.updateFrame(node: self.effectNode, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size))
|
||||
transition.updateFrame(node: self.contentBackgroundNode, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size))
|
||||
transition.updateFrame(node: self.wrappingScrollNode, frame: CGRect(origin: CGPoint(), size: layout.size))
|
||||
transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size))
|
||||
|
||||
let titleSize = self.titleNode.measure(CGSize(width: width, height: titleHeight))
|
||||
let titleFrame = CGRect(origin: CGPoint(x: floor((contentFrame.width - titleSize.width) / 2.0), y: 16.0), size: titleSize)
|
||||
transition.updateFrame(node: self.titleNode, frame: titleFrame)
|
||||
|
||||
let cancelSize = self.cancelButton.measure(CGSize(width: width, height: titleHeight))
|
||||
let cancelFrame = CGRect(origin: CGPoint(x: 16.0, y: 16.0), size: cancelSize)
|
||||
transition.updateFrame(node: self.cancelButton, frame: cancelFrame)
|
||||
|
||||
let buttonInset: CGFloat = 16.0
|
||||
let doneButtonHeight = self.doneButton.updateLayout(width: contentFrame.width - buttonInset * 2.0, transition: transition)
|
||||
transition.updateFrame(node: self.doneButton, frame: CGRect(x: buttonInset, y: contentHeight - doneButtonHeight - insets.bottom - 16.0 - buttonOffset, width: contentFrame.width, height: doneButtonHeight))
|
||||
|
||||
self.pickerView?.frame = CGRect(origin: CGPoint(x: 0.0, y: 54.0), size: CGSize(width: contentFrame.width, height: pickerHeight))
|
||||
|
||||
transition.updateFrame(node: self.contentContainerNode, frame: contentContainerFrame)
|
||||
}
|
||||
}
|
@ -4,6 +4,7 @@ import AsyncDisplayKit
|
||||
import Display
|
||||
import TelegramPresentationData
|
||||
import AppBundle
|
||||
import CoreLocation
|
||||
|
||||
private let panelInset: CGFloat = 4.0
|
||||
private let panelButtonSize = CGSize(width: 46.0, height: 46.0)
|
||||
@ -38,7 +39,7 @@ final class LocationMapHeaderNode: ASDisplayNode {
|
||||
private let toggleMapModeSelection: () -> Void
|
||||
private let goToUserLocation: () -> Void
|
||||
private let showPlacesInThisArea: () -> Void
|
||||
private let setupProximityNotification: () -> Void
|
||||
private let setupProximityNotification: (CLLocationCoordinate2D, Bool) -> Void
|
||||
|
||||
private var displayingPlacesButton = false
|
||||
private var proximityNotification: Bool?
|
||||
@ -56,7 +57,7 @@ final class LocationMapHeaderNode: ASDisplayNode {
|
||||
|
||||
private var validLayout: (ContainerViewLayout, CGFloat, CGFloat, CGFloat, CGSize)?
|
||||
|
||||
init(presentationData: PresentationData, toggleMapModeSelection: @escaping () -> Void, goToUserLocation: @escaping () -> Void, setupProximityNotification: @escaping () -> Void = {}, showPlacesInThisArea: @escaping () -> Void = {}) {
|
||||
init(presentationData: PresentationData, toggleMapModeSelection: @escaping () -> Void, goToUserLocation: @escaping () -> Void, setupProximityNotification: @escaping (CLLocationCoordinate2D, Bool) -> Void = { _, _ in }, showPlacesInThisArea: @escaping () -> Void = {}) {
|
||||
self.presentationData = presentationData
|
||||
self.toggleMapModeSelection = toggleMapModeSelection
|
||||
self.goToUserLocation = goToUserLocation
|
||||
@ -88,6 +89,8 @@ final class LocationMapHeaderNode: ASDisplayNode {
|
||||
|
||||
self.notificationButtonNode = HighlightableButtonNode()
|
||||
self.notificationButtonNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "Location/NotificationIcon"), color: presentationData.theme.rootController.navigationBar.buttonColor), for: .normal)
|
||||
self.notificationButtonNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Title Panels/MuteIcon"), color: presentationData.theme.rootController.navigationBar.buttonColor), for: .selected)
|
||||
self.notificationButtonNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Title Panels/MuteIcon"), color: presentationData.theme.rootController.navigationBar.buttonColor), for: [.selected, .highlighted])
|
||||
|
||||
self.placesBackgroundNode = ASImageNode()
|
||||
self.placesBackgroundNode.contentMode = .scaleToFill
|
||||
@ -129,6 +132,7 @@ final class LocationMapHeaderNode: ASDisplayNode {
|
||||
func updateState(mapMode: LocationMapMode, displayingMapModeOptions: Bool, displayingPlacesButton: Bool, proximityNotification: Bool?, animated: Bool) {
|
||||
self.mapNode.mapMode = mapMode
|
||||
self.infoButtonNode.isSelected = displayingMapModeOptions
|
||||
self.notificationButtonNode.isSelected = proximityNotification ?? false
|
||||
|
||||
let updateLayout = self.displayingPlacesButton != displayingPlacesButton || self.proximityNotification != proximityNotification
|
||||
self.displayingPlacesButton = displayingPlacesButton
|
||||
@ -190,10 +194,18 @@ final class LocationMapHeaderNode: ASDisplayNode {
|
||||
transition.updateFrame(node: self.optionsBackgroundNode, frame: CGRect(x: size.width - inset - panelButtonSize.width - panelInset * 2.0, y: navigationBarHeight + topPadding + inset, width: panelButtonSize.width + panelInset * 2.0, height: panelHeight + panelInset * 2.0))
|
||||
|
||||
let alphaTransition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut)
|
||||
let optionsAlpha: CGFloat = size.height > 160.0 + navigationBarHeight ? 1.0 : 0.0
|
||||
let optionsAlpha: CGFloat = size.height > 160.0 + navigationBarHeight && !self.forceIsHidden ? 1.0 : 0.0
|
||||
alphaTransition.updateAlpha(node: self.optionsBackgroundNode, alpha: optionsAlpha)
|
||||
}
|
||||
|
||||
var forceIsHidden: Bool = false {
|
||||
didSet {
|
||||
if let (layout, navigationBarHeight, topPadding, offset, size) = self.validLayout {
|
||||
self.updateLayout(layout: layout, navigationBarHeight: navigationBarHeight, topPadding: topPadding, offset: offset, size: size, transition: .immediate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateHighlight(_ highlighted: Bool) {
|
||||
self.shadowNode.image = generateShadowImage(theme: self.presentationData.theme, highlighted: highlighted)
|
||||
}
|
||||
@ -207,7 +219,9 @@ final class LocationMapHeaderNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
@objc private func notificationPressed() {
|
||||
self.setupProximityNotification()
|
||||
if let proximityNotification = self.proximityNotification, let location = self.mapNode.currentUserLocation {
|
||||
self.setupProximityNotification(location.coordinate, proximityNotification)
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func placesPressed() {
|
||||
|
@ -68,22 +68,33 @@ private class LocationMapView: MKMapView, UIGestureRecognizerDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
private func generateHeadingArrowImage() -> UIImage? {
|
||||
return generateImage(CGSize(width: 28.0, height: 28.0), contextGenerator: { size, context in
|
||||
func generateHeadingArrowImage() -> UIImage? {
|
||||
return generateImage(CGSize(width: 88.0, height: 88.0), contextGenerator: { size, context in
|
||||
let bounds = CGRect(origin: CGPoint(), size: size)
|
||||
context.clear(bounds)
|
||||
|
||||
context.move(to: CGPoint(x: 44.0, y: 44.0))
|
||||
context.addArc(center: CGPoint(x: 44.0, y: 44.0), radius: 44.0, startAngle: CGFloat.pi / 2.0 + CGFloat.pi / 6.0, endAngle: CGFloat.pi / 2.0 - CGFloat.pi / 6.0, clockwise: true)
|
||||
context.clip()
|
||||
|
||||
context.setFillColor(UIColor(rgb: 0x3393fe).cgColor)
|
||||
var locations: [CGFloat] = [0.0, 0.5, 1.0]
|
||||
let colors: [CGColor] = [UIColor(rgb: 0x007ee5, alpha: 0.0).cgColor, UIColor(rgb: 0x007ee5, alpha: 0.75).cgColor, UIColor(rgb: 0x007ee5, alpha: 0.0).cgColor]
|
||||
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
||||
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
|
||||
|
||||
context.move(to: CGPoint(x: 14.0, y: 0.0))
|
||||
context.addLine(to: CGPoint(x: 19.0, y: 7.0))
|
||||
context.addLine(to: CGPoint(x: 9.0, y: 7.0))
|
||||
context.closePath()
|
||||
context.fillPath()
|
||||
context.drawLinearGradient(gradient, start: CGPoint(), end: CGPoint(x: 0.0, y: bounds.size.height), options: CGGradientDrawingOptions())
|
||||
})
|
||||
}
|
||||
|
||||
private func generateProximityDim(size: CGSize, rect: CGRect) -> UIImage {
|
||||
return generateImage(size, rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
context.setFillColor(UIColor(rgb: 0x000000, alpha: 0.4).cgColor)
|
||||
context.fill(CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
context.setBlendMode(.clear)
|
||||
context.fillEllipse(in: bounds.insetBy(dx: 5.0, dy: 5.0))
|
||||
})
|
||||
context.fillEllipse(in: rect)
|
||||
})!
|
||||
}
|
||||
|
||||
final class LocationMapNode: ASDisplayNode, MKMapViewDelegate {
|
||||
@ -107,6 +118,37 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate {
|
||||
|
||||
var annotationSelected: ((LocationPinAnnotation?) -> Void)?
|
||||
var userLocationAnnotationSelected: (() -> Void)?
|
||||
|
||||
var proximityDimView = UIImageView()
|
||||
var proximityRadius: Double? {
|
||||
didSet {
|
||||
if let radius = self.proximityRadius, let mapView = self.mapView {
|
||||
let region = MKCoordinateRegion(center: mapView.userLocation.coordinate, latitudinalMeters: radius * 2.0, longitudinalMeters: radius * 2.0)
|
||||
let rect = mapView.convert(region, toRectTo: mapView)
|
||||
if proximityDimView.image == nil {
|
||||
proximityDimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
|
||||
if oldValue == 0 {
|
||||
UIView.transition(with: proximityDimView, duration: 0.2, options: .transitionCrossDissolve) {
|
||||
self.proximityDimView.image = generateProximityDim(size: mapView.bounds.size, rect: rect)
|
||||
} completion: { _ in
|
||||
|
||||
}
|
||||
} else {
|
||||
proximityDimView.image = generateProximityDim(size: mapView.bounds.size, rect: rect)
|
||||
}
|
||||
} else {
|
||||
if proximityDimView.image != nil {
|
||||
UIView.transition(with: proximityDimView, duration: 0.2, options: .transitionCrossDissolve) {
|
||||
self.proximityDimView.image = nil
|
||||
} completion: { _ in
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override init() {
|
||||
self.pickerAnnotationContainerView = PickerAnnotationContainerView()
|
||||
@ -123,7 +165,7 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate {
|
||||
super.didLoad()
|
||||
|
||||
self.headingArrowView = UIImageView()
|
||||
self.headingArrowView?.frame = CGRect(origin: CGPoint(), size: CGSize(width: 28.0, height: 28.0))
|
||||
self.headingArrowView?.frame = CGRect(origin: CGPoint(), size: CGSize(width: 88.0, height: 88.0))
|
||||
self.headingArrowView?.image = generateHeadingArrowImage()
|
||||
|
||||
self.mapView?.interactiveTransitionGestureRecognizerTest = { p in
|
||||
@ -151,6 +193,7 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate {
|
||||
return false
|
||||
}
|
||||
|
||||
self.mapView?.addSubview(self.proximityDimView)
|
||||
self.view.addSubview(self.pickerAnnotationContainerView)
|
||||
}
|
||||
|
||||
@ -303,6 +346,52 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
var circleOverlay: MKCircle?
|
||||
var activeProximityRadius: Double? {
|
||||
didSet {
|
||||
if let activeProximityRadius = self.activeProximityRadius {
|
||||
if let circleOverlay = self.circleOverlay {
|
||||
self.circleOverlay = nil
|
||||
self.mapView?.removeOverlay(circleOverlay)
|
||||
}
|
||||
if let location = self.currentUserLocation {
|
||||
let overlay = MKCircle(center: location.coordinate, radius: activeProximityRadius)
|
||||
self.circleOverlay = overlay
|
||||
self.mapView?.addOverlay(overlay)
|
||||
}
|
||||
} else {
|
||||
if let circleOverlay = self.circleOverlay {
|
||||
self.circleOverlay = nil
|
||||
self.mapView?.removeOverlay(circleOverlay)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
|
||||
if let circle = overlay as? MKCircle {
|
||||
let renderer = MKCircleRenderer(circle: circle)
|
||||
renderer.fillColor = .clear
|
||||
renderer.strokeColor = UIColor(rgb: 0xc3baaf)
|
||||
renderer.lineWidth = 1.0
|
||||
renderer.lineDashPattern = [5, 3]
|
||||
return renderer
|
||||
} else {
|
||||
return MKOverlayRenderer()
|
||||
}
|
||||
}
|
||||
|
||||
var distance: Double? {
|
||||
if let annotation = self.annotations.first, let location = self.currentUserLocation {
|
||||
return location.distance(from: CLLocation(latitude: annotation.coordinate.latitude, longitude: annotation.coordinate.longitude))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var currentUserLocation: CLLocation? {
|
||||
return self.mapView?.userLocation.location
|
||||
}
|
||||
|
||||
var userLocation: Signal<CLLocation?, NoError> {
|
||||
return self.locationPromise.get()
|
||||
}
|
||||
@ -314,6 +403,13 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate {
|
||||
return mapView.convert(CGPoint(x: (mapView.frame.width + pinOffset.x) / 2.0, y: (mapView.frame.height + pinOffset.y) / 2.0), toCoordinateFrom: mapView)
|
||||
}
|
||||
|
||||
var mapSpan: MKCoordinateSpan? {
|
||||
guard let mapView = self.mapView else {
|
||||
return nil
|
||||
}
|
||||
return mapView.region.span
|
||||
}
|
||||
|
||||
func resetAnnotationSelection() {
|
||||
guard let mapView = self.mapView else {
|
||||
return
|
||||
@ -359,6 +455,8 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate {
|
||||
var userLocationAnnotation: LocationPinAnnotation? = nil {
|
||||
didSet {
|
||||
if let annotation = self.userLocationAnnotation {
|
||||
self.customUserLocationAnnotationView?.removeFromSuperview()
|
||||
|
||||
let annotationView = LocationPinAnnotationView(annotation: annotation)
|
||||
annotationView.frame = annotationView.frame.offsetBy(dx: 21.0, dy: 22.0)
|
||||
if let parentView = self.userLocationAnnotationView {
|
||||
@ -448,6 +546,7 @@ final class LocationMapNode: ASDisplayNode, MKMapViewDelegate {
|
||||
}
|
||||
|
||||
func updateLayout(size: CGSize) {
|
||||
self.proximityDimView.frame = CGRect(origin: CGPoint(), size: size)
|
||||
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)
|
||||
|
@ -139,7 +139,7 @@ private enum LocationPickerEntry: Comparable, Identifiable {
|
||||
} else {
|
||||
icon = .location
|
||||
}
|
||||
return LocationActionListItem(presentationData: ItemListPresentationData(presentationData), account: account, title: title, subtitle: subtitle, icon: icon, action: {
|
||||
return LocationActionListItem(presentationData: ItemListPresentationData(presentationData), account: account, title: title, subtitle: subtitle, icon: icon, beginTimeAndTimeout: nil, action: {
|
||||
if let venue = venue {
|
||||
interaction?.sendVenue(venue)
|
||||
} else if let coordinate = coordinate {
|
||||
@ -149,7 +149,7 @@ private enum LocationPickerEntry: Comparable, Identifiable {
|
||||
interaction?.updateSendActionHighlight(highlighted)
|
||||
})
|
||||
case let .liveLocation(_, title, subtitle, coordinate):
|
||||
return LocationActionListItem(presentationData: ItemListPresentationData(presentationData), account: account, title: title, subtitle: subtitle, icon: .liveLocation, action: {
|
||||
return LocationActionListItem(presentationData: ItemListPresentationData(presentationData), account: account, title: title, subtitle: subtitle, icon: .liveLocation, beginTimeAndTimeout: nil, action: {
|
||||
if let coordinate = coordinate {
|
||||
interaction?.sendLiveLocation(coordinate)
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ import CoreLocation
|
||||
import PresentationDataUtils
|
||||
import OpenInExternalAppUI
|
||||
import ShareController
|
||||
import DeviceAccess
|
||||
|
||||
public class LocationViewParams {
|
||||
let sendLiveLocation: (TelegramMediaMap) -> Void
|
||||
@ -35,9 +36,12 @@ class LocationViewInteraction {
|
||||
let goToCoordinate: (CLLocationCoordinate2D) -> Void
|
||||
let requestDirections: () -> Void
|
||||
let share: () -> Void
|
||||
let setupProximityNotification: () -> Void
|
||||
|
||||
init(toggleMapModeSelection: @escaping () -> Void, updateMapMode: @escaping (LocationMapMode) -> Void, goToUserLocation: @escaping () -> Void, goToCoordinate: @escaping (CLLocationCoordinate2D) -> Void, requestDirections: @escaping () -> Void, share: @escaping () -> Void, setupProximityNotification: @escaping () -> Void) {
|
||||
let setupProximityNotification: (CLLocationCoordinate2D, Bool) -> Void
|
||||
let updateSendActionHighlight: (Bool) -> Void
|
||||
let sendLiveLocation: (CLLocationCoordinate2D) -> Void
|
||||
let stopLiveLocation: () -> Void
|
||||
|
||||
init(toggleMapModeSelection: @escaping () -> Void, updateMapMode: @escaping (LocationMapMode) -> Void, goToUserLocation: @escaping () -> Void, goToCoordinate: @escaping (CLLocationCoordinate2D) -> Void, requestDirections: @escaping () -> Void, share: @escaping () -> Void, setupProximityNotification: @escaping (CLLocationCoordinate2D, Bool) -> Void, updateSendActionHighlight: @escaping (Bool) -> Void, sendLiveLocation: @escaping (CLLocationCoordinate2D) -> Void, stopLiveLocation: @escaping () -> Void) {
|
||||
self.toggleMapModeSelection = toggleMapModeSelection
|
||||
self.updateMapMode = updateMapMode
|
||||
self.goToUserLocation = goToUserLocation
|
||||
@ -45,23 +49,31 @@ class LocationViewInteraction {
|
||||
self.requestDirections = requestDirections
|
||||
self.share = share
|
||||
self.setupProximityNotification = setupProximityNotification
|
||||
self.updateSendActionHighlight = updateSendActionHighlight
|
||||
self.sendLiveLocation = sendLiveLocation
|
||||
self.stopLiveLocation = stopLiveLocation
|
||||
}
|
||||
}
|
||||
|
||||
var CURRENT_DISTANCE: Double? = nil
|
||||
|
||||
public final class LocationViewController: ViewController {
|
||||
private var controllerNode: LocationViewControllerNode {
|
||||
return self.displayNode as! LocationViewControllerNode
|
||||
}
|
||||
private let context: AccountContext
|
||||
private var mapMedia: TelegramMediaMap
|
||||
private var subject: Message
|
||||
private var presentationData: PresentationData
|
||||
private var presentationDataDisposable: Disposable?
|
||||
|
||||
|
||||
private let locationManager = LocationManager()
|
||||
private var permissionDisposable: Disposable?
|
||||
|
||||
private var interaction: LocationViewInteraction?
|
||||
|
||||
public init(context: AccountContext, mapMedia: TelegramMediaMap, params: LocationViewParams) {
|
||||
public init(context: AccountContext, subject: Message, params: LocationViewParams) {
|
||||
self.context = context
|
||||
self.mapMedia = mapMedia
|
||||
self.subject = subject
|
||||
|
||||
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
@ -89,7 +101,7 @@ public final class LocationViewController: ViewController {
|
||||
strongSelf.controllerNode.updatePresentationData(presentationData)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
self.interaction = LocationViewInteraction(toggleMapModeSelection: { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
@ -126,27 +138,115 @@ public final class LocationViewController: ViewController {
|
||||
strongSelf.controllerNode.updateState { state in
|
||||
var state = state
|
||||
state.displayingMapModeOptions = false
|
||||
state.selectedLocation = .coordinate(coordinate)
|
||||
state.selectedLocation = .coordinate(coordinate, false)
|
||||
return state
|
||||
}
|
||||
}, requestDirections: { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.present(OpenInActionSheetController(context: context, item: .location(location: mapMedia, withDirections: true), additionalAction: nil, openUrl: params.openUrl), in: .window(.root), with: nil)
|
||||
if let location = getLocation(from: strongSelf.subject) {
|
||||
strongSelf.present(OpenInActionSheetController(context: context, item: .location(location: location, withDirections: true), additionalAction: nil, openUrl: params.openUrl), in: .window(.root), with: nil)
|
||||
}
|
||||
}, share: { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
let shareAction = OpenInControllerAction(title: strongSelf.presentationData.strings.Conversation_ContextMenuShare, action: {
|
||||
strongSelf.present(ShareController(context: context, subject: .mapMedia(mapMedia), externalShare: true), in: .window(.root), with: nil)
|
||||
})
|
||||
strongSelf.present(OpenInActionSheetController(context: context, item: .location(location: mapMedia, withDirections: false), additionalAction: shareAction, openUrl: params.openUrl), in: .window(.root), with: nil)
|
||||
}, setupProximityNotification: { [weak self] in
|
||||
if let location = getLocation(from: strongSelf.subject) {
|
||||
let shareAction = OpenInControllerAction(title: strongSelf.presentationData.strings.Conversation_ContextMenuShare, action: {
|
||||
strongSelf.present(ShareController(context: context, subject: .mapMedia(location), externalShare: true), in: .window(.root), with: nil)
|
||||
})
|
||||
strongSelf.present(OpenInActionSheetController(context: context, item: .location(location: location, withDirections: false), additionalAction: shareAction, openUrl: params.openUrl), in: .window(.root), with: nil)
|
||||
}
|
||||
}, setupProximityNotification: { [weak self] coordinate, reset in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
if reset {
|
||||
strongSelf.controllerNode.setProximityRadius(radius: nil)
|
||||
CURRENT_DISTANCE = nil
|
||||
} else {
|
||||
strongSelf.controllerNode.setProximityIndicator(radius: 0)
|
||||
|
||||
let controller = LocationDistancePickerScreen(context: context, style: .default, currentDistance: strongSelf.controllerNode.headerNode.mapNode.distance, updated: { [weak self] distance in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.controllerNode.setProximityIndicator(radius: distance)
|
||||
}, completion: { [weak self] distance in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.controllerNode.setProximityIndicator(radius: nil)
|
||||
if let distance = distance {
|
||||
strongSelf.controllerNode.setProximityRadius(radius: distance)
|
||||
|
||||
let _ = requestProximityNotification(postbox: context.account.postbox, network: context.account.network, messageId: subject.id, distance: distance, coordinate: (coordinate.latitude, longitude: coordinate.longitude, accuracyRadius: nil)).start()
|
||||
|
||||
CURRENT_DISTANCE = Double(distance)
|
||||
}
|
||||
})
|
||||
strongSelf.present(controller, in: .window(.root))
|
||||
}
|
||||
}, updateSendActionHighlight: { [weak self] highlighted in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.controllerNode.updateSendActionHighlight(highlighted)
|
||||
}, sendLiveLocation: { [weak self] coordinate in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
DeviceAccess.authorizeAccess(to: .location(.live), locationManager: strongSelf.locationManager, presentationData: strongSelf.presentationData, present: { c, a in
|
||||
strongSelf.present(c, in: .window(.root), with: a)
|
||||
}, openSettings: {
|
||||
strongSelf.context.sharedContext.applicationBindings.openSettings()
|
||||
}) { [weak self] authorized in
|
||||
guard let strongSelf = self, authorized else {
|
||||
return
|
||||
}
|
||||
let controller = ActionSheetController(presentationData: strongSelf.presentationData)
|
||||
var title = strongSelf.presentationData.strings.Map_LiveLocationGroupDescription
|
||||
if subject.id.peerId.namespace == Namespaces.Peer.CloudUser {
|
||||
// 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 {
|
||||
params.sendLiveLocation(TelegramMediaMap(coordinate: coordinate, liveBroadcastingTimeout: 15 * 60))
|
||||
strongSelf.dismiss()
|
||||
}
|
||||
}),
|
||||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Map_LiveLocationFor1Hour, color: .accent, action: { [weak self, weak controller] in
|
||||
controller?.dismissAnimated()
|
||||
if let strongSelf = self {
|
||||
params.sendLiveLocation(TelegramMediaMap(coordinate: coordinate, liveBroadcastingTimeout: 60 * 60 - 1))
|
||||
strongSelf.dismiss()
|
||||
}
|
||||
}),
|
||||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Map_LiveLocationFor8Hours, color: .accent, action: { [weak self, weak controller] in
|
||||
controller?.dismissAnimated()
|
||||
if let strongSelf = self {
|
||||
params.sendLiveLocation(TelegramMediaMap(coordinate: coordinate, liveBroadcastingTimeout: 8 * 60 * 60))
|
||||
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))
|
||||
}
|
||||
}, stopLiveLocation: { [weak self] in
|
||||
params.stopLiveLocation()
|
||||
self?.dismiss()
|
||||
})
|
||||
|
||||
self.scrollToTop = { [weak self] in
|
||||
@ -170,8 +270,14 @@ public final class LocationViewController: ViewController {
|
||||
return
|
||||
}
|
||||
|
||||
self.displayNode = LocationViewControllerNode(context: self.context, presentationData: self.presentationData, mapMedia: self.mapMedia, interaction: interaction)
|
||||
self.displayNode = LocationViewControllerNode(context: self.context, presentationData: self.presentationData, subject: self.subject, interaction: interaction)
|
||||
self.displayNodeDidLoad()
|
||||
|
||||
self.controllerNode.updateState { state -> LocationViewState in
|
||||
var state = state
|
||||
state.proximityRadius = CURRENT_DISTANCE
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
|
@ -14,6 +14,11 @@ import AccountContext
|
||||
import AppBundle
|
||||
import CoreLocation
|
||||
import Geocoding
|
||||
import TelegramStringFormatting
|
||||
|
||||
func getLocation(from message: Message) -> TelegramMediaMap? {
|
||||
return message.media.first(where: { $0 is TelegramMediaMap } ) as? TelegramMediaMap
|
||||
}
|
||||
|
||||
private func areMessagesEqual(_ lhsMessage: Message, _ rhsMessage: Message) -> Bool {
|
||||
if lhsMessage.stableVersion != rhsMessage.stableVersion {
|
||||
@ -39,8 +44,8 @@ private enum LocationViewEntryId: Hashable {
|
||||
|
||||
private enum LocationViewEntry: Comparable, Identifiable {
|
||||
case info(PresentationTheme, TelegramMediaMap, String?, Double?, Double?)
|
||||
case toggleLiveLocation(PresentationTheme, String, String)
|
||||
case liveLocation(PresentationTheme, Message, Int)
|
||||
case toggleLiveLocation(PresentationTheme, String, String, CLLocationCoordinate2D?, Double?, Double?)
|
||||
case liveLocation(PresentationTheme, Message, Double?, Int)
|
||||
|
||||
var stableId: LocationViewEntryId {
|
||||
switch self {
|
||||
@ -48,8 +53,12 @@ private enum LocationViewEntry: Comparable, Identifiable {
|
||||
return .info
|
||||
case .toggleLiveLocation:
|
||||
return .toggleLiveLocation
|
||||
case let .liveLocation(_, message, _):
|
||||
return .liveLocation(message.id.peerId)
|
||||
case let .liveLocation(_, message, _, _):
|
||||
if let author = message.author {
|
||||
return .liveLocation(author.id)
|
||||
} else {
|
||||
return .liveLocation(message.id.peerId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -61,14 +70,14 @@ private enum LocationViewEntry: Comparable, Identifiable {
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .toggleLiveLocation(lhsTheme, lhsTitle, lhsSubtitle):
|
||||
if case let .toggleLiveLocation(rhsTheme, rhsTitle, rhsSubtitle) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsSubtitle == rhsSubtitle {
|
||||
case let .toggleLiveLocation(lhsTheme, lhsTitle, lhsSubtitle, lhsCoordinate, lhsBeginTimestamp, lhsTimeout):
|
||||
if case let .toggleLiveLocation(rhsTheme, rhsTitle, rhsSubtitle, rhsCoordinate, rhsBeginTimestamp, rhsTimeout) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsSubtitle == rhsSubtitle, lhsCoordinate == rhsCoordinate, lhsBeginTimestamp == rhsBeginTimestamp, lhsTimeout == rhsTimeout {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .liveLocation(lhsTheme, lhsMessage, lhsIndex):
|
||||
if case let .liveLocation(rhsTheme, rhsMessage, rhsIndex) = rhs, lhsTheme === rhsTheme, areMessagesEqual(lhsMessage, rhsMessage), lhsIndex == rhsIndex {
|
||||
case let .liveLocation(lhsTheme, lhsMessage, lhsDistance, lhsIndex):
|
||||
if case let .liveLocation(rhsTheme, rhsMessage, rhsDistance, rhsIndex) = rhs, lhsTheme === rhsTheme, areMessagesEqual(lhsMessage, rhsMessage), lhsDistance == rhsDistance, lhsIndex == rhsIndex {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
@ -92,11 +101,11 @@ private enum LocationViewEntry: Comparable, Identifiable {
|
||||
case .liveLocation:
|
||||
return true
|
||||
}
|
||||
case let .liveLocation(_, _, lhsIndex):
|
||||
case let .liveLocation(_, _, _, lhsIndex):
|
||||
switch rhs {
|
||||
case .info, .toggleLiveLocation:
|
||||
return false
|
||||
case let .liveLocation(_, _, rhsIndex):
|
||||
case let .liveLocation(_, _, _, rhsIndex):
|
||||
return lhsIndex < rhsIndex
|
||||
}
|
||||
}
|
||||
@ -123,13 +132,29 @@ private enum LocationViewEntry: Comparable, Identifiable {
|
||||
}, getDirections: {
|
||||
interaction?.requestDirections()
|
||||
})
|
||||
case let .toggleLiveLocation(theme, title, subtitle):
|
||||
return LocationActionListItem(presentationData: ItemListPresentationData(presentationData), account: account, title: title, subtitle: subtitle, icon: .liveLocation, action: {
|
||||
// if let coordinate = coordinate {
|
||||
// interaction?.sendLiveLocation(coordinate)
|
||||
// }
|
||||
case let .toggleLiveLocation(theme, title, subtitle, coordinate, beginTimstamp, timeout):
|
||||
let beginTimeAndTimeout: (Double, Double)?
|
||||
if let beginTimstamp = beginTimstamp, let timeout = timeout {
|
||||
beginTimeAndTimeout = (beginTimstamp, timeout)
|
||||
} else {
|
||||
beginTimeAndTimeout = nil
|
||||
}
|
||||
return LocationActionListItem(presentationData: ItemListPresentationData(presentationData), account: account, title: title, subtitle: subtitle, icon: beginTimeAndTimeout != nil ? .stopLiveLocation : .liveLocation, beginTimeAndTimeout: beginTimeAndTimeout, action: {
|
||||
if beginTimeAndTimeout != nil {
|
||||
interaction?.stopLiveLocation()
|
||||
} else if let coordinate = coordinate {
|
||||
interaction?.sendLiveLocation(coordinate)
|
||||
}
|
||||
}, highlighted: { highlight in
|
||||
interaction?.updateSendActionHighlight(highlight)
|
||||
})
|
||||
case let .liveLocation(theme, message, _):
|
||||
case let .liveLocation(theme, message, distance, _):
|
||||
let distanceString: String?
|
||||
if let distance = distance {
|
||||
distanceString = distance < 10 ? presentationData.strings.Map_YouAreHere : presentationData.strings.Map_DistanceAway(stringForDistance(strings: presentationData.strings, distance: distance)).0
|
||||
} else {
|
||||
distanceString = nil
|
||||
}
|
||||
return ItemListTextItem(presentationData: ItemListPresentationData(presentationData), text: .plain(""), sectionId: 0)
|
||||
}
|
||||
}
|
||||
@ -148,7 +173,7 @@ private func preparedTransition(from fromEntries: [LocationViewEntry], to toEntr
|
||||
enum LocationViewLocation: Equatable {
|
||||
case initial
|
||||
case user
|
||||
case coordinate(CLLocationCoordinate2D)
|
||||
case coordinate(CLLocationCoordinate2D, Bool)
|
||||
case custom
|
||||
}
|
||||
|
||||
@ -156,11 +181,13 @@ struct LocationViewState {
|
||||
var mapMode: LocationMapMode
|
||||
var displayingMapModeOptions: Bool
|
||||
var selectedLocation: LocationViewLocation
|
||||
var proximityRadius: Double?
|
||||
|
||||
init() {
|
||||
self.mapMode = .map
|
||||
self.displayingMapModeOptions = false
|
||||
self.selectedLocation = .initial
|
||||
self.proximityRadius = nil
|
||||
}
|
||||
}
|
||||
|
||||
@ -168,11 +195,11 @@ final class LocationViewControllerNode: ViewControllerTracingNode {
|
||||
private let context: AccountContext
|
||||
private var presentationData: PresentationData
|
||||
private let presentationDataPromise: Promise<PresentationData>
|
||||
private var mapMedia: TelegramMediaMap
|
||||
private var subject: Message
|
||||
private let interaction: LocationViewInteraction
|
||||
|
||||
private let listNode: ListView
|
||||
private let headerNode: LocationMapHeaderNode
|
||||
let headerNode: LocationMapHeaderNode
|
||||
private let optionsNode: LocationOptionsNode
|
||||
|
||||
private var enqueuedTransitions: [LocationViewTransaction] = []
|
||||
@ -185,11 +212,11 @@ final class LocationViewControllerNode: ViewControllerTracingNode {
|
||||
private var validLayout: (layout: ContainerViewLayout, navigationHeight: CGFloat)?
|
||||
private var listOffset: CGFloat?
|
||||
|
||||
init(context: AccountContext, presentationData: PresentationData, mapMedia: TelegramMediaMap, interaction: LocationViewInteraction) {
|
||||
init(context: AccountContext, presentationData: PresentationData, subject: Message, interaction: LocationViewInteraction) {
|
||||
self.context = context
|
||||
self.presentationData = presentationData
|
||||
self.presentationDataPromise = Promise(presentationData)
|
||||
self.mapMedia = mapMedia
|
||||
self.subject = subject
|
||||
self.interaction = interaction
|
||||
|
||||
self.state = LocationViewState()
|
||||
@ -213,44 +240,131 @@ final class LocationViewControllerNode: ViewControllerTracingNode {
|
||||
self.addSubnode(self.headerNode)
|
||||
self.addSubnode(self.optionsNode)
|
||||
|
||||
let distance: Signal<Double?, NoError> = .single(nil)
|
||||
let userLocation: Signal<CLLocation?, NoError> = .single(nil)
|
||||
|> then(
|
||||
throttledUserLocation(self.headerNode.mapNode.userLocation)
|
||||
|> map { userLocation -> Double? in
|
||||
let location = CLLocation(latitude: mapMedia.latitude, longitude: mapMedia.longitude)
|
||||
return userLocation.flatMap { location.distance(from: $0) }
|
||||
}
|
||||
)
|
||||
let address: Signal<String?, NoError>
|
||||
|
||||
var eta: Signal<Double?, NoError> = .single(nil)
|
||||
|> then(
|
||||
driveEta(coordinate: mapMedia.coordinate)
|
||||
)
|
||||
if let venue = mapMedia.venue, let venueAddress = venue.address, !venueAddress.isEmpty {
|
||||
address = .single(venueAddress)
|
||||
} else if mapMedia.liveBroadcastingTimeout == nil {
|
||||
address = .single(nil)
|
||||
|> then(
|
||||
reverseGeocodeLocation(latitude: mapMedia.latitude, longitude: mapMedia.longitude)
|
||||
|> map { placemark -> String? in
|
||||
return placemark?.compactDisplayAddress ?? ""
|
||||
}
|
||||
)
|
||||
} else {
|
||||
address = .single(nil)
|
||||
var address: Signal<String?, NoError> = .single(nil)
|
||||
|
||||
if let location = getLocation(from: subject), location.liveBroadcastingTimeout == nil {
|
||||
eta = .single(nil)
|
||||
|> then(driveEta(coordinate: location.coordinate))
|
||||
|
||||
if let venue = location.venue, let venueAddress = venue.address, !venueAddress.isEmpty {
|
||||
address = .single(venueAddress)
|
||||
} else {
|
||||
address = .single(nil)
|
||||
|> then(
|
||||
reverseGeocodeLocation(latitude: location.latitude, longitude: location.longitude)
|
||||
|> map { placemark -> String? in
|
||||
return placemark?.compactDisplayAddress ?? ""
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
let liveLocations = topPeerActiveLiveLocationMessages(viewTracker: context.account.viewTracker, accountPeerId: context.account.peerId, peerId: subject.id.peerId)
|
||||
|> map { _, messages -> [Message] in
|
||||
return messages
|
||||
}
|
||||
|
||||
let previousState = Atomic<LocationViewState?>(value: nil)
|
||||
let previousUserAnnotation = Atomic<LocationPinAnnotation?>(value: nil)
|
||||
let previousAnnotations = Atomic<[LocationPinAnnotation]>(value: [])
|
||||
let previousEntries = Atomic<[LocationViewEntry]?>(value: nil)
|
||||
|
||||
self.disposable = (combineLatest(self.presentationDataPromise.get(), self.statePromise.get(), self.headerNode.mapNode.userLocation, distance, address, eta)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] presentationData, state, userLocation, distance, address, eta in
|
||||
if let strongSelf = self {
|
||||
let selfPeer = context.account.postbox.transaction { transaction -> Peer? in
|
||||
return transaction.getPeer(context.account.peerId)
|
||||
}
|
||||
|
||||
self.disposable = (combineLatest(self.presentationDataPromise.get(), self.statePromise.get(), selfPeer, liveLocations, self.headerNode.mapNode.userLocation, userLocation, address, eta)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] presentationData, state, selfPeer, liveLocations, userLocation, distance, address, eta in
|
||||
if let strongSelf = self, let location = getLocation(from: subject) {
|
||||
var entries: [LocationViewEntry] = []
|
||||
|
||||
entries.append(.info(presentationData.theme, mapMedia, address, distance, eta))
|
||||
var annotations: [LocationPinAnnotation] = []
|
||||
var userAnnotation: LocationPinAnnotation? = nil
|
||||
var effectiveLiveLocations: [Message] = liveLocations
|
||||
|
||||
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
|
||||
|
||||
var proximityNotification: Bool? = nil
|
||||
var index: Int = 0
|
||||
|
||||
if location.liveBroadcastingTimeout == nil {
|
||||
let subjectLocation = CLLocation(latitude: location.latitude, longitude: location.longitude)
|
||||
let distance = userLocation.flatMap { subjectLocation.distance(from: $0) }
|
||||
|
||||
entries.append(.info(presentationData.theme, location, address, distance, eta))
|
||||
|
||||
annotations.append(LocationPinAnnotation(context: context, theme: presentationData.theme, location: location, forcedSelection: true))
|
||||
} else {
|
||||
var activeOwnLiveLocation: Message?
|
||||
for message in effectiveLiveLocations {
|
||||
if message.localTags.contains(.OutgoingLiveLocation) {
|
||||
activeOwnLiveLocation = message
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
let title: String
|
||||
let subtitle: String
|
||||
let beginTime: Double?
|
||||
let timeout: Double?
|
||||
|
||||
if let message = activeOwnLiveLocation {
|
||||
var liveBroadcastingTimeout: Int32 = 0
|
||||
if let location = getLocation(from: message), let timeout = location.liveBroadcastingTimeout {
|
||||
liveBroadcastingTimeout = timeout
|
||||
}
|
||||
title = presentationData.strings.Map_StopLiveLocation
|
||||
|
||||
var updateTimestamp = message.timestamp
|
||||
for attribute in message.attributes {
|
||||
if let attribute = attribute as? EditedMessageAttribute {
|
||||
updateTimestamp = attribute.date
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
subtitle = stringForRelativeLiveLocationTimestamp(strings: presentationData.strings, relativeTimestamp: updateTimestamp, relativeTo: currentTime, dateTimeFormat: presentationData.dateTimeFormat)
|
||||
beginTime = Double(message.timestamp)
|
||||
timeout = Double(liveBroadcastingTimeout)
|
||||
} else {
|
||||
title = presentationData.strings.Map_ShareLiveLocation
|
||||
subtitle = presentationData.strings.Map_ShareLiveLocation
|
||||
beginTime = nil
|
||||
timeout = nil
|
||||
}
|
||||
|
||||
entries.append(.toggleLiveLocation(presentationData.theme, title, subtitle, userLocation?.coordinate, beginTime, timeout))
|
||||
if effectiveLiveLocations.isEmpty {
|
||||
effectiveLiveLocations = [subject]
|
||||
}
|
||||
}
|
||||
|
||||
for message in effectiveLiveLocations {
|
||||
var liveBroadcastingTimeout: Int32 = 0
|
||||
if let location = getLocation(from: message), let timeout = location.liveBroadcastingTimeout {
|
||||
liveBroadcastingTimeout = timeout
|
||||
}
|
||||
let remainingTime = max(0, message.timestamp + liveBroadcastingTimeout - currentTime)
|
||||
if message.flags.contains(.Incoming) && remainingTime != 0 {
|
||||
proximityNotification = state.proximityRadius != nil
|
||||
}
|
||||
|
||||
let subjectLocation = CLLocation(latitude: location.latitude, longitude: location.longitude)
|
||||
let distance = userLocation.flatMap { subjectLocation.distance(from: $0) }
|
||||
entries.append(.liveLocation(presentationData.theme, message, distance, index))
|
||||
|
||||
if message.localTags.contains(.OutgoingLiveLocation), let selfPeer = selfPeer {
|
||||
userAnnotation = LocationPinAnnotation(context: context, theme: presentationData.theme, message: message, selfPeer: selfPeer, heading: nil)
|
||||
} else {
|
||||
annotations.append(LocationPinAnnotation(context: context, theme: presentationData.theme, message: message, selfPeer: nil, heading: nil))
|
||||
}
|
||||
index += 1
|
||||
}
|
||||
|
||||
let previousEntries = previousEntries.swap(entries)
|
||||
let previousState = previousState.swap(state)
|
||||
@ -258,17 +372,17 @@ final class LocationViewControllerNode: ViewControllerTracingNode {
|
||||
let transition = preparedTransition(from: previousEntries ?? [], to: entries, account: context.account, presentationData: presentationData, interaction: strongSelf.interaction)
|
||||
strongSelf.enqueueTransition(transition)
|
||||
|
||||
strongSelf.headerNode.updateState(mapMode: state.mapMode, displayingMapModeOptions: state.displayingMapModeOptions, displayingPlacesButton: false, proximityNotification: false, animated: false)
|
||||
strongSelf.headerNode.updateState(mapMode: state.mapMode, displayingMapModeOptions: state.displayingMapModeOptions, displayingPlacesButton: false, proximityNotification: proximityNotification, animated: false)
|
||||
|
||||
switch state.selectedLocation {
|
||||
case .initial:
|
||||
if previousState?.selectedLocation != .initial {
|
||||
strongSelf.headerNode.mapNode.setMapCenter(coordinate: mapMedia.coordinate, span: viewMapSpan, animated: previousState != nil)
|
||||
strongSelf.headerNode.mapNode.setMapCenter(coordinate: location.coordinate, span: viewMapSpan, animated: previousState != nil)
|
||||
}
|
||||
case let .coordinate(coordinate):
|
||||
if let previousState = previousState, case let .coordinate(previousCoordinate) = previousState.selectedLocation, previousCoordinate == coordinate {
|
||||
case let .coordinate(coordinate, defaultSpan):
|
||||
if let previousState = previousState, case let .coordinate(previousCoordinate, _) = previousState.selectedLocation, previousCoordinate == coordinate {
|
||||
} else {
|
||||
strongSelf.headerNode.mapNode.setMapCenter(coordinate: coordinate, span: viewMapSpan, animated: true)
|
||||
strongSelf.headerNode.mapNode.setMapCenter(coordinate: coordinate, span: defaultSpan ? defaultMapSpan : viewMapSpan, animated: true)
|
||||
}
|
||||
case .user:
|
||||
if previousState?.selectedLocation != .user, let userLocation = userLocation {
|
||||
@ -277,18 +391,21 @@ final class LocationViewControllerNode: ViewControllerTracingNode {
|
||||
case .custom:
|
||||
break
|
||||
}
|
||||
|
||||
let annotations: [LocationPinAnnotation] = [LocationPinAnnotation(context: context, theme: presentationData.theme, location: mapMedia, forcedSelection: true)]
|
||||
|
||||
let previousAnnotations = previousAnnotations.swap(annotations)
|
||||
let previousUserAnnotation = previousUserAnnotation.swap(userAnnotation)
|
||||
if (userAnnotation == nil) != (previousUserAnnotation == nil) {
|
||||
strongSelf.headerNode.mapNode.userLocationAnnotation = userAnnotation
|
||||
}
|
||||
if annotations != previousAnnotations {
|
||||
strongSelf.headerNode.mapNode.annotations = annotations
|
||||
}
|
||||
|
||||
strongSelf.headerNode.mapNode.activeProximityRadius = state.proximityRadius
|
||||
|
||||
if let (layout, navigationBarHeight) = strongSelf.validLayout {
|
||||
var updateLayout = false
|
||||
var transition: ContainedViewLayoutTransition = .animated(duration: 0.45, curve: .spring)
|
||||
|
||||
let transition: ContainedViewLayoutTransition = .animated(duration: 0.45, curve: .spring)
|
||||
if previousState?.displayingMapModeOptions != state.displayingMapModeOptions {
|
||||
updateLayout = true
|
||||
}
|
||||
@ -355,6 +472,10 @@ final class LocationViewControllerNode: ViewControllerTracingNode {
|
||||
self.statePromise.set(.single(self.state))
|
||||
}
|
||||
|
||||
func updateSendActionHighlight(_ highlighted: Bool) {
|
||||
self.headerNode.updateHighlight(highlighted)
|
||||
}
|
||||
|
||||
private func enqueueTransition(_ transition: LocationViewTransaction) {
|
||||
self.enqueuedTransitions.append(transition)
|
||||
|
||||
@ -371,9 +492,7 @@ final class LocationViewControllerNode: ViewControllerTracingNode {
|
||||
}
|
||||
self.enqueuedTransitions.remove(at: 0)
|
||||
|
||||
var options = ListViewDeleteAndInsertOptions()
|
||||
|
||||
|
||||
let options = ListViewDeleteAndInsertOptions()
|
||||
self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { [weak self] _ in
|
||||
})
|
||||
}
|
||||
@ -382,6 +501,39 @@ final class LocationViewControllerNode: ViewControllerTracingNode {
|
||||
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
|
||||
}
|
||||
|
||||
func setProximityIndicator(radius: Int32?) {
|
||||
if let radius = radius {
|
||||
self.headerNode.forceIsHidden = true
|
||||
|
||||
if var coordinate = self.headerNode.mapNode.currentUserLocation?.coordinate, let span = self.headerNode.mapNode.mapSpan {
|
||||
coordinate.latitude -= span.latitudeDelta * 0.11
|
||||
self.updateState { state in
|
||||
var state = state
|
||||
state.selectedLocation = .coordinate(coordinate, true)
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
self.headerNode.mapNode.proximityRadius = Double(radius)
|
||||
} else {
|
||||
self.headerNode.forceIsHidden = false
|
||||
self.headerNode.mapNode.proximityRadius = nil
|
||||
self.updateState { state in
|
||||
var state = state
|
||||
state.selectedLocation = .user
|
||||
return state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func setProximityRadius(radius: Int32?) {
|
||||
self.updateState { state in
|
||||
var state = state
|
||||
state.proximityRadius = radius.flatMap { Double($0) }
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
let isFirstLayout = self.validLayout == nil
|
||||
self.validLayout = (layout, navigationHeight)
|
||||
@ -397,7 +549,11 @@ final class LocationViewControllerNode: ViewControllerTracingNode {
|
||||
}
|
||||
|
||||
let overlap: CGFloat = 6.0
|
||||
let topInset: CGFloat = layout.size.height - layout.intrinsicInsets.bottom - 126.0 - overlap
|
||||
var topInset: CGFloat = layout.size.height - layout.intrinsicInsets.bottom - 126.0 - overlap
|
||||
if let location = getLocation(from: self.subject), location.liveBroadcastingTimeout != nil {
|
||||
topInset += 66.0
|
||||
}
|
||||
|
||||
let headerHeight: CGFloat
|
||||
if let listOffset = self.listOffset {
|
||||
headerHeight = max(0.0, listOffset + overlap)
|
||||
|
@ -231,10 +231,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
||||
dict[1893427255] = { return Api.Update.parse_updateChannelAvailableMessages($0) }
|
||||
dict[-513517117] = { return Api.Update.parse_updateDialogUnreadMark($0) }
|
||||
dict[1180041828] = { return Api.Update.parse_updateLangPackTooLong($0) }
|
||||
dict[1279515160] = { return Api.Update.parse_updateUserPinnedMessage($0) }
|
||||
dict[-1398708869] = { return Api.Update.parse_updateMessagePoll($0) }
|
||||
dict[1421875280] = { return Api.Update.parse_updateChatDefaultBannedRights($0) }
|
||||
dict[-519195831] = { return Api.Update.parse_updateChatPinnedMessage($0) }
|
||||
dict[422972864] = { return Api.Update.parse_updateFolderPeers($0) }
|
||||
dict[1852826908] = { return Api.Update.parse_updateDialogPinned($0) }
|
||||
dict[-99664734] = { return Api.Update.parse_updatePinnedDialogs($0) }
|
||||
@ -258,6 +256,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
||||
dict[1178116716] = { return Api.Update.parse_updateReadChannelDiscussionOutbox($0) }
|
||||
dict[610945826] = { return Api.Update.parse_updatePeerBlocked($0) }
|
||||
dict[-13975905] = { return Api.Update.parse_updateChannelUserTyping($0) }
|
||||
dict[-309990731] = { return Api.Update.parse_updatePinnedMessages($0) }
|
||||
dict[-2054649973] = { return Api.Update.parse_updatePinnedChannelMessages($0) }
|
||||
dict[136574537] = { return Api.messages.VotesList.parse_votesList($0) }
|
||||
dict[1558266229] = { return Api.PopularContact.parse_popularContact($0) }
|
||||
@ -541,8 +540,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
||||
dict[-1963717851] = { return Api.WallPaper.parse_wallPaperNoFile($0) }
|
||||
dict[-1938715001] = { return Api.messages.Messages.parse_messages($0) }
|
||||
dict[1951620897] = { return Api.messages.Messages.parse_messagesNotModified($0) }
|
||||
dict[-1725551049] = { return Api.messages.Messages.parse_channelMessages($0) }
|
||||
dict[-923939298] = { return Api.messages.Messages.parse_messagesSlice($0) }
|
||||
dict[1682413576] = { return Api.messages.Messages.parse_channelMessages($0) }
|
||||
dict[978610270] = { return Api.messages.Messages.parse_messagesSlice($0) }
|
||||
dict[-1022713000] = { return Api.Invoice.parse_invoice($0) }
|
||||
dict[1933519201] = { return Api.PeerSettings.parse_peerSettings($0) }
|
||||
dict[1577067778] = { return Api.auth.SentCode.parse_sentCode($0) }
|
||||
|
@ -1093,8 +1093,8 @@ public struct messages {
|
||||
public enum Messages: TypeConstructorDescription {
|
||||
case messages(messages: [Api.Message], chats: [Api.Chat], users: [Api.User])
|
||||
case messagesNotModified(count: Int32)
|
||||
case channelMessages(flags: Int32, pts: Int32, count: Int32, messages: [Api.Message], chats: [Api.Chat], users: [Api.User])
|
||||
case messagesSlice(flags: Int32, count: Int32, nextRate: Int32?, messages: [Api.Message], chats: [Api.Chat], users: [Api.User])
|
||||
case channelMessages(flags: Int32, pts: Int32, count: Int32, offsetIdOffset: Int32?, messages: [Api.Message], chats: [Api.Chat], users: [Api.User])
|
||||
case messagesSlice(flags: Int32, count: Int32, nextRate: Int32?, offsetIdOffset: Int32?, messages: [Api.Message], chats: [Api.Chat], users: [Api.User])
|
||||
|
||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||
switch self {
|
||||
@ -1124,13 +1124,14 @@ public struct messages {
|
||||
}
|
||||
serializeInt32(count, buffer: buffer, boxed: false)
|
||||
break
|
||||
case .channelMessages(let flags, let pts, let count, let messages, let chats, let users):
|
||||
case .channelMessages(let flags, let pts, let count, let offsetIdOffset, let messages, let chats, let users):
|
||||
if boxed {
|
||||
buffer.appendInt32(-1725551049)
|
||||
buffer.appendInt32(1682413576)
|
||||
}
|
||||
serializeInt32(flags, buffer: buffer, boxed: false)
|
||||
serializeInt32(pts, buffer: buffer, boxed: false)
|
||||
serializeInt32(count, buffer: buffer, boxed: false)
|
||||
if Int(flags) & Int(1 << 2) != 0 {serializeInt32(offsetIdOffset!, buffer: buffer, boxed: false)}
|
||||
buffer.appendInt32(481674261)
|
||||
buffer.appendInt32(Int32(messages.count))
|
||||
for item in messages {
|
||||
@ -1147,13 +1148,14 @@ public struct messages {
|
||||
item.serialize(buffer, true)
|
||||
}
|
||||
break
|
||||
case .messagesSlice(let flags, let count, let nextRate, let messages, let chats, let users):
|
||||
case .messagesSlice(let flags, let count, let nextRate, let offsetIdOffset, let messages, let chats, let users):
|
||||
if boxed {
|
||||
buffer.appendInt32(-923939298)
|
||||
buffer.appendInt32(978610270)
|
||||
}
|
||||
serializeInt32(flags, buffer: buffer, boxed: false)
|
||||
serializeInt32(count, buffer: buffer, boxed: false)
|
||||
if Int(flags) & Int(1 << 0) != 0 {serializeInt32(nextRate!, buffer: buffer, boxed: false)}
|
||||
if Int(flags) & Int(1 << 2) != 0 {serializeInt32(offsetIdOffset!, buffer: buffer, boxed: false)}
|
||||
buffer.appendInt32(481674261)
|
||||
buffer.appendInt32(Int32(messages.count))
|
||||
for item in messages {
|
||||
@ -1179,10 +1181,10 @@ public struct messages {
|
||||
return ("messages", [("messages", messages), ("chats", chats), ("users", users)])
|
||||
case .messagesNotModified(let count):
|
||||
return ("messagesNotModified", [("count", count)])
|
||||
case .channelMessages(let flags, let pts, let count, let messages, let chats, let users):
|
||||
return ("channelMessages", [("flags", flags), ("pts", pts), ("count", count), ("messages", messages), ("chats", chats), ("users", users)])
|
||||
case .messagesSlice(let flags, let count, let nextRate, let messages, let chats, let users):
|
||||
return ("messagesSlice", [("flags", flags), ("count", count), ("nextRate", nextRate), ("messages", messages), ("chats", chats), ("users", users)])
|
||||
case .channelMessages(let flags, let pts, let count, let offsetIdOffset, let messages, let chats, let users):
|
||||
return ("channelMessages", [("flags", flags), ("pts", pts), ("count", count), ("offsetIdOffset", offsetIdOffset), ("messages", messages), ("chats", chats), ("users", users)])
|
||||
case .messagesSlice(let flags, let count, let nextRate, let offsetIdOffset, let messages, let chats, let users):
|
||||
return ("messagesSlice", [("flags", flags), ("count", count), ("nextRate", nextRate), ("offsetIdOffset", offsetIdOffset), ("messages", messages), ("chats", chats), ("users", users)])
|
||||
}
|
||||
}
|
||||
|
||||
@ -1227,26 +1229,29 @@ public struct messages {
|
||||
_2 = reader.readInt32()
|
||||
var _3: Int32?
|
||||
_3 = reader.readInt32()
|
||||
var _4: [Api.Message]?
|
||||
var _4: Int32?
|
||||
if Int(_1!) & Int(1 << 2) != 0 {_4 = reader.readInt32() }
|
||||
var _5: [Api.Message]?
|
||||
if let _ = reader.readInt32() {
|
||||
_4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Message.self)
|
||||
_5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Message.self)
|
||||
}
|
||||
var _5: [Api.Chat]?
|
||||
var _6: [Api.Chat]?
|
||||
if let _ = reader.readInt32() {
|
||||
_5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self)
|
||||
_6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self)
|
||||
}
|
||||
var _6: [Api.User]?
|
||||
var _7: [Api.User]?
|
||||
if let _ = reader.readInt32() {
|
||||
_6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self)
|
||||
_7 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self)
|
||||
}
|
||||
let _c1 = _1 != nil
|
||||
let _c2 = _2 != nil
|
||||
let _c3 = _3 != nil
|
||||
let _c4 = _4 != nil
|
||||
let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil
|
||||
let _c5 = _5 != nil
|
||||
let _c6 = _6 != nil
|
||||
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 {
|
||||
return Api.messages.Messages.channelMessages(flags: _1!, pts: _2!, count: _3!, messages: _4!, chats: _5!, users: _6!)
|
||||
let _c7 = _7 != nil
|
||||
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 {
|
||||
return Api.messages.Messages.channelMessages(flags: _1!, pts: _2!, count: _3!, offsetIdOffset: _4, messages: _5!, chats: _6!, users: _7!)
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
@ -1259,26 +1264,29 @@ public struct messages {
|
||||
_2 = reader.readInt32()
|
||||
var _3: Int32?
|
||||
if Int(_1!) & Int(1 << 0) != 0 {_3 = reader.readInt32() }
|
||||
var _4: [Api.Message]?
|
||||
var _4: Int32?
|
||||
if Int(_1!) & Int(1 << 2) != 0 {_4 = reader.readInt32() }
|
||||
var _5: [Api.Message]?
|
||||
if let _ = reader.readInt32() {
|
||||
_4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Message.self)
|
||||
_5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Message.self)
|
||||
}
|
||||
var _5: [Api.Chat]?
|
||||
var _6: [Api.Chat]?
|
||||
if let _ = reader.readInt32() {
|
||||
_5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self)
|
||||
_6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self)
|
||||
}
|
||||
var _6: [Api.User]?
|
||||
var _7: [Api.User]?
|
||||
if let _ = reader.readInt32() {
|
||||
_6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self)
|
||||
_7 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self)
|
||||
}
|
||||
let _c1 = _1 != nil
|
||||
let _c2 = _2 != nil
|
||||
let _c3 = (Int(_1!) & Int(1 << 0) == 0) || _3 != nil
|
||||
let _c4 = _4 != nil
|
||||
let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil
|
||||
let _c5 = _5 != nil
|
||||
let _c6 = _6 != nil
|
||||
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 {
|
||||
return Api.messages.Messages.messagesSlice(flags: _1!, count: _2!, nextRate: _3, messages: _4!, chats: _5!, users: _6!)
|
||||
let _c7 = _7 != nil
|
||||
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 {
|
||||
return Api.messages.Messages.messagesSlice(flags: _1!, count: _2!, nextRate: _3, offsetIdOffset: _4, messages: _5!, chats: _6!, users: _7!)
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
@ -6158,10 +6166,8 @@ public extension Api {
|
||||
case updateChannelAvailableMessages(channelId: Int32, availableMinId: Int32)
|
||||
case updateDialogUnreadMark(flags: Int32, peer: Api.DialogPeer)
|
||||
case updateLangPackTooLong(langCode: String)
|
||||
case updateUserPinnedMessage(userId: Int32, id: Int32)
|
||||
case updateMessagePoll(flags: Int32, pollId: Int64, poll: Api.Poll?, results: Api.PollResults)
|
||||
case updateChatDefaultBannedRights(peer: Api.Peer, defaultBannedRights: Api.ChatBannedRights, version: Int32)
|
||||
case updateChatPinnedMessage(chatId: Int32, id: Int32, version: Int32)
|
||||
case updateFolderPeers(folderPeers: [Api.FolderPeer], pts: Int32, ptsCount: Int32)
|
||||
case updateDialogPinned(flags: Int32, folderId: Int32?, peer: Api.DialogPeer)
|
||||
case updatePinnedDialogs(flags: Int32, folderId: Int32?, order: [Api.DialogPeer]?)
|
||||
@ -6185,6 +6191,7 @@ public extension Api {
|
||||
case updateReadChannelDiscussionOutbox(channelId: Int32, topMsgId: Int32, readMaxId: Int32)
|
||||
case updatePeerBlocked(peerId: Api.Peer, blocked: Api.Bool)
|
||||
case updateChannelUserTyping(flags: Int32, channelId: Int32, topMsgId: Int32?, userId: Int32, action: Api.SendMessageAction)
|
||||
case updatePinnedMessages(flags: Int32, peer: Api.Peer, messages: [Int32], pts: Int32, ptsCount: Int32)
|
||||
case updatePinnedChannelMessages(flags: Int32, channelId: Int32, messages: [Int32], pts: Int32, ptsCount: Int32)
|
||||
|
||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||
@ -6669,13 +6676,6 @@ public extension Api {
|
||||
}
|
||||
serializeString(langCode, buffer: buffer, boxed: false)
|
||||
break
|
||||
case .updateUserPinnedMessage(let userId, let id):
|
||||
if boxed {
|
||||
buffer.appendInt32(1279515160)
|
||||
}
|
||||
serializeInt32(userId, buffer: buffer, boxed: false)
|
||||
serializeInt32(id, buffer: buffer, boxed: false)
|
||||
break
|
||||
case .updateMessagePoll(let flags, let pollId, let poll, let results):
|
||||
if boxed {
|
||||
buffer.appendInt32(-1398708869)
|
||||
@ -6693,14 +6693,6 @@ public extension Api {
|
||||
defaultBannedRights.serialize(buffer, true)
|
||||
serializeInt32(version, buffer: buffer, boxed: false)
|
||||
break
|
||||
case .updateChatPinnedMessage(let chatId, let id, let version):
|
||||
if boxed {
|
||||
buffer.appendInt32(-519195831)
|
||||
}
|
||||
serializeInt32(chatId, buffer: buffer, boxed: false)
|
||||
serializeInt32(id, buffer: buffer, boxed: false)
|
||||
serializeInt32(version, buffer: buffer, boxed: false)
|
||||
break
|
||||
case .updateFolderPeers(let folderPeers, let pts, let ptsCount):
|
||||
if boxed {
|
||||
buffer.appendInt32(422972864)
|
||||
@ -6908,6 +6900,20 @@ public extension Api {
|
||||
serializeInt32(userId, buffer: buffer, boxed: false)
|
||||
action.serialize(buffer, true)
|
||||
break
|
||||
case .updatePinnedMessages(let flags, let peer, let messages, let pts, let ptsCount):
|
||||
if boxed {
|
||||
buffer.appendInt32(-309990731)
|
||||
}
|
||||
serializeInt32(flags, buffer: buffer, boxed: false)
|
||||
peer.serialize(buffer, true)
|
||||
buffer.appendInt32(481674261)
|
||||
buffer.appendInt32(Int32(messages.count))
|
||||
for item in messages {
|
||||
serializeInt32(item, buffer: buffer, boxed: false)
|
||||
}
|
||||
serializeInt32(pts, buffer: buffer, boxed: false)
|
||||
serializeInt32(ptsCount, buffer: buffer, boxed: false)
|
||||
break
|
||||
case .updatePinnedChannelMessages(let flags, let channelId, let messages, let pts, let ptsCount):
|
||||
if boxed {
|
||||
buffer.appendInt32(-2054649973)
|
||||
@ -7043,14 +7049,10 @@ public extension Api {
|
||||
return ("updateDialogUnreadMark", [("flags", flags), ("peer", peer)])
|
||||
case .updateLangPackTooLong(let langCode):
|
||||
return ("updateLangPackTooLong", [("langCode", langCode)])
|
||||
case .updateUserPinnedMessage(let userId, let id):
|
||||
return ("updateUserPinnedMessage", [("userId", userId), ("id", id)])
|
||||
case .updateMessagePoll(let flags, let pollId, let poll, let results):
|
||||
return ("updateMessagePoll", [("flags", flags), ("pollId", pollId), ("poll", poll), ("results", results)])
|
||||
case .updateChatDefaultBannedRights(let peer, let defaultBannedRights, let version):
|
||||
return ("updateChatDefaultBannedRights", [("peer", peer), ("defaultBannedRights", defaultBannedRights), ("version", version)])
|
||||
case .updateChatPinnedMessage(let chatId, let id, let version):
|
||||
return ("updateChatPinnedMessage", [("chatId", chatId), ("id", id), ("version", version)])
|
||||
case .updateFolderPeers(let folderPeers, let pts, let ptsCount):
|
||||
return ("updateFolderPeers", [("folderPeers", folderPeers), ("pts", pts), ("ptsCount", ptsCount)])
|
||||
case .updateDialogPinned(let flags, let folderId, let peer):
|
||||
@ -7097,6 +7099,8 @@ public extension Api {
|
||||
return ("updatePeerBlocked", [("peerId", peerId), ("blocked", blocked)])
|
||||
case .updateChannelUserTyping(let flags, let channelId, let topMsgId, let userId, let action):
|
||||
return ("updateChannelUserTyping", [("flags", flags), ("channelId", channelId), ("topMsgId", topMsgId), ("userId", userId), ("action", action)])
|
||||
case .updatePinnedMessages(let flags, let peer, let messages, let pts, let ptsCount):
|
||||
return ("updatePinnedMessages", [("flags", flags), ("peer", peer), ("messages", messages), ("pts", pts), ("ptsCount", ptsCount)])
|
||||
case .updatePinnedChannelMessages(let flags, let channelId, let messages, let pts, let ptsCount):
|
||||
return ("updatePinnedChannelMessages", [("flags", flags), ("channelId", channelId), ("messages", messages), ("pts", pts), ("ptsCount", ptsCount)])
|
||||
}
|
||||
@ -8062,20 +8066,6 @@ public extension Api {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
public static func parse_updateUserPinnedMessage(_ reader: BufferReader) -> Update? {
|
||||
var _1: Int32?
|
||||
_1 = reader.readInt32()
|
||||
var _2: Int32?
|
||||
_2 = reader.readInt32()
|
||||
let _c1 = _1 != nil
|
||||
let _c2 = _2 != nil
|
||||
if _c1 && _c2 {
|
||||
return Api.Update.updateUserPinnedMessage(userId: _1!, id: _2!)
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
public static func parse_updateMessagePoll(_ reader: BufferReader) -> Update? {
|
||||
var _1: Int32?
|
||||
_1 = reader.readInt32()
|
||||
@ -8121,23 +8111,6 @@ public extension Api {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
public static func parse_updateChatPinnedMessage(_ reader: BufferReader) -> Update? {
|
||||
var _1: Int32?
|
||||
_1 = reader.readInt32()
|
||||
var _2: Int32?
|
||||
_2 = reader.readInt32()
|
||||
var _3: Int32?
|
||||
_3 = reader.readInt32()
|
||||
let _c1 = _1 != nil
|
||||
let _c2 = _2 != nil
|
||||
let _c3 = _3 != nil
|
||||
if _c1 && _c2 && _c3 {
|
||||
return Api.Update.updateChatPinnedMessage(chatId: _1!, id: _2!, version: _3!)
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
public static func parse_updateFolderPeers(_ reader: BufferReader) -> Update? {
|
||||
var _1: [Api.FolderPeer]?
|
||||
if let _ = reader.readInt32() {
|
||||
@ -8550,6 +8523,33 @@ public extension Api {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
public static func parse_updatePinnedMessages(_ reader: BufferReader) -> Update? {
|
||||
var _1: Int32?
|
||||
_1 = reader.readInt32()
|
||||
var _2: Api.Peer?
|
||||
if let signature = reader.readInt32() {
|
||||
_2 = Api.parse(reader, signature: signature) as? Api.Peer
|
||||
}
|
||||
var _3: [Int32]?
|
||||
if let _ = reader.readInt32() {
|
||||
_3 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self)
|
||||
}
|
||||
var _4: Int32?
|
||||
_4 = reader.readInt32()
|
||||
var _5: Int32?
|
||||
_5 = reader.readInt32()
|
||||
let _c1 = _1 != nil
|
||||
let _c2 = _2 != nil
|
||||
let _c3 = _3 != nil
|
||||
let _c4 = _4 != nil
|
||||
let _c5 = _5 != nil
|
||||
if _c1 && _c2 && _c3 && _c4 && _c5 {
|
||||
return Api.Update.updatePinnedMessages(flags: _1!, peer: _2!, messages: _3!, pts: _4!, ptsCount: _5!)
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
public static func parse_updatePinnedChannelMessages(_ reader: BufferReader) -> Update? {
|
||||
var _1: Int32?
|
||||
_1 = reader.readInt32()
|
||||
|
@ -3781,15 +3781,14 @@ public extension Api {
|
||||
})
|
||||
}
|
||||
|
||||
public static func requestProximityNotification(flags: Int32, peer: Api.InputPeer, msgId: Int32, ownLocation: Api.InputGeoPoint?, maxDistance: Int32?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
|
||||
public static func requestProximityNotification(flags: Int32, peer: Api.InputPeer, msgId: Int32, maxDistance: Int32?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
|
||||
let buffer = Buffer()
|
||||
buffer.appendInt32(-699657935)
|
||||
buffer.appendInt32(-1322540260)
|
||||
serializeInt32(flags, buffer: buffer, boxed: false)
|
||||
peer.serialize(buffer, true)
|
||||
serializeInt32(msgId, buffer: buffer, boxed: false)
|
||||
if Int(flags) & Int(1 << 0) != 0 {ownLocation!.serialize(buffer, true)}
|
||||
if Int(flags) & Int(1 << 0) != 0 {serializeInt32(maxDistance!, buffer: buffer, boxed: false)}
|
||||
return (FunctionDescription(name: "messages.requestProximityNotification", parameters: [("flags", flags), ("peer", peer), ("msgId", msgId), ("ownLocation", ownLocation), ("maxDistance", maxDistance)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in
|
||||
return (FunctionDescription(name: "messages.requestProximityNotification", parameters: [("flags", flags), ("peer", peer), ("msgId", msgId), ("maxDistance", maxDistance)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in
|
||||
let reader = BufferReader(buffer)
|
||||
var result: Api.Bool?
|
||||
if let signature = reader.readInt32() {
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -442,8 +442,8 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
|
||||
attributedString = addAttributesToStringWithRanges(strings.Notification_Joined(authorName), body: bodyAttributes, argumentAttributes: peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: [(0, message.author?.id)]))
|
||||
case .phoneNumberRequest:
|
||||
attributedString = nil
|
||||
case .geoProximityReached:
|
||||
attributedString = nil
|
||||
case let .geoProximityReached(distance):
|
||||
attributedString = NSAttributedString(string: "\(message.peers[message.id.peerId]?.compactDisplayTitle ?? "") is now within \(distance) m from you", font: titleFont, textColor: primaryTextColor)
|
||||
case .unknown:
|
||||
attributedString = nil
|
||||
}
|
||||
|
Binary file not shown.
@ -320,7 +320,7 @@ final class AuthorizedApplicationContext {
|
||||
}
|
||||
}
|
||||
|
||||
if chatIsVisible {
|
||||
if false, chatIsVisible {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -63,33 +63,20 @@ func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool {
|
||||
return nil
|
||||
}))
|
||||
return true
|
||||
case let .map(mapMedia):
|
||||
case .map:
|
||||
params.dismissInput()
|
||||
|
||||
if mapMedia.liveBroadcastingTimeout == nil {
|
||||
let controllerParams = LocationViewParams(sendLiveLocation: { location in
|
||||
let outMessage: EnqueueMessage = .message(text: "", attributes: [], mediaReference: .standalone(media: location), replyToMessageId: nil, localGroupingKey: nil)
|
||||
params.enqueueMessage(outMessage)
|
||||
}, stopLiveLocation: {
|
||||
params.context.liveLocationManager?.cancelLiveLocation(peerId: params.message.id.peerId)
|
||||
}, openUrl: params.openUrl, openPeer: { peer in
|
||||
params.openPeer(peer, .info)
|
||||
})
|
||||
let controller = LocationViewController(context: params.context, mapMedia: mapMedia, params: controllerParams)
|
||||
controller.navigationPresentation = .modal
|
||||
params.navigationController?.pushViewController(controller)
|
||||
} else {
|
||||
let controller = legacyLocationController(message: params.message, mapMedia: mapMedia, context: params.context, openPeer: { peer in
|
||||
params.openPeer(peer, .info)
|
||||
}, sendLiveLocation: { coordinate, period in
|
||||
let outMessage: EnqueueMessage = .message(text: "", attributes: [], mediaReference: .standalone(media: TelegramMediaMap(latitude: coordinate.latitude, longitude: coordinate.longitude, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: nil, liveBroadcastingTimeout: period)), replyToMessageId: nil, localGroupingKey: nil)
|
||||
params.enqueueMessage(outMessage)
|
||||
}, stopLiveLocation: {
|
||||
params.context.liveLocationManager?.cancelLiveLocation(peerId: params.message.id.peerId)
|
||||
}, openUrl: params.openUrl)
|
||||
controller.navigationPresentation = .modal
|
||||
params.navigationController?.pushViewController(controller)
|
||||
}
|
||||
let controllerParams = LocationViewParams(sendLiveLocation: { location in
|
||||
let outMessage: EnqueueMessage = .message(text: "", attributes: [], mediaReference: .standalone(media: location), replyToMessageId: nil, localGroupingKey: nil)
|
||||
params.enqueueMessage(outMessage)
|
||||
}, stopLiveLocation: {
|
||||
params.context.liveLocationManager?.cancelLiveLocation(peerId: params.message.id.peerId)
|
||||
}, openUrl: params.openUrl, openPeer: { peer in
|
||||
params.openPeer(peer, .info)
|
||||
})
|
||||
let controller = LocationViewController(context: params.context, subject: params.message, params: controllerParams)
|
||||
controller.navigationPresentation = .modal
|
||||
params.navigationController?.pushViewController(controller)
|
||||
return true
|
||||
case let .stickerPack(reference):
|
||||
let controller = StickerPackScreen(context: params.context, mainStickerPack: reference, stickerPacks: [reference], parentNavigationController: params.navigationController, sendSticker: params.sendSticker, actionPerformed: { info, items, action in
|
||||
|
Loading…
x
Reference in New Issue
Block a user