Added .gitignore

This commit is contained in:
Peter Iakovlev 2018-11-14 23:03:33 +04:00
commit 77ee5c4dab
869 changed files with 54858 additions and 0 deletions

24
.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
fastlane/README.md
fastlane/report.xml
fastlane/test_output/*
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
xcuserdata
*.xccheckout
*.xcscmblueprint
*.moved-aside
DerivedData
*.hmap
*.ipa
*.xcuserstate
.DS_Store
*.dSYM
*.dSYM.zip
*.ipa
*/xcuserdata/*

33
.gitmodules vendored Normal file
View File

@ -0,0 +1,33 @@
[submodule "submodules/AsyncDisplayKit"]
path = submodules/AsyncDisplayKit
url = git@github.com:peter-iakovlev/AsyncDisplayKit.git
[submodule "submodules/Display"]
path = submodules/Display
url = git@github.com:peter-iakovlev/Display.git
[submodule "submodules/HockeySDK-iOS"]
path = submodules/HockeySDK-iOS
url = git@github.com:peter-iakovlev/HockeySDK-iOS.git
[submodule "submodules/LegacyComponents"]
path = submodules/LegacyComponents
url = git@github.com:peter-iakovlev/LegacyComponents.git
[submodule "submodules/libtgvoip"]
path = submodules/libtgvoip
url=https://github.com/grishka/libtgvoip.git
[submodule "submodules/lottie-ios"]
path = submodules/lottie-ios
url=git@github.com:peter-iakovlev/lottie-ios.git
[submodule "submodules/MtProtoKit"]
path = submodules/MtProtoKit
url=git@github.com:peter-iakovlev/MtProtoKit.git
[submodule "submodules/Postbox"]
path = submodules/Postbox
url = git@github.com:peter-iakovlev/Postbox.git
[submodule "submodules/SSignalKit"]
path = submodules/SSignalKit
url = git@github.com:peter-iakovlev/Signals.git
[submodule "submodules/TelegramCore"]
path = submodules/TelegramCore
url = git@github.com:peter-iakovlev/TelegramCore.git
[submodule "submodules/TelegramUI"]
path = submodules/TelegramUI
url = git@github.com:peter-iakovlev/TelegramUI.git

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14313.18" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="M4Y-Lb-cyx">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14283.14"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Notification View Controller-->
<scene sceneID="cwh-vc-ff4">
<objects>
<viewController id="M4Y-Lb-cyx" userLabel="Notification View Controller" customClass="NotificationViewController" customModule="NotificationContent" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" simulatedAppContext="notificationCenter" id="S3S-Oj-5AN">
<rect key="frame" x="0.0" y="0.0" width="320" height="37"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<viewLayoutGuide key="safeArea" id="2BE-c3-nQJ"/>
</view>
<extendedEdge key="edgesForExtendedLayout"/>
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
<size key="freeformSize" width="320" height="37"/>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="vXp-U4-Rya" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>

View File

@ -0,0 +1,260 @@
import Foundation
import Display
import TelegramCore
import TelegramUI
import Postbox
import SwiftSignalKit
private let accountCache = Atomic<[AccountRecordId: Account]>(value: [:])
private struct ChatHistoryFragmentEntry: Comparable, Identifiable {
let message: Message
let read: Bool
var stableId: UInt32 {
return self.message.stableId
}
}
private func==(lhs: ChatHistoryFragmentEntry, rhs: ChatHistoryFragmentEntry) -> Bool {
if MessageIndex(lhs.message) == MessageIndex(rhs.message) && lhs.message.flags == rhs.message.flags {
if lhs.message.media.count != rhs.message.media.count {
return false
}
if lhs.read != rhs.read {
return false
}
for i in 0 ..< lhs.message.media.count {
if !lhs.message.media[i].isEqual(rhs.message.media[i]) {
return false
}
}
return true
} else {
return false
}
}
private func <(lhs: ChatHistoryFragmentEntry, rhs: ChatHistoryFragmentEntry) -> Bool {
return MessageIndex(lhs.message) < MessageIndex(rhs.message)
}
private final class ChatHistoryFragmentDisplayItem {
fileprivate let item: ListViewItem
fileprivate var node: ListViewItemNode?
init(item: ListViewItem) {
self.item = item
}
init(item: ListViewItem, node: ListViewItemNode?) {
self.item = item
self.node = node
}
}
final class ChatHistoryFragmentView: UIView {
private let sizeUpdated: (CGSize) -> Void
private var layoutWidth: CGFloat?
private var displayItems: [ChatHistoryFragmentDisplayItem] = []
private let disposable = MetaDisposable()
let account = Promise<Account>()
init(peerId: PeerId, width: CGFloat, sizeUpdated: @escaping (CGSize) -> Void) {
self.sizeUpdated = sizeUpdated
self.layoutWidth = width
super.init(frame: CGRect())
/*let appBundleIdentifier = Bundle.main.bundleIdentifier!
guard let lastDotRange = appBundleIdentifier.range(of: ".", options: [.backwards]) else {
return
}
let appGroupName = "group.\(appBundleIdentifier.substring(to: lastDotRange.lowerBound))"
let maybeAppGroupUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupName)
guard let appGroupUrl = maybeAppGroupUrl else {
return
}
let accountPromise = self.account
let accountId = currentAccountId(appGroupPath: appGroupUrl.path, testingEnvironment: false)
let authorizedAccount: Signal<Account, NoError>
let cachedAccount = accountCache.with { dict -> Account? in
if let account = dict[accountId] {
return account
} else {
return nil
}
}
if let cachedAccount = cachedAccount {
authorizedAccount = .single(cachedAccount)
} else {
authorizedAccount = accountWithId(accountId, appGroupPath: appGroupUrl.path, logger: .named("notification-content"), testingEnvironment: false) |> mapToSignal { account -> Signal<Account, NoError> in
switch account {
case .left:
return .complete()
case let .right(authorizedAccount):
setupAccount(authorizedAccount)
let _ = accountCache.modify { dict in
var dict = dict
dict[accountId] = authorizedAccount
return dict
}
return .single(authorizedAccount)
}
}
}
let view = authorizedAccount
|> take(1)
|> mapToSignal { account -> Signal<(Account, MessageHistoryView, ViewUpdateType), NoError> in
accountPromise.set(.single(account))
account.stateManager.reset()
account.shouldBeServiceTaskMaster.set(.single(.now))
let view = account.viewTracker.aroundMessageHistoryViewForPeerId(peerId, index: MessageIndex.upperBound(peerId: peerId), count: 20, anchorIndex: MessageIndex.upperBound(peerId: peerId), fixedCombinedReadStates: nil, tagMask: nil)
|> map { view, updateType, _ -> (Account, MessageHistoryView, ViewUpdateType) in
return (account, view, updateType)
}
return view
}
let previousEntries = Atomic<[ChatHistoryFragmentEntry]>(value: [])
let controllerInteraction = ChatControllerInteraction(openMessage: { _ in }, openSecretMessagePreview: { _ in }, closeSecretMessagePreview: { }, openPeer: { _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _ in }, navigateToMessage: { _ in }, clickThroughMessage: { }, toggleMessagesSelection: { _ in }, sendMessage: { _ in }, sendSticker: { _ in }, requestMessageActionCallback: { _ in }, openUrl: { _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _ in }, openHashtag: { _ in }, updateInputState: { _ in })
let messages = view
|> map { (account, view, viewUpdateType) -> (Account, [ChatHistoryFragmentEntry], [Int: Int]) in
var entries: [ChatHistoryFragmentEntry] = []
for entry in view.entries.reversed() {
switch entry {
case let .MessageEntry(message, read, _):
entries.append(ChatHistoryFragmentEntry(message: message, read: read))
default:
break
}
}
var previousIndices: [Int: Int] = [:]
let _ = previousEntries.modify { previousEntries in
var index = 0
for entry in entries {
var previousIndex = 0
for previousEntry in previousEntries {
if previousEntry.stableId == entry.stableId {
previousIndices[index] = previousIndex
break
}
previousIndex += 1
}
index += 1
}
return entries
}
return (account, entries, previousIndices)
}
let displayItems = messages
|> map { (account, messages, previousIndices) -> ([ChatHistoryFragmentDisplayItem], [Int: Int]) in
var result: [ChatHistoryFragmentDisplayItem] = []
for entry in messages {
result.append(ChatHistoryFragmentDisplayItem(item: ChatMessageItem(account: account, peerId: peerId, controllerInteraction: controllerInteraction, message: entry.message, read: entry.read)))
}
return (result, previousIndices)
}
let semaphore = DispatchSemaphore(value: 0)
var resultItems: [ChatHistoryFragmentDisplayItem]?
disposable.set(displayItems.start(next: { [weak self] (displayItems, previousIndices) in
if resultItems == nil {
resultItems = displayItems
semaphore.signal()
} else {
Queue.mainQueue().async {
if let strongSelf = self {
var updatedDisplayItems: [ChatHistoryFragmentDisplayItem] = []
for i in 0 ..< displayItems.count {
if let previousIndex = previousIndices[i] {
updatedDisplayItems.append(ChatHistoryFragmentDisplayItem(item: displayItems[i].item, node: strongSelf.displayItems[previousIndex].node))
} else {
updatedDisplayItems.append(displayItems[i])
}
}
let previousIndexSet = Set(previousIndices.values)
for i in 0 ..< strongSelf.displayItems.count {
if !previousIndexSet.contains(i) {
strongSelf.displayItems[i].node?.removeFromSupernode()
}
}
strongSelf.displayItems = updatedDisplayItems
if let layoutWidth = strongSelf.layoutWidth {
strongSelf.updateDisplayItems(width: layoutWidth)
}
}
}
}
}))
semaphore.wait()
if let resultItems = resultItems {
self.displayItems = resultItems
}
if let layoutWidth = self.layoutWidth {
self.updateDisplayItems(width: layoutWidth)
}*/
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.disposable.dispose()
}
private func updateDisplayItems(width: CGFloat) {
for i in 0 ..< self.displayItems.count {
if let node = self.displayItems[i].node {
self.displayItems[i].item.updateNode(async: { $0() }, node: node, width: width, previousItem: i == 0 ? nil : self.displayItems[i - 1].item, nextItem: i == self.displayItems.count - 1 ? nil : self.displayItems[i + 1].item, animation: .None, completion: { layout, apply in
node.insets = layout.insets
node.contentSize = layout.contentSize
apply()
})
node.layoutForWidth(width, item: self.displayItems[i].item, previousItem: i == 0 ? nil : self.displayItems[i - 1].item, nextItem: i == self.displayItems.count - 1 ? nil : self.displayItems[i + 1].item)
} else {
self.displayItems[i].item.nodeConfiguredForWidth(async: { $0() }, width: width, previousItem: i == 0 ? nil : self.displayItems[i - 1].item, nextItem: i == self.displayItems.count - 1 ? nil : self.displayItems[i + 1].item, completion: { node, apply in
apply()
self.displayItems[i].node = node
self.addSubnode(node)
})
}
}
var verticalOffset: CGFloat = 4.0
for displayItem in self.displayItems {
if let node = displayItem.node {
node.frame = CGRect(origin: CGPoint(x: 0.0, y: verticalOffset), size: node.layout.size)
verticalOffset += node.layout.size.height
}
}
let displaySize = CGSize(width: width, height: verticalOffset + 4.0)
self.sizeUpdated(displaySize)
}
override func layoutSubviews() {
super.layoutSubviews()
if self.layoutWidth != self.bounds.size.width {
self.layoutWidth = self.bounds.size.width
self.updateDisplayItems(width: self.bounds.size.width)
}
}
}

View File

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>NotificationContent</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
<string>5.0.17</string>
<key>CFBundleVersion</key>
<string>624</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionAttributes</key>
<dict>
<key>UNNotificationExtensionCategory</key>
<array>
<string>withReplyMedia</string>
<string>withMuteMedia</string>
</array>
<key>UNNotificationExtensionInitialContentSizeRatio</key>
<real>0.0001</real>
</dict>
<key>NSExtensionMainStoryboard</key>
<string>MainInterface</string>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.usernotifications.content-extension</string>
</dict>
</dict>
</plist>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.org.telegram.TelegramHD</string>
</array>
</dict>
</plist>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.ph.telegra.Telegraph</string>
</array>
</dict>
</plist>

View File

@ -0,0 +1,6 @@
#ifndef Share_Bridging_Header_h
#define Share_Bridging_Header_h
#import "../Telegram-iOS/BuildConfig.h"
#endif

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.org.telegram.Telegram-iOS</string>
</array>
</dict>
</plist>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array/>
</dict>
</plist>

View File

@ -0,0 +1,189 @@
import UIKit
import UserNotifications
import UserNotificationsUI
import Display
import TelegramCore
import TelegramUI
import SwiftSignalKit
import Postbox
private enum NotificationContentAuthorizationError {
case unauthorized
}
private var accountCache: (Account, AccountManager)?
private var installedSharedLogger = false
private func setupSharedLogger(_ path: String) {
if !installedSharedLogger {
installedSharedLogger = true
Logger.setSharedLogger(Logger(basePath: path))
}
}
class NotificationViewController: UIViewController, UNNotificationContentExtension {
private let accountPromise = Promise<Account>()
private let imageNode = TransformImageNode()
private var imageDimensions: CGSize?
private let applyDisposable = MetaDisposable()
private let fetchedDisposable = MetaDisposable()
deinit {
self.applyDisposable.dispose()
self.fetchedDisposable.dispose()
}
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubnode(self.imageNode)
let appBundleIdentifier = Bundle.main.bundleIdentifier!
guard let lastDotRange = appBundleIdentifier.range(of: ".", options: [.backwards]) else {
return
}
let apiId: Int32 = BuildConfig.shared().apiId
let languagesCategory = "ios"
let appGroupName = "group.\(appBundleIdentifier[..<lastDotRange.lowerBound])"
let maybeAppGroupUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupName)
guard let appGroupUrl = maybeAppGroupUrl else {
return
}
let rootPath = rootPathForBasePath(appGroupUrl.path)
performAppGroupUpgrades(appGroupPath: appGroupUrl.path, rootPath: rootPath)
TempBox.initializeShared(basePath: rootPath, processType: "notification-content", launchSpecificId: arc4random64())
let logsPath = rootPath + "/notificationcontent-logs"
let _ = try? FileManager.default.createDirectory(atPath: logsPath, withIntermediateDirectories: true, attributes: nil)
setupSharedLogger(logsPath)
let account: Signal<(Account, AccountManager), NotificationContentAuthorizationError>
if let accountCache = accountCache {
account = .single(accountCache)
} else {
let appVersion = (Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String) ?? "unknown"
initializeAccountManagement()
account = accountManager(basePath: rootPath + "/accounts-metadata")
|> take(1)
|> introduceError(NotificationContentAuthorizationError.self)
|> mapToSignal { accountManager -> Signal<(Account, AccountManager), NotificationContentAuthorizationError> in
return currentAccount(allocateIfNotExists: false, networkArguments: NetworkInitializationArguments(apiId: apiId, languagesCategory: languagesCategory, appVersion: appVersion), supplementary: true, manager: accountManager, rootPath: rootPath, beginWithTestingEnvironment: false, auxiliaryMethods: telegramAccountAuxiliaryMethods)
|> introduceError(NotificationContentAuthorizationError.self)
|> mapToSignal { account -> Signal<(Account, AccountManager), NotificationContentAuthorizationError> in
if let account = account {
switch account {
case .upgrading:
return .complete()
case let .authorized(account):
setupAccount(account)
accountCache = (account, accountManager)
return .single((account, accountManager))
case .unauthorized:
return .fail(.unauthorized)
}
} else {
return .complete()
}
}
}
|> take(1)
}
self.accountPromise.set(account
|> map { $0.0 }
|> `catch` { _ -> Signal<Account, NoError> in
return .complete()
})
}
func didReceive(_ notification: UNNotification) {
if let peerIdValue = notification.request.content.userInfo["peerId"] as? Int64, let messageIdNamespace = notification.request.content.userInfo["messageId.namespace"] as? Int32, let messageIdId = notification.request.content.userInfo["messageId.id"] as? Int32, let dict = notification.request.content.userInfo["mediaInfo"] as? [String: Any] {
let messageId = MessageId(peerId: PeerId(peerIdValue), namespace: messageIdNamespace, id: messageIdId)
if let imageInfo = dict["image"] as? [String: Any] {
guard let width = imageInfo["width"] as? Int, let height = imageInfo["height"] as? Int else {
return
}
guard let thumbnailInfo = imageInfo["thumbnail"] as? [String: Any] else {
return
}
guard let fullSizeInfo = imageInfo["fullSize"] as? [String: Any] else {
return
}
let dimensions = CGSize(width: CGFloat(width), height: CGFloat(height))
let fittedSize = dimensions.fitted(CGSize(width: self.view.bounds.width, height: 1000.0))
self.view.frame = CGRect(origin: self.view.frame.origin, size: fittedSize)
self.preferredContentSize = fittedSize
self.imageDimensions = dimensions
self.updateImageLayout(boundingSize: self.view.bounds.size)
if let path = fullSizeInfo["path"] as? String, let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) {
self.imageNode.setSignal(chatMessagePhotoInternal(photoData: .single((nil, data, true)))
|> map { $0.1 })
return
}
if let path = thumbnailInfo["path"] as? String, let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: .mappedRead) {
self.imageNode.setSignal(chatMessagePhotoInternal(photoData: .single((data, nil, false)))
|> map { $0.1 })
}
self.applyDisposable.set((self.accountPromise.get()
|> take(1)
|> mapToSignal { account -> Signal<(Account, ImageMediaReference?), NoError> in
return account.postbox.messageAtId(messageId)
|> take(1)
|> map { message in
var imageReference: ImageMediaReference?
if let message = message {
for media in message.media {
if let image = media as? TelegramMediaImage {
imageReference = .message(message: MessageReference(message), media: image)
}
}
}
return (account, imageReference)
}
}
|> deliverOnMainQueue).start(next: { [weak self] accountAndImage in
guard let strongSelf = self else {
return
}
if let imageReference = accountAndImage.1 {
strongSelf.imageNode.setSignal(chatMessagePhoto(postbox: accountAndImage.0.postbox, photoReference: imageReference))
accountAndImage.0.network.shouldExplicitelyKeepWorkerConnections.set(.single(true))
strongSelf.fetchedDisposable.set(chatMessagePhotoInteractiveFetched(account: accountAndImage.0, photoReference: imageReference, storeToDownloadsPeerType: nil).start())
}
}))
}
}
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
self.updateImageLayout(boundingSize: size)
}
private func updateImageLayout(boundingSize: CGSize) {
if let imageDimensions = self.imageDimensions {
let makeLayout = self.imageNode.asyncLayout()
let fittedSize = imageDimensions.fitted(CGSize(width: boundingSize.width, height: 1000.0))
let apply = makeLayout(TransformImageArguments(corners: ImageCorners(radius: 0.0), imageSize: fittedSize, boundingSize: fittedSize, intrinsicInsets: UIEdgeInsets()))
apply()
self.imageNode.frame = CGRect(origin: CGPoint(), size: boundingSize)
}
}
}

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>
<string>NotificationService</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>55</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.usernotifications.service</string>
<key>NSExtensionPrincipalClass</key>
<string>NotificationService</string>
</dict>
</dict>
</plist>

View File

@ -0,0 +1,6 @@
import Foundation
import Postbox
import SwiftSignalKit
import MtProtoKitDynamic

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.org.telegram.Telegram-iOS</string>
</array>
</dict>
</plist>

View File

@ -0,0 +1,212 @@
import UserNotifications
import Postbox
import SwiftSignalKit
import TelegramCore
private func reportMemory() {
// constant
let MACH_TASK_BASIC_INFO_COUNT = (MemoryLayout<mach_task_basic_info_data_t>.size / MemoryLayout<natural_t>.size)
// prepare parameters
let name = mach_task_self_
let flavor = task_flavor_t(MACH_TASK_BASIC_INFO)
var size = mach_msg_type_number_t(MACH_TASK_BASIC_INFO_COUNT)
// allocate pointer to mach_task_basic_info
let infoPointer = UnsafeMutablePointer<mach_task_basic_info>.allocate(capacity: 1)
// call task_info - note extra UnsafeMutablePointer(...) call
let kerr = infoPointer.withMemoryRebound(to: Int32.self, capacity: 1, { pointer in
return task_info(name, flavor, pointer, &size)
})
// get mach_task_basic_info struct out of pointer
let info = infoPointer.move()
// deallocate pointer
infoPointer.deallocate(capacity: 1)
// check return value for success / failure
if kerr == KERN_SUCCESS {
NSLog("Memory in use (in MB): \(info.resident_size/1000000)")
}
}
private struct ResolvedNotificationContent {
let text: String
let attachment: UNNotificationAttachment?
}
@objc(NotificationService)
class NotificationService: UNNotificationServiceExtension {
private let disposable = MetaDisposable()
private var bestEffortContent: UNMutableNotificationContent?
private var currentContentHandler: ((UNNotificationContent) -> Void)?
var timer: SwiftSignalKit.Timer?
deinit {
self.disposable.dispose()
}
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
contentHandler(request.content)
/*self.timer?.invalidate()
reportMemory()
self.timer = SwiftSignalKit.Timer(timeout: 0.01, repeat: true, completion: {
reportMemory()
}, queue: Queue.mainQueue())
self.timer?.start()
NSLog("before api")
reportMemory()
let a = TelegramCore.Api.User.userEmpty(id: 1)
NSLog("after api \(a)")
reportMemory()
if let content = request.content.mutableCopy() as? UNMutableNotificationContent {
var peerId: PeerId?
if let fromId = request.content.userInfo["from_id"] {
var idValue: Int32?
if let id = fromId as? NSNumber {
idValue = Int32(id.intValue)
} else if let id = fromId as? NSString {
idValue = id.intValue
}
if let idValue = idValue {
if idValue > 0 {
peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: idValue)
} else {
}
}
} else if let fromId = request.content.userInfo["chat_id"] {
var idValue: Int32?
if let id = fromId as? NSNumber {
idValue = Int32(id.intValue)
} else if let id = fromId as? NSString {
idValue = id.intValue
}
if let idValue = idValue {
if idValue > 0 {
peerId = PeerId(namespace: Namespaces.Peer.CloudGroup, id: idValue)
}
}
} else if let fromId = request.content.userInfo["channel_id"] {
var idValue: Int32?
if let id = fromId as? NSNumber {
idValue = Int32(id.intValue)
} else if let id = fromId as? NSString {
idValue = id.intValue
}
if let idValue = idValue {
if idValue > 0 {
peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: idValue)
}
}
}
var messageId: MessageId?
if let peerId = peerId, let mid = request.content.userInfo["msg_id"] {
var idValue: Int32?
if let id = mid as? NSNumber {
idValue = Int32(id.intValue)
} else if let id = mid as? NSString {
idValue = id.intValue
}
if let idValue = idValue {
messageId = MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: idValue)
}
}
content.body = "[timeout] \(messageId) \(content.body)"
self.bestEffortContent = content
self.currentContentHandler = contentHandler
var signal: Signal<Void, NoError> = .complete()
if let messageId = messageId {
let appBundleIdentifier = Bundle.main.bundleIdentifier!
guard let lastDotRange = appBundleIdentifier.range(of: ".", options: [.backwards]) else {
return
}
let appGroupName = "group.\(appBundleIdentifier.substring(to: lastDotRange.lowerBound))"
let maybeAppGroupUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupName)
guard let appGroupUrl = maybeAppGroupUrl else {
return
}
let authorizedAccount = accountWithId(currentAccountId(appGroupPath: appGroupUrl.path), appGroupPath: appGroupUrl.path) |> mapToSignal { account -> Signal<Account, NoError> in
switch account {
case .left:
return .complete()
case let .right(authorizedAccount):
return .single(authorizedAccount)
}
}
signal = (authorizedAccount
|> take(1)
|> mapToSignal { account -> Signal<Message?, NoError> in
setupAccount(account)
return downloadMessage(account: account, message: messageId)
}
|> mapToSignal { message -> Signal<ResolvedNotificationContent?, NoError> in
Queue.mainQueue().async {
content.body = "[timeout5] \(message) \(content.body)"
contentHandler(content)
}
if let message = message {
return .single(ResolvedNotificationContent(text: "R " + message.text, attachment: nil))
} else {
return .complete()
}
}
|> deliverOnMainQueue
|> mapToSignal { [weak self] resolvedContent -> Signal<Void, NoError> in
if let strongSelf = self, let resolvedContent = resolvedContent {
content.body = resolvedContent.text
if let attachment = resolvedContent.attachment {
content.attachments = [attachment]
}
contentHandler(content)
strongSelf.bestEffortContent = nil
}
return .complete()
})
|> afterDisposed { [weak self] in
Queue.mainQueue().async {
if let strongSelf = self {
if let bestEffortContent = strongSelf.bestEffortContent {
contentHandler(bestEffortContent)
}
}
}
}
}
self.disposable.set(signal.start())
} else {
contentHandler(request.content)
}*/
}
override func serviceExtensionTimeWillExpire() {
self.disposable.dispose()
if let currentContentHandler = self.currentContentHandler, let bestEffortContent = self.bestEffortContent {
currentContentHandler(bestEffortContent)
}
}
}

52
Share/Info.plist Normal file
View File

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>
<string>${APP_NAME}</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
<string>5.0.17</string>
<key>CFBundleVersion</key>
<string>624</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionAttributes</key>
<dict>
<key>NSExtensionActivationRule</key>
<string>SUBQUERY (
extensionItems,
$extensionItem,
SUBQUERY (
$extensionItem.attachments,
$attachment,
ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.file-url" ||
ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.movie" ||
ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.image" ||
ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.url" ||
ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.text" ||
ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.audio" ||
ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.data" ||
ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.vcard" ||
ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "com.apple.pkpass"
).@count == $extensionItem.attachments.@count
).@count &gt; 0</string>
</dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.share-services</string>
<key>NSExtensionPrincipalClass</key>
<string>ShareRootController</string>
</dict>
</dict>
</plist>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.org.telegram.TelegramHD</string>
</array>
</dict>
</plist>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.ph.telegra.Telegraph</string>
</array>
</dict>
</plist>

View File

@ -0,0 +1,10 @@
#ifndef Share_Bridging_Header_h
#define Share_Bridging_Header_h
#import "TGContactModel.h"
#import "TGItemProviderSignals.h"
#import "TGShareLocationSignals.h"
#import "../Telegram-iOS/BuildConfig.h"
#endif

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.org.telegram.Telegram-iOS</string>
</array>
</dict>
</plist>

363
Share/ShareItems.swift Normal file
View File

@ -0,0 +1,363 @@
import Foundation
import SwiftSignalKit
import Postbox
import TelegramCore
import MtProtoKitDynamic
import Display
import TelegramUI
import LegacyComponents
enum UnpreparedShareItemContent {
case contact(DeviceContactExtendedData)
}
enum PreparedShareItemContent {
case text(String)
case media(StandaloneUploadMediaResult)
}
enum PreparedShareItem {
case preparing
case progress(Float)
case userInteractionRequired(UnpreparedShareItemContent)
case done(PreparedShareItemContent)
}
enum PreparedShareItems {
case preparing
case progress(Float)
case userInteractionRequired([UnpreparedShareItemContent])
case done([PreparedShareItemContent])
}
private func scalePhotoImage(_ image: UIImage, dimensions: CGSize) -> UIImage? {
if #available(iOSApplicationExtension 10.0, *) {
let format = UIGraphicsImageRendererFormat()
format.scale = 1.0
let renderer = UIGraphicsImageRenderer(size: dimensions, format: format)
return renderer.image { _ in
image.draw(in: CGRect(origin: .zero, size: dimensions))
}
} else {
return TGScaleImageToPixelSize(image, dimensions)
}
}
private func preparedShareItem(account: Account, to peerId: PeerId, value: [String: Any]) -> Signal<PreparedShareItem, Void> {
if let imageData = value["scaledImageData"] as? Data, let dimensions = value["scaledImageDimensions"] as? NSValue {
return .single(.preparing)
|> then(
standaloneUploadedImage(account: account, peerId: peerId, text: "", data: imageData, dimensions: dimensions.cgSizeValue)
|> mapError { _ -> Void in
return Void()
}
|> mapToSignal { event -> Signal<PreparedShareItem, Void> in
switch event {
case let .progress(value):
return .single(.progress(value))
case let .result(media):
return .single(.done(.media(media)))
}
}
)
} else if let image = value["image"] as? UIImage {
let nativeImageSize = CGSize(width: image.size.width * image.scale, height: image.size.height * image.scale)
let dimensions = nativeImageSize.fitted(CGSize(width: 1280.0, height: 1280.0))
if let scaledImage = scalePhotoImage(image, dimensions: dimensions), let imageData = UIImageJPEGRepresentation(scaledImage, 0.52) {
return .single(.preparing)
|> then(standaloneUploadedImage(account: account, peerId: peerId, text: "", data: imageData, dimensions: dimensions)
|> mapError { _ -> Void in
return Void()
}
|> mapToSignal { event -> Signal<PreparedShareItem, Void> in
switch event {
case let .progress(value):
return .single(.progress(value))
case let .result(media):
return .single(.done(.media(media)))
}
}
)
} else {
return .never()
}
} else if let asset = value["video"] as? AVURLAsset {
var flags: TelegramMediaVideoFlags = [.supportsStreaming]
let sendAsInstantRoundVideo = value["isRoundMessage"] as? Bool ?? false
var adjustments: TGVideoEditAdjustments? = nil
if sendAsInstantRoundVideo {
flags.insert(.instantRoundVideo)
if let width = value["width"] as? CGFloat, let height = value["height"] as? CGFloat {
let size = CGSize(width: width, height: height)
var cropRect = CGRect(origin: CGPoint(), size: size)
if abs(width - height) < CGFloat.ulpOfOne {
cropRect = cropRect.insetBy(dx: 13.0, dy: 13.0)
cropRect.offsetBy(dx: 2.0, dy: 3.0)
} else {
let shortestSide = min(size.width, size.height)
cropRect = CGRect(x: (size.width - shortestSide) / 2.0, y: (size.height - shortestSide) / 2.0, width: shortestSide, height: shortestSide)
}
adjustments = TGVideoEditAdjustments(originalSize: size, cropRect: cropRect, cropOrientation: .up, cropLockedAspectRatio: 1.0, cropMirrored: false, trimStartValue: 0.0, trimEndValue: 0.0, paintingData: nil, sendAsGif: false, preset: TGMediaVideoConversionPresetVideoMessage)
}
}
var finalDuration: Double = CMTimeGetSeconds(asset.duration)
let finalDimensions = TGMediaVideoConverter.dimensions(for: asset.originalSize, adjustments: adjustments, preset: adjustments?.preset ?? TGMediaVideoConversionPresetCompressedMedium)
var resourceAdjustments: VideoMediaResourceAdjustments?
if let adjustments = adjustments {
if adjustments.trimApplied() {
finalDuration = adjustments.trimEndValue - adjustments.trimStartValue
}
let adjustmentsData = MemoryBuffer(data: NSKeyedArchiver.archivedData(withRootObject: adjustments.dictionary()))
let digest = MemoryBuffer(data: adjustmentsData.md5Digest())
resourceAdjustments = VideoMediaResourceAdjustments(data: adjustmentsData, digest: digest)
}
let resource = LocalFileVideoMediaResource(randomId: arc4random64(), path: asset.url.path, adjustments: resourceAdjustments)
return standaloneUploadedFile(account: account, peerId: peerId, text: "", source: .resource(.standalone(resource: resource)), mimeType: "video/mp4", attributes: [.Video(duration: Int(finalDuration), size: finalDimensions, flags: flags)], hintFileIsLarge: finalDuration > 3.0 * 60.0)
|> mapError { _ -> Void in
return Void()
}
|> mapToSignal { event -> Signal<PreparedShareItem, Void> in
switch event {
case let .progress(value):
return .single(.progress(value))
case let .result(media):
return .single(.done(.media(media)))
}
}
} else if let data = value["data"] as? Data {
let fileName = value["fileName"] as? String
let mimeType = (value["mimeType"] as? String) ?? "application/octet-stream"
if let image = UIImage(data: data) {
var isGif = false
if data.count > 4 {
data.withUnsafeBytes { (bytes: UnsafePointer<UInt8>) -> Void in
if bytes.advanced(by: 0).pointee == 71 // G
&& bytes.advanced(by: 1).pointee == 73 // I
&& bytes.advanced(by: 2).pointee == 70 // F
&& bytes.advanced(by: 3).pointee == 56 // 8
{
isGif = true
}
}
}
if isGif {
return standaloneUploadedFile(account: account, peerId: peerId, text: "", source: .data(data), mimeType: "animation/gif", attributes: [.ImageSize(size: image.size), .Animated, .FileName(fileName: fileName ?? "animation.gif")], hintFileIsLarge: data.count > 5 * 1024 * 1024)
|> mapError { _ -> Void in return Void() }
|> mapToSignal { event -> Signal<PreparedShareItem, Void> in
switch event {
case let .progress(value):
return .single(.progress(value))
case let .result(media):
return .single(.done(.media(media)))
}
}
} else {
let scaledImage = TGScaleImageToPixelSize(image, CGSize(width: image.size.width * image.scale, height: image.size.height * image.scale).fitted(CGSize(width: 1280.0, height: 1280.0)))!
let imageData = UIImageJPEGRepresentation(scaledImage, 0.54)!
return standaloneUploadedImage(account: account, peerId: peerId, text: "", data: imageData, dimensions: scaledImage.size)
|> mapError { _ -> Void in return Void() }
|> mapToSignal { event -> Signal<PreparedShareItem, Void> in
switch event {
case let .progress(value):
return .single(.progress(value))
case let .result(media):
return .single(.done(.media(media)))
}
}
}
} else {
return standaloneUploadedFile(account: account, peerId: peerId, text: "", source: .data(data), mimeType: mimeType, attributes: [.FileName(fileName: fileName ?? "file")], hintFileIsLarge: data.count > 5 * 1024 * 1024)
|> mapError { _ -> Void in return Void() }
|> mapToSignal { event -> Signal<PreparedShareItem, Void> in
switch event {
case let .progress(value):
return .single(.progress(value))
case let .result(media):
return .single(.done(.media(media)))
}
}
}
} else if let url = value["audio"] as? URL {
if let audioData = try? Data(contentsOf: url, options: [.mappedIfSafe]) {
let fileName = url.lastPathComponent
let duration = (value["duration"] as? NSNumber)?.doubleValue ?? 0.0
let isVoice = ((value["isVoice"] as? NSNumber)?.boolValue ?? false) || (duration.isZero && duration < 30.0)
let title = value["title"] as? String
let artist = value["artist"] as? String
var waveform: MemoryBuffer?
if let waveformData = TGItemProviderSignals.audioWaveform(url) {
waveform = MemoryBuffer(data: waveformData)
}
return standaloneUploadedFile(account: account, peerId: peerId, text: "", source: .data(audioData), mimeType: "audio/ogg", attributes: [.Audio(isVoice: isVoice, duration: Int(duration), title: title, performer: artist, waveform: waveform), .FileName(fileName: fileName)], hintFileIsLarge: audioData.count > 5 * 1024 * 1024)
|> mapError { _ -> Void in return Void() }
|> mapToSignal { event -> Signal<PreparedShareItem, Void> in
switch event {
case let .progress(value):
return .single(.progress(value))
case let .result(media):
return .single(.done(.media(media)))
}
}
} else {
return .never()
}
} else if let text = value["text"] as? String {
return .single(.done(.text(text)))
} else if let url = value["url"] as? URL {
if TGShareLocationSignals.isLocationURL(url) {
return Signal<PreparedShareItem, Void> { subscriber in
subscriber.putNext(.preparing)
let disposable = TGShareLocationSignals.locationMessageContent(for: url).start(next: { value in
if let value = value as? TGShareLocationResult {
if let title = value.title {
subscriber.putNext(.done(.media(.media(.standalone(media: TelegramMediaMap(latitude: value.latitude, longitude: value.longitude, geoPlace: nil, venue: MapVenue(title: title, address: value.address, provider: value.provider, id: value.venueId, type: value.venueType), liveBroadcastingTimeout: nil))))))
} else {
subscriber.putNext(.done(.media(.media(.standalone(media: TelegramMediaMap(latitude: value.latitude, longitude: value.longitude, geoPlace: nil, venue: nil, liveBroadcastingTimeout: nil))))))
}
subscriber.putCompletion()
} else if let value = value as? String {
subscriber.putNext(.done(.text(value)))
subscriber.putCompletion()
}
})
return ActionDisposable {
disposable?.dispose()
}
}
} else {
return .single(.done(.text(url.absoluteString)))
}
} else if let vcard = value["contact"] as? Data, let contactData = DeviceContactExtendedData(vcard: vcard) {
return .single(.userInteractionRequired(.contact(contactData)))
} else {
return .never()
}
}
func preparedShareItems(account: Account, to peerId: PeerId, dataItems: [MTSignal], additionalText: String) -> Signal<PreparedShareItems, Void> {
var dataSignals: Signal<[String: Any], Void> = .complete()
for dataItem in dataItems {
let wrappedSignal: Signal<[String: Any], NoError> = Signal { subscriber in
let disposable = dataItem.start(next: { value in
subscriber.putNext(value as! [String : Any])
}, error: { _ in
}, completed: {
subscriber.putCompletion()
})
return ActionDisposable {
disposable?.dispose()
}
}
dataSignals = dataSignals
|> then(
wrappedSignal
|> introduceError(Void.self)
|> take(1)
)
}
let shareItems = dataSignals
|> map { [$0] }
|> reduceLeft(value: [[String: Any]](), f: { list, rest in
return list + rest
})
|> mapToSignal { items -> Signal<[PreparedShareItem], Void> in
return combineLatest(items.map {
preparedShareItem(account: account, to: peerId, value: $0)
})
}
return shareItems
|> map { items -> PreparedShareItems in
var result: [PreparedShareItemContent] = []
var progresses: [Float] = []
for item in items {
switch item {
case .preparing:
return .preparing
case let .progress(value):
progresses.append(value)
case let .userInteractionRequired(value):
return .userInteractionRequired([value])
case let .done(content):
result.append(content)
progresses.append(1.0)
}
}
if result.count == items.count {
if !additionalText.isEmpty {
result.insert(PreparedShareItemContent.text(additionalText), at: 0)
}
return .done(result)
} else {
let value = progresses.reduce(0.0, +) / Float(progresses.count)
return .progress(value)
}
}
|> distinctUntilChanged(isEqual: { lhs, rhs in
if case .preparing = lhs, case .preparing = rhs {
return true
} else {
return false
}
})
}
func sentShareItems(account: Account, to peerIds: [PeerId], items: [PreparedShareItemContent]) -> Signal<Float, Void> {
var messages: [EnqueueMessage] = []
for item in items {
switch item {
case let .text(text):
messages.append(.message(text: text, attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil))
case let .media(media):
switch media {
case let .media(reference):
messages.append(.message(text: "", attributes: [], mediaReference: reference, replyToMessageId: nil, localGroupingKey: nil))
}
}
}
return enqueueMessagesToMultiplePeers(account: account, peerIds: peerIds, messages: messages)
|> introduceError(Void.self)
|> mapToSignal { messageIds -> Signal<Float, Void> in
var signals: [Signal<Bool, NoError>] = []
for messageId in messageIds {
signals.append(account.postbox.messageView(messageId)
|> map { view -> Bool in
if let message = view.message {
if message.flags.contains(.Unsent) {
return false
}
}
return true
}
|> filter { $0 }
|> take(1))
}
return combineLatest(signals)
|> introduceError(Void.self)
|> map { flags -> Bool in
for flag in flags {
if !flag {
return false
}
}
return true
}
|> filter { $0 }
|> take(1)
|> mapToSignal { _ -> Signal<Float, Void> in
return .single(1.0)
}
}
}

View File

@ -0,0 +1,397 @@
import UIKit
import Display
import TelegramCore
import TelegramUI
import SwiftSignalKit
import Postbox
private var accountCache: (Account, AccountManager)?
private var installedSharedLogger = false
private func setupSharedLogger(_ path: String) {
if !installedSharedLogger {
installedSharedLogger = true
Logger.setSharedLogger(Logger(basePath: path))
}
}
private enum ShareAuthorizationError {
case unauthorized
}
@objc(ShareRootController)
class ShareRootController: UIViewController {
private var mainWindow: Window1?
private var currentShareController: ShareController?
private var currentPasscodeController: ViewController?
private var shouldBeMaster = Promise<Bool>()
private let disposable = MetaDisposable()
private var observer1: AnyObject?
private var observer2: AnyObject?
deinit {
self.disposable.dispose()
self.shouldBeMaster.set(.single(false))
if let observer = self.observer1 {
NotificationCenter.default.removeObserver(observer)
}
if let observer = self.observer2 {
NotificationCenter.default.removeObserver(observer)
}
}
override func loadView() {
telegramUIDeclareEncodables()
super.loadView()
self.view.backgroundColor = nil
self.view.isOpaque = false
if #available(iOSApplicationExtension 8.2, *) {
self.observer1 = NotificationCenter.default.addObserver(forName: NSNotification.Name.NSExtensionHostDidBecomeActive, object: nil, queue: nil, using: { [weak self] _ in
if let strongSelf = self {
strongSelf.shouldBeMaster.set(.single(true))
}
})
self.observer2 = NotificationCenter.default.addObserver(forName: NSNotification.Name.NSExtensionHostWillResignActive, object: nil, queue: nil, using: { [weak self] _ in
if let strongSelf = self {
strongSelf.shouldBeMaster.set(.single(false))
}
})
}
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.shouldBeMaster.set(.single(true))
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
self.disposable.dispose()
self.shouldBeMaster.set(.single(false))
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if self.mainWindow == nil {
let mainWindow = Window1(hostView: childWindowHostView(parent: self.view), statusBarHost: nil)
mainWindow.hostView.eventView.backgroundColor = UIColor.clear
mainWindow.hostView.eventView.isHidden = false
self.mainWindow = mainWindow
self.view.addSubview(mainWindow.hostView.containerView)
mainWindow.hostView.containerView.frame = self.view.bounds
let appBundleIdentifier = Bundle.main.bundleIdentifier!
guard let lastDotRange = appBundleIdentifier.range(of: ".", options: [.backwards]) else {
return
}
let apiId: Int32 = BuildConfig.shared().apiId
let languagesCategory = "ios"
let appGroupName = "group.\(appBundleIdentifier[..<lastDotRange.lowerBound])"
let maybeAppGroupUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupName)
guard let appGroupUrl = maybeAppGroupUrl else {
return
}
let rootPath = rootPathForBasePath(appGroupUrl.path)
performAppGroupUpgrades(appGroupPath: appGroupUrl.path, rootPath: rootPath)
TempBox.initializeShared(basePath: rootPath, processType: "share", launchSpecificId: arc4random64())
let logsPath = rootPath + "/share-logs"
let _ = try? FileManager.default.createDirectory(atPath: logsPath, withIntermediateDirectories: true, attributes: nil)
setupSharedLogger(logsPath)
let applicationBindings = TelegramApplicationBindings(isMainApp: false, openUrl: { _ in
}, openUniversalUrl: { _, completion in
completion.completion(false)
return
}, canOpenUrl: { _ in
return false
}, getTopWindow: {
return nil
}, displayNotification: { _ in
}, applicationInForeground: .single(false), applicationIsActive: .single(false), clearMessageNotifications: { _ in
}, pushIdleTimerExtension: {
return EmptyDisposable
}, openSettings: {}, openAppStorePage: {}, getWindowHost: {
return nil
}, presentNativeController: { _ in
}, dismissNativeController: {
})
let account: Signal<(Account, AccountManager), ShareAuthorizationError>
if let accountCache = accountCache {
account = .single(accountCache)
} else {
let appVersion = (Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String) ?? "unknown"
initializeAccountManagement()
account = accountManager(basePath: rootPath + "/accounts-metadata")
|> take(1)
|> mapToSignal { accountManager -> Signal<(AccountManager, LoggingSettings), NoError> in
return accountManager.transaction { transaction -> (AccountManager, LoggingSettings) in
return (accountManager, transaction.getSharedData(SharedDataKeys.loggingSettings) as? LoggingSettings ?? LoggingSettings.defaultSettings)
}
}
|> introduceError(ShareAuthorizationError.self)
|> mapToSignal { accountManager, loggingSettings -> Signal<(Account, AccountManager), ShareAuthorizationError> in
Logger.shared.logToFile = loggingSettings.logToFile
Logger.shared.logToConsole = loggingSettings.logToConsole
Logger.shared.redactSensitiveData = loggingSettings.redactSensitiveData
return currentAccount(allocateIfNotExists: false, networkArguments: NetworkInitializationArguments(apiId: apiId, languagesCategory: languagesCategory, appVersion: appVersion), supplementary: true, manager: accountManager, rootPath: rootPath, beginWithTestingEnvironment: false, auxiliaryMethods: telegramAccountAuxiliaryMethods)
|> introduceError(ShareAuthorizationError.self) |> mapToSignal { account -> Signal<(Account, AccountManager), ShareAuthorizationError> in
if let account = account {
switch account {
case .upgrading:
return .complete()
case let .authorized(account):
return .single((account, accountManager))
case .unauthorized:
return .fail(.unauthorized)
}
} else {
return .complete()
}
}
}
|> take(1)
}
let preferencesKey: PostboxViewKey = .preferences(keys: Set<ValueBoxKey>([ApplicationSpecificPreferencesKeys.presentationPasscodeSettings]))
let shouldBeMaster = self.shouldBeMaster
let applicationInterface = account
|> mapToSignal { account, accountManager -> Signal<(Account, PostboxAccessChallengeData), ShareAuthorizationError> in
return combineLatest(currentPresentationDataAndSettings(postbox: account.postbox), account.postbox.combinedView(keys: [.accessChallengeData, preferencesKey]) |> take(1))
|> deliverOnMainQueue
|> introduceError(ShareAuthorizationError.self)
|> map { dataAndSettings, data -> (Account, PostboxAccessChallengeData) in
accountCache = (account, accountManager)
account.applicationContext = TelegramApplicationContext(applicationBindings: applicationBindings, accountManager: accountManager, account: account, initialPresentationDataAndSettings: dataAndSettings, postbox: account.postbox)
return (account, (data.views[.accessChallengeData] as! AccessChallengeDataView).data)
}
}
|> afterNext { account, _ in
setupAccount(account)
}
|> deliverOnMainQueue
|> afterNext { [weak self] account, accessChallengeData in
updateLegacyComponentsAccount(account)
initializeLegacyComponents(application: nil, currentSizeClassGetter: { return .compact }, currentHorizontalClassGetter: { return .compact }, documentsPath: "", currentApplicationBounds: { return CGRect() }, canOpenUrl: { _ in return false}, openUrl: { _ in })
let displayShare: () -> Void = {
var cancelImpl: (() -> Void)?
let requestUserInteraction: ([UnpreparedShareItemContent]) -> Signal<[PreparedShareItemContent], NoError> = { content in
return Signal { [weak self] subscriber in
switch content[0] {
case let .contact(data):
let controller = deviceContactInfoController(account: account, subject: .filter(peer: nil, contactId: nil, contactData: data, completion: { peer, contactData in
let phone = contactData.basicData.phoneNumbers[0].value
if let vCardData = contactData.serializedVCard() {
subscriber.putNext([.media(.media(.standalone(media: TelegramMediaContact(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumber: phone, peerId: nil, vCardData: vCardData))))])
}
subscriber.putCompletion()
}), cancelled: {
cancelImpl?()
})
if let strongSelf = self, let window = strongSelf.mainWindow {
controller.presentationArguments = ViewControllerPresentationArguments(presentationAnimation: .modalSheet)
window.present(controller, on: .root)
}
break
}
return ActionDisposable {
}
} |> runOn(Queue.mainQueue())
}
let sentItems: ([PeerId], [PreparedShareItemContent]) -> Signal<ShareControllerExternalStatus, NoError> = { peerIds, contents in
let sentItems = sentShareItems(account: account, to: peerIds, items: contents)
|> `catch` { _ -> Signal<
Float, NoError> in
return .complete()
}
return sentItems
|> map { value -> ShareControllerExternalStatus in
return .progress(value)
}
|> then(.single(.done))
}
let shareController = ShareController(account: account, subject: .fromExternal({ peerIds, additionalText in
if let strongSelf = self, let inputItems = strongSelf.extensionContext?.inputItems, !inputItems.isEmpty, !peerIds.isEmpty {
let rawSignals = TGItemProviderSignals.itemSignals(forInputItems: inputItems)!
return preparedShareItems(account: account, to: peerIds[0], dataItems: rawSignals, additionalText: additionalText)
|> map(Optional.init)
|> `catch` { _ -> Signal<PreparedShareItems?, NoError> in
return .single(nil)
}
|> mapToSignal { state -> Signal<ShareControllerExternalStatus, NoError> in
guard let state = state else {
return .single(.done)
}
switch state {
case .preparing:
return .single(.preparing)
case let .progress(value):
return .single(.progress(value))
case let .userInteractionRequired(value):
return requestUserInteraction(value)
|> mapToSignal { contents -> Signal<ShareControllerExternalStatus, NoError> in
return sentItems(peerIds, contents)
}
case let .done(contents):
return sentItems(peerIds, contents)
}
}
} else {
return .single(.done)
}
}), externalShare: false)
shareController.presentationArguments = ViewControllerPresentationArguments(presentationAnimation: .modalSheet)
shareController.dismissed = {
self?.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil)
}
cancelImpl = { [weak shareController] in
shareController?.dismiss()
}
if let strongSelf = self {
if let currentShareController = strongSelf.currentShareController {
currentShareController.dismiss()
}
strongSelf.currentShareController = shareController
strongSelf.mainWindow?.present(shareController, on: .root)
}
account.resetStateManagement()
account.network.shouldKeepConnection.set(shouldBeMaster.get()
|> map({ $0 }))
}
let _ = passcodeEntryController(account: account, animateIn: true, completion: { value in
if value {
displayShare()
} else {
Queue.mainQueue().after(0.5, {
self?.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil)
})
}
}).start(next: { controller in
guard let strongSelf = self, let controller = controller else {
return
}
if let currentPasscodeController = strongSelf.currentPasscodeController {
currentPasscodeController.dismiss()
}
strongSelf.currentPasscodeController = controller
strongSelf.mainWindow?.present(controller, on: .root)
})
/*var attemptData: TGPasscodeEntryAttemptData?
if let attempts = accessChallengeData.attempts {
attemptData = TGPasscodeEntryAttemptData(numberOfInvalidAttempts: Int(attempts.count), dateOfLastInvalidAttempt: Double(attempts.timestamp))
}
let mode: TGPasscodeEntryControllerMode
switch accessChallengeData {
case .none:
displayShare()
return
case .numericalPassword:
mode = TGPasscodeEntryControllerModeVerifySimple
case .plaintextPassword:
mode = TGPasscodeEntryControllerModeVerifyComplex
}
let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 }
let legacyController = LegacyController(presentation: LegacyControllerPresentation.modal(animateIn: true), theme: presentationData.theme)
let controller = TGPasscodeEntryController(context: legacyController.context, style: TGPasscodeEntryControllerStyleDefault, mode: mode, cancelEnabled: true, allowTouchId: false, attemptData: attemptData, completion: { value in
if value != nil {
displayShare()
} else {
self?.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil)
}
})!
controller.checkCurrentPasscode = { value in
if let value = value {
switch accessChallengeData {
case .none:
return true
case let .numericalPassword(code, _, _):
return value == code
case let .plaintextPassword(code, _, _):
return value == code
}
} else {
return false
}
}
controller.updateAttemptData = { attemptData in
let _ = account.postbox.transaction({ transaction -> Void in
var attempts: AccessChallengeAttempts?
if let attemptData = attemptData {
attempts = AccessChallengeAttempts(count: Int32(attemptData.numberOfInvalidAttempts), timestamp: Int32(attemptData.dateOfLastInvalidAttempt))
}
var data = transaction.getAccessChallengeData()
switch data {
case .none:
break
case let .numericalPassword(value, timeout, _):
data = .numericalPassword(value: value, timeout: timeout, attempts: attempts)
case let .plaintextPassword(value, timeout, _):
data = .plaintextPassword(value: value, timeout: timeout, attempts: attempts)
}
transaction.setAccessChallengeData(data)
}).start()
}
/*controller.touchIdCompletion = {
let _ = (account.postbox.transaction { transaction -> Void in
let data = transaction.getAccessChallengeData().withUpdatedAutolockDeadline(nil)
transaction.setAccessChallengeData(data)
}).start()
}*/
legacyController.bind(controller: controller)
legacyController.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .portrait, compactSize: .portrait)
legacyController.statusBar.statusBarStyle = .White
*/
}
self.disposable.set(applicationInterface.start(next: { _, _ in }, error: { [weak self] error in
guard let strongSelf = self else {
return
}
let presentationData = defaultPresentationData()
let controller = standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: presentationData.strings.Share_AuthTitle, text: presentationData.strings.Share_AuthDescription, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {
self?.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil)
})])
strongSelf.mainWindow?.present(controller, on: .root)
}, completed: {}))
}
}
}

24
Share/TGContactModel.h Normal file
View File

@ -0,0 +1,24 @@
#import <Foundation/Foundation.h>
@interface TGPhoneNumberModel : NSObject
@property (nonatomic, strong, readonly) NSString *phoneNumber;
@property (nonatomic, strong, readonly) NSString *displayPhoneNumber;
@property (nonatomic, strong, readonly) NSString *label;
- (instancetype)initWithPhoneNumber:(NSString *)string label:(NSString *)label;
@end
@interface TGContactModel : NSObject
@property (nonatomic, strong, readonly) NSString *firstName;
@property (nonatomic, strong, readonly) NSString *lastName;
@property (nonatomic, strong, readonly) NSArray *phoneNumbers;
- (instancetype)initWithFirstName:(NSString *)firstName lastName:(NSString *)lastName phoneNumbers:(NSArray *)phoneNumbers;
@end

35
Share/TGContactModel.m Normal file
View File

@ -0,0 +1,35 @@
#import "TGContactModel.h"
#import <LegacyComponents/TGPhoneUtils.h>
@implementation TGPhoneNumberModel
- (instancetype)initWithPhoneNumber:(NSString *)phoneNumber label:(NSString *)label
{
self = [super init];
if (self != nil)
{
_phoneNumber = [TGPhoneUtils cleanInternationalPhone:phoneNumber forceInternational:false];
_displayPhoneNumber = [TGPhoneUtils formatPhone:_phoneNumber forceInternational:false];
_label = label;
}
return self;
}
@end
@implementation TGContactModel
- (instancetype)initWithFirstName:(NSString *)firstName lastName:(NSString *)lastName phoneNumbers:(NSArray *)phoneNumbers
{
self = [super init];
if (self != nil)
{
_firstName = firstName;
_lastName = lastName;
_phoneNumbers = phoneNumbers;
}
return self;
}
@end

View File

@ -0,0 +1,8 @@
#import <MTProtoKitDynamic/MTProtoKitDynamic.h>
@interface TGItemProviderSignals : NSObject
+ (NSArray<MTSignal *> *)itemSignalsForInputItems:(NSArray *)inputItems;
+ (NSData *)audioWaveform:(NSURL *)url;
@end

View File

@ -0,0 +1,659 @@
#import "TGItemProviderSignals.h"
#import <UIKit/UIKit.h>
#import <MobileCoreServices/MobileCoreServices.h>
#import <AddressBook/AddressBook.h>
#import <AVFoundation/AVFoundation.h>
#import <PassKit/PassKit.h>
#import <LegacyComponents/TGPhoneUtils.h>
#import "TGMimeTypeMap.h"
#import "TGContactModel.h"
@implementation TGItemProviderSignals
+ (NSArray<MTSignal *> *)itemSignalsForInputItems:(NSArray *)inputItems
{
NSMutableArray *itemSignals = [[NSMutableArray alloc] init];
NSMutableArray *providers = [[NSMutableArray alloc] init];
for (NSExtensionItem *item in inputItems)
{
for (NSItemProvider *provider in item.attachments)
{
if ([provider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeMovie])
[providers addObject:provider];
else if ([provider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeAudio])
[providers addObject:provider];
else if ([provider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeImage])
[providers addObject:provider];
else if ([provider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeFileURL])
[providers addObject:provider];
else if ([provider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeURL]) {
[providers removeAllObjects];
[providers addObject:provider];
break;
}
else if ([provider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeVCard])
[providers addObject:provider];
else if ([provider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeText])
[providers addObject:provider];
else if ([provider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeData])
[providers addObject:provider];
else if ([provider hasItemConformingToTypeIdentifier:@"com.apple.pkpass"])
[providers addObject:provider];
}
}
NSInteger providerIndex = -1;
for (NSItemProvider *provider in providers)
{
providerIndex++;
MTSignal *dataSignal = nil;
if ([provider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeAudio])
dataSignal = [self signalForAudioItemProvider:provider];
else if ([provider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeMovie])
dataSignal = [self signalForVideoItemProvider:provider];
else if ([provider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeGIF])
dataSignal = [self signalForDataItemProvider:provider];
else if ([provider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeImage])
dataSignal = [self signalForImageItemProvider:provider];
else if ([provider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeFileURL])
{
dataSignal = [[self signalForUrlItemProvider:provider] mapToSignal:^MTSignal *(NSURL *url)
{
NSData *data = [[NSData alloc] initWithContentsOfURL:url options:NSDataReadingMappedIfSafe error:nil];
if (data == nil)
return [MTSignal fail:nil];
NSString *fileName = [[url pathComponents] lastObject];
if (fileName.length == 0)
fileName = @"file.bin";
NSString *extension = [fileName pathExtension];
NSString *mimeType = [TGMimeTypeMap mimeTypeForExtension:[extension lowercaseString]];
if (mimeType == nil)
mimeType = @"application/octet-stream";
return [MTSignal single:@{@"data": data, @"fileName": fileName, @"mimeType": mimeType}];
}];
}
else if ([provider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeVCard])
dataSignal = [self signalForVCardItemProvider:provider];
else if ([provider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeText])
dataSignal = [self signalForTextItemProvider:provider];
else if ([provider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeURL])
dataSignal = [self signalForTextUrlItemProvider:provider];
else if ([provider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeData])
{
dataSignal = [[self signalForDataItemProvider:provider] map:^id(NSDictionary *dict)
{
if (dict[@"fileName"] == nil)
{
NSMutableDictionary *updatedDict = [[NSMutableDictionary alloc] initWithDictionary:dict];
for (NSString *typeIdentifier in provider.registeredTypeIdentifiers)
{
NSString *extension = [TGMimeTypeMap extensionForMimeType:typeIdentifier];
if (extension == nil)
extension = [TGMimeTypeMap extensionForMimeType:[@"application/" stringByAppendingString:typeIdentifier]];
if (extension != nil) {
updatedDict[@"fileName"] = [@"file" stringByAppendingPathExtension:extension];
updatedDict[@"mimeType"] = [TGMimeTypeMap mimeTypeForExtension:extension];
}
}
return updatedDict;
}
else
{
return dict;
}
}];
}
else if ([provider hasItemConformingToTypeIdentifier:@"com.apple.pkpass"])
{
dataSignal = [self signalForPassKitItemProvider:provider];
}
if (dataSignal != nil)
[itemSignals addObject:dataSignal];
}
return itemSignals;
}
+ (MTSignal *)signalForDataItemProvider:(NSItemProvider *)itemProvider
{
return [[MTSignal alloc] initWithGenerator:^id<MTDisposable>(MTSubscriber *subscriber)
{
[itemProvider loadItemForTypeIdentifier:(NSString *)kUTTypeData options:nil completionHandler:^(NSData *data, NSError *error)
{
if (error != nil)
[subscriber putError:nil];
else
{
[subscriber putNext:@{@"data": data}];
[subscriber putCompletion];
}
}];
return nil;
}];
}
static UIImage *TGScaleImageToPixelSize(UIImage *image, CGSize size) {
UIGraphicsBeginImageContextWithOptions(size, true, 1.0f);
[image drawInRect:CGRectMake(0, 0, size.width, size.height) blendMode:kCGBlendModeCopy alpha:1.0f];
UIImage *result = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return result;
}
static CGSize TGFitSize(CGSize size, CGSize maxSize) {
if (size.width < 1)
size.width = 1;
if (size.height < 1)
size.height = 1;
if (size.width > maxSize.width)
{
size.height = floor((size.height * maxSize.width / size.width));
size.width = maxSize.width;
}
if (size.height > maxSize.height)
{
size.width = floor((size.width * maxSize.height / size.height));
size.height = maxSize.height;
}
return size;
}
+ (MTSignal *)signalForImageItemProvider:(NSItemProvider *)itemProvider
{
return [[MTSignal alloc] initWithGenerator:^id<MTDisposable>(MTSubscriber *subscriber)
{
CGSize maxSize = CGSizeMake(1280.0, 1280.0);
NSDictionary *imageOptions = @{
NSItemProviderPreferredImageSizeKey: [NSValue valueWithCGSize:maxSize]
};
if ([itemProvider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeImage]) {
[itemProvider loadItemForTypeIdentifier:(NSString *)kUTTypeImage options:imageOptions completionHandler:^(id<NSSecureCoding> _Nullable item, NSError * _Null_unspecified error) {
if (error != nil && ![(NSObject *)item respondsToSelector:@selector(CGImage)] && ![(NSObject *)item respondsToSelector:@selector(absoluteString)]) {
[itemProvider loadItemForTypeIdentifier:(NSString *)kUTTypeData options:nil completionHandler:^(UIImage *image, NSError *error)
{
if (error != nil)
[subscriber putError:nil];
else
{
[subscriber putNext:@{@"image": image}];
[subscriber putCompletion];
}
}];
} else {
if ([(NSObject *)item respondsToSelector:@selector(absoluteString)]) {
NSURL *url = (NSURL *)item;
UIImage *image = [[UIImage alloc] initWithContentsOfFile:[url path]];
if (image != nil) {
UIImage *result = TGScaleImageToPixelSize(image, TGFitSize(image.size, maxSize));
NSData *resultData = UIImageJPEGRepresentation(result, 0.52f);
if (resultData != nil) {
[subscriber putNext:@{@"scaledImageData": resultData, @"scaledImageDimensions": [NSValue valueWithCGSize:result.size]}];
[subscriber putCompletion];
} else {
[subscriber putError:nil];
}
} else {
[subscriber putError:nil];
}
} else {
[subscriber putNext:@{@"image": item}];
[subscriber putCompletion];
}
}
}];
} else {
[itemProvider loadItemForTypeIdentifier:(NSString *)kUTTypeData options:nil completionHandler:^(UIImage *image, NSError *error)
{
if (error != nil)
[subscriber putError:nil];
else
{
[subscriber putNext:@{@"image": image}];
[subscriber putCompletion];
}
}];
}
return nil;
}];
}
+ (MTSignal *)signalForAudioItemProvider:(NSItemProvider *)itemProvider
{
MTSignal *itemSignal = [[MTSignal alloc] initWithGenerator:^id<MTDisposable>(MTSubscriber *subscriber)
{
[itemProvider loadItemForTypeIdentifier:(NSString *)kUTTypeAudio options:nil completionHandler:^(NSURL *url, NSError *error)
{
if (error != nil)
[subscriber putError:nil];
else
{
[subscriber putNext:url];
[subscriber putCompletion];
}
}];
return nil;
}];
return [itemSignal map:^id(NSURL *url)
{
AVAsset *asset = [AVURLAsset URLAssetWithURL:url options:nil];
if (asset == nil)
return [MTSignal fail:nil];
NSString *extension = url.pathExtension;
NSString *mimeType = [TGMimeTypeMap mimeTypeForExtension:[extension lowercaseString]];
if (mimeType == nil)
mimeType = @"application/octet-stream";
NSString *title = (NSString *)[[AVMetadataItem metadataItemsFromArray:asset.commonMetadata withKey:AVMetadataCommonKeyTitle keySpace:AVMetadataKeySpaceCommon] firstObject];
NSString *artist = (NSString *)[[AVMetadataItem metadataItemsFromArray:asset.commonMetadata withKey:AVMetadataCommonKeyArtist keySpace:AVMetadataKeySpaceCommon] firstObject];
NSString *software = nil;
AVMetadataItem *softwareItem = [[AVMetadataItem metadataItemsFromArray:asset.commonMetadata withKey:AVMetadataCommonKeySoftware keySpace:AVMetadataKeySpaceCommon] firstObject];
if ([softwareItem isKindOfClass:[AVMetadataItem class]] && ([softwareItem.value isKindOfClass:[NSString class]]))
software = (NSString *)[softwareItem value];
bool isVoice = [software hasPrefix:@"com.apple.VoiceMemos"];
NSTimeInterval duration = CMTimeGetSeconds(asset.duration);
NSMutableDictionary *result = [[NSMutableDictionary alloc] init];
result[@"audio"] = url;
result[@"mimeType"] = mimeType;
result[@"duration"] = @(duration);
result[@"isVoice"] = @(isVoice);
NSString *artistString = @"";
if ([artist respondsToSelector:@selector(characterAtIndex:)]) {
artistString = artist;
} else if ([artist isKindOfClass:[AVMetadataItem class]]) {
artistString = [(AVMetadataItem *)artist stringValue];
}
NSString *titleString = @"";
if ([artist respondsToSelector:@selector(characterAtIndex:)]) {
titleString = title;
} else if ([title isKindOfClass:[AVMetadataItem class]]) {
titleString = [(AVMetadataItem *)title stringValue];
}
if (artistString.length > 0)
result[@"artist"] = artistString;
if (titleString.length > 0)
result[@"title"] = titleString;
return result;
}];
}
+ (MTSignal *)detectRoundVideo:(AVAsset *)asset
{
MTSignal *imageSignal = [[MTSignal alloc] initWithGenerator:^id<MTDisposable>(MTSubscriber *subsriber)
{
AVAssetImageGenerator *imageGenerator = [[AVAssetImageGenerator alloc] initWithAsset:asset];
imageGenerator.appliesPreferredTrackTransform = true;
[imageGenerator generateCGImagesAsynchronouslyForTimes:@[ [NSValue valueWithCMTime:kCMTimeZero] ] completionHandler:^(CMTime requestedTime, CGImageRef _Nullable image, CMTime actualTime, AVAssetImageGeneratorResult result, NSError * _Nullable error)
{
if (error != nil)
{
[subsriber putError:nil];
}
else
{
[subsriber putNext:[UIImage imageWithCGImage:image]];
[subsriber putCompletion];
}
}];
return [[MTBlockDisposable alloc] initWithBlock:^
{
[imageGenerator cancelAllCGImageGeneration];
}];
}];
return [imageSignal map:^NSNumber *(UIImage *image)
{
CFDataRef pixelData = CGDataProviderCopyData(CGImageGetDataProvider(image.CGImage));
const UInt8 *data = CFDataGetBytePtr(pixelData);
bool (^isWhitePixel)(NSInteger, NSInteger) = ^bool(NSInteger x, NSInteger y)
{
int pixelInfo = ((image.size.width * y) + x ) * 4;
UInt8 red = data[pixelInfo];
UInt8 green = data[(pixelInfo + 1)];
UInt8 blue = data[pixelInfo + 2];
return (red > 250 && green > 250 && blue > 250);
};
CFRelease(pixelData);
return @(isWhitePixel(0, 0) && isWhitePixel(image.size.width - 1, 0) && isWhitePixel(0, image.size.height - 1) && isWhitePixel(image.size.width - 1, image.size.height - 1));
}];
}
+ (MTSignal *)signalForVideoItemProvider:(NSItemProvider *)itemProvider
{
MTSignal *assetSignal = [[MTSignal alloc] initWithGenerator:^id<MTDisposable>(MTSubscriber *subscriber)
{
[itemProvider loadItemForTypeIdentifier:(NSString *)kUTTypeMovie options:nil completionHandler:^(NSURL *url, NSError *error)
{
if (error != nil)
{
[subscriber putError:nil];
}
else
{
AVURLAsset *asset = [[AVURLAsset alloc] initWithURL:url options:nil];
[subscriber putNext:asset];
[subscriber putCompletion];
}
}];
return nil;
}];
return [assetSignal mapToSignal:^MTSignal *(AVURLAsset *asset)
{
AVAssetTrack *videoTrack = [[asset tracksWithMediaType:AVMediaTypeVideo] firstObject];
if (videoTrack == nil)
{
return [MTSignal fail:nil];
}
else
{
CGSize dimensions = CGRectApplyAffineTransform((CGRect){CGPointZero, videoTrack.naturalSize}, videoTrack.preferredTransform).size;
NSString *extension = asset.URL.pathExtension;
NSString *mimeType = [TGMimeTypeMap mimeTypeForExtension:[extension lowercaseString]];
if (mimeType == nil)
mimeType = @"application/octet-stream";
NSString *software = nil;
AVMetadataItem *softwareItem = [[AVMetadataItem metadataItemsFromArray:asset.metadata withKey:AVMetadataCommonKeySoftware keySpace:AVMetadataKeySpaceCommon] firstObject];
if ([softwareItem isKindOfClass:[AVMetadataItem class]] && ([softwareItem.value isKindOfClass:[NSString class]]))
software = (NSString *)[softwareItem value];
bool isAnimation = false;
if ([software hasPrefix:@"Boomerang"])
isAnimation = true;
if (isAnimation || fabs(dimensions.width - dimensions.height) > FLT_EPSILON)
{
return [MTSignal single:@{@"video": asset, @"mimeType": mimeType, @"isAnimation": @(isAnimation), @"width": @(dimensions.width), @"height": @(dimensions.height)}];
}
else
{
return [[self detectRoundVideo:asset] mapToSignal:^MTSignal *(NSNumber *isRoundVideo)
{
return [MTSignal single:@{@"video": asset, @"mimeType": mimeType, @"isAnimation": @(isAnimation), @"width": @(dimensions.width), @"height": @(dimensions.height), @"isRoundMessage": isRoundVideo}];
}];
}
}
}];
}
+ (MTSignal *)signalForUrlItemProvider:(NSItemProvider *)itemProvider
{
return [[MTSignal alloc] initWithGenerator:^id<MTDisposable>(MTSubscriber *subscriber)
{
[itemProvider loadItemForTypeIdentifier:(NSString *)kUTTypeFileURL options:nil completionHandler:^(NSURL *url, NSError *error)
{
if (error != nil)
[subscriber putError:nil];
else
{
[subscriber putNext:url];
[subscriber putCompletion];
}
}];
return nil;
}];
}
+ (MTSignal *)signalForTextItemProvider:(NSItemProvider *)itemProvider
{
return [[MTSignal alloc] initWithGenerator:^id<MTDisposable>(MTSubscriber *subscriber)
{
[itemProvider loadItemForTypeIdentifier:(NSString *)kUTTypeText options:nil completionHandler:^(NSString *text, NSError *error)
{
if (error != nil)
[subscriber putError:nil];
else
{
[subscriber putNext:@{@"text": text}];
[subscriber putCompletion];
}
}];
return nil;
}];
}
+ (MTSignal *)signalForTextUrlItemProvider:(NSItemProvider *)itemProvider
{
return [[MTSignal alloc] initWithGenerator:^id<MTDisposable>(MTSubscriber *subscriber)
{
[itemProvider loadItemForTypeIdentifier:(NSString *)kUTTypeURL options:nil completionHandler:^(NSURL *url, NSError *error)
{
if (error != nil)
[subscriber putError:nil];
else
{
[subscriber putNext:@{@"url": url}];
[subscriber putCompletion];
}
}];
return nil;
}];
}
+ (MTSignal *)signalForVCardItemProvider:(NSItemProvider *)itemProvider
{
return [[MTSignal alloc] initWithGenerator:^id<MTDisposable>(MTSubscriber *subscriber)
{
[itemProvider loadItemForTypeIdentifier:(NSString *)kUTTypeVCard options:nil completionHandler:^(NSData *vcard, NSError *error)
{
if (error != nil)
[subscriber putError:nil];
else
{
[subscriber putNext:@{@"contact": vcard}];
[subscriber putCompletion];
}
}];
return nil;
}];
}
+ (MTSignal *)signalForPassKitItemProvider:(NSItemProvider *)itemProvider
{
return [[MTSignal alloc] initWithGenerator:^id<MTDisposable>(MTSubscriber *subscriber)
{
[itemProvider loadItemForTypeIdentifier:@"com.apple.pkpass" options:nil completionHandler:^(id data, NSError *error)
{
if (error != nil)
{
[subscriber putError:nil];
}
else
{
NSError *parseError;
PKPass *pass = [[PKPass alloc] initWithData:data error:&parseError];
if (parseError != nil)
{
[subscriber putError:nil];
}
else
{
NSString *fileName = [NSString stringWithFormat:@"%@.pkpass", pass.serialNumber];
[subscriber putNext:@{@"data": data, @"fileName": fileName, @"mimeType": @"application/vnd.apple.pkpass"}];
[subscriber putCompletion];
}
}
}];
return nil;
}];
}
static void set_bits(uint8_t *bytes, int32_t bitOffset, int32_t numBits, int32_t value) {
numBits = (unsigned int)pow(2, numBits) - 1; //this will only work up to 32 bits, of course
uint8_t *data = bytes;
data += bitOffset / 8;
bitOffset %= 8;
*((int32_t *)data) |= ((value) << bitOffset);
}
+ (NSData *)audioWaveform:(NSURL *)url {
NSDictionary *outputSettings = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithInt:kAudioFormatLinearPCM], AVFormatIDKey,
[NSNumber numberWithFloat:44100.0], AVSampleRateKey,
[NSNumber numberWithInt:16], AVLinearPCMBitDepthKey,
[NSNumber numberWithBool:NO], AVLinearPCMIsNonInterleaved,
[NSNumber numberWithBool:NO], AVLinearPCMIsFloatKey,
[NSNumber numberWithBool:NO], AVLinearPCMIsBigEndianKey,
nil];
AVURLAsset *asset = [AVURLAsset URLAssetWithURL:url options:nil];
if (asset == nil) {
NSLog(@"asset is not defined!");
return nil;
}
NSError *assetError = nil;
AVAssetReader *iPodAssetReader = [AVAssetReader assetReaderWithAsset:asset error:&assetError];
if (assetError) {
NSLog (@"error: %@", assetError);
return nil;
}
AVAssetReaderOutput *readerOutput = [AVAssetReaderAudioMixOutput assetReaderAudioMixOutputWithAudioTracks:asset.tracks audioSettings:outputSettings];
if (! [iPodAssetReader canAddOutput: readerOutput]) {
NSLog (@"can't add reader output... die!");
return nil;
}
// add output reader to reader
[iPodAssetReader addOutput: readerOutput];
if (! [iPodAssetReader startReading]) {
NSLog(@"Unable to start reading!");
return nil;
}
NSMutableData *_waveformSamples = [[NSMutableData alloc] init];
int16_t _waveformPeak = 0;
int _waveformPeakCount = 0;
while (iPodAssetReader.status == AVAssetReaderStatusReading) {
// Check if the available buffer space is enough to hold at least one cycle of the sample data
CMSampleBufferRef nextBuffer = [readerOutput copyNextSampleBuffer];
if (nextBuffer) {
AudioBufferList abl;
CMBlockBufferRef blockBuffer = NULL;
CMSampleBufferGetAudioBufferListWithRetainedBlockBuffer(nextBuffer, NULL, &abl, sizeof(abl), NULL, NULL, kCMSampleBufferFlag_AudioBufferList_Assure16ByteAlignment, &blockBuffer);
UInt64 size = CMSampleBufferGetTotalSampleSize(nextBuffer);
if (size != 0) {
int16_t *samples = (int16_t *)(abl.mBuffers[0].mData);
int count = (int)size / 2;
for (int i = 0; i < count; i++) {
int16_t sample = samples[i];
if (sample < 0) {
sample = -sample;
}
if (_waveformPeak < sample) {
_waveformPeak = sample;
}
_waveformPeakCount++;
if (_waveformPeakCount >= 100) {
[_waveformSamples appendBytes:&_waveformPeak length:2];
_waveformPeak = 0;
_waveformPeakCount = 0;
}
}
}
CFRelease(nextBuffer);
if (blockBuffer) {
CFRelease(blockBuffer);
}
}
else {
break;
}
}
int16_t scaledSamples[100];
memset(scaledSamples, 0, 100 * 2);
int16_t *samples = _waveformSamples.mutableBytes;
int count = (int)_waveformSamples.length / 2;
for (int i = 0; i < count; i++) {
int16_t sample = samples[i];
int index = i * 100 / count;
if (scaledSamples[index] < sample) {
scaledSamples[index] = sample;
}
}
int16_t peak = 0;
int64_t sumSamples = 0;
for (int i = 0; i < 100; i++) {
int16_t sample = scaledSamples[i];
if (peak < sample) {
peak = sample;
}
sumSamples += sample;
}
uint16_t calculatedPeak = 0;
calculatedPeak = (uint16_t)(sumSamples * 1.8f / 100);
if (calculatedPeak < 2500) {
calculatedPeak = 2500;
}
for (int i = 0; i < 100; i++) {
uint16_t sample = (uint16_t)((int64_t)samples[i]);
if (sample > calculatedPeak) {
scaledSamples[i] = calculatedPeak;
}
}
int numSamples = 100;
int bitstreamLength = (numSamples * 5) / 8 + (((numSamples * 5) % 8) == 0 ? 0 : 1);
NSMutableData *result = [[NSMutableData alloc] initWithLength:bitstreamLength];
{
int32_t maxSample = peak;
uint16_t const *samples = (uint16_t *)scaledSamples;
uint8_t *bytes = result.mutableBytes;
for (int i = 0; i < numSamples; i++) {
int32_t value = MIN(31, ABS((int32_t)samples[i]) * 31 / maxSample);
set_bits(bytes, i * 5, 5, value & 31);
}
}
return result;
}
@end

8
Share/TGMimeTypeMap.h Normal file
View File

@ -0,0 +1,8 @@
#import <Foundation/Foundation.h>
@interface TGMimeTypeMap : NSObject
+ (NSString *)mimeTypeForExtension:(NSString *)extension;
+ (NSString *)extensionForMimeType:(NSString *)mimeType;
@end

347
Share/TGMimeTypeMap.m Normal file
View File

@ -0,0 +1,347 @@
#import "TGMimeTypeMap.h"
static NSDictionary *mimeToExtensionMap = nil;
static NSDictionary *extensionToMimeMap = nil;
static void initializeMapping()
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^
{
NSMutableDictionary *mimeToExtension = [[NSMutableDictionary alloc] init];
NSMutableDictionary *extensionToMime = [[NSMutableDictionary alloc] init];
mimeToExtension[@"application/andrew-inset"] = @"ez"; extensionToMime[@"ez"] = @"application/andrew-inset";
mimeToExtension[@"application/dsptype"] = @"tsp"; extensionToMime[@"tsp"] = @"application/dsptype";
mimeToExtension[@"application/futuresplash"] = @"spl"; extensionToMime[@"spl"] = @"application/futuresplash";
mimeToExtension[@"application/hta"] = @"hta"; extensionToMime[@"hta"] = @"application/hta";
mimeToExtension[@"application/mac-binhex40"] = @"hqx"; extensionToMime[@"hqx"] = @"application/mac-binhex40";
mimeToExtension[@"application/mac-compactpro"] = @"cpt"; extensionToMime[@"cpt"] = @"application/mac-compactpro";
mimeToExtension[@"application/mathematica"] = @"nb"; extensionToMime[@"nb"] = @"application/mathematica";
mimeToExtension[@"application/msaccess"] = @"mdb"; extensionToMime[@"mdb"] = @"application/msaccess";
mimeToExtension[@"application/oda"] = @"oda"; extensionToMime[@"oda"] = @"application/oda";
mimeToExtension[@"application/ogg"] = @"ogg"; extensionToMime[@"ogg"] = @"application/ogg";
mimeToExtension[@"application/pdf"] = @"pdf"; extensionToMime[@"pdf"] = @"application/pdf";
mimeToExtension[@"application/com.adobe.pdf"] = @"pdf";
mimeToExtension[@"application/pgp-keys"] = @"key"; extensionToMime[@"key"] = @"application/pgp-keys";
mimeToExtension[@"application/pgp-signature"] = @"pgp"; extensionToMime[@"pgp"] = @"application/pgp-signature";
mimeToExtension[@"application/pics-rules"] = @"prf"; extensionToMime[@"prf"] = @"application/pics-rules";
mimeToExtension[@"application/rar"] = @"rar"; extensionToMime[@"rar"] = @"application/rar";
mimeToExtension[@"application/rdf+xml"] = @"rdf"; extensionToMime[@"rdf"] = @"application/rdf+xml";
mimeToExtension[@"application/rss+xml"] = @"rss"; extensionToMime[@"rss"] = @"application/rss+xml";
mimeToExtension[@"application/zip"] = @"zip"; extensionToMime[@"zip"] = @"application/zip";
mimeToExtension[@"application/vnd.android.package-archive"] = @"apk"; extensionToMime[@"apk"] = @"application/vnd.android.package-archive";
mimeToExtension[@"application/vnd.cinderella"] = @"cdy"; extensionToMime[@"cdy"] = @"application/vnd.cinderella";
mimeToExtension[@"application/vnd.ms-pki.stl"] = @"stl"; extensionToMime[@"stl"] = @"application/vnd.ms-pki.stl";
mimeToExtension[@"application/vnd.oasis.opendocument.database"] = @"odb"; extensionToMime[@"odb"] = @"application/vnd.oasis.opendocument.database";
mimeToExtension[@"application/vnd.oasis.opendocument.formula"] = @"odf"; extensionToMime[@"odf"] = @"application/vnd.oasis.opendocument.formula";
mimeToExtension[@"application/vnd.oasis.opendocument.graphics"] = @"odg"; extensionToMime[@"odg"] = @"application/vnd.oasis.opendocument.graphics";
mimeToExtension[@"application/vnd.oasis.opendocument.graphics-template"] = @"otg"; extensionToMime[@"otg"] = @"application/vnd.oasis.opendocument.graphics-template";
mimeToExtension[@"application/vnd.oasis.opendocument.image"] = @"odi"; extensionToMime[@"odi"] = @"application/vnd.oasis.opendocument.image";
mimeToExtension[@"application/vnd.oasis.opendocument.spreadsheet"] = @"ods"; extensionToMime[@"ods"] = @"application/vnd.oasis.opendocument.spreadsheet";
mimeToExtension[@"application/vnd.oasis.opendocument.spreadsheet-template"] = @"ots"; extensionToMime[@"ots"] = @"application/vnd.oasis.opendocument.spreadsheet-template";
mimeToExtension[@"application/vnd.oasis.opendocument.text"] = @"odt"; extensionToMime[@"odt"] = @"application/vnd.oasis.opendocument.text";
mimeToExtension[@"application/vnd.oasis.opendocument.text-master"] = @"odm"; extensionToMime[@"odm"] = @"application/vnd.oasis.opendocument.text-master";
mimeToExtension[@"application/vnd.oasis.opendocument.text-template"] = @"ott"; extensionToMime[@"ott"] = @"application/vnd.oasis.opendocument.text-template";
mimeToExtension[@"application/vnd.oasis.opendocument.text-web"] = @"oth"; extensionToMime[@"oth"] = @"application/vnd.oasis.opendocument.text-web";
mimeToExtension[@"application/msword"] = @"doc"; extensionToMime[@"doc"] = @"application/msword";
mimeToExtension[@"application/msword"] = @"dot"; extensionToMime[@"dot"] = @"application/msword";
mimeToExtension[@"application/vnd.openxmlformats-officedocument.wordprocessingml.document"] = @"docx"; extensionToMime[@"docx"] = @"application/vnd.openxmlformats-officedocument.wordprocessingml.document";
mimeToExtension[@"application/vnd.openxmlformats-officedocument.wordprocessingml.template"] = @"dotx"; extensionToMime[@"dotx"] = @"application/vnd.openxmlformats-officedocument.wordprocessingml.template";
mimeToExtension[@"application/vnd.ms-excel"] = @"xls"; extensionToMime[@"xls"] = @"application/vnd.ms-excel";
mimeToExtension[@"application/vnd.ms-excel"] = @"xlt"; extensionToMime[@"xlt"] = @"application/vnd.ms-excel";
mimeToExtension[@"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"] = @"xlsx"; extensionToMime[@"xlsx"] = @"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
mimeToExtension[@"application/vnd.openxmlformats-officedocument.spreadsheetml.template"] = @"xltx"; extensionToMime[@"xltx"] = @"application/vnd.openxmlformats-officedocument.spreadsheetml.template";
mimeToExtension[@"application/vnd.ms-powerpoint"] = @"ppt"; extensionToMime[@"ppt"] = @"application/vnd.ms-powerpoint";
mimeToExtension[@"application/vnd.ms-powerpoint"] = @"pot"; extensionToMime[@"pot"] = @"application/vnd.ms-powerpoint";
mimeToExtension[@"application/vnd.ms-powerpoint"] = @"pps"; extensionToMime[@"pps"] = @"application/vnd.ms-powerpoint";
mimeToExtension[@"application/vnd.openxmlformats-officedocument.presentationml.presentation"] = @"pptx"; extensionToMime[@"pptx"] = @"application/vnd.openxmlformats-officedocument.presentationml.presentation";
mimeToExtension[@"application/vnd.openxmlformats-officedocument.presentationml.template"] = @"potx"; extensionToMime[@"potx"] = @"application/vnd.openxmlformats-officedocument.presentationml.template";
mimeToExtension[@"application/vnd.openxmlformats-officedocument.presentationml.slideshow"] = @"ppsx"; extensionToMime[@"ppsx"] = @"application/vnd.openxmlformats-officedocument.presentationml.slideshow";
mimeToExtension[@"application/vnd.rim.cod"] = @"cod"; extensionToMime[@"cod"] = @"application/vnd.rim.cod";
mimeToExtension[@"application/vnd.smaf"] = @"mmf"; extensionToMime[@"mmf"] = @"application/vnd.smaf";
mimeToExtension[@"application/vnd.stardivision.calc"] = @"sdc"; extensionToMime[@"sdc"] = @"application/vnd.stardivision.calc";
mimeToExtension[@"application/vnd.stardivision.draw"] = @"sda"; extensionToMime[@"sda"] = @"application/vnd.stardivision.draw";
mimeToExtension[@"application/vnd.stardivision.impress"] = @"sdd"; extensionToMime[@"sdd"] = @"application/vnd.stardivision.impress";
mimeToExtension[@"application/vnd.stardivision.impress"] = @"sdp"; extensionToMime[@"sdp"] = @"application/vnd.stardivision.impress";
mimeToExtension[@"application/vnd.stardivision.math"] = @"smf"; extensionToMime[@"smf"] = @"application/vnd.stardivision.math";
mimeToExtension[@"application/vnd.stardivision.writer"] = @"sdw"; extensionToMime[@"sdw"] = @"application/vnd.stardivision.writer";
mimeToExtension[@"application/vnd.stardivision.writer"] = @"vor"; extensionToMime[@"vor"] = @"application/vnd.stardivision.writer";
mimeToExtension[@"application/vnd.stardivision.writer-global"] = @"sgl"; extensionToMime[@"sgl"] = @"application/vnd.stardivision.writer-global";
mimeToExtension[@"application/vnd.sun.xml.calc"] = @"sxc"; extensionToMime[@"sxc"] = @"application/vnd.sun.xml.calc";
mimeToExtension[@"application/vnd.sun.xml.calc.template"] = @"stc"; extensionToMime[@"stc"] = @"application/vnd.sun.xml.calc.template";
mimeToExtension[@"application/vnd.sun.xml.draw"] = @"sxd"; extensionToMime[@"sxd"] = @"application/vnd.sun.xml.draw";
mimeToExtension[@"application/vnd.sun.xml.draw.template"] = @"std"; extensionToMime[@"std"] = @"application/vnd.sun.xml.draw.template";
mimeToExtension[@"application/vnd.sun.xml.impress"] = @"sxi"; extensionToMime[@"sxi"] = @"application/vnd.sun.xml.impress";
mimeToExtension[@"application/vnd.sun.xml.impress.template"] = @"sti"; extensionToMime[@"sti"] = @"application/vnd.sun.xml.impress.template";
mimeToExtension[@"application/vnd.sun.xml.math"] = @"sxm"; extensionToMime[@"sxm"] = @"application/vnd.sun.xml.math";
mimeToExtension[@"application/vnd.sun.xml.writer"] = @"sxw"; extensionToMime[@"sxw"] = @"application/vnd.sun.xml.writer";
mimeToExtension[@"application/vnd.sun.xml.writer.global"] = @"sxg"; extensionToMime[@"sxg"] = @"application/vnd.sun.xml.writer.global";
mimeToExtension[@"application/vnd.sun.xml.writer.template"] = @"stw"; extensionToMime[@"stw"] = @"application/vnd.sun.xml.writer.template";
mimeToExtension[@"application/vnd.visio"] = @"vsd"; extensionToMime[@"vsd"] = @"application/vnd.visio";
mimeToExtension[@"application/x-abiword"] = @"abw"; extensionToMime[@"abw"] = @"application/x-abiword";
mimeToExtension[@"application/x-apple-diskimage"] = @"dmg"; extensionToMime[@"dmg"] = @"application/x-apple-diskimage";
mimeToExtension[@"application/x-bcpio"] = @"bcpio"; extensionToMime[@"bcpio"] = @"application/x-bcpio";
mimeToExtension[@"application/x-bittorrent"] = @"torrent"; extensionToMime[@"torrent"] = @"application/x-bittorrent";
mimeToExtension[@"application/x-cdf"] = @"cdf"; extensionToMime[@"cdf"] = @"application/x-cdf";
mimeToExtension[@"application/x-cdlink"] = @"vcd"; extensionToMime[@"vcd"] = @"application/x-cdlink";
mimeToExtension[@"application/x-chess-pgn"] = @"pgn"; extensionToMime[@"pgn"] = @"application/x-chess-pgn";
mimeToExtension[@"application/x-cpio"] = @"cpio"; extensionToMime[@"cpio"] = @"application/x-cpio";
mimeToExtension[@"application/x-debian-package"] = @"deb"; extensionToMime[@"deb"] = @"application/x-debian-package";
mimeToExtension[@"application/x-debian-package"] = @"udeb"; extensionToMime[@"udeb"] = @"application/x-debian-package";
mimeToExtension[@"application/x-director"] = @"dcr"; extensionToMime[@"dcr"] = @"application/x-director";
mimeToExtension[@"application/x-director"] = @"dir"; extensionToMime[@"dir"] = @"application/x-director";
mimeToExtension[@"application/x-director"] = @"dxr"; extensionToMime[@"dxr"] = @"application/x-director";
mimeToExtension[@"application/x-dms"] = @"dms"; extensionToMime[@"dms"] = @"application/x-dms";
mimeToExtension[@"application/x-doom"] = @"wad"; extensionToMime[@"wad"] = @"application/x-doom";
mimeToExtension[@"application/x-dvi"] = @"dvi"; extensionToMime[@"dvi"] = @"application/x-dvi";
mimeToExtension[@"application/x-flac"] = @"flac"; extensionToMime[@"flac"] = @"application/x-flac";
mimeToExtension[@"application/x-font"] = @"pfa"; extensionToMime[@"pfa"] = @"application/x-font";
mimeToExtension[@"application/x-font"] = @"pfb"; extensionToMime[@"pfb"] = @"application/x-font";
mimeToExtension[@"application/x-font"] = @"gsf"; extensionToMime[@"gsf"] = @"application/x-font";
mimeToExtension[@"application/x-font"] = @"pcf"; extensionToMime[@"pcf"] = @"application/x-font";
mimeToExtension[@"application/x-font"] = @"pcf.Z"; extensionToMime[@"pcf.Z"] = @"application/x-font";
mimeToExtension[@"application/x-freemind"] = @"mm"; extensionToMime[@"mm"] = @"application/x-freemind";
mimeToExtension[@"application/x-futuresplash"] = @"spl"; extensionToMime[@"spl"] = @"application/x-futuresplash";
mimeToExtension[@"application/x-gnumeric"] = @"gnumeric"; extensionToMime[@"gnumeric"] = @"application/x-gnumeric";
mimeToExtension[@"application/x-go-sgf"] = @"sgf"; extensionToMime[@"sgf"] = @"application/x-go-sgf";
mimeToExtension[@"application/x-graphing-calculator"] = @"gcf"; extensionToMime[@"gcf"] = @"application/x-graphing-calculator";
mimeToExtension[@"application/x-gtar"] = @"gtar"; extensionToMime[@"gtar"] = @"application/x-gtar";
mimeToExtension[@"application/x-gtar"] = @"tgz"; extensionToMime[@"tgz"] = @"application/x-gtar";
mimeToExtension[@"application/x-gtar"] = @"taz"; extensionToMime[@"taz"] = @"application/x-gtar";
mimeToExtension[@"application/x-hdf"] = @"hdf"; extensionToMime[@"hdf"] = @"application/x-hdf";
mimeToExtension[@"application/x-ica"] = @"ica"; extensionToMime[@"ica"] = @"application/x-ica";
mimeToExtension[@"application/x-internet-signup"] = @"ins"; extensionToMime[@"ins"] = @"application/x-internet-signup";
mimeToExtension[@"application/x-internet-signup"] = @"isp"; extensionToMime[@"isp"] = @"application/x-internet-signup";
mimeToExtension[@"application/x-iphone"] = @"iii"; extensionToMime[@"iii"] = @"application/x-iphone";
mimeToExtension[@"application/x-iso9660-image"] = @"iso"; extensionToMime[@"iso"] = @"application/x-iso9660-image";
mimeToExtension[@"application/x-jmol"] = @"jmz"; extensionToMime[@"jmz"] = @"application/x-jmol";
mimeToExtension[@"application/x-kchart"] = @"chrt"; extensionToMime[@"chrt"] = @"application/x-kchart";
mimeToExtension[@"application/x-killustrator"] = @"kil"; extensionToMime[@"kil"] = @"application/x-killustrator";
mimeToExtension[@"application/x-koan"] = @"skp"; extensionToMime[@"skp"] = @"application/x-koan";
mimeToExtension[@"application/x-koan"] = @"skd"; extensionToMime[@"skd"] = @"application/x-koan";
mimeToExtension[@"application/x-koan"] = @"skt"; extensionToMime[@"skt"] = @"application/x-koan";
mimeToExtension[@"application/x-koan"] = @"skm"; extensionToMime[@"skm"] = @"application/x-koan";
mimeToExtension[@"application/x-kpresenter"] = @"kpr"; extensionToMime[@"kpr"] = @"application/x-kpresenter";
mimeToExtension[@"application/x-kpresenter"] = @"kpt"; extensionToMime[@"kpt"] = @"application/x-kpresenter";
mimeToExtension[@"application/x-kspread"] = @"ksp"; extensionToMime[@"ksp"] = @"application/x-kspread";
mimeToExtension[@"application/x-kword"] = @"kwd"; extensionToMime[@"kwd"] = @"application/x-kword";
mimeToExtension[@"application/x-kword"] = @"kwt"; extensionToMime[@"kwt"] = @"application/x-kword";
mimeToExtension[@"application/x-latex"] = @"latex"; extensionToMime[@"latex"] = @"application/x-latex";
mimeToExtension[@"application/x-lha"] = @"lha"; extensionToMime[@"lha"] = @"application/x-lha";
mimeToExtension[@"application/x-lzh"] = @"lzh"; extensionToMime[@"lzh"] = @"application/x-lzh";
mimeToExtension[@"application/x-lzx"] = @"lzx"; extensionToMime[@"lzx"] = @"application/x-lzx";
mimeToExtension[@"application/x-maker"] = @"frm"; extensionToMime[@"frm"] = @"application/x-maker";
mimeToExtension[@"application/x-maker"] = @"maker"; extensionToMime[@"maker"] = @"application/x-maker";
mimeToExtension[@"application/x-maker"] = @"frame"; extensionToMime[@"frame"] = @"application/x-maker";
mimeToExtension[@"application/x-maker"] = @"fb"; extensionToMime[@"fb"] = @"application/x-maker";
mimeToExtension[@"application/x-maker"] = @"book"; extensionToMime[@"book"] = @"application/x-maker";
mimeToExtension[@"application/x-maker"] = @"fbdoc"; extensionToMime[@"fbdoc"] = @"application/x-maker";
mimeToExtension[@"application/x-mif"] = @"mif"; extensionToMime[@"mif"] = @"application/x-mif";
mimeToExtension[@"application/x-ms-wmd"] = @"wmd"; extensionToMime[@"wmd"] = @"application/x-ms-wmd";
mimeToExtension[@"application/x-ms-wmz"] = @"wmz"; extensionToMime[@"wmz"] = @"application/x-ms-wmz";
mimeToExtension[@"application/x-msi"] = @"msi"; extensionToMime[@"msi"] = @"application/x-msi";
mimeToExtension[@"application/x-ns-proxy-autoconfig"] = @"pac"; extensionToMime[@"pac"] = @"application/x-ns-proxy-autoconfig";
mimeToExtension[@"application/x-nwc"] = @"nwc"; extensionToMime[@"nwc"] = @"application/x-nwc";
mimeToExtension[@"application/x-object"] = @"o"; extensionToMime[@"o"] = @"application/x-object";
mimeToExtension[@"application/x-oz-application"] = @"oza"; extensionToMime[@"oza"] = @"application/x-oz-application";
mimeToExtension[@"application/x-pkcs12"] = @"p12"; extensionToMime[@"p12"] = @"application/x-pkcs12";
mimeToExtension[@"application/x-pkcs7-certreqresp"] = @"p7r"; extensionToMime[@"p7r"] = @"application/x-pkcs7-certreqresp";
mimeToExtension[@"application/x-pkcs7-crl"] = @"crl"; extensionToMime[@"crl"] = @"application/x-pkcs7-crl";
mimeToExtension[@"application/x-quicktimeplayer"] = @"qtl"; extensionToMime[@"qtl"] = @"application/x-quicktimeplayer";
mimeToExtension[@"application/x-shar"] = @"shar"; extensionToMime[@"shar"] = @"application/x-shar";
mimeToExtension[@"application/x-shockwave-flash"] = @"swf"; extensionToMime[@"swf"] = @"application/x-shockwave-flash";
mimeToExtension[@"application/x-stuffit"] = @"sit"; extensionToMime[@"sit"] = @"application/x-stuffit";
mimeToExtension[@"application/x-sv4cpio"] = @"sv4cpio"; extensionToMime[@"sv4cpio"] = @"application/x-sv4cpio";
mimeToExtension[@"application/x-sv4crc"] = @"sv4crc"; extensionToMime[@"sv4crc"] = @"application/x-sv4crc";
mimeToExtension[@"application/x-tar"] = @"tar"; extensionToMime[@"tar"] = @"application/x-tar";
mimeToExtension[@"application/x-texinfo"] = @"texinfo"; extensionToMime[@"texinfo"] = @"application/x-texinfo";
mimeToExtension[@"application/x-texinfo"] = @"texi"; extensionToMime[@"texi"] = @"application/x-texinfo";
mimeToExtension[@"application/x-troff"] = @"t"; extensionToMime[@"t"] = @"application/x-troff";
mimeToExtension[@"application/x-troff"] = @"roff"; extensionToMime[@"roff"] = @"application/x-troff";
mimeToExtension[@"application/x-troff-man"] = @"man"; extensionToMime[@"man"] = @"application/x-troff-man";
mimeToExtension[@"application/x-ustar"] = @"ustar"; extensionToMime[@"ustar"] = @"application/x-ustar";
mimeToExtension[@"application/x-wais-source"] = @"src"; extensionToMime[@"src"] = @"application/x-wais-source";
mimeToExtension[@"application/x-wingz"] = @"wz"; extensionToMime[@"wz"] = @"application/x-wingz";
mimeToExtension[@"application/x-webarchive"] = @"webarchive"; extensionToMime[@"webarchive"] = @"application/x-webarchive";
mimeToExtension[@"application/x-x509-ca-cert"] = @"crt"; extensionToMime[@"crt"] = @"application/x-x509-ca-cert";
mimeToExtension[@"application/x-x509-user-cert"] = @"crt"; extensionToMime[@"crt"] = @"application/x-x509-user-cert";
mimeToExtension[@"application/x-xcf"] = @"xcf"; extensionToMime[@"xcf"] = @"application/x-xcf";
mimeToExtension[@"application/x-xfig"] = @"fig"; extensionToMime[@"fig"] = @"application/x-xfig";
mimeToExtension[@"application/xhtml+xml"] = @"xhtml"; extensionToMime[@"xhtml"] = @"application/xhtml+xml";
mimeToExtension[@"audio/3gpp"] = @"3gpp"; extensionToMime[@"3gpp"] = @"audio/3gpp";
mimeToExtension[@"audio/basic"] = @"snd"; extensionToMime[@"snd"] = @"audio/basic";
mimeToExtension[@"audio/midi"] = @"mid"; extensionToMime[@"mid"] = @"audio/midi";
mimeToExtension[@"audio/midi"] = @"midi"; extensionToMime[@"midi"] = @"audio/midi";
mimeToExtension[@"audio/midi"] = @"kar"; extensionToMime[@"kar"] = @"audio/midi";
mimeToExtension[@"audio/mpeg"] = @"mpga"; extensionToMime[@"mpga"] = @"audio/mpeg";
mimeToExtension[@"audio/mpeg"] = @"mpega"; extensionToMime[@"mpega"] = @"audio/mpeg";
mimeToExtension[@"audio/mpeg"] = @"mp2"; extensionToMime[@"mp2"] = @"audio/mpeg";
mimeToExtension[@"audio/mpeg"] = @"mp3"; extensionToMime[@"mp3"] = @"audio/mpeg";
mimeToExtension[@"audio/mpeg"] = @"m4a"; extensionToMime[@"m4a"] = @"audio/mpeg";
mimeToExtension[@"audio/mpegurl"] = @"m3u"; extensionToMime[@"m3u"] = @"audio/mpegurl";
mimeToExtension[@"audio/prs.sid"] = @"sid"; extensionToMime[@"sid"] = @"audio/prs.sid";
mimeToExtension[@"audio/x-aiff"] = @"aif"; extensionToMime[@"aif"] = @"audio/x-aiff";
mimeToExtension[@"audio/x-aiff"] = @"aiff"; extensionToMime[@"aiff"] = @"audio/x-aiff";
mimeToExtension[@"audio/x-aiff"] = @"aifc"; extensionToMime[@"aifc"] = @"audio/x-aiff";
mimeToExtension[@"audio/x-gsm"] = @"gsm"; extensionToMime[@"gsm"] = @"audio/x-gsm";
mimeToExtension[@"audio/x-mpegurl"] = @"m3u"; extensionToMime[@"m3u"] = @"audio/x-mpegurl";
mimeToExtension[@"audio/x-ms-wma"] = @"wma"; extensionToMime[@"wma"] = @"audio/x-ms-wma";
mimeToExtension[@"audio/x-ms-wax"] = @"wax"; extensionToMime[@"wax"] = @"audio/x-ms-wax";
mimeToExtension[@"audio/x-pn-realaudio"] = @"ra"; extensionToMime[@"ra"] = @"audio/x-pn-realaudio";
mimeToExtension[@"audio/x-pn-realaudio"] = @"rm"; extensionToMime[@"rm"] = @"audio/x-pn-realaudio";
mimeToExtension[@"audio/x-pn-realaudio"] = @"ram"; extensionToMime[@"ram"] = @"audio/x-pn-realaudio";
mimeToExtension[@"audio/x-realaudio"] = @"ra"; extensionToMime[@"ra"] = @"audio/x-realaudio";
mimeToExtension[@"audio/x-scpls"] = @"pls"; extensionToMime[@"pls"] = @"audio/x-scpls";
mimeToExtension[@"audio/x-sd2"] = @"sd2"; extensionToMime[@"sd2"] = @"audio/x-sd2";
mimeToExtension[@"audio/x-wav"] = @"wav"; extensionToMime[@"wav"] = @"audio/x-wav";
mimeToExtension[@"image/bmp"] = @"bmp"; extensionToMime[@"bmp"] = @"image/bmp";
mimeToExtension[@"image/gif"] = @"gif"; extensionToMime[@"gif"] = @"image/gif";
mimeToExtension[@"image/ico"] = @"cur"; extensionToMime[@"cur"] = @"image/ico";
mimeToExtension[@"image/ico"] = @"ico"; extensionToMime[@"ico"] = @"image/ico";
mimeToExtension[@"image/ief"] = @"ief"; extensionToMime[@"ief"] = @"image/ief";
mimeToExtension[@"image/jpeg"] = @"jpeg"; extensionToMime[@"jpeg"] = @"image/jpeg";
mimeToExtension[@"image/jpeg"] = @"jpg"; extensionToMime[@"jpg"] = @"image/jpeg";
mimeToExtension[@"image/jpeg"] = @"jpe"; extensionToMime[@"jpe"] = @"image/jpeg";
mimeToExtension[@"image/pcx"] = @"pcx"; extensionToMime[@"pcx"] = @"image/pcx";
mimeToExtension[@"image/png"] = @"png"; extensionToMime[@"png"] = @"image/png";
mimeToExtension[@"image/svg+xml"] = @"svg"; extensionToMime[@"svg"] = @"image/svg+xml";
mimeToExtension[@"image/svg+xml"] = @"svgz"; extensionToMime[@"svgz"] = @"image/svg+xml";
mimeToExtension[@"image/tiff"] = @"tiff"; extensionToMime[@"tiff"] = @"image/tiff";
mimeToExtension[@"image/tiff"] = @"tif"; extensionToMime[@"tif"] = @"image/tiff";
mimeToExtension[@"image/vnd.djvu"] = @"djvu"; extensionToMime[@"djvu"] = @"image/vnd.djvu";
mimeToExtension[@"image/vnd.djvu"] = @"djv"; extensionToMime[@"djv"] = @"image/vnd.djvu";
mimeToExtension[@"image/vnd.wap.wbmp"] = @"wbmp"; extensionToMime[@"wbmp"] = @"image/vnd.wap.wbmp";
mimeToExtension[@"image/x-cmu-raster"] = @"ras"; extensionToMime[@"ras"] = @"image/x-cmu-raster";
mimeToExtension[@"image/x-coreldraw"] = @"cdr"; extensionToMime[@"cdr"] = @"image/x-coreldraw";
mimeToExtension[@"image/x-coreldrawpattern"] = @"pat"; extensionToMime[@"pat"] = @"image/x-coreldrawpattern";
mimeToExtension[@"image/x-coreldrawtemplate"] = @"cdt"; extensionToMime[@"cdt"] = @"image/x-coreldrawtemplate";
mimeToExtension[@"image/x-corelphotopaint"] = @"cpt"; extensionToMime[@"cpt"] = @"image/x-corelphotopaint";
mimeToExtension[@"image/x-icon"] = @"ico"; extensionToMime[@"ico"] = @"image/x-icon";
mimeToExtension[@"image/x-jg"] = @"art"; extensionToMime[@"art"] = @"image/x-jg";
mimeToExtension[@"image/x-jng"] = @"jng"; extensionToMime[@"jng"] = @"image/x-jng";
mimeToExtension[@"image/x-ms-bmp"] = @"bmp"; extensionToMime[@"bmp"] = @"image/x-ms-bmp";
mimeToExtension[@"image/x-photoshop"] = @"psd"; extensionToMime[@"psd"] = @"image/x-photoshop";
mimeToExtension[@"image/x-portable-anymap"] = @"pnm"; extensionToMime[@"pnm"] = @"image/x-portable-anymap";
mimeToExtension[@"image/x-portable-bitmap"] = @"pbm"; extensionToMime[@"pbm"] = @"image/x-portable-bitmap";
mimeToExtension[@"image/x-portable-graymap"] = @"pgm"; extensionToMime[@"pgm"] = @"image/x-portable-graymap";
mimeToExtension[@"image/x-portable-pixmap"] = @"ppm"; extensionToMime[@"ppm"] = @"image/x-portable-pixmap";
mimeToExtension[@"image/x-rgb"] = @"rgb"; extensionToMime[@"rgb"] = @"image/x-rgb";
mimeToExtension[@"image/x-xbitmap"] = @"xbm"; extensionToMime[@"xbm"] = @"image/x-xbitmap";
mimeToExtension[@"image/x-xpixmap"] = @"xpm"; extensionToMime[@"xpm"] = @"image/x-xpixmap";
mimeToExtension[@"image/x-xwindowdump"] = @"xwd"; extensionToMime[@"xwd"] = @"image/x-xwindowdump";
mimeToExtension[@"model/iges"] = @"igs"; extensionToMime[@"igs"] = @"model/iges";
mimeToExtension[@"model/iges"] = @"iges"; extensionToMime[@"iges"] = @"model/iges";
mimeToExtension[@"model/mesh"] = @"msh"; extensionToMime[@"msh"] = @"model/mesh";
mimeToExtension[@"model/mesh"] = @"mesh"; extensionToMime[@"mesh"] = @"model/mesh";
mimeToExtension[@"model/mesh"] = @"silo"; extensionToMime[@"silo"] = @"model/mesh";
mimeToExtension[@"text/calendar"] = @"ics"; extensionToMime[@"ics"] = @"text/calendar";
mimeToExtension[@"text/calendar"] = @"icz"; extensionToMime[@"icz"] = @"text/calendar";
mimeToExtension[@"text/comma-separated-values"] = @"csv"; extensionToMime[@"csv"] = @"text/comma-separated-values";
mimeToExtension[@"text/css"] = @"css"; extensionToMime[@"css"] = @"text/css";
mimeToExtension[@"text/html"] = @"htm"; extensionToMime[@"htm"] = @"text/html";
mimeToExtension[@"text/html"] = @"html"; extensionToMime[@"html"] = @"text/html";
mimeToExtension[@"text/h323"] = @"323"; extensionToMime[@"323"] = @"text/h323";
mimeToExtension[@"text/iuls"] = @"uls"; extensionToMime[@"uls"] = @"text/iuls";
mimeToExtension[@"text/mathml"] = @"mml"; extensionToMime[@"mml"] = @"text/mathml";
// add it first so it will be the default for ExtensionFromMimeType
mimeToExtension[@"text/plain"] = @"txt"; extensionToMime[@"txt"] = @"text/plain";
mimeToExtension[@"text/plain"] = @"asc"; extensionToMime[@"asc"] = @"text/plain";
mimeToExtension[@"text/plain"] = @"text"; extensionToMime[@"text"] = @"text/plain";
mimeToExtension[@"text/plain"] = @"diff"; extensionToMime[@"diff"] = @"text/plain";
mimeToExtension[@"text/plain"] = @"po"; extensionToMime[@"po"] = @"text/plain"; // reserve "pot" for vnd.ms-powerpoint
mimeToExtension[@"text/richtext"] = @"rtx"; extensionToMime[@"rtx"] = @"text/richtext";
mimeToExtension[@"text/rtf"] = @"rtf"; extensionToMime[@"rtf"] = @"text/rtf";
mimeToExtension[@"text/texmacs"] = @"ts"; extensionToMime[@"ts"] = @"text/texmacs";
mimeToExtension[@"text/text"] = @"phps"; extensionToMime[@"phps"] = @"text/text";
mimeToExtension[@"text/tab-separated-values"] = @"tsv"; extensionToMime[@"tsv"] = @"text/tab-separated-values";
mimeToExtension[@"text/xml"] = @"xml"; extensionToMime[@"xml"] = @"text/xml";
mimeToExtension[@"text/x-bibtex"] = @"bib"; extensionToMime[@"bib"] = @"text/x-bibtex";
mimeToExtension[@"text/x-boo"] = @"boo"; extensionToMime[@"boo"] = @"text/x-boo";
mimeToExtension[@"text/x-c++hdr"] = @"h++"; extensionToMime[@"h++"] = @"text/x-c++hdr";
mimeToExtension[@"text/x-c++hdr"] = @"hpp"; extensionToMime[@"hpp"] = @"text/x-c++hdr";
mimeToExtension[@"text/x-c++hdr"] = @"hxx"; extensionToMime[@"hxx"] = @"text/x-c++hdr";
mimeToExtension[@"text/x-c++hdr"] = @"hh"; extensionToMime[@"hh"] = @"text/x-c++hdr";
mimeToExtension[@"text/x-c++src"] = @"c++"; extensionToMime[@"c++"] = @"text/x-c++src";
mimeToExtension[@"text/x-c++src"] = @"cpp"; extensionToMime[@"cpp"] = @"text/x-c++src";
mimeToExtension[@"text/x-c++src"] = @"cxx"; extensionToMime[@"cxx"] = @"text/x-c++src";
mimeToExtension[@"text/x-chdr"] = @"h"; extensionToMime[@"h"] = @"text/x-chdr";
mimeToExtension[@"text/x-component"] = @"htc"; extensionToMime[@"htc"] = @"text/x-component";
mimeToExtension[@"text/x-csh"] = @"csh"; extensionToMime[@"csh"] = @"text/x-csh";
mimeToExtension[@"text/x-csrc"] = @"c"; extensionToMime[@"c"] = @"text/x-csrc";
mimeToExtension[@"text/x-dsrc"] = @"d"; extensionToMime[@"d"] = @"text/x-dsrc";
mimeToExtension[@"text/x-haskell"] = @"hs"; extensionToMime[@"hs"] = @"text/x-haskell";
mimeToExtension[@"text/x-java"] = @"java"; extensionToMime[@"java"] = @"text/x-java";
mimeToExtension[@"text/x-literate-haskell"] = @"lhs"; extensionToMime[@"lhs"] = @"text/x-literate-haskell";
mimeToExtension[@"text/x-moc"] = @"moc"; extensionToMime[@"moc"] = @"text/x-moc";
mimeToExtension[@"text/x-pascal"] = @"p"; extensionToMime[@"p"] = @"text/x-pascal";
mimeToExtension[@"text/x-pascal"] = @"pas"; extensionToMime[@"pas"] = @"text/x-pascal";
mimeToExtension[@"text/x-pcs-gcd"] = @"gcd"; extensionToMime[@"gcd"] = @"text/x-pcs-gcd";
mimeToExtension[@"text/x-setext"] = @"etx"; extensionToMime[@"etx"] = @"text/x-setext";
mimeToExtension[@"text/x-tcl"] = @"tcl"; extensionToMime[@"tcl"] = @"text/x-tcl";
mimeToExtension[@"text/x-tex"] = @"tex"; extensionToMime[@"tex"] = @"text/x-tex";
mimeToExtension[@"text/x-tex"] = @"ltx"; extensionToMime[@"ltx"] = @"text/x-tex";
mimeToExtension[@"text/x-tex"] = @"sty"; extensionToMime[@"sty"] = @"text/x-tex";
mimeToExtension[@"text/x-tex"] = @"cls"; extensionToMime[@"cls"] = @"text/x-tex";
mimeToExtension[@"text/x-vcalendar"] = @"vcs"; extensionToMime[@"vcs"] = @"text/x-vcalendar";
mimeToExtension[@"text/x-vcard"] = @"vcf"; extensionToMime[@"vcf"] = @"text/x-vcard";
mimeToExtension[@"video/3gpp"] = @"3gpp"; extensionToMime[@"3gpp"] = @"video/3gpp";
mimeToExtension[@"video/3gpp"] = @"3gp"; extensionToMime[@"3gp"] = @"video/3gpp";
mimeToExtension[@"video/3gpp"] = @"3g2"; extensionToMime[@"3g2"] = @"video/3gpp";
mimeToExtension[@"video/dl"] = @"dl"; extensionToMime[@"dl"] = @"video/dl";
mimeToExtension[@"video/dv"] = @"dif"; extensionToMime[@"dif"] = @"video/dv";
mimeToExtension[@"video/dv"] = @"dv"; extensionToMime[@"dv"] = @"video/dv";
mimeToExtension[@"video/fli"] = @"fli"; extensionToMime[@"fli"] = @"video/fli";
mimeToExtension[@"video/m4v"] = @"m4v"; extensionToMime[@"m4v"] = @"video/m4v";
mimeToExtension[@"video/mpeg"] = @"mpeg"; extensionToMime[@"mpeg"] = @"video/mpeg";
mimeToExtension[@"video/mpeg"] = @"mpg"; extensionToMime[@"mpg"] = @"video/mpeg";
mimeToExtension[@"video/mpeg"] = @"mpe"; extensionToMime[@"mpe"] = @"video/mpeg";
mimeToExtension[@"video/mp4"] = @"mp4"; extensionToMime[@"mp4"] = @"video/mp4";
mimeToExtension[@"video/mpeg"] = @"VOB"; extensionToMime[@"VOB"] = @"video/mpeg";
mimeToExtension[@"video/quicktime"] = @"qt"; extensionToMime[@"qt"] = @"video/quicktime";
mimeToExtension[@"video/quicktime"] = @"mov"; extensionToMime[@"mov"] = @"video/quicktime";
mimeToExtension[@"video/vnd.mpegurl"] = @"mxu"; extensionToMime[@"mxu"] = @"video/vnd.mpegurl";
mimeToExtension[@"video/x-la-asf"] = @"lsf"; extensionToMime[@"lsf"] = @"video/x-la-asf";
mimeToExtension[@"video/x-la-asf"] = @"lsx"; extensionToMime[@"lsx"] = @"video/x-la-asf";
mimeToExtension[@"video/x-mng"] = @"mng"; extensionToMime[@"mng"] = @"video/x-mng";
mimeToExtension[@"video/x-ms-asf"] = @"asf"; extensionToMime[@"asf"] = @"video/x-ms-asf";
mimeToExtension[@"video/x-ms-asf"] = @"asx"; extensionToMime[@"asx"] = @"video/x-ms-asf";
mimeToExtension[@"video/x-ms-wm"] = @"wm"; extensionToMime[@"wm"] = @"video/x-ms-wm";
mimeToExtension[@"video/x-ms-wmv"] = @"wmv"; extensionToMime[@"wmv"] = @"video/x-ms-wmv";
mimeToExtension[@"video/x-ms-wmx"] = @"wmx"; extensionToMime[@"wmx"] = @"video/x-ms-wmx";
mimeToExtension[@"video/x-ms-wvx"] = @"wvx"; extensionToMime[@"wvx"] = @"video/x-ms-wvx";
mimeToExtension[@"video/x-msvideo"] = @"avi"; extensionToMime[@"avi"] = @"video/x-msvideo";
mimeToExtension[@"video/x-sgi-movie"] = @"movie"; extensionToMime[@"movie"] = @"video/x-sgi-movie";
mimeToExtension[@"x-conference/x-cooltalk"] = @"ice"; extensionToMime[@"ice"] = @"x-conference/x-cooltalk";
mimeToExtension[@"x-epoc/x-sisx-app"] = @"sisx"; extensionToMime[@"sisx"] = @"x-epoc/x-sisx-app";
mimeToExtension[@"application/epub+zip"] = @"epub"; extensionToMime[@"epub"] = @"application/epub+zip";
mimeToExtension[@"text/swift"] = @"swift"; extensionToMime[@"swift"] = @"text/swift";
mimeToExtensionMap = mimeToExtension;
extensionToMimeMap = extensionToMime;
});
}
@implementation TGMimeTypeMap
+ (NSString *)mimeTypeForExtension:(NSString *)extension
{
if (extension == nil)
return nil;
initializeMapping();
return extensionToMimeMap[extension];
}
+ (NSString *)extensionForMimeType:(NSString *)mimeType
{
if (mimeType == nil)
return nil;
initializeMapping();
return mimeToExtensionMap[mimeType];
}
@end

View File

@ -0,0 +1,22 @@
#import <MTProtoKitDynamic/MTProtoKitDynamic.h>
@interface TGShareLocationResult : NSObject
@property (nonatomic, readonly) double latitude;
@property (nonatomic, readonly) double longitude;
@property (nonatomic, readonly) NSString *title;
@property (nonatomic, readonly) NSString *address;
@property (nonatomic, readonly) NSString *provider;
@property (nonatomic, readonly) NSString *venueId;
@property (nonatomic, readonly) NSString *venueType;
- (instancetype)initWithLatitude:(double)latitude longitude:(double)longitude title:(NSString *)title address:(NSString *)address provider:(NSString *)provider venueId:(NSString *)venueId venueType:(NSString *)venueType;
@end
@interface TGShareLocationSignals : NSObject
+ (MTSignal *)locationMessageContentForURL:(NSURL *)url;
+ (bool)isLocationURL:(NSURL *)url;
@end

View File

@ -0,0 +1,364 @@
#import "TGShareLocationSignals.h"
NSString *const TGShareAppleMapsHost = @"maps.apple.com";
NSString *const TGShareAppleMapsPath = @"/maps";
NSString *const TGShareAppleMapsLatLonKey = @"ll";
NSString *const TGShareAppleMapsNameKey = @"q";
NSString *const TGShareAppleMapsAddressKey = @"address";
NSString *const TGShareAppleMapsIdKey = @"auid";
NSString *const TGShareAppleMapsProvider = @"apple";
NSString *const TGShareFoursquareHost = @"foursquare.com";
NSString *const TGShareFoursquareVenuePath = @"/v";
NSString *const TGShareFoursquareVenueEndpointUrl = @"https://api.foursquare.com/v2/venues/";
NSString *const TGShareFoursquareClientId = @"BN3GWQF1OLMLKKQTFL0OADWD1X1WCDNISPPOT1EMMUYZTQV1";
NSString *const TGShareFoursquareClientSecret = @"WEEZHCKI040UVW2KWW5ZXFAZ0FMMHKQ4HQBWXVSX4WXWBWYN";
NSString *const TGShareFoursquareVersion = @"20150326";
NSString *const TGShareFoursquareVenuesCountLimit = @"25";
NSString *const TGShareFoursquareLocale = @"en";
NSString *const TGShareFoursquareProvider = @"foursquare";
NSString *const TGShareGoogleShortenerEndpointUrl = @"https://www.googleapis.com/urlshortener/v1/url";
NSString *const TGShareGoogleAPIKey = @"AIzaSyBCTH4aAdvi0MgDGlGNmQAaFS8GTNBrfj4";
NSString *const TGShareGoogleMapsShortHost = @"goo.gl";
NSString *const TGShareGoogleMapsShortPath = @"/maps";
NSString *const TGShareGoogleMapsHost = @"google.com";
NSString *const TGShareGoogleMapsSearchPath = @"maps/search";
NSString *const TGShareGoogleMapsPlacePath = @"maps/place";
NSString *const TGShareGoogleProvider = @"google";
@implementation TGShareLocationResult
- (instancetype)initWithLatitude:(double)latitude longitude:(double)longitude title:(NSString *)title address:(NSString *)address provider:(NSString *)provider venueId:(NSString *)venueId venueType:(NSString *)venueType {
self = [super init];
if (self != nil) {
_latitude = latitude;
_longitude = longitude;
_title = title;
_address = address;
_provider = provider;
_venueId = venueId;
_venueType = venueType;
}
return self;
}
@end
@interface TGQueryStringComponent : NSObject {
@private
NSString *_key;
NSString *_value;
}
@property (readwrite, nonatomic, retain) id key;
@property (readwrite, nonatomic, retain) id value;
- (id)initWithKey:(id)key value:(id)value;
- (NSString *)URLEncodedStringValueWithEncoding:(NSStringEncoding)stringEncoding;
@end
NSString * TGURLEncodedStringFromStringWithEncoding(NSString *string, NSStringEncoding encoding) {
static NSString * const kAFLegalCharactersToBeEscaped = @"?!@#$^&%*+=,:;'\"`<>()[]{}/\\|~ ";
NSString *unescapedString = [string stringByReplacingPercentEscapesUsingEncoding:encoding];
if (unescapedString) {
string = unescapedString;
}
return (__bridge NSString *)CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault, (__bridge CFStringRef)string, NULL, (__bridge CFStringRef)kAFLegalCharactersToBeEscaped, CFStringConvertNSStringEncodingToEncoding(encoding));
}
@implementation TGQueryStringComponent
@synthesize key = _key;
@synthesize value = _value;
- (id)initWithKey:(id)key value:(id)value {
self = [super init];
if (!self) {
return nil;
}
self.key = key;
self.value = value;
return self;
}
- (NSString *)URLEncodedStringValueWithEncoding:(NSStringEncoding)stringEncoding {
return [NSString stringWithFormat:@"%@=%@", self.key, TGURLEncodedStringFromStringWithEncoding([self.value description], stringEncoding)];
}
@end
static NSString * TGQueryStringFromParametersWithEncoding(NSDictionary *parameters, NSStringEncoding stringEncoding);
static NSArray * TGQueryStringComponentsFromKeyAndValue(NSString *key, id value);
NSArray * TGQueryStringComponentsFromKeyAndDictionaryValue(NSString *key, NSDictionary *value);
NSArray * TGQueryStringComponentsFromKeyAndArrayValue(NSString *key, NSArray *value);
static NSString * TGQueryStringFromParametersWithEncoding(NSDictionary *parameters, NSStringEncoding stringEncoding) {
NSMutableArray *mutableComponents = [NSMutableArray array];
for (TGQueryStringComponent *component in TGQueryStringComponentsFromKeyAndValue(nil, parameters)) {
[mutableComponents addObject:[component URLEncodedStringValueWithEncoding:stringEncoding]];
}
return [mutableComponents componentsJoinedByString:@"&"];
}
static NSArray * TGQueryStringComponentsFromKeyAndValue(NSString *key, id value) {
NSMutableArray *mutableQueryStringComponents = [NSMutableArray array];
if([value isKindOfClass:[NSDictionary class]]) {
[mutableQueryStringComponents addObjectsFromArray:TGQueryStringComponentsFromKeyAndDictionaryValue(key, value)];
} else if([value isKindOfClass:[NSArray class]]) {
[mutableQueryStringComponents addObjectsFromArray:TGQueryStringComponentsFromKeyAndArrayValue(key, value)];
} else {
[mutableQueryStringComponents addObject:[[TGQueryStringComponent alloc] initWithKey:key value:value]];
}
return mutableQueryStringComponents;
}
NSArray * TGQueryStringComponentsFromKeyAndDictionaryValue(NSString *key, NSDictionary *value){
NSMutableArray *mutableQueryStringComponents = [NSMutableArray array];
[value enumerateKeysAndObjectsUsingBlock:^(id nestedKey, id nestedValue, __unused BOOL *stop) {
[mutableQueryStringComponents addObjectsFromArray:TGQueryStringComponentsFromKeyAndValue((key ? [NSString stringWithFormat:@"%@[%@]", key, nestedKey] : nestedKey), nestedValue)];
}];
return mutableQueryStringComponents;
}
NSArray * TGQueryStringComponentsFromKeyAndArrayValue(NSString *key, NSArray *value) {
NSMutableArray *mutableQueryStringComponents = [NSMutableArray array];
[value enumerateObjectsUsingBlock:^(id nestedValue, __unused NSUInteger idx, __unused BOOL *stop) {
[mutableQueryStringComponents addObjectsFromArray:TGQueryStringComponentsFromKeyAndValue([NSString stringWithFormat:@"%@[]", key], nestedValue)];
}];
return mutableQueryStringComponents;
}
@implementation TGShareLocationSignals
+ (MTSignal *)locationMessageContentForURL:(NSURL *)url
{
if ([self isAppleMapsURL:url])
return [self _appleMapsLocationContentForURL:url];
else if ([self isFoursquareURL:url])
return [self _foursquareLocationForURL:url];
return [MTSignal single:nil];
}
+ (MTSignal *)_appleMapsLocationContentForURL:(NSURL *)url
{
NSURLComponents *urlComponents = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:false];
NSArray *queryItems = urlComponents.queryItems;
NSString *latLon = nil;
NSString *name = nil;
NSString *address = nil;
NSString *venueId = nil;
for (NSURLQueryItem *queryItem in queryItems)
{
if ([queryItem.name isEqualToString:TGShareAppleMapsLatLonKey])
{
latLon = queryItem.value;
}
else if ([queryItem.name isEqualToString:TGShareAppleMapsNameKey])
{
if (![queryItem.value isEqualToString:latLon])
name = queryItem.value;
}
else if ([queryItem.name isEqualToString:TGShareAppleMapsAddressKey])
{
address = queryItem.value;
}
else if ([queryItem.name isEqualToString:TGShareAppleMapsIdKey])
{
venueId = queryItem.value;
}
}
if (latLon == nil)
return [MTSignal fail:nil];
NSArray *coordComponents = [latLon componentsSeparatedByString:@","];
if (coordComponents.count != 2)
return [MTSignal fail:nil];
double latitude = [coordComponents.firstObject floatValue];
double longitude = [coordComponents.lastObject floatValue];
return [MTSignal single:[[TGShareLocationResult alloc] initWithLatitude:latitude longitude:longitude title:name address:address provider:TGShareAppleMapsProvider venueId:venueId venueType:@""]];
}
+ (MTSignal *)_foursquareLocationForURL:(NSURL *)url
{
NSArray *pathComponents = url.pathComponents;
NSString *venueId = nil;
for (NSString *component in pathComponents)
{
if (component.length == 24)
{
venueId = component;
break;
}
}
if (venueId == nil)
return [MTSignal fail:nil];
NSString *urlString = [NSString stringWithFormat:@"%@?%@", [TGShareFoursquareVenueEndpointUrl stringByAppendingPathComponent:venueId], TGQueryStringFromParametersWithEncoding([self _defaultParametersForFoursquare], NSUTF8StringEncoding)];
return [[MTHttpRequestOperation dataForHttpUrl:[NSURL URLWithString:urlString]] mapToSignal:^id(NSData *data)
{
id json = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
if (![json respondsToSelector:@selector(objectForKey:)])
return nil;
NSDictionary *venue = json[@"response"][@"venue"];
if (![venue respondsToSelector:@selector(objectForKey:)])
return nil;
NSString *name = venue[@"name"];
NSDictionary *location = venue[@"location"];
NSString *address = location[@"address"];
if (address.length == 0)
address = location[@"crossStreet"];
if (address.length == 0)
address = location[@"city"];
if (address.length == 0)
address = location[@"country"];
if (address.length == 0)
address = @"";
double latitude = [location[@"lat"] doubleValue];
double longitude = [location[@"lng"] doubleValue];
if (name.length == 0)
return [MTSignal fail:nil];
return [MTSignal single:[[TGShareLocationResult alloc] initWithLatitude:latitude longitude:longitude title:name address:address provider:TGShareFoursquareProvider venueId:venueId venueType:@""]];
}];
}
+ (MTSignal *)_googleMapsLocationForURL:(NSURL *)url
{
NSString *shortenerUrl = [NSString stringWithFormat:@"%@?fields=longUrl,status&shortUrl=%@&key=%@", TGShareGoogleShortenerEndpointUrl, TGURLEncodedStringFromStringWithEncoding(url.absoluteString, NSUTF8StringEncoding), TGShareGoogleAPIKey];
MTSignal *shortenerSignal = [[MTHttpRequestOperation dataForHttpUrl:[NSURL URLWithString:shortenerUrl]] mapToSignal:^MTSignal *(NSData *data)
{
id json = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
if (![json respondsToSelector:@selector(objectForKey:)])
return [MTSignal fail:nil];
NSString *status = json[@"status"];
if (![status isEqualToString:@"OK"])
return [MTSignal fail:nil];
return [MTSignal single:[NSURL URLWithString:json[@"longUrl"]]];
}];
MTSignal *(^processLongUrl)(NSURL *) = ^MTSignal *(NSURL *longUrl)
{
NSArray *pathComponents = longUrl.pathComponents;
bool isSearch = false;
double latitude = 0.0;
double longitude = 0.0;
for (NSString *component in pathComponents)
{
if ([component isEqualToString:@"search"])
{
isSearch = true;
}
else if ([component isEqualToString:@"place"])
{
return [MTSignal fail:nil];
}
else if (isSearch && [component containsString:@","])
{
NSArray *coordinates = [component componentsSeparatedByString:@","];
if (coordinates.count == 2)
{
latitude = [coordinates.firstObject doubleValue];
longitude = [coordinates.lastObject doubleValue];
break;
}
}
}
if (fabs(latitude) < DBL_EPSILON && fabs(longitude) < DBL_EPSILON)
return [MTSignal fail:nil];
return [MTSignal single:[[TGShareLocationResult alloc] initWithLatitude:latitude longitude:longitude title:nil address:nil provider:nil venueId:nil venueType:nil]];
};
MTSignal *signal = nil;
if ([self _isShortGoogleMapsURL:url])
{
signal = [shortenerSignal mapToSignal:^MTSignal *(NSURL *longUrl)
{
return processLongUrl(longUrl);
}];
}
else
{
signal = processLongUrl(url);
}
return [signal catch:^MTSignal *(id error)
{
return [MTSignal single:url.absoluteString];
}];
}
+ (NSDictionary *)_defaultParametersForFoursquare
{
return @
{
@"v": TGShareFoursquareVersion,
@"locale": TGShareFoursquareLocale,
@"client_id": TGShareFoursquareClientId,
@"client_secret" :TGShareFoursquareClientSecret
};
}
+ (bool)isLocationURL:(NSURL *)url
{
return [self isAppleMapsURL:url] || [self isFoursquareURL:url];
}
+ (bool)isAppleMapsURL:(NSURL *)url
{
return ([url.host isEqualToString:TGShareAppleMapsHost] && [url.path isEqualToString:TGShareAppleMapsPath]);
}
+ (bool)isFoursquareURL:(NSURL *)url
{
return ([url.host isEqualToString:TGShareFoursquareHost] && [url.path hasPrefix:TGShareFoursquareVenuePath]);
}
+ (bool)_isShortGoogleMapsURL:(NSURL *)url
{
return ([url.host isEqualToString:TGShareGoogleMapsShortHost] && [url.path hasPrefix:TGShareGoogleMapsShortPath]);
}
+ (bool)_isLongGoogleMapsURL:(NSURL *)url
{
return ([url.host isEqualToString:TGShareGoogleMapsHost] && ([url.path hasPrefix:TGShareGoogleMapsSearchPath] || [url.path hasPrefix:TGShareGoogleMapsPlacePath]));
}
+ (bool)isGoogleMapsURL:(NSURL *)url
{
return [self _isShortGoogleMapsURL:url] || [self _isLongGoogleMapsURL:url];
}
@end

View File

@ -0,0 +1,3 @@
"Common.OK" = "OK";
"Share.AuthTitle" = "Log in to Telegram";
"Share.AuthDescription" = "Open Telegram and log in to share.";

43
SiriIntents/Info.plist Normal file
View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>
<string>${APP_NAME}</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
<string>5.0.17</string>
<key>CFBundleVersion</key>
<string>624</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionAttributes</key>
<dict>
<key>IntentsRestrictedWhileLocked</key>
<array/>
<key>IntentsRestrictedWhileProtectedDataUnavailable</key>
<array/>
<key>IntentsSupported</key>
<array>
<string>INSendMessageIntent</string>
<string>INStartAudioCallIntent</string>
</array>
</dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.intents-service</string>
<key>NSExtensionPrincipalClass</key>
<string>$(PRODUCT_MODULE_NAME).IntentHandler</string>
</dict>
</dict>
</plist>

View File

@ -0,0 +1,62 @@
import Foundation
import SwiftSignalKit
import Postbox
import TelegramCore
import Contacts
import Intents
struct MathingDeviceContact {
let stableId: String
let firstName: String
let lastName: String
let phoneNumbers: [String]
}
func matchingDeviceContacts(stableIds: [String]) -> Signal<[MathingDeviceContact], NoError> {
guard CNContactStore.authorizationStatus(for: .contacts) == .authorized else {
return .single([])
}
let store = CNContactStore()
guard let contacts = try? store.unifiedContacts(matching: CNContact.predicateForContacts(withIdentifiers: stableIds), keysToFetch: [CNContactFormatter.descriptorForRequiredKeys(for: .fullName), CNContactPhoneNumbersKey as CNKeyDescriptor]) else {
return .single([])
}
return .single(contacts.map({ contact in
let phoneNumbers = contact.phoneNumbers.compactMap({ number -> String? in
if !number.value.stringValue.isEmpty {
return number.value.stringValue
} else {
return nil
}
})
return MathingDeviceContact(stableId: contact.identifier, firstName: contact.givenName, lastName: contact.familyName, phoneNumbers: phoneNumbers)
}))
}
func matchingCloudContacts(postbox: Postbox, contacts: [MathingDeviceContact]) -> Signal<[(String, TelegramUser)], NoError> {
return postbox.transaction { transaction -> [(String, TelegramUser)] in
var result: [(String, TelegramUser)] = []
outer: for peerId in transaction.getContactPeerIds() {
if let peer = transaction.getPeer(peerId) as? TelegramUser, let phone = peer.phone {
let formattedPhone = formatPhoneNumber(phone)
for contact in contacts {
for phoneNumber in contact.phoneNumbers {
if formatPhoneNumber(phoneNumber) == formattedPhone {
result.append((contact.stableId, peer))
continue outer
}
}
}
}
}
return result
}
}
func personWithUser(stableId: String, user: TelegramUser) -> INPerson {
var nameComponents = PersonNameComponents()
nameComponents.givenName = user.firstName
nameComponents.familyName = user.lastName
return INPerson(personHandle: INPersonHandle(value: stableId, type: .unknown), nameComponents: nameComponents, displayName: user.displayTitle, image: nil, contactIdentifier: stableId, customIdentifier: "tg\(user.id.toInt64())")
}

View File

@ -0,0 +1,325 @@
import Foundation
import Intents
import TelegramCore
import Postbox
import SwiftSignalKit
private var accountCache: Account?
private var installedSharedLogger = false
private func setupSharedLogger(_ path: String) {
if !installedSharedLogger {
installedSharedLogger = true
Logger.setSharedLogger(Logger(basePath: path))
}
}
private let accountAuxiliaryMethods = AccountAuxiliaryMethods(updatePeerChatInputState: { interfaceState, inputState -> PeerChatInterfaceState? in
return interfaceState
}, fetchResource: { account, resource, ranges, _ in
return nil
}, fetchResourceMediaReferenceHash: { resource in
return .single(nil)
})
private struct ApplicationSettings {
let logging: LoggingSettings
}
private func applicationSettings(accountManager: AccountManager) -> Signal<ApplicationSettings, NoError> {
return accountManager.transaction { transaction -> ApplicationSettings in
let loggingSettings: LoggingSettings
if let value = transaction.getSharedData(SharedDataKeys.loggingSettings) as? LoggingSettings {
loggingSettings = value
} else {
loggingSettings = LoggingSettings.defaultSettings
}
return ApplicationSettings(logging: loggingSettings)
}
}
class IntentHandler: INExtension, INSendMessageIntentHandling, INSearchForMessagesIntentHandling, INSetMessageAttributeIntentHandling {
private let accountPromise = Promise<Account>()
private let resolveRecipientsDisposable = MetaDisposable()
private let sendMessageDisposable = MetaDisposable()
override init() {
super.init()
let appBundleIdentifier = Bundle.main.bundleIdentifier!
guard let lastDotRange = appBundleIdentifier.range(of: ".", options: [.backwards]) else {
return
}
let apiId: Int32 = BuildConfig.shared().apiId
let languagesCategory = "ios"
let appGroupName = "group.\(appBundleIdentifier[..<lastDotRange.lowerBound])"
let maybeAppGroupUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupName)
guard let appGroupUrl = maybeAppGroupUrl else {
return
}
let rootPath = rootPathForBasePath(appGroupUrl.path)
performAppGroupUpgrades(appGroupPath: appGroupUrl.path, rootPath: rootPath)
TempBox.initializeShared(basePath: rootPath, processType: "siri", launchSpecificId: arc4random64())
let logsPath = rootPath + "/siri-logs"
let _ = try? FileManager.default.createDirectory(atPath: logsPath, withIntermediateDirectories: true, attributes: nil)
setupSharedLogger(logsPath)
let appVersion = (Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String) ?? "unknown"
let account: Signal<Account, NoError>
if let accountCache = accountCache {
account = .single(accountCache)
} else {
initializeAccountManagement()
account = accountManager(basePath: rootPath + "/accounts-metadata")
|> take(1)
|> mapToSignal { accountManager -> Signal<Account, NoError> in
return currentAccount(allocateIfNotExists: false, networkArguments: NetworkInitializationArguments(apiId: apiId, languagesCategory: languagesCategory, appVersion: appVersion), supplementary: true, manager: accountManager, rootPath: rootPath, beginWithTestingEnvironment: false, auxiliaryMethods: accountAuxiliaryMethods)
|> mapToSignal { account -> Signal<Account, NoError> in
if let account = account {
switch account {
case .upgrading:
return .complete()
case let .authorized(account):
return applicationSettings(accountManager: accountManager)
|> deliverOnMainQueue
|> map { settings -> Account in
accountCache = account
Logger.shared.logToFile = settings.logging.logToFile
Logger.shared.logToConsole = settings.logging.logToConsole
Logger.shared.redactSensitiveData = settings.logging.redactSensitiveData
return account
}
case .unauthorized:
return .complete()
}
} else {
return .complete()
}
}
}
|> take(1)
}
accountPromise.set(account)
}
deinit {
self.resolveRecipientsDisposable.dispose()
self.sendMessageDisposable.dispose()
}
override func handler(for intent: INIntent) -> Any {
return self
}
func resolveRecipients(for intent: INSendMessageIntent, with completion: @escaping ([INPersonResolutionResult]) -> Void) {
guard let initialRecipients = intent.recipients, !initialRecipients.isEmpty else {
completion([INPersonResolutionResult.needsValue()])
return
}
let filteredRecipients = initialRecipients.filter({ recipient in
if let contactIdentifier = recipient.contactIdentifier, !contactIdentifier.isEmpty {
return true
}
if #available(iOSApplicationExtension 10.3, *) {
if let siriMatches = recipient.siriMatches {
for match in siriMatches {
if let contactIdentifier = match.contactIdentifier, !contactIdentifier.isEmpty {
return true
}
}
}
}
return false
})
if filteredRecipients.isEmpty {
completion([INPersonResolutionResult.needsValue()])
return
}
if filteredRecipients.count > 1 {
completion([INPersonResolutionResult.disambiguation(with: filteredRecipients)])
return
}
var allRecipientsAlreadyMatched = true
for recipient in filteredRecipients {
if !(recipient.customIdentifier ?? "").hasPrefix("tg") {
allRecipientsAlreadyMatched = false
break
}
}
if allRecipientsAlreadyMatched {
completion([INPersonResolutionResult.success(with: filteredRecipients[0])])
return
}
let stableIds = filteredRecipients.compactMap({ recipient -> String? in
if let contactIdentifier = recipient.contactIdentifier {
return contactIdentifier
}
if #available(iOSApplicationExtension 10.3, *) {
if let siriMatches = recipient.siriMatches {
for match in siriMatches {
if let contactIdentifier = match.contactIdentifier, !contactIdentifier.isEmpty {
return contactIdentifier
}
}
}
}
return nil
})
let account = self.accountPromise.get()
let signal = matchingDeviceContacts(stableIds: stableIds)
|> take(1)
|> mapToSignal { matchedContacts in
return account
|> mapToSignal { account in
return matchingCloudContacts(postbox: account.postbox, contacts: matchedContacts)
}
}
self.resolveRecipientsDisposable.set((signal
|> deliverOnMainQueue).start(next: { peers in
completion(peers.map { stableId, user in
let person = personWithUser(stableId: stableId, user: user)
return INPersonResolutionResult.success(with: person)
})
}))
}
func resolveContent(for intent: INSendMessageIntent, with completion: @escaping (INStringResolutionResult) -> Void) {
if let text = intent.content, !text.isEmpty {
completion(INStringResolutionResult.success(with: text))
} else {
completion(INStringResolutionResult.needsValue())
}
}
func confirm(intent: INSendMessageIntent, completion: @escaping (INSendMessageIntentResponse) -> Void) {
let userActivity = NSUserActivity(activityType: NSStringFromClass(INSendMessageIntent.self))
let response = INSendMessageIntentResponse(code: .ready, userActivity: userActivity)
completion(response)
}
func handle(intent: INSendMessageIntent, completion: @escaping (INSendMessageIntentResponse) -> Void) {
self.sendMessageDisposable.set((self.accountPromise.get()
|> take(1)
|> mapError { _ -> StandaloneSendMessageError in
return .generic
}
|> mapToSignal { account -> Signal<Void, StandaloneSendMessageError> in
guard let recipient = intent.recipients?.first, let customIdentifier = recipient.customIdentifier, customIdentifier.hasPrefix("tg") else {
return .fail(.generic)
}
guard let peerIdValue = Int64(String(customIdentifier[customIdentifier.index(customIdentifier.startIndex, offsetBy: 2)...])) else {
return .fail(.generic)
}
let peerId = PeerId(peerIdValue)
if peerId.namespace != Namespaces.Peer.CloudUser {
return .fail(.generic)
}
account.shouldBeServiceTaskMaster.set(.single(.now))
return standaloneSendMessage(account: account, peerId: peerId, text: intent.content ?? "", attributes: [], media: nil, replyToMessageId: nil)
|> mapToSignal { _ -> Signal<Void, StandaloneSendMessageError> in
return .complete()
}
|> afterDisposed {
account.shouldBeServiceTaskMaster.set(.single(.never))
}
}
|> deliverOnMainQueue).start(error: { _ in
let userActivity = NSUserActivity(activityType: NSStringFromClass(INSendMessageIntent.self))
let response = INSendMessageIntentResponse(code: .failure, userActivity: userActivity)
completion(response)
}, completed: {
let userActivity = NSUserActivity(activityType: NSStringFromClass(INSendMessageIntent.self))
let response = INSendMessageIntentResponse(code: .success, userActivity: userActivity)
completion(response)
}))
}
func handle(intent: INStartAudioCallIntent, completion: @escaping (INStartAudioCallIntentResponse) -> Void) {
self.sendMessageDisposable.set((self.accountPromise.get()
|> take(1)
|> mapError { _ -> StandaloneSendMessageError in
return .generic
}
|> mapToSignal { account -> Signal<PeerId, StandaloneSendMessageError> in
guard let contact = intent.contacts?.first, let customIdentifier = contact.customIdentifier, customIdentifier.hasPrefix("tg") else {
return .fail(.generic)
}
guard let peerIdValue = Int64(String(customIdentifier[customIdentifier.index(customIdentifier.startIndex, offsetBy: 2)...])) else {
return .fail(.generic)
}
let peerId = PeerId(peerIdValue)
if peerId.namespace != Namespaces.Peer.CloudUser {
return .fail(.generic)
}
return .single(peerId)
}
|> deliverOnMainQueue).start(next: { peerId in
let userActivity = NSUserActivity(activityType: NSStringFromClass(INStartAudioCallIntent.self))
//userActivity.userInfo = @{ @"handle": [NSString stringWithFormat:@"TGCA%d", next.firstObject.userId] };
let response = INStartAudioCallIntentResponse(code: .continueInApp, userActivity: userActivity)
completion(response)
}, error: { _ in
let userActivity = NSUserActivity(activityType: NSStringFromClass(INStartAudioCallIntent.self))
let response = INStartAudioCallIntentResponse(code: .failure, userActivity: userActivity)
completion(response)
}))
}
// Implement handlers for each intent you wish to handle. As an example for messages, you may wish to also handle searchForMessages and setMessageAttributes.
// MARK: - INSearchForMessagesIntentHandling
func handle(intent: INSearchForMessagesIntent, completion: @escaping (INSearchForMessagesIntentResponse) -> Void) {
// Implement your application logic to find a message that matches the information in the intent.
let userActivity = NSUserActivity(activityType: NSStringFromClass(INSearchForMessagesIntent.self))
let response = INSearchForMessagesIntentResponse(code: .success, userActivity: userActivity)
// Initialize with found message's attributes
response.messages = [INMessage(
identifier: "identifier",
content: "I am so excited about SiriKit!",
dateSent: Date(),
sender: INPerson(personHandle: INPersonHandle(value: "sarah@example.com", type: .emailAddress), nameComponents: nil, displayName: "Sarah", image: nil, contactIdentifier: nil, customIdentifier: nil),
recipients: [INPerson(personHandle: INPersonHandle(value: "+1-415-555-5555", type: .phoneNumber), nameComponents: nil, displayName: "John", image: nil, contactIdentifier: nil, customIdentifier: nil)]
)]
completion(response)
}
// MARK: - INSetMessageAttributeIntentHandling
func handle(intent: INSetMessageAttributeIntent, completion: @escaping (INSetMessageAttributeIntentResponse) -> Void) {
// Implement your application logic to set the message attribute here.
let userActivity = NSUserActivity(activityType: NSStringFromClass(INSetMessageAttributeIntent.self))
let response = INSetMessageAttributeIntentResponse(code: .success, userActivity: userActivity)
completion(response)
}
}

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.org.telegram.TelegramHD</string>
</array>
</dict>
</plist>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.ph.telegra.Telegraph</string>
</array>
</dict>
</plist>

View File

@ -0,0 +1,6 @@
#ifndef SiriIntents_Bridging_Header_h
#define SiriIntents_Bridging_Header_h
#import "../Telegram-iOS/BuildConfig.h"
#endif

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.org.telegram.Telegram-iOS</string>
</array>
</dict>
</plist>

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="11134" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="ObA-dk-sSI">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="11106"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Intent View Controller-->
<scene sceneID="7MM-of-jgj">
<objects>
<viewController id="ObA-dk-sSI" customClass="IntentViewController" customModuleProvider="target" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="qkL-Od-lgU"/>
<viewControllerLayoutGuide type="bottom" id="n38-gi-rB5"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="zMn-AG-sqS">
<rect key="frame" x="0.0" y="0.0" width="320" height="150"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</view>
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
<size key="freeformSize" width="320" height="150"/>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="X47-rx-isc" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>

38
SiriIntentsUI/Info.plist Normal file
View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>
<string>SiriIntentsUI</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>104</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionAttributes</key>
<dict>
<key>IntentsSupported</key>
<array>
<string>INSendMessageIntent</string>
</array>
</dict>
<key>NSExtensionMainStoryboard</key>
<string>MainInterface</string>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.intents-ui-service</string>
</dict>
</dict>
</plist>

View File

@ -0,0 +1,46 @@
//
// IntentViewController.swift
// SiriIntentsUI
//
// Created by Peter on 9/2/16.
// Copyright © 2016 Telegram. All rights reserved.
//
import IntentsUI
// As an example, this extension's Info.plist has been configured to handle interactions for INSendMessageIntent.
// You will want to replace this or add other intents as appropriate.
// The intents whose interactions you wish to handle must be declared in the extension's Info.plist.
// You can test this example integration by saying things to Siri like:
// "Send a message using <myApp>"
class IntentViewController: UIViewController, INUIHostedViewControlling {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// MARK: - INUIHostedViewControlling
// Prepare your view controller for the interaction to handle.
func configure(with interaction: INInteraction!, context: INUIHostedViewContext, completion: ((CGSize) -> Void)!) {
// Do configuration here, including preparing views and calculating a desired size for presentation.
if let completion = completion {
completion(self.desiredSize)
}
}
var desiredSize: CGSize {
//return self.extensionContext!.hostedViewMaximumAllowedSize
return CGSize()
}
}

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.org.telegram.Telegram-iOS</string>
</array>
</dict>
</plist>

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>BNDL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>624</string>
</dict>
</plist>

View File

@ -0,0 +1,196 @@
//
// SnapshotHelper.swift
// Example
//
// Created by Felix Krause on 10/8/15.
// Copyright © 2015 Felix Krause. All rights reserved.
//
// -----------------------------------------------------
// IMPORTANT: When modifying this file, make sure to
// increment the version number at the very
// bottom of the file to notify users about
// the new SnapshotHelper.swift
// -----------------------------------------------------
import Foundation
import XCTest
var deviceLanguage = ""
var locale = ""
@available(*, deprecated, message: "use setupSnapshot: instead")
func setLanguage(_ app: XCUIApplication) {
setupSnapshot(app)
}
func setupSnapshot(_ app: XCUIApplication) {
Snapshot.setupSnapshot(app)
}
func snapshot(_ name: String, waitForLoadingIndicator: Bool = true) {
Snapshot.snapshot(name, waitForLoadingIndicator: waitForLoadingIndicator)
}
enum SnapshotError: Error, CustomDebugStringConvertible {
case cannotDetectUser
case cannotFindHomeDirectory
case cannotFindSimulatorHomeDirectory
case cannotAccessSimulatorHomeDirectory(String)
var debugDescription: String {
switch self {
case .cannotDetectUser:
return "Couldn't find Snapshot configuration files - can't detect current user "
case .cannotFindHomeDirectory:
return "Couldn't find Snapshot configuration files - can't detect `Users` dir"
case .cannotFindSimulatorHomeDirectory:
return "Couldn't find simulator home location. Please, check SIMULATOR_HOST_HOME env variable."
case .cannotAccessSimulatorHomeDirectory(let simulatorHostHome):
return "Can't prepare environment. Simulator home location is inaccessible. Does \(simulatorHostHome) exist?"
}
}
}
open class Snapshot: NSObject {
static var app: XCUIApplication!
static var cacheDirectory: URL!
static var screenshotsDirectory: URL? {
return cacheDirectory.appendingPathComponent("screenshots", isDirectory: true)
}
open class func setupSnapshot(_ app: XCUIApplication) {
do {
let cacheDir = try pathPrefix()
Snapshot.cacheDirectory = cacheDir
print("cacheDir \(cacheDir)")
Snapshot.app = app
setLanguage(app)
setLocale(app)
setLaunchArguments(app)
} catch let error {
print(error)
}
}
class func setLanguage(_ app: XCUIApplication) {
let path = cacheDirectory.appendingPathComponent("language.txt")
do {
let trimCharacterSet = CharacterSet.whitespacesAndNewlines
deviceLanguage = try String(contentsOf: path, encoding: .utf8).trimmingCharacters(in: trimCharacterSet)
app.launchArguments += ["-AppleLanguages", "(\(deviceLanguage))"]
} catch {
print("Couldn't detect/set language...")
}
}
class func setLocale(_ app: XCUIApplication) {
let path = cacheDirectory.appendingPathComponent("locale.txt")
do {
let trimCharacterSet = CharacterSet.whitespacesAndNewlines
locale = try String(contentsOf: path, encoding: .utf8).trimmingCharacters(in: trimCharacterSet)
} catch {
print("Couldn't detect/set locale...")
}
if locale.isEmpty {
locale = Locale(identifier: deviceLanguage).identifier
}
app.launchArguments += ["-AppleLocale", "\"\(locale)\""]
}
class func setLaunchArguments(_ app: XCUIApplication) {
let path = cacheDirectory.appendingPathComponent("snapshot-launch_arguments.txt")
app.launchArguments += ["-FASTLANE_SNAPSHOT", "YES", "-ui_testing"]
do {
let launchArguments = try String(contentsOf: path, encoding: String.Encoding.utf8)
let regex = try NSRegularExpression(pattern: "(\\\".+?\\\"|\\S+)", options: [])
let matches = regex.matches(in: launchArguments, options: [], range: NSRange(location:0, length:launchArguments.characters.count))
let results = matches.map { result -> String in
(launchArguments as NSString).substring(with: result.range)
}
app.launchArguments += results
} catch {
print("Couldn't detect/set launch_arguments...")
}
}
open class func snapshot(_ name: String, waitForLoadingIndicator: Bool = true) {
if waitForLoadingIndicator {
waitForLoadingIndicatorToDisappear()
}
print("snapshot: \(name)") // more information about this, check out https://github.com/fastlane/fastlane/tree/master/snapshot#how-does-it-work
sleep(1) // Waiting for the animation to be finished (kind of)
#if os(OSX)
XCUIApplication().typeKey(XCUIKeyboardKeySecondaryFn, modifierFlags: [])
#else
let screenshot = app.windows.firstMatch.screenshot()
guard let simulator = ProcessInfo().environment["SIMULATOR_DEVICE_NAME"], let screenshotsDir = screenshotsDirectory else { return }
let path = screenshotsDir.appendingPathComponent("\(simulator)-\(name).png")
do {
try screenshot.pngRepresentation.write(to: path)
} catch let error {
print("Problem writing screenshot: \(name) to \(path)")
print(error)
}
#endif
}
class func waitForLoadingIndicatorToDisappear() {
#if os(tvOS)
return
#endif
let query = XCUIApplication().statusBars.children(matching: .other).element(boundBy: 1).children(matching: .other)
while (0..<query.count).map({ query.element(boundBy: $0) }).contains(where: { $0.isLoadingIndicator }) {
sleep(1)
print("Waiting for loading indicator to disappear...")
}
}
class func pathPrefix() throws -> URL? {
let homeDir: URL
// on OSX config is stored in /Users/<username>/Library
// and on iOS/tvOS/WatchOS it's in simulator's home dir
#if os(OSX)
guard let user = ProcessInfo().environment["USER"] else {
throw SnapshotError.cannotDetectUser
}
guard let usersDir = FileManager.default.urls(for: .userDirectory, in: .localDomainMask).first else {
throw SnapshotError.cannotFindHomeDirectory
}
homeDir = usersDir.appendingPathComponent(user)
#else
guard let simulatorHostHome = ProcessInfo().environment["SIMULATOR_HOST_HOME"] else {
throw SnapshotError.cannotFindSimulatorHomeDirectory
}
guard let homeDirUrl = URL(string: simulatorHostHome) else {
throw SnapshotError.cannotAccessSimulatorHomeDirectory(simulatorHostHome)
}
homeDir = URL(fileURLWithPath: homeDirUrl.path)
#endif
return homeDir.appendingPathComponent("Library/Caches/tools.fastlane")
}
}
extension XCUIElement {
var isLoadingIndicator: Bool {
let whiteListedLoaders = ["GeofenceLocationTrackingOn", "StandardLocationTrackingOn"]
if whiteListedLoaders.contains(self.identifier) {
return false
}
return self.frame.size == CGSize(width: 10, height: 20)
}
}
// Please don't remove the lines below
// They are used to detect outdated configuration files
// SnapshotHelperVersion [1.5]

View File

@ -0,0 +1,53 @@
import XCTest
class Telegram_iOS_UITests: XCTestCase {
var app: XCUIApplication!
override func setUp() {
super.setUp()
self.continueAfterFailure = false
self.app = XCUIApplication()
let path = Bundle(for: type(of: self)).bundlePath
self.app.launchEnvironment["snapshot-data-path"] = path
setupSnapshot(app)
}
override func tearDown() {
super.tearDown()
}
func testChatList() {
self.app.launchArguments = ["snapshot:chat-list"]
self.app.launch()
XCTAssert(self.app.wait(for: .runningForeground, timeout: 10.0))
snapshot("01ChatList")
sleep(1)
}
func testSecretChat() {
self.app.launchArguments = ["snapshot:secret-chat"]
self.app.launch()
XCTAssert(self.app.wait(for: .runningForeground, timeout: 10.0))
snapshot("02SecretChat")
sleep(1)
}
func testSettings() {
self.app.launchArguments = ["snapshot:settings"]
self.app.launch()
XCTAssert(self.app.wait(for: .runningForeground, timeout: 10.0))
snapshot("04Settings")
sleep(1)
}
func testAppearanceSettings() {
self.app.launchArguments = ["snapshot:appearance-settings"]
self.app.launch()
XCTAssert(self.app.wait(for: .runningForeground, timeout: 10.0))
snapshot("05AppearanceSettings")
sleep(1)
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:Telegram-iOS.xcodeproj">
</FileRef>
</Workspace>

View File

@ -0,0 +1,112 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1000"
wasCreatedForAppExtension = "YES"
version = "2.0">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D02CF5FB215D9ABE00E0F56A"
BuildableName = "NotificationContent.appex"
BlueprintName = "NotificationContent"
ReferencedContainer = "container:Telegram-iOS.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D008599B1B28189D00EAF753"
BuildableName = "Telegram X.app"
BlueprintName = "Telegram-iOS"
ReferencedContainer = "container:Telegram-iOS.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug Hockeyapp"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D02CF5FB215D9ABE00E0F56A"
BuildableName = "NotificationContent.appex"
BlueprintName = "NotificationContent"
ReferencedContainer = "container:Telegram-iOS.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug Hockeyapp"
selectedDebuggerIdentifier = ""
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES"
launchAutomaticallySubstyle = "2">
<RemoteRunnable
runnableDebuggingMode = "0"
BundleIdentifier = "org.telegram.Telegram-iOS"
RemotePath = "/var/containers/Bundle/Application/40124EF3-F9E6-4274-8750-77DC100D9950/Telegram X.app">
</RemoteRunnable>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D008599B1B28189D00EAF753"
BuildableName = "Telegram X.app"
BlueprintName = "Telegram-iOS"
ReferencedContainer = "container:Telegram-iOS.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release AppStore"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES"
launchAutomaticallySubstyle = "2">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D008599B1B28189D00EAF753"
BuildableName = "Telegram X.app"
BlueprintName = "Telegram-iOS"
ReferencedContainer = "container:Telegram-iOS.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug Hockeyapp">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release AppStore"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -0,0 +1,91 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0920"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D008599B1B28189D00EAF753"
BuildableName = "Telegram X.app"
BlueprintName = "Telegram-iOS"
ReferencedContainer = "container:Telegram-iOS.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug AppStore LLC"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D008599B1B28189D00EAF753"
BuildableName = "Telegram X.app"
BlueprintName = "Telegram-iOS"
ReferencedContainer = "container:Telegram-iOS.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug AppStore LLC"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D008599B1B28189D00EAF753"
BuildableName = "Telegram X.app"
BlueprintName = "Telegram-iOS"
ReferencedContainer = "container:Telegram-iOS.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release AppStore LLC"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D008599B1B28189D00EAF753"
BuildableName = "Telegram X.app"
BlueprintName = "Telegram-iOS"
ReferencedContainer = "container:Telegram-iOS.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug AppStore LLC">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release AppStore LLC"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -0,0 +1,91 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0920"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D008599B1B28189D00EAF753"
BuildableName = "Telegram X.app"
BlueprintName = "Telegram-iOS"
ReferencedContainer = "container:Telegram-iOS.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug Hockeyapp"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D008599B1B28189D00EAF753"
BuildableName = "Telegram X.app"
BlueprintName = "Telegram-iOS"
ReferencedContainer = "container:Telegram-iOS.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug Hockeyapp"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D008599B1B28189D00EAF753"
BuildableName = "Telegram X.app"
BlueprintName = "Telegram-iOS"
ReferencedContainer = "container:Telegram-iOS.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release AppStore"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D008599B1B28189D00EAF753"
BuildableName = "Telegram X.app"
BlueprintName = "Telegram-iOS"
ReferencedContainer = "container:Telegram-iOS.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug Hockeyapp">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release AppStore"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -0,0 +1,86 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0920"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
</BuildAction>
<TestAction
buildConfiguration = "Debug Hockeyapp"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
language = ""
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D0ECCB7B1FE9C38500609802"
BuildableName = "Telegram-iOS UITests.xctest"
BlueprintName = "Telegram-iOS UITests"
ReferencedContainer = "container:Telegram-iOS.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D008599B1B28189D00EAF753"
BuildableName = "Telegram X.app"
BlueprintName = "Telegram-iOS"
ReferencedContainer = "container:Telegram-iOS.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug Hockeyapp"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
language = ""
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D008599B1B28189D00EAF753"
BuildableName = "Telegram X.app"
BlueprintName = "Telegram-iOS"
ReferencedContainer = "container:Telegram-iOS.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release AppStore"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D008599B1B28189D00EAF753"
BuildableName = "Telegram X.app"
BlueprintName = "Telegram-iOS"
ReferencedContainer = "container:Telegram-iOS.xcodeproj">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug Hockeyapp">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release AppStore"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:submodules/AsyncDisplayKit/AsyncDisplayKit.xcodeproj">
</FileRef>
<FileRef
location = "group:submodules/Display/Display.xcodeproj">
</FileRef>
<FileRef
location = "group:submodules/Postbox/Postbox.xcodeproj">
</FileRef>
<FileRef
location = "group:submodules/MtProtoKit/MtProtoKit.xcodeproj">
</FileRef>
<FileRef
location = "group:submodules/SSignalKit/SSignalKit.xcodeproj">
</FileRef>
<FileRef
location = "group:submodules/TelegramCore/TelegramCore.xcodeproj">
</FileRef>
<FileRef
location = "group:submodules/LegacyComponents/LegacyComponents.xcodeproj">
</FileRef>
<FileRef
location = "group:submodules/lottie-ios/Lottie.xcodeproj">
</FileRef>
<FileRef
location = "group:submodules/libtgvoip/libtgvoip.xcodeproj">
</FileRef>
<FileRef
location = "group:submodules/TelegramUI/TelegramUI.xcodeproj">
</FileRef>
<FileRef
location = "group:submodules/HockeySDK-iOS/Support/HockeySDK.xcodeproj">
</FileRef>
<FileRef
location = "group:Telegram-iOS.xcodeproj">
</FileRef>
</Workspace>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>BuildSystemType</key>
<string>Latest</string>
</dict>
</plist>

View File

@ -0,0 +1,134 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0900"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D008599B1B28189D00EAF753"
BuildableName = "Telegram X.app"
BlueprintName = "Telegram-iOS"
ReferencedContainer = "container:Telegram-iOS.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "NO"
buildForProfiling = "NO"
buildForArchiving = "NO"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D00859B01B28189D00EAF753"
BuildableName = "Telegram-iOSTests.xctest"
BlueprintName = "Telegram-iOSTests"
ReferencedContainer = "container:Telegram-iOS.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug Hockeyapp"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D00859B01B28189D00EAF753"
BuildableName = "Telegram-iOSTests.xctest"
BlueprintName = "Telegram-iOSTests"
ReferencedContainer = "container:Telegram-iOS.xcodeproj">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D0ECCB7B1FE9C38500609802"
BuildableName = "Telegram-iOS UITests.xctest"
BlueprintName = "Telegram-iOS UITests"
ReferencedContainer = "container:Telegram-iOS.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D008599B1B28189D00EAF753"
BuildableName = "Telegram X.app"
BlueprintName = "Telegram-iOS"
ReferencedContainer = "container:Telegram-iOS.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug AppStore"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
stopOnEveryThreadSanitizerIssue = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES"
queueDebuggingEnabled = "No">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D008599B1B28189D00EAF753"
BuildableName = "Telegram X.app"
BlueprintName = "Telegram-iOS"
ReferencedContainer = "container:Telegram-iOS.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<EnvironmentVariables>
<EnvironmentVariable
key = "OS_ACTIVITY_MODE"
value = "disable"
isEnabled = "YES">
</EnvironmentVariable>
</EnvironmentVariables>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Debug Hockeyapp"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D008599B1B28189D00EAF753"
BuildableName = "Telegram X.app"
BlueprintName = "Telegram-iOS"
ReferencedContainer = "container:Telegram-iOS.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug Hockeyapp">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release AppStore"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -0,0 +1,134 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0900"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D008599B1B28189D00EAF753"
BuildableName = "Telegram X.app"
BlueprintName = "Telegram-iOS"
ReferencedContainer = "container:Telegram-iOS.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "NO"
buildForProfiling = "NO"
buildForArchiving = "NO"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D00859B01B28189D00EAF753"
BuildableName = "Telegram-iOSTests.xctest"
BlueprintName = "Telegram-iOSTests"
ReferencedContainer = "container:Telegram-iOS.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug Hockeyapp"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D00859B01B28189D00EAF753"
BuildableName = "Telegram-iOSTests.xctest"
BlueprintName = "Telegram-iOSTests"
ReferencedContainer = "container:Telegram-iOS.xcodeproj">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D0ECCB7B1FE9C38500609802"
BuildableName = "Telegram-iOS UITests.xctest"
BlueprintName = "Telegram-iOS UITests"
ReferencedContainer = "container:Telegram-iOS.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D008599B1B28189D00EAF753"
BuildableName = "Telegram X.app"
BlueprintName = "Telegram-iOS"
ReferencedContainer = "container:Telegram-iOS.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug Hockeyapp"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
enableASanStackUseAfterReturn = "YES"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES"
queueDebuggingEnabled = "No">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D008599B1B28189D00EAF753"
BuildableName = "Telegram X.app"
BlueprintName = "Telegram-iOS"
ReferencedContainer = "container:Telegram-iOS.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<EnvironmentVariables>
<EnvironmentVariable
key = "OS_ACTIVITY_MODE"
value = "disable"
isEnabled = "YES">
</EnvironmentVariable>
</EnvironmentVariables>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Debug Hockeyapp"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D008599B1B28189D00EAF753"
BuildableName = "Telegram X.app"
BlueprintName = "Telegram-iOS"
ReferencedContainer = "container:Telegram-iOS.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug Hockeyapp">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release Hockeyapp Internal"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -0,0 +1,145 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0900"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<PreActions>
<ExecutionAction
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
<ActionContent
title = "Run Script"
scriptText = "export DYLD_INSERT_LIBRARIES=/Users/peter/Library/Developer/Xcode/DerivedData/BuildClusterFileRemap-bcqudxadbptcjbdlnzensopnyzzc/Build/Products/Debug/libBuildClusterFileRemap.dylib&#10;export DYLD_FORCE_FLAT_NAMESPACE=1">
</ActionContent>
</ExecutionAction>
</PreActions>
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D008599B1B28189D00EAF753"
BuildableName = "Telegram X.app"
BlueprintName = "Telegram-iOS"
ReferencedContainer = "container:Telegram-iOS.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "NO"
buildForProfiling = "NO"
buildForArchiving = "NO"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D00859B01B28189D00EAF753"
BuildableName = "Telegram-iOSTests.xctest"
BlueprintName = "Telegram-iOSTests"
ReferencedContainer = "container:Telegram-iOS.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug Hockeyapp"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D00859B01B28189D00EAF753"
BuildableName = "Telegram-iOSTests.xctest"
BlueprintName = "Telegram-iOSTests"
ReferencedContainer = "container:Telegram-iOS.xcodeproj">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D0ECCB7B1FE9C38500609802"
BuildableName = "Telegram-iOS UITests.xctest"
BlueprintName = "Telegram-iOS UITests"
ReferencedContainer = "container:Telegram-iOS.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D008599B1B28189D00EAF753"
BuildableName = "Telegram X.app"
BlueprintName = "Telegram-iOS"
ReferencedContainer = "container:Telegram-iOS.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug Hockeyapp"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
stopOnEveryThreadSanitizerIssue = "YES"
stopOnEveryUBSanitizerIssue = "YES"
stopOnEveryMainThreadCheckerIssue = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES"
queueDebuggingEnabled = "No">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D008599B1B28189D00EAF753"
BuildableName = "Telegram X.app"
BlueprintName = "Telegram-iOS"
ReferencedContainer = "container:Telegram-iOS.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<EnvironmentVariables>
<EnvironmentVariable
key = "OS_ACTIVITY_MODE"
value = "disable"
isEnabled = "YES">
</EnvironmentVariable>
</EnvironmentVariables>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Debug Hockeyapp"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D008599B1B28189D00EAF753"
BuildableName = "Telegram X.app"
BlueprintName = "Telegram-iOS"
ReferencedContainer = "container:Telegram-iOS.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug Hockeyapp">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release Hockeyapp"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,5 @@
import UIKit
@objc(Application) class Application: UIApplication {
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,42 @@
import Foundation
import UIKit
import TelegramUI
enum ApplicationShortcutItemType: String {
case search
case compose
case camera
case savedMessages
}
struct ApplicationShortcutItem: Equatable {
let type: ApplicationShortcutItemType
let title: String
}
@available(iOS 9.1, *)
extension ApplicationShortcutItem {
func shortcutItem() -> UIApplicationShortcutItem {
let icon: UIApplicationShortcutIcon
switch self.type {
case .search:
icon = UIApplicationShortcutIcon(type: .search)
case .compose:
icon = UIApplicationShortcutIcon(type: .compose)
case .camera:
icon = UIApplicationShortcutIcon(type: .capturePhoto)
case .savedMessages:
icon = UIApplicationShortcutIcon(templateImageName: "Shortcuts/SavedMessages")
}
return UIApplicationShortcutItem(type: self.type.rawValue, localizedTitle: self.title, localizedSubtitle: nil, icon: icon, userInfo: nil)
}
}
func applicationShortcutItems(strings: PresentationStrings) -> [ApplicationShortcutItem] {
return [
ApplicationShortcutItem(type: .search, title: strings.Common_Search),
ApplicationShortcutItem(type: .compose, title: strings.Compose_NewMessage),
ApplicationShortcutItem(type: .camera, title: strings.Camera_Title),
ApplicationShortcutItem(type: .savedMessages, title: strings.Conversation_SavedMessages)
]
}

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="11163.2" systemVersion="16A239j" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" colorMatched="YES">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="11133"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<view contentMode="scaleToFill" id="O8c-13-3vw">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</view>
</objects>
</document>

View File

@ -0,0 +1,14 @@
#import <Foundation/Foundation.h>
@interface BuildConfig : NSObject
+ (instancetype _Nonnull)sharedBuildConfig;
@property (nonatomic, strong, readonly) NSString * _Nonnull hockeyAppId;
@property (nonatomic, readonly) int32_t apiId;
@property (nonatomic, strong, readonly) NSString * _Nonnull apiHash;
@property (nonatomic, readonly) bool isInternalBuild;
@property (nonatomic, readonly) bool isAppStoreBuild;
@property (nonatomic, readonly) int64_t appStoreId;
@end

View File

@ -0,0 +1,38 @@
#import "BuildConfig.h"
@implementation BuildConfig
+ (instancetype _Nonnull)sharedBuildConfig {
static BuildConfig *instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[BuildConfig alloc] init];
});
return instance;
}
- (int32_t)apiId {
return APP_CONFIG_API_ID;
}
- (NSString * _Nonnull)apiHash {
return @(APP_CONFIG_API_HASH);
}
- (NSString * _Nonnull)hockeyAppId {
return @(APP_CONFIG_HOCKEYAPPID);
}
- (bool)isInternalBuild {
return APP_CONFIG_IS_INTERNAL_BUILD;
}
- (bool)isAppStoreBuild {
return APP_CONFIG_IS_APPSTORE_BUILD;
}
- (int64_t)appStoreId {
return APP_CONFIG_APPSTORE_ID;
}
@end

View File

@ -0,0 +1,91 @@
import Foundation
import SwiftSignalKit
import Postbox
final class ClearNotificationIdsCompletion {
let f: ([(String, NotificationManagedNotificationRequestId)]) -> Void
init(f: @escaping ([(String, NotificationManagedNotificationRequestId)]) -> Void) {
self.f = f
}
}
final class ClearNotificationsManager {
private let getNotificationIds: (ClearNotificationIdsCompletion) -> Void
private let getPendingNotificationIds: (ClearNotificationIdsCompletion) -> Void
private let removeNotificationIds: ([String]) -> Void
private let removePendingNotificationIds: ([String]) -> Void
private var ids: [PeerId: MessageId] = [:]
private var timer: SwiftSignalKit.Timer?
init(getNotificationIds: @escaping (ClearNotificationIdsCompletion) -> Void, removeNotificationIds: @escaping ([String]) -> Void, getPendingNotificationIds: @escaping (ClearNotificationIdsCompletion) -> Void, removePendingNotificationIds: @escaping ([String]) -> Void) {
self.getNotificationIds = getNotificationIds
self.removeNotificationIds = removeNotificationIds
self.getPendingNotificationIds = getPendingNotificationIds
self.removePendingNotificationIds = removePendingNotificationIds
}
deinit {
self.timer?.invalidate()
}
func append(_ id: MessageId) {
if let current = self.ids[id.peerId] {
if current < id {
self.ids[id.peerId] = id
}
} else {
self.ids[id.peerId] = id
}
self.timer?.invalidate()
let timer = SwiftSignalKit.Timer(timeout: 2.0, repeat: false, completion: { [weak self] in
self?.commitNow()
}, queue: Queue.mainQueue())
self.timer = timer
timer.start()
}
func commitNow() {
self.timer?.invalidate()
self.timer = nil
let ids = self.ids
self.ids.removeAll()
self.getNotificationIds(ClearNotificationIdsCompletion { [weak self] result in
Queue.mainQueue().async {
var removeKeys: [String] = []
for (identifier, requestId) in result {
if case let .messageId(messageId) = requestId {
if let maxId = ids[messageId.peerId], messageId <= maxId {
removeKeys.append(identifier)
}
}
}
if let strongSelf = self, !removeKeys.isEmpty {
strongSelf.removeNotificationIds(removeKeys)
}
}
})
self.getPendingNotificationIds(ClearNotificationIdsCompletion { [weak self] result in
Queue.mainQueue().async {
var removeKeys: [String] = []
for (identifier, requestId) in result {
if case let .messageId(messageId) = requestId {
if let maxId = ids[messageId.peerId], messageId <= maxId {
removeKeys.append(identifier)
}
}
}
if let strongSelf = self, !removeKeys.isEmpty {
strongSelf.removePendingNotificationIds(removeKeys)
}
}
})
}
}

View File

@ -0,0 +1 @@
#include "../../Telegram-iOS-Shared/Config-AppStore.xcconfig"

View File

@ -0,0 +1 @@
#include "../../Telegram-iOS-Shared/Config-AppStoreLLC.xcconfig"

View File

@ -0,0 +1 @@
#include "../../Telegram-iOS-Shared/Config-Hockeyapp Internal.xcconfig"

View File

@ -0,0 +1 @@
#include "../../Telegram-iOS-Shared/Config-Hockeyapp.xcconfig"

View File

@ -0,0 +1,104 @@
{
"images" : [
{
"idiom" : "iphone",
"size" : "20x20",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "20x20",
"scale" : "3x"
},
{
"idiom" : "iphone",
"size" : "29x29",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "29x29",
"scale" : "3x"
},
{
"idiom" : "iphone",
"size" : "40x40",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "40x40",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "icon@120px.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "icon@180px.png",
"scale" : "3x"
},
{
"idiom" : "ipad",
"size" : "20x20",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "20x20",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "29x29",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "29x29",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "40x40",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "40x40",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "icon@76px.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "icon@152px.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "icon@167px.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "icon@1024px.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 370 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

@ -0,0 +1,116 @@
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-40.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-40@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-Small@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-87.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-120.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-Small@0s.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-42.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-Small@2x-1.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-41.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-40@2x-1.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-76.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-167.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "T-Ipad_1024.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Some files were not shown because too many files have changed in this diff Show More