Merge commit '7621e2f8dec938cf48181c8b10afc9b01f444e68' into beta

This commit is contained in:
Ilya Laktyushin
2025-12-06 02:17:48 +04:00
commit 8344b97e03
28070 changed files with 7995182 additions and 0 deletions
+26
View File
@@ -0,0 +1,26 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "OpenInExternalAppUI",
module_name = "OpenInExternalAppUI",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/Display:Display",
"//submodules/TelegramCore:TelegramCore",
"//submodules/AccountContext:AccountContext",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/PhotoResources:PhotoResources",
"//submodules/UrlEscaping:UrlEscaping",
"//submodules/AppBundle:AppBundle",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,264 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import MapKit
import TelegramPresentationData
import AccountContext
import PhotoResources
import AppBundle
public struct OpenInControllerAction {
public let title: String
public let action: () -> Void
public init(title: String, action: @escaping () -> Void) {
self.title = title
self.action = action
}
}
public final class OpenInActionSheetController: ActionSheetController {
private var presentationDisposable: Disposable?
private let _ready = Promise<Bool>()
override public var ready: Promise<Bool> {
return self._ready
}
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, forceTheme: PresentationTheme? = nil, item: OpenInItem, additionalAction: OpenInControllerAction? = nil, openUrl: @escaping (String) -> Void) {
var presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }
if let forceTheme = forceTheme {
presentationData = presentationData.withUpdated(theme: forceTheme)
}
let strings = presentationData.strings
super.init(theme: ActionSheetControllerTheme(presentationData: presentationData))
self.presentationDisposable = (updatedPresentationData?.signal ?? context.sharedContext.presentationData).start(next: { [weak self] presentationData in
if let strongSelf = self {
var presentationData = presentationData
if let forceTheme = forceTheme {
presentationData = presentationData.withUpdated(theme: forceTheme)
}
strongSelf.theme = ActionSheetControllerTheme(presentationData: presentationData)
}
})
self._ready.set(.single(true))
let invokeActionImpl: (OpenInAction) -> Void = { action in
switch action {
case let .openUrl(url):
openUrl(url)
case let .openLocation(latitude, longitude, directions):
let placemark = MKPlacemark(coordinate: CLLocationCoordinate2DMake(latitude, longitude), addressDictionary: [:])
let mapItem = MKMapItem(placemark: placemark)
if let directions = directions {
let options = [ MKLaunchOptionsDirectionsModeKey: directions.launchOptions ]
MKMapItem.openMaps(with: [MKMapItem.forCurrentLocation(), mapItem], launchOptions: options)
} else {
mapItem.openInMaps(launchOptions: nil)
}
default:
break
}
}
var items: [ActionSheetItem] = []
items.append(OpenInActionSheetItem(context: context, strings: strings, options: availableOpenInOptions(context: context, item: item), invokeAction: invokeActionImpl))
if let action = additionalAction {
items.append(ActionSheetButtonItem(title: action.title, action: { [weak self] in
action.action()
self?.dismissAnimated()
}))
}
self.setItemGroups([
ActionSheetItemGroup(items: items),
ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: strings.Common_Cancel, action: { [weak self] in
self?.dismissAnimated()
})
])
])
}
required public init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.presentationDisposable?.dispose()
}
}
private final class OpenInActionSheetItem: ActionSheetItem {
let context: AccountContext
let strings: PresentationStrings
let options: [OpenInOption]
let invokeAction: (OpenInAction) -> Void
init(context: AccountContext, strings: PresentationStrings, options: [OpenInOption], invokeAction: @escaping (OpenInAction) -> Void) {
self.context = context
self.strings = strings
self.options = options
self.invokeAction = invokeAction
}
func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode {
return OpenInActionSheetItemNode(context: self.context, theme: theme, strings: self.strings, options: self.options, invokeAction: self.invokeAction)
}
func updateNode(_ node: ActionSheetItemNode) {
}
}
private final class OpenInActionSheetItemNode: ActionSheetItemNode {
let theme: ActionSheetControllerTheme
let strings: PresentationStrings
let titleNode: ASTextNode
let scrollNode: ASScrollNode
let openInNodes: [OpenInAppNode]
init(context: AccountContext, theme: ActionSheetControllerTheme, strings: PresentationStrings, options: [OpenInOption], invokeAction: @escaping (OpenInAction) -> Void) {
self.theme = theme
self.strings = strings
let titleFont = Font.medium(floor(theme.baseFontSize * 20.0 / 17.0))
self.titleNode = ASTextNode()
self.titleNode.isUserInteractionEnabled = false
self.titleNode.displaysAsynchronously = true
self.titleNode.attributedText = NSAttributedString(string: strings.Map_OpenIn, font: titleFont, textColor: theme.primaryTextColor, paragraphAlignment: .center)
self.scrollNode = ASScrollNode()
self.scrollNode.view.showsVerticalScrollIndicator = false
self.scrollNode.view.showsHorizontalScrollIndicator = false
self.scrollNode.view.clipsToBounds = false
self.scrollNode.view.scrollsToTop = false
self.scrollNode.view.delaysContentTouches = false
self.scrollNode.scrollableDirections = [.left, .right]
self.openInNodes = options.map { option in
let node = OpenInAppNode()
node.setup(context: context, theme: theme, option: option, invokeAction: invokeAction)
return node
}
super.init(theme: theme)
self.addSubnode(self.titleNode)
if !self.openInNodes.isEmpty {
for openInNode in openInNodes {
self.scrollNode.addSubnode(openInNode)
}
self.addSubnode(self.scrollNode)
}
}
public override func updateLayout(constrainedSize: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
let size = CGSize(width: constrainedSize.width, height: 148.0)
let titleSize = self.titleNode.measure(size)
self.titleNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 16.0), size: CGSize(width: size.width, height: titleSize.height))
self.scrollNode.frame = CGRect(origin: CGPoint(x: 0, y: 36.0), size: CGSize(width: size.width, height: size.height - 36.0))
let nodeInset: CGFloat = 2.0
let nodeSize = CGSize(width: 80.0, height: 112.0)
var nodeOffset = nodeInset
for node in self.openInNodes {
node.frame = CGRect(origin: CGPoint(x: nodeOffset, y: 0.0), size: nodeSize)
nodeOffset += nodeSize.width
}
if let lastNode = self.openInNodes.last {
let contentSize = CGSize(width: lastNode.frame.maxX + nodeInset, height: self.scrollNode.frame.height)
if self.scrollNode.view.contentSize != contentSize {
self.scrollNode.view.contentSize = contentSize
}
}
self.updateInternalLayout(size, constrainedSize: constrainedSize)
return size
}
}
private final class OpenInAppNode : ASDisplayNode {
private let iconNode: TransformImageNode
private let textNode: ASTextNode
private var action: (() -> Void)?
override init() {
self.iconNode = TransformImageNode()
self.iconNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 60.0, height: 60.0))
self.iconNode.isLayerBacked = true
self.textNode = ASTextNode()
self.textNode.isUserInteractionEnabled = false
self.textNode.displaysAsynchronously = true
super.init()
self.addSubnode(self.iconNode)
self.addSubnode(self.textNode)
}
func setup(context: AccountContext, theme: ActionSheetControllerTheme, option: OpenInOption, invokeAction: @escaping (OpenInAction) -> Void) {
let textFont = Font.regular(floor(theme.baseFontSize * 11.0 / 17.0))
self.textNode.attributedText = NSAttributedString(string: option.title, font: textFont, textColor: theme.primaryTextColor, paragraphAlignment: .center)
let iconSize = CGSize(width: 60.0, height: 60.0)
let makeLayout = self.iconNode.asyncLayout()
let applyLayout = makeLayout(TransformImageArguments(corners: ImageCorners(radius: 16.0), imageSize: iconSize, boundingSize: iconSize, intrinsicInsets: UIEdgeInsets()))
applyLayout()
switch option.application {
case .safari:
if let image = UIImage(bundleImageName: "Open In/Safari") {
self.iconNode.setSignal(openInAppIcon(engine: context.engine, appIcon: .image(image: image)))
}
case .maps:
if let image = UIImage(bundleImageName: "Open In/Maps") {
self.iconNode.setSignal(openInAppIcon(engine: context.engine, appIcon: .image(image: image)))
}
case let .other(_, identifier, _, store):
self.iconNode.setSignal(openInAppIcon(engine: context.engine, appIcon: .resource(resource: OpenInAppIconResource(appStoreId: identifier, store: store))))
}
self.action = {
invokeAction(option.action())
}
}
override func didLoad() {
super.didLoad()
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
}
@objc func tapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
self.action?()
}
}
override func layout() {
super.layout()
let bounds = self.bounds
self.iconNode.frame = CGRect(origin: CGPoint(x: 10.0, y: 14.0), size: CGSize(width: 60.0, height: 60.0))
self.textNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 14.0 + 60.0 + 4.0), size: CGSize(width: bounds.size.width, height: 16.0))
}
}
@@ -0,0 +1,180 @@
import Foundation
import UIKit
import TelegramCore
import SwiftSignalKit
import Display
public struct OpenInAppIconResourceId {
public let appStoreId: Int64
public var uniqueId: String {
return "app-icon-\(appStoreId)"
}
public var hashValue: Int {
return self.appStoreId.hashValue
}
}
public class OpenInAppIconResource {
public let appStoreId: Int64
public let store: String?
public init(appStoreId: Int64, store: String?) {
self.appStoreId = appStoreId
self.store = store
}
public var id: EngineMediaResource.Id {
return EngineMediaResource.Id(OpenInAppIconResourceId(appStoreId: self.appStoreId).uniqueId)
}
}
public func fetchOpenInAppIconResource(engine: TelegramEngine, resource: OpenInAppIconResource) -> Signal<EngineMediaResource.Fetch.Result, EngineMediaResource.Fetch.Error> {
return Signal { subscriber in
let metaUrl: String
if let store = resource.store {
metaUrl = "https://itunes.apple.com/\(store)/lookup?id=\(resource.appStoreId)"
} else {
metaUrl = "https://itunes.apple.com/lookup?id=\(resource.appStoreId)"
}
let fetchDisposable = MetaDisposable()
let disposable = fetchHttpResource(url: metaUrl).start(next: { result in
if case let .dataPart(_, data, _, complete) = result, complete {
guard let dict = (try? JSONSerialization.jsonObject(with: data, options: [])) as? [String: Any] else {
subscriber.putError(.generic)
return
}
guard let results = dict["results"] as? [Any] else {
subscriber.putError(.generic)
return
}
guard let result = results.first as? [String: Any] else {
subscriber.putError(.generic)
return
}
guard let artworkUrl = result["artworkUrl100"] as? String else {
subscriber.putError(.generic)
return
}
if artworkUrl.isEmpty {
subscriber.putError(.generic)
return
} else {
fetchDisposable.set(engine.resources.httpData(url: artworkUrl).start(next: { data in
let file = EngineTempBox.shared.tempFile(fileName: "image.jpg")
let _ = try? data.write(to: URL(fileURLWithPath: file.path))
subscriber.putNext(.moveTempFile(file: file))
}, completed: {
subscriber.putCompletion()
}))
}
}
})
return ActionDisposable {
disposable.dispose()
fetchDisposable.dispose()
}
}
}
private func openInAppIconData(engine: TelegramEngine, appIcon: OpenInAppIconResource) -> Signal<Data?, NoError> {
let appIconResource = engine.resources.custom(
id: appIcon.id.stringRepresentation,
fetch: EngineMediaResource.Fetch {
return fetchOpenInAppIconResource(engine: engine, resource: appIcon)
}
)
return appIconResource
|> map { data -> Data? in
if data.isComplete {
let loadedData: Data? = try? Data(contentsOf: URL(fileURLWithPath: data.path), options: [])
return loadedData
} else {
return nil
}
}
}
public enum OpenInAppIcon {
case resource(resource: OpenInAppIconResource)
case image(image: UIImage)
}
private func drawOpenInAppIconBorder(into c: CGContext, arguments: TransformImageArguments) {
c.setBlendMode(.normal)
c.setStrokeColor(UIColor(rgb: 0xe5e5e5).cgColor)
let lineWidth: CGFloat = arguments.drawingRect.size.width < 30.0 ? 1.0 - UIScreenPixel : 1.0
c.setLineWidth(lineWidth)
var radius: CGFloat = 0.0
if case let .Corner(cornerRadius) = arguments.corners.topLeft, cornerRadius > CGFloat.ulpOfOne {
radius = max(0, cornerRadius - 0.5)
}
let rect = arguments.drawingRect.insetBy(dx: lineWidth / 2.0, dy: lineWidth / 2.0)
c.move(to: CGPoint(x: rect.minX, y: rect.midY))
c.addArc(tangent1End: CGPoint(x: rect.minX, y: rect.minY), tangent2End: CGPoint(x: rect.midX, y: rect.minY), radius: radius)
c.addArc(tangent1End: CGPoint(x: rect.maxX, y: rect.minY), tangent2End: CGPoint(x: rect.maxX, y: rect.midY), radius: radius)
c.addArc(tangent1End: CGPoint(x: rect.maxX, y: rect.maxY), tangent2End: CGPoint(x: rect.midX, y: rect.maxY), radius: radius)
c.addArc(tangent1End: CGPoint(x: rect.minX, y: rect.maxY), tangent2End: CGPoint(x: rect.minX, y: rect.midY), radius: radius)
c.closePath()
c.strokePath()
}
public func openInAppIcon(engine: TelegramEngine, appIcon: OpenInAppIcon) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> {
switch appIcon {
case let .resource(resource):
return openInAppIconData(engine: engine, appIcon: resource) |> map { data in
return { arguments in
guard let context = DrawingContext(size: arguments.drawingSize, clear: true) else {
return nil
}
var sourceImage: UIImage?
if let data = data, let image = UIImage(data: data) {
sourceImage = image
}
if let sourceImage = sourceImage, let cgImage = sourceImage.cgImage {
context.withFlippedContext { c in
c.draw(cgImage, in: CGRect(origin: CGPoint(), size: arguments.drawingRect.size))
drawOpenInAppIconBorder(into: c, arguments: arguments)
}
} else {
context.withFlippedContext { c in
drawOpenInAppIconBorder(into: c, arguments: arguments)
}
}
addCorners(context, arguments: arguments)
return context
}
}
case let .image(image):
return .single({ arguments in
guard let context = DrawingContext(size: arguments.drawingSize, clear: true) else {
return nil
}
context.withFlippedContext { c in
c.draw(image.cgImage!, in: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: arguments.drawingSize))
drawOpenInAppIconBorder(into: c, arguments: arguments)
}
addCorners(context, arguments: arguments)
return context
})
}
}
@@ -0,0 +1,335 @@
import Foundation
import UIKit
import TelegramCore
import CoreLocation
import MapKit
import AccountContext
import UrlEscaping
public enum OpenInItem {
case url(url: String)
case location(location: TelegramMediaMap, directions: OpenInLocationDirections?)
}
public enum OpenInLocationDirections: Equatable {
case walking
case driving
case transit
var transportType: MKDirectionsTransportType {
switch self {
case .walking:
return .walking
case .transit:
return .transit
case .driving:
return .automobile
}
}
public var launchOptions: String {
switch self {
case .walking:
return MKLaunchOptionsDirectionsModeWalking
case .transit:
return MKLaunchOptionsDirectionsModeTransit
case .driving:
return MKLaunchOptionsDirectionsModeDriving
}
}
}
public enum OpenInApplication: Equatable {
case safari
case maps
case other(title: String, identifier: Int64, scheme: String, store: String?)
}
public enum OpenInAction {
case none
case openUrl(url: String)
case openLocation(latitude: Double, longitude: Double, directions: OpenInLocationDirections?)
}
public final class OpenInOption {
public let identifier: String
public let application: OpenInApplication
public let action: () -> OpenInAction
public init(identifier: String, application: OpenInApplication, action: @escaping () -> OpenInAction) {
self.identifier = identifier
self.application = application
self.action = action
}
public var title: String {
get {
switch self.application {
case .safari:
return "Safari"
case .maps:
return "Maps"
case let .other(title, _, _, _):
return title
}
}
}
}
public func availableOpenInOptions(context: AccountContext, item: OpenInItem) -> [OpenInOption] {
return allOpenInOptions(context: context, item: item).filter { option in
if case let .other(_, _, scheme, _) = option.application {
return context.sharedContext.applicationBindings.canOpenUrl("\(scheme)://")
} else {
return true
}
}
}
private func allOpenInOptions(context: AccountContext, item: OpenInItem) -> [OpenInOption] {
var options: [OpenInOption] = []
switch item {
case let .url(url):
var skipSafari = false
if url.contains("youtube.com/") || url.contains("youtu.be/") {
let updatedUrl = url.replacingOccurrences(of: "https://", with: "youtube://").replacingOccurrences(of: "http://", with: "youtube://")
options.append(OpenInOption(identifier: "youtube", application: .other(title: "YouTube", identifier: 544007664, scheme: "youtube", store: nil), action: {
return .openUrl(url: updatedUrl)
}))
skipSafari = true
}
if !skipSafari {
options.append(OpenInOption(identifier: "safari", application: .safari, action: {
return .openUrl(url: url)
}))
}
options.append(OpenInOption(identifier: "chrome", application: .other(title: "Chrome", identifier: 535886823, scheme: "googlechrome", store: nil), action: {
if let url = URL(string: url), var components = URLComponents(url: url, resolvingAgainstBaseURL: true) {
components.scheme = components.scheme == "https" ? "googlechromes" : "googlechrome"
if let url = components.string {
return .openUrl(url: url)
}
}
return .none
}))
options.append(OpenInOption(identifier: "firefox", application: .other(title: "Firefox", identifier: 989804926, scheme: "firefox", store: nil), action: {
if let escapedUrl = url.addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed) {
return .openUrl(url: "firefox://open-url?url=\(escapedUrl)")
}
return .none
}))
options.append(OpenInOption(identifier: "firefoxFocus", application: .other(title: "Firefox Focus", identifier: 1055677337, scheme: "firefox-focus", store: nil), action: {
if let escapedUrl = url.addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed) {
return .openUrl(url: "firefox-focus://open-url?url=\(escapedUrl)")
}
return .none
}))
options.append(OpenInOption(identifier: "operaMini", application: .other(title: "Opera Mini", identifier: 363729560, scheme: "opera-http", store: "es"), action: {
if let url = URL(string: url), var components = URLComponents(url: url, resolvingAgainstBaseURL: true) {
components.scheme = components.scheme == "https" ? "opera-https" : "opera-http"
if let url = components.string {
return .openUrl(url: url)
}
}
return .none
}))
options.append(OpenInOption(identifier: "operaTouch", application: .other(title: "Opera Touch", identifier: 1411869974, scheme: "touch-http", store: nil), action: {
if let url = URL(string: url), var components = URLComponents(url: url, resolvingAgainstBaseURL: true) {
components.scheme = components.scheme == "https" ? "touch-https" : "touch-http"
if let url = components.string {
return .openUrl(url: url)
}
}
return .none
}))
options.append(OpenInOption(identifier: "duckDuckGo", application: .other(title: "DuckDuckGo", identifier: 663592361, scheme: "ddgQuickLink", store: nil), action: {
return .openUrl(url: "ddgQuickLink://\(url)")
}))
options.append(OpenInOption(identifier: "edge", application: .other(title: "Microsoft Edge", identifier: 1288723196, scheme: "microsoft-edge-http", store: nil), action: {
if let url = URL(string: url), var components = URLComponents(url: url, resolvingAgainstBaseURL: true) {
components.scheme = components.scheme == "https" ? "microsoft-edge-https" : "microsoft-edge-http"
if let url = components.string {
return .openUrl(url: url)
}
}
return .none
}))
options.append(OpenInOption(identifier: "yandex", application: .other(title: "Yandex Browser", identifier: 483693909, scheme: "yandexbrowser-open-url", store: nil), action: {
if let escapedUrl = url.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) {
return .openUrl(url: "yandexbrowser-open-url://\(escapedUrl)")
}
return .none
}))
options.append(OpenInOption(identifier: "brave", application: .other(title: "Brave", identifier: 1052879175, scheme: "brave", store: nil), action: {
if let escapedUrl = url.addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed) {
return .openUrl(url: "brave://open-url?url=\(escapedUrl)")
}
return .none
}))
options.append(OpenInOption(identifier: "dolphin", application: .other(title: "Dolphin", identifier: 1440710469, scheme: "dolphin", store: "us"), action: {
return .openUrl(url: "dolphin://\(url)")
}))
options.append(OpenInOption(identifier: "onion", application: .other(title: "Onion Browser", identifier: 519296448, scheme: "onionhttp", store: nil), action: {
if let url = URL(string: url), var components = URLComponents(url: url, resolvingAgainstBaseURL: true) {
components.scheme = components.scheme == "https" ? "onionhttps" : "onionhttp"
if let url = components.string {
return .openUrl(url: url)
}
}
return .none
}))
options.append(OpenInOption(identifier: "ucbrowser", application: .other(title: "UC Browser", identifier: 1048518592, scheme: "ucbrowser", store: nil), action: {
return .openUrl(url: "ucbrowser://\(url)")
}))
options.append(OpenInOption(identifier: "alook", application: .other(title: "Alook Browser", identifier: 1261944766, scheme: "alook", store: nil), action: {
return .openUrl(url: "alook://\(url)")
}))
case let .location(location, directions):
let lat = location.latitude
let lon = location.longitude
if directions == nil {
if let venue = location.venue, let venueId = venue.id, let provider = venue.provider, provider == "foursquare" {
options.append(OpenInOption(identifier: "foursquare", application: .other(title: "Foursquare", identifier: 306934924, scheme: "foursquare", store: nil), action: {
return .openUrl(url: "foursquare://venues/\(venueId)")
}))
}
}
options.append(OpenInOption(identifier: "appleMaps", application: .maps, action: {
return .openLocation(latitude: lat, longitude: lon, directions: directions)
}))
options.append(OpenInOption(identifier: "googleMaps", application: .other(title: "Google Maps", identifier: 585027354, scheme: "comgooglemaps-x-callback", store: nil), action: {
let coordinates = "\(lat),\(lon)"
if let directions = directions {
let directionsMode: String
switch directions {
case .walking:
directionsMode = "walking"
case .driving:
directionsMode = "driving"
case .transit:
directionsMode = "transit"
}
return .openUrl(url: "comgooglemaps-x-callback://?daddr=\(coordinates)&directionsmode=\(directionsMode)&x-success=telegram://?resume=true&x-source=Telegram")
} else {
if let venue = location.venue, let venueId = venue.id, let provider = venue.provider, provider == "gplaces" {
return .openUrl(url: "https://www.google.com/maps/search/?api=1&query=\(venue.address ?? "")&query_place_id=\(venueId)")
} else {
return .openUrl(url: "comgooglemaps-x-callback://?center=\(coordinates)&q=\(coordinates)&x-success=telegram://?resume=true&x-source=Telegram")
}
}
}))
options.append(OpenInOption(identifier: "yangoMaps", application: .other(title: "Yango Maps", identifier: 1665672451, scheme: "yangomaps", store: nil), action: {
if let _ = directions {
return .openUrl(url: "yangomaps://build_route_on_map?lat_to=\(lat)&lon_to=\(lon)")
} else {
return .openUrl(url: "yangomaps://maps.yango.com/?pt=\(lon),\(lat)&z=16")
}
}))
options.append(OpenInOption(identifier: "yandexMaps", application: .other(title: "Yandex.Maps", identifier: 313877526, scheme: "yandexmaps", store: nil), action: {
if let _ = directions {
return .openUrl(url: "yandexmaps://build_route_on_map?lat_to=\(lat)&lon_to=\(lon)")
} else {
return .openUrl(url: "yandexmaps://maps.yandex.ru/?pt=\(lon),\(lat)&z=16")
}
}))
options.append(OpenInOption(identifier: "uber", application: .other(title: "Uber", identifier: 368677368, scheme: "uber", store: nil), action: {
let dropoffName: String
let dropoffAddress: String
if let title = location.venue?.title.addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed), title.count > 0 {
dropoffName = title
} else {
dropoffName = ""
}
if let address = location.venue?.address?.addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed), address.count > 0 {
dropoffAddress = address
} else {
dropoffAddress = ""
}
return .openUrl(url: "uber://?client_id=&action=setPickup&pickup=my_location&dropoff[latitude]=\(lat)&dropoff[longitude]=\(lon)&dropoff[nickname]=\(dropoffName)&dropoff[formatted_address]=\(dropoffAddress)")
}))
options.append(OpenInOption(identifier: "lyft", application: .other(title: "Lyft", identifier: 529379082, scheme: "lyft", store: nil), action: {
return .openUrl(url: "lyft://ridetype?id=lyft&destination[latitude]=\(lat)&destination[longitude]=\(lon)")
}))
if let _ = directions {
options.append(OpenInOption(identifier: "citymapper", application: .other(title: "Citymapper", identifier: 469463298, scheme: "citymapper", store: nil), action: {
let endName: String
let endAddress: String
if let title = location.venue?.title.addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed), title.count > 0 {
endName = title
} else {
endName = ""
}
if let address = location.venue?.address?.addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed), address.count > 0 {
endAddress = address
} else {
endAddress = ""
}
return .openUrl(url: "citymapper://directions?endcoord=\(lat),\(lon)&endname=\(endName)&endaddress=\(endAddress)")
}))
options.append(OpenInOption(identifier: "yandexNavi", application: .other(title: "Yandex.Navi", identifier: 474500851, scheme: "yandexnavi", store: nil), action: {
return .openUrl(url: "yandexnavi://build_route_on_map?lat_to=\(lat)&lon_to=\(lon)")
}))
}
options.append(OpenInOption(identifier: "2gis", application: .other(title: "2GIS", identifier: 481627348, scheme: "dgis", store: "ru"), action: {
let coordinates = "\(lon),\(lat)"
if let _ = directions {
return .openUrl(url: "dgis://2gis.ru/routeSearch/to/\(coordinates)/go")
} else {
return .openUrl(url: "dgis://2gis.ru/geo/\(coordinates)")
}
}))
options.append(OpenInOption(identifier: "moovit", application: .other(title: "Moovit", identifier: 498477945, scheme: "moovit", store: nil), action: {
if let _ = directions {
let destName: String
if let title = location.venue?.title.addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed), title.count > 0 {
destName = title
} else {
destName = ""
}
return .openUrl(url: "moovit://directions?dest_lat=\(lat)&dest_lon=\(lon)&dest_name=\(destName)&partner_id=Telegram")
} else {
return .openUrl(url: "moovit://nearby?lat=\(lat)&lon=\(lon)&partner_id=Telegram")
}
}))
if directions == nil {
options.append(OpenInOption(identifier: "hereMaps", application: .other(title: "HERE Maps", identifier: 955837609, scheme: "here-location", store: nil), action: {
return .openUrl(url: "here-location://\(lat),\(lon)")
}))
}
options.append(OpenInOption(identifier: "waze", application: .other(title: "Waze", identifier: 323229106, scheme: "waze", store: nil), action: {
let url = "waze://?ll=\(lat),\(lon)"
if let _ = directions {
return .openUrl(url: url.appending("&navigate=yes"))
} else {
return .openUrl(url: url)
}
}))
}
return options
}