commit 77ee5c4dabdd6eb5f1e2ff76219edf7e18b45c00 Author: Peter Iakovlev Date: Wed Nov 14 23:03:33 2018 +0400 Added .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..249d3670f3 --- /dev/null +++ b/.gitignore @@ -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/* diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000000..e627d7447d --- /dev/null +++ b/.gitmodules @@ -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 diff --git a/NotificationContent/Base.lproj/MainInterface.storyboard b/NotificationContent/Base.lproj/MainInterface.storyboard new file mode 100644 index 0000000000..97c475ba24 --- /dev/null +++ b/NotificationContent/Base.lproj/MainInterface.storyboard @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/NotificationContent/ChatHistoryFragmentView.swift b/NotificationContent/ChatHistoryFragmentView.swift new file mode 100644 index 0000000000..c252aa9651 --- /dev/null +++ b/NotificationContent/ChatHistoryFragmentView.swift @@ -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() + + 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 + 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 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) + } + } +} diff --git a/NotificationContent/Info.plist b/NotificationContent/Info.plist new file mode 100644 index 0000000000..17a0b0298d --- /dev/null +++ b/NotificationContent/Info.plist @@ -0,0 +1,41 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + NotificationContent + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + XPC! + CFBundleShortVersionString + 5.0.17 + CFBundleVersion + 624 + NSExtension + + NSExtensionAttributes + + UNNotificationExtensionCategory + + withReplyMedia + withMuteMedia + + UNNotificationExtensionInitialContentSizeRatio + 0.0001 + + NSExtensionMainStoryboard + MainInterface + NSExtensionPointIdentifier + com.apple.usernotifications.content-extension + + + diff --git a/NotificationContent/NotificationContent-AppStore.entitlements b/NotificationContent/NotificationContent-AppStore.entitlements new file mode 100644 index 0000000000..5e963c4f0f --- /dev/null +++ b/NotificationContent/NotificationContent-AppStore.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.org.telegram.TelegramHD + + + diff --git a/NotificationContent/NotificationContent-AppStoreLLC.entitlements b/NotificationContent/NotificationContent-AppStoreLLC.entitlements new file mode 100644 index 0000000000..c9a9054223 --- /dev/null +++ b/NotificationContent/NotificationContent-AppStoreLLC.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.ph.telegra.Telegraph + + + diff --git a/NotificationContent/NotificationContent-Bridging-Header.h b/NotificationContent/NotificationContent-Bridging-Header.h new file mode 100644 index 0000000000..f207b3b666 --- /dev/null +++ b/NotificationContent/NotificationContent-Bridging-Header.h @@ -0,0 +1,6 @@ +#ifndef Share_Bridging_Header_h +#define Share_Bridging_Header_h + +#import "../Telegram-iOS/BuildConfig.h" + +#endif diff --git a/NotificationContent/NotificationContent-HockeyApp.entitlements b/NotificationContent/NotificationContent-HockeyApp.entitlements new file mode 100644 index 0000000000..65f2a19d32 --- /dev/null +++ b/NotificationContent/NotificationContent-HockeyApp.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.org.telegram.Telegram-iOS + + + diff --git a/NotificationContent/NotificationContent.entitlements b/NotificationContent/NotificationContent.entitlements new file mode 100644 index 0000000000..2eb7e333a6 --- /dev/null +++ b/NotificationContent/NotificationContent.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.application-groups + + + diff --git a/NotificationContent/NotificationViewController.swift b/NotificationContent/NotificationViewController.swift new file mode 100644 index 0000000000..35aa79011e --- /dev/null +++ b/NotificationContent/NotificationViewController.swift @@ -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() + + 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[.. + 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 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) + } + } +} diff --git a/NotificationService/Info.plist b/NotificationService/Info.plist new file mode 100644 index 0000000000..cb1c2bad9b --- /dev/null +++ b/NotificationService/Info.plist @@ -0,0 +1,31 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + NotificationService + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + XPC! + CFBundleShortVersionString + 1.0 + CFBundleVersion + 55 + NSExtension + + NSExtensionPointIdentifier + com.apple.usernotifications.service + NSExtensionPrincipalClass + NotificationService + + + diff --git a/NotificationService/MiniAccount.swift b/NotificationService/MiniAccount.swift new file mode 100644 index 0000000000..191ab4f280 --- /dev/null +++ b/NotificationService/MiniAccount.swift @@ -0,0 +1,6 @@ +import Foundation +import Postbox +import SwiftSignalKit +import MtProtoKitDynamic + + diff --git a/NotificationService/NotificationService.entitlements b/NotificationService/NotificationService.entitlements new file mode 100644 index 0000000000..65f2a19d32 --- /dev/null +++ b/NotificationService/NotificationService.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.org.telegram.Telegram-iOS + + + diff --git a/NotificationService/NotificationService.swift b/NotificationService/NotificationService.swift new file mode 100644 index 0000000000..af1f28f21c --- /dev/null +++ b/NotificationService/NotificationService.swift @@ -0,0 +1,212 @@ +import UserNotifications +import Postbox +import SwiftSignalKit +import TelegramCore + +private func reportMemory() { + // constant + let MACH_TASK_BASIC_INFO_COUNT = (MemoryLayout.size / MemoryLayout.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.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 = .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 in + switch account { + case .left: + return .complete() + case let .right(authorizedAccount): + return .single(authorizedAccount) + } + } + + + signal = (authorizedAccount + |> take(1) + |> mapToSignal { account -> Signal in + setupAccount(account) + return downloadMessage(account: account, message: messageId) + } + |> mapToSignal { message -> Signal 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 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) + } + } +} diff --git a/Share/Info.plist b/Share/Info.plist new file mode 100644 index 0000000000..ea54eed7a7 --- /dev/null +++ b/Share/Info.plist @@ -0,0 +1,52 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + ${APP_NAME} + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + XPC! + CFBundleShortVersionString + 5.0.17 + CFBundleVersion + 624 + NSExtension + + NSExtensionAttributes + + NSExtensionActivationRule + 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 > 0 + + NSExtensionPointIdentifier + com.apple.share-services + NSExtensionPrincipalClass + ShareRootController + + + diff --git a/Share/Share-AppStore.entitlements b/Share/Share-AppStore.entitlements new file mode 100644 index 0000000000..5e963c4f0f --- /dev/null +++ b/Share/Share-AppStore.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.org.telegram.TelegramHD + + + diff --git a/Share/Share-AppStoreLLC.entitlements b/Share/Share-AppStoreLLC.entitlements new file mode 100644 index 0000000000..c9a9054223 --- /dev/null +++ b/Share/Share-AppStoreLLC.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.ph.telegra.Telegraph + + + diff --git a/Share/Share-Bridging-Header.h b/Share/Share-Bridging-Header.h new file mode 100644 index 0000000000..620e1b0868 --- /dev/null +++ b/Share/Share-Bridging-Header.h @@ -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 diff --git a/Share/Share-HockeyApp.entitlements b/Share/Share-HockeyApp.entitlements new file mode 100644 index 0000000000..65f2a19d32 --- /dev/null +++ b/Share/Share-HockeyApp.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.org.telegram.Telegram-iOS + + + diff --git a/Share/ShareItems.swift b/Share/ShareItems.swift new file mode 100644 index 0000000000..da792d8492 --- /dev/null +++ b/Share/ShareItems.swift @@ -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 { + 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 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 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 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) -> 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 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 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 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 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 { 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 { + 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 { + 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 in + var signals: [Signal] = [] + 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 in + return .single(1.0) + } + } +} diff --git a/Share/ShareRootController.swift b/Share/ShareRootController.swift new file mode 100644 index 0000000000..f5f643de3e --- /dev/null +++ b/Share/ShareRootController.swift @@ -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() + 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[.. + 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([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 = { 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 in + return .single(nil) + } + |> mapToSignal { state -> Signal 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 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: {})) + } + } +} diff --git a/Share/TGContactModel.h b/Share/TGContactModel.h new file mode 100644 index 0000000000..5218304bd0 --- /dev/null +++ b/Share/TGContactModel.h @@ -0,0 +1,24 @@ +#import + +@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 diff --git a/Share/TGContactModel.m b/Share/TGContactModel.m new file mode 100644 index 0000000000..e84edabbcb --- /dev/null +++ b/Share/TGContactModel.m @@ -0,0 +1,35 @@ +#import "TGContactModel.h" + +#import + +@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 diff --git a/Share/TGItemProviderSignals.h b/Share/TGItemProviderSignals.h new file mode 100644 index 0000000000..7b4b7d5d93 --- /dev/null +++ b/Share/TGItemProviderSignals.h @@ -0,0 +1,8 @@ +#import + +@interface TGItemProviderSignals : NSObject + ++ (NSArray *)itemSignalsForInputItems:(NSArray *)inputItems; ++ (NSData *)audioWaveform:(NSURL *)url; + +@end diff --git a/Share/TGItemProviderSignals.m b/Share/TGItemProviderSignals.m new file mode 100644 index 0000000000..5901b34885 --- /dev/null +++ b/Share/TGItemProviderSignals.m @@ -0,0 +1,659 @@ +#import "TGItemProviderSignals.h" + +#import +#import +#import +#import +#import + +#import +#import "TGMimeTypeMap.h" + +#import "TGContactModel.h" + +@implementation TGItemProviderSignals + ++ (NSArray *)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(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(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 _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(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(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(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(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(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(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(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(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 diff --git a/Share/TGMimeTypeMap.h b/Share/TGMimeTypeMap.h new file mode 100644 index 0000000000..af8f26570e --- /dev/null +++ b/Share/TGMimeTypeMap.h @@ -0,0 +1,8 @@ +#import + +@interface TGMimeTypeMap : NSObject + ++ (NSString *)mimeTypeForExtension:(NSString *)extension; ++ (NSString *)extensionForMimeType:(NSString *)mimeType; + +@end diff --git a/Share/TGMimeTypeMap.m b/Share/TGMimeTypeMap.m new file mode 100644 index 0000000000..123d880dd3 --- /dev/null +++ b/Share/TGMimeTypeMap.m @@ -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 diff --git a/Share/TGShareLocationSignals.h b/Share/TGShareLocationSignals.h new file mode 100644 index 0000000000..d579d8c5f1 --- /dev/null +++ b/Share/TGShareLocationSignals.h @@ -0,0 +1,22 @@ +#import + +@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 diff --git a/Share/TGShareLocationSignals.m b/Share/TGShareLocationSignals.m new file mode 100644 index 0000000000..0dbe232c86 --- /dev/null +++ b/Share/TGShareLocationSignals.m @@ -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 diff --git a/Share/en.lproj/Localizable.strings b/Share/en.lproj/Localizable.strings new file mode 100644 index 0000000000..a18a8349cf --- /dev/null +++ b/Share/en.lproj/Localizable.strings @@ -0,0 +1,3 @@ +"Common.OK" = "OK"; +"Share.AuthTitle" = "Log in to Telegram"; +"Share.AuthDescription" = "Open Telegram and log in to share."; diff --git a/SiriIntents/Info.plist b/SiriIntents/Info.plist new file mode 100644 index 0000000000..e75d15e53e --- /dev/null +++ b/SiriIntents/Info.plist @@ -0,0 +1,43 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + ${APP_NAME} + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + XPC! + CFBundleShortVersionString + 5.0.17 + CFBundleVersion + 624 + NSExtension + + NSExtensionAttributes + + IntentsRestrictedWhileLocked + + IntentsRestrictedWhileProtectedDataUnavailable + + IntentsSupported + + INSendMessageIntent + INStartAudioCallIntent + + + NSExtensionPointIdentifier + com.apple.intents-service + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).IntentHandler + + + diff --git a/SiriIntents/IntentContacts.swift b/SiriIntents/IntentContacts.swift new file mode 100644 index 0000000000..a8757c55e9 --- /dev/null +++ b/SiriIntents/IntentContacts.swift @@ -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())") +} diff --git a/SiriIntents/IntentHandler.swift b/SiriIntents/IntentHandler.swift new file mode 100644 index 0000000000..f3811f9943 --- /dev/null +++ b/SiriIntents/IntentHandler.swift @@ -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 { + 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() + + 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[.. + if let accountCache = accountCache { + account = .single(accountCache) + } else { + initializeAccountManagement() + account = accountManager(basePath: rootPath + "/accounts-metadata") + |> take(1) + |> mapToSignal { accountManager -> Signal 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 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 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 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 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) + } +} + diff --git a/SiriIntents/SiriIntents-AppStore.entitlements b/SiriIntents/SiriIntents-AppStore.entitlements new file mode 100644 index 0000000000..5e963c4f0f --- /dev/null +++ b/SiriIntents/SiriIntents-AppStore.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.org.telegram.TelegramHD + + + diff --git a/SiriIntents/SiriIntents-AppStoreLLC.entitlements b/SiriIntents/SiriIntents-AppStoreLLC.entitlements new file mode 100644 index 0000000000..c9a9054223 --- /dev/null +++ b/SiriIntents/SiriIntents-AppStoreLLC.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.ph.telegra.Telegraph + + + diff --git a/SiriIntents/SiriIntents-Bridging-Header.h b/SiriIntents/SiriIntents-Bridging-Header.h new file mode 100644 index 0000000000..303f8d1230 --- /dev/null +++ b/SiriIntents/SiriIntents-Bridging-Header.h @@ -0,0 +1,6 @@ +#ifndef SiriIntents_Bridging_Header_h +#define SiriIntents_Bridging_Header_h + +#import "../Telegram-iOS/BuildConfig.h" + +#endif diff --git a/SiriIntents/SiriIntents-Hockeyapp.entitlements b/SiriIntents/SiriIntents-Hockeyapp.entitlements new file mode 100644 index 0000000000..65f2a19d32 --- /dev/null +++ b/SiriIntents/SiriIntents-Hockeyapp.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.org.telegram.Telegram-iOS + + + diff --git a/SiriIntentsUI/Base.lproj/MainInterface.storyboard b/SiriIntentsUI/Base.lproj/MainInterface.storyboard new file mode 100644 index 0000000000..831ee40359 --- /dev/null +++ b/SiriIntentsUI/Base.lproj/MainInterface.storyboard @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SiriIntentsUI/Info.plist b/SiriIntentsUI/Info.plist new file mode 100644 index 0000000000..2141d9b4bb --- /dev/null +++ b/SiriIntentsUI/Info.plist @@ -0,0 +1,38 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + SiriIntentsUI + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + XPC! + CFBundleShortVersionString + 1.0 + CFBundleVersion + 104 + NSExtension + + NSExtensionAttributes + + IntentsSupported + + INSendMessageIntent + + + NSExtensionMainStoryboard + MainInterface + NSExtensionPointIdentifier + com.apple.intents-ui-service + + + diff --git a/SiriIntentsUI/IntentViewController.swift b/SiriIntentsUI/IntentViewController.swift new file mode 100644 index 0000000000..9ef77998d7 --- /dev/null +++ b/SiriIntentsUI/IntentViewController.swift @@ -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 " + +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() + } + +} diff --git a/SiriIntentsUI/SiriIntentsUI.entitlements b/SiriIntentsUI/SiriIntentsUI.entitlements new file mode 100644 index 0000000000..65f2a19d32 --- /dev/null +++ b/SiriIntentsUI/SiriIntentsUI.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.org.telegram.Telegram-iOS + + + diff --git a/Telegram-iOS UITests/Images/Bitmap1.png b/Telegram-iOS UITests/Images/Bitmap1.png new file mode 100644 index 0000000000..f0dbe85f17 Binary files /dev/null and b/Telegram-iOS UITests/Images/Bitmap1.png differ diff --git a/Telegram-iOS UITests/Images/Bitmap10.png b/Telegram-iOS UITests/Images/Bitmap10.png new file mode 100644 index 0000000000..ac46a069b7 Binary files /dev/null and b/Telegram-iOS UITests/Images/Bitmap10.png differ diff --git a/Telegram-iOS UITests/Images/Bitmap11.png b/Telegram-iOS UITests/Images/Bitmap11.png new file mode 100644 index 0000000000..d64c41bae2 Binary files /dev/null and b/Telegram-iOS UITests/Images/Bitmap11.png differ diff --git a/Telegram-iOS UITests/Images/Bitmap12.png b/Telegram-iOS UITests/Images/Bitmap12.png new file mode 100644 index 0000000000..2f450cf3eb Binary files /dev/null and b/Telegram-iOS UITests/Images/Bitmap12.png differ diff --git a/Telegram-iOS UITests/Images/Bitmap2.png b/Telegram-iOS UITests/Images/Bitmap2.png new file mode 100644 index 0000000000..149b4d88e5 Binary files /dev/null and b/Telegram-iOS UITests/Images/Bitmap2.png differ diff --git a/Telegram-iOS UITests/Images/Bitmap3.png b/Telegram-iOS UITests/Images/Bitmap3.png new file mode 100644 index 0000000000..66b3bef10f Binary files /dev/null and b/Telegram-iOS UITests/Images/Bitmap3.png differ diff --git a/Telegram-iOS UITests/Images/Bitmap5.png b/Telegram-iOS UITests/Images/Bitmap5.png new file mode 100644 index 0000000000..a053de0159 Binary files /dev/null and b/Telegram-iOS UITests/Images/Bitmap5.png differ diff --git a/Telegram-iOS UITests/Images/Bitmap6.png b/Telegram-iOS UITests/Images/Bitmap6.png new file mode 100644 index 0000000000..7f3854f431 Binary files /dev/null and b/Telegram-iOS UITests/Images/Bitmap6.png differ diff --git a/Telegram-iOS UITests/Images/Bitmap7.png b/Telegram-iOS UITests/Images/Bitmap7.png new file mode 100644 index 0000000000..346939bbd7 Binary files /dev/null and b/Telegram-iOS UITests/Images/Bitmap7.png differ diff --git a/Telegram-iOS UITests/Images/Bitmap8.png b/Telegram-iOS UITests/Images/Bitmap8.png new file mode 100644 index 0000000000..7a32f43fdb Binary files /dev/null and b/Telegram-iOS UITests/Images/Bitmap8.png differ diff --git a/Telegram-iOS UITests/Images/Bitmap9.png b/Telegram-iOS UITests/Images/Bitmap9.png new file mode 100644 index 0000000000..07fa8d4aa5 Binary files /dev/null and b/Telegram-iOS UITests/Images/Bitmap9.png differ diff --git a/Telegram-iOS UITests/Info.plist b/Telegram-iOS UITests/Info.plist new file mode 100644 index 0000000000..60cac13c99 --- /dev/null +++ b/Telegram-iOS UITests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 624 + + diff --git a/Telegram-iOS UITests/SnapshotHelper.swift b/Telegram-iOS UITests/SnapshotHelper.swift new file mode 100644 index 0000000000..77b3ede31e --- /dev/null +++ b/Telegram-iOS UITests/SnapshotHelper.swift @@ -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.. URL? { + let homeDir: URL + // on OSX config is stored in /Users//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] diff --git a/Telegram-iOS UITests/Telegram_iOS_UITests.swift b/Telegram-iOS UITests/Telegram_iOS_UITests.swift new file mode 100644 index 0000000000..77877422c0 --- /dev/null +++ b/Telegram-iOS UITests/Telegram_iOS_UITests.swift @@ -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) + } +} diff --git a/Telegram-iOS.xcodeproj/project.pbxproj b/Telegram-iOS.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..495aa39950 --- /dev/null +++ b/Telegram-iOS.xcodeproj/project.pbxproj @@ -0,0 +1,7317 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 092F368521542D6C001A9F49 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 092F368321542D6C001A9F49 /* Localizable.strings */; }; + 0956AF2C217B4642008106D0 /* WatchCommunicationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0956AF2B217B4642008106D0 /* WatchCommunicationManager.swift */; }; + 0956AF2F217B8109008106D0 /* TGNeoUnsupportedMessageViewModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 0956AF2E217B8109008106D0 /* TGNeoUnsupportedMessageViewModel.m */; }; + 0972C6E021791D950069E98A /* UserNotifications.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0972C6DF21791D950069E98A /* UserNotifications.framework */; }; + 0972C6E421792D130069E98A /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 0972C6E221792D120069E98A /* InfoPlist.strings */; }; + 09C50DE721729D7C009E676F /* TGBridgeAudioSignals.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C572C1217292BA00BDF00F /* TGBridgeAudioSignals.m */; }; + 09C50DE821729D7C009E676F /* TGBridgeBotSignals.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C572BB217292B900BDF00F /* TGBridgeBotSignals.m */; }; + 09C50DE921729D7C009E676F /* TGBridgeChatListSignals.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C572AF217292B900BDF00F /* TGBridgeChatListSignals.m */; }; + 09C50DEA21729D7C009E676F /* TGBridgeChatMessageListSignals.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C572C6217292BA00BDF00F /* TGBridgeChatMessageListSignals.m */; }; + 09C50DEB21729D7C009E676F /* TGBridgeContactsSignals.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C572BE217292BA00BDF00F /* TGBridgeContactsSignals.m */; }; + 09C50DEC21729D7C009E676F /* TGBridgeConversationSignals.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C572B0217292B900BDF00F /* TGBridgeConversationSignals.m */; }; + 09C50DED21729D7C009E676F /* TGBridgeLocationSignals.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C572C4217292BA00BDF00F /* TGBridgeLocationSignals.m */; }; + 09C50DEE21729D7C009E676F /* TGBridgeMediaSignals.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C572C5217292BA00BDF00F /* TGBridgeMediaSignals.m */; }; + 09C50DEF21729D7C009E676F /* TGBridgePeerSettingsSignals.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C572C3217292BA00BDF00F /* TGBridgePeerSettingsSignals.m */; }; + 09C50DF021729D7C009E676F /* TGBridgePresetsSignals.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C572AE217292B900BDF00F /* TGBridgePresetsSignals.m */; }; + 09C50DF121729D7C009E676F /* TGBridgeRemoteSignals.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C572AD217292B800BDF00F /* TGBridgeRemoteSignals.m */; }; + 09C50DF221729D7C009E676F /* TGBridgeSendMessageSignals.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C572B5217292B900BDF00F /* TGBridgeSendMessageSignals.m */; }; + 09C50DF321729D7C009E676F /* TGBridgeStateSignal.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C572C9217292BB00BDF00F /* TGBridgeStateSignal.m */; }; + 09C50DF421729D7C009E676F /* TGBridgeStickersSignals.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C572B8217292B900BDF00F /* TGBridgeStickersSignals.m */; }; + 09C50DF521729D7C009E676F /* TGBridgeUserInfoSignals.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C572CA217292BB00BDF00F /* TGBridgeUserInfoSignals.m */; }; + 09C50E0321729DB5009E676F /* TGBotCommandController.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C571F6217287EF00BDF00F /* TGBotCommandController.m */; }; + 09C50E0421729DB5009E676F /* TGBotKeyboardController.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C571F8217287F000BDF00F /* TGBotKeyboardController.m */; }; + 09C50E0521729DE6009E676F /* TGBotKeyboardButtonController.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C571F4217287E500BDF00F /* TGBotKeyboardButtonController.m */; }; + 09C50E7B21738178009E676F /* TGBridgeServer.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C50E7A21738178009E676F /* TGBridgeServer.m */; }; + 09C50E8321738514009E676F /* TGBridgeContext.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C573072172953700BDF00F /* TGBridgeContext.m */; }; + 09C50E842173853E009E676F /* TGBridgeCommon.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C572CE2172939F00BDF00F /* TGBridgeCommon.m */; }; + 09C50E88217385CF009E676F /* WatchConnectivity.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 09C50E87217385CF009E676F /* WatchConnectivity.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; + 09C50E8A2173AEDB009E676F /* WatchRequestHandlers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09C50E892173AEDB009E676F /* WatchRequestHandlers.swift */; }; + 09C50E912173B247009E676F /* TGBridgeSubscriptions.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C50E8F2173B247009E676F /* TGBridgeSubscriptions.m */; }; + 09C50E922173B247009E676F /* TGBridgeSubscriptions.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C50E8F2173B247009E676F /* TGBridgeSubscriptions.m */; }; + 09C56F8F2172797200BDF00F /* Interface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 09C56F8D2172797200BDF00F /* Interface.storyboard */; }; + 09C56F912172797400BDF00F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 09C56F902172797400BDF00F /* Assets.xcassets */; }; + 09C56F982172797500BDF00F /* Watch Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 09C56F972172797400BDF00F /* Watch Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 09C56FA52172797500BDF00F /* Watch.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = 09C56F8B2172797200BDF00F /* Watch.app */; }; + 09C5713E21727D9E00BDF00F /* TGInterfaceController.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C5712021727BF800BDF00F /* TGInterfaceController.m */; }; + 09C5713F21727DA000BDF00F /* TGExtensionDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C5712221727BF800BDF00F /* TGExtensionDelegate.m */; }; + 09C5714021727DAA00BDF00F /* TGDateUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C5713921727CFD00BDF00F /* TGDateUtils.m */; }; + 09C5714121727DAA00BDF00F /* TGGeometry.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C5712F21727CFC00BDF00F /* TGGeometry.m */; }; + 09C5714221727DAA00BDF00F /* TGIndexPath.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C5713D21727CFD00BDF00F /* TGIndexPath.m */; }; + 09C5714321727DAA00BDF00F /* TGLocationUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C5712E21727CFC00BDF00F /* TGLocationUtils.m */; }; + 09C5714421727DAA00BDF00F /* TGStringUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C5713A21727CFD00BDF00F /* TGStringUtils.m */; }; + 09C5714521727DAA00BDF00F /* TGWatchColor.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C5713221727CFC00BDF00F /* TGWatchColor.m */; }; + 09C5714621727DAA00BDF00F /* TGWatchCommon.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C5713521727CFC00BDF00F /* TGWatchCommon.m */; }; + 09C5714721727DAA00BDF00F /* WKInterface+TGInterface.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C5713B21727CFD00BDF00F /* WKInterface+TGInterface.m */; }; + 09C5714821727DAA00BDF00F /* WKInterfaceGroup+Signals.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C5712C21727CFC00BDF00F /* WKInterfaceGroup+Signals.m */; }; + 09C5714921727DAA00BDF00F /* WKInterfaceImage+Signals.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C5712B21727CFC00BDF00F /* WKInterfaceImage+Signals.m */; }; + 09C5714A21727DAA00BDF00F /* WKInterfaceTable+TGDataDrivenTable.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C5713121727CFC00BDF00F /* WKInterfaceTable+TGDataDrivenTable.m */; }; + 09C5715321727DD900BDF00F /* MediaAudio@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 09C5714B21727DD900BDF00F /* MediaAudio@2x.png */; }; + 09C5715421727DD900BDF00F /* MediaPhoto@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 09C5714C21727DD900BDF00F /* MediaPhoto@2x.png */; }; + 09C5715521727DD900BDF00F /* Location@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 09C5714D21727DD900BDF00F /* Location@2x.png */; }; + 09C5715621727DD900BDF00F /* File@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 09C5714E21727DD900BDF00F /* File@2x.png */; }; + 09C5715721727DD900BDF00F /* MediaDocument@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 09C5714F21727DD900BDF00F /* MediaDocument@2x.png */; }; + 09C5715821727DD900BDF00F /* MediaLocation@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 09C5715021727DD900BDF00F /* MediaLocation@2x.png */; }; + 09C5715921727DD900BDF00F /* MediaVideo@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 09C5715121727DD900BDF00F /* MediaVideo@2x.png */; }; + 09C5715A21727DD900BDF00F /* VerifiedList@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 09C5715221727DD900BDF00F /* VerifiedList@2x.png */; }; + 09C5716021727EE700BDF00F /* TGBridgeUserCache.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C5715E21727EE700BDF00F /* TGBridgeUserCache.m */; }; + 09C5716121727EE700BDF00F /* TGFileCache.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C5715F21727EE700BDF00F /* TGFileCache.m */; }; + 09C5716821727F1500BDF00F /* TGTableDeltaUpdater.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C5716221727F1500BDF00F /* TGTableDeltaUpdater.m */; }; + 09C5716921727F1500BDF00F /* TGInputController.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C5716321727F1500BDF00F /* TGInputController.m */; }; + 09C5716A21727F1500BDF00F /* TGInterfaceMenu.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C5716621727F1500BDF00F /* TGInterfaceMenu.m */; }; + 09C5718E2172806600BDF00F /* TGAvatarViewModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C571882172805700BDF00F /* TGAvatarViewModel.m */; }; + 09C5718F2172806600BDF00F /* TGMessageViewModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C571862172805700BDF00F /* TGMessageViewModel.m */; }; + 09C571902172806600BDF00F /* TGNeoAttachmentViewModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C571822172805700BDF00F /* TGNeoAttachmentViewModel.m */; }; + 09C571912172806600BDF00F /* TGNeoImageViewModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C5718D2172805800BDF00F /* TGNeoImageViewModel.m */; }; + 09C571922172806600BDF00F /* TGNeoLabelViewModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C571832172805700BDF00F /* TGNeoLabelViewModel.m */; }; + 09C571932172806600BDF00F /* TGNeoRenderableViewModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C571892172805700BDF00F /* TGNeoRenderableViewModel.m */; }; + 09C571942172806600BDF00F /* TGNeoViewModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C571872172805700BDF00F /* TGNeoViewModel.m */; }; + 09C571952172806900BDF00F /* TGUserRowController.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C5717D2172802200BDF00F /* TGUserRowController.m */; }; + 09C571962172806D00BDF00F /* TGComplicationController.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C5717C2172800800BDF00F /* TGComplicationController.m */; }; + 09C571972172806D00BDF00F /* TGNotificationController.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C5717B2172800800BDF00F /* TGNotificationController.m */; }; + 09C571982172807100BDF00F /* TGAudioMicAlertController.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C5717821727FE900BDF00F /* TGAudioMicAlertController.m */; }; + 09C5719B217280E900BDF00F /* TGNeoChatsController.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C5719A217280E500BDF00F /* TGNeoChatsController.m */; }; + 09C571CD2172874B00BDF00F /* TGGroupInfoFooterController.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C571C72172874500BDF00F /* TGGroupInfoFooterController.m */; }; + 09C571CE2172874B00BDF00F /* TGGroupInfoHeaderController.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C571C42172874500BDF00F /* TGGroupInfoHeaderController.m */; }; + 09C571CF2172874B00BDF00F /* TGUserHandle.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C571C92172874500BDF00F /* TGUserHandle.m */; }; + 09C571D02172874B00BDF00F /* TGUserHandleRowController.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C571CA2172874500BDF00F /* TGUserHandleRowController.m */; }; + 09C571D12172874B00BDF00F /* TGUserInfoHeaderController.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C571CC2172874600BDF00F /* TGUserInfoHeaderController.m */; }; + 09C571D22172875100BDF00F /* TGGroupInfoController.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C571BD2172872B00BDF00F /* TGGroupInfoController.m */; }; + 09C571D32172875100BDF00F /* TGProfilePhotoController.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C571C12172872C00BDF00F /* TGProfilePhotoController.m */; }; + 09C571D42172875100BDF00F /* TGUserInfoController.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C571C02172872C00BDF00F /* TGUserInfoController.m */; }; + 09C571D52172875500BDF00F /* TGMessageViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C571B3217286BA00BDF00F /* TGMessageViewController.m */; }; + 09C571D62172875A00BDF00F /* TGMessageViewFooterController.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C571B9217286D700BDF00F /* TGMessageViewFooterController.m */; }; + 09C571D72172875A00BDF00F /* TGMessageViewMessageRowController.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C571B5217286D700BDF00F /* TGMessageViewMessageRowController.m */; }; + 09C571D82172875A00BDF00F /* TGMessageViewWebPageRowController.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C571B6217286D700BDF00F /* TGMessageViewWebPageRowController.m */; }; + 09C571D92172876300BDF00F /* TGNeoChatRowController.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C571A0217285CE00BDF00F /* TGNeoChatRowController.m */; }; + 09C571DA2172876300BDF00F /* TGNeoChatViewModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C5719F217285CE00BDF00F /* TGNeoChatViewModel.m */; }; + 09C571DB2172876700BDF00F /* TGNeoConversationController.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C571A42172861600BDF00F /* TGNeoConversationController.m */; }; + 09C571DC2172876C00BDF00F /* TGLocationController.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C571AC2172867400BDF00F /* TGLocationController.m */; }; + 09C571DD2172876F00BDF00F /* TGLocationMapHeaderController.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C571AE2172869500BDF00F /* TGLocationMapHeaderController.m */; }; + 09C571DE2172876F00BDF00F /* TGLocationVenueRowController.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C571AD2172869500BDF00F /* TGLocationVenueRowController.m */; }; + 09C571EB2172878900BDF00F /* TGStickersHeaderController.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C571DF2172878800BDF00F /* TGStickersHeaderController.m */; }; + 09C571EC2172878900BDF00F /* TGStickerPackRowController.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C571E22172878800BDF00F /* TGStickerPackRowController.m */; }; + 09C571ED2172878900BDF00F /* TGStickerPacksController.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C571E32172878900BDF00F /* TGStickerPacksController.m */; }; + 09C571EE2172878900BDF00F /* TGStickersRowController.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C571E42172878900BDF00F /* TGStickersRowController.m */; }; + 09C571EF2172878900BDF00F /* TGStickersSectionHeaderController.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C571E52172878900BDF00F /* TGStickersSectionHeaderController.m */; }; + 09C571F02172878900BDF00F /* TGStickersController.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C571E62172878900BDF00F /* TGStickersController.m */; }; + 09C571F12172879800BDF00F /* TGContactsController.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C571A92172865400BDF00F /* TGContactsController.m */; }; + 09C571F22172879C00BDF00F /* TGComposeController.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C571A62172863D00BDF00F /* TGComposeController.m */; }; + 09C5722521728AA500BDF00F /* TGNeoAudioMessageViewModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C5721F2172889200BDF00F /* TGNeoAudioMessageViewModel.m */; }; + 09C5722621728AA500BDF00F /* TGNeoBackgroundViewModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C572202172889200BDF00F /* TGNeoBackgroundViewModel.m */; }; + 09C5722721728AA500BDF00F /* TGNeoBubbleMessageViewModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C572072172888F00BDF00F /* TGNeoBubbleMessageViewModel.m */; }; + 09C5722821728AA500BDF00F /* TGNeoContactMessageViewModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C5720D2172889000BDF00F /* TGNeoContactMessageViewModel.m */; }; + 09C5722921728AA500BDF00F /* TGNeoConversationMediaRowController.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C572162172889100BDF00F /* TGNeoConversationMediaRowController.m */; }; + 09C5722A21728AA500BDF00F /* TGNeoConversationSimpleRowController.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C572042172888F00BDF00F /* TGNeoConversationSimpleRowController.m */; }; + 09C5722B21728AA500BDF00F /* TGNeoConversationStaticRowController.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C572002172888E00BDF00F /* TGNeoConversationStaticRowController.m */; }; + 09C5722C21728AA500BDF00F /* TGNeoConversationTimeRowController.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C572102172889000BDF00F /* TGNeoConversationTimeRowController.m */; }; + 09C5722D21728AA500BDF00F /* TGNeoFileMessageViewModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C572012172888E00BDF00F /* TGNeoFileMessageViewModel.m */; }; + 09C5722E21728AA500BDF00F /* TGNeoForwardHeaderViewModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C5721B2172889100BDF00F /* TGNeoForwardHeaderViewModel.m */; }; + 09C5722F21728AA500BDF00F /* TGNeoMediaMessageViewModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C5720F2172889000BDF00F /* TGNeoMediaMessageViewModel.m */; }; + 09C5723021728AA500BDF00F /* TGNeoMessageViewModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C572172172889100BDF00F /* TGNeoMessageViewModel.m */; }; + 09C5723121728AA500BDF00F /* TGNeoReplyHeaderViewModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C5720E2172889000BDF00F /* TGNeoReplyHeaderViewModel.m */; }; + 09C5723221728AA500BDF00F /* TGNeoRowController.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C5720A2172888F00BDF00F /* TGNeoRowController.m */; }; + 09C5723321728AA500BDF00F /* TGNeoServiceMessageViewModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C5720C2172889000BDF00F /* TGNeoServiceMessageViewModel.m */; }; + 09C5723421728AA500BDF00F /* TGNeoSmiliesMessageViewModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C572132172889000BDF00F /* TGNeoSmiliesMessageViewModel.m */; }; + 09C5723521728AA500BDF00F /* TGNeoStickerMessageViewModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C572062172888F00BDF00F /* TGNeoStickerMessageViewModel.m */; }; + 09C5723621728AA500BDF00F /* TGNeoTextMessageViewModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C572152172889100BDF00F /* TGNeoTextMessageViewModel.m */; }; + 09C5723721728AA500BDF00F /* TGNeoVenueMessageViewModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C572082172888F00BDF00F /* TGNeoVenueMessageViewModel.m */; }; + 09C5723821728AA500BDF00F /* TGConversationFooterController.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C571FC2172882D00BDF00F /* TGConversationFooterController.m */; }; + 09C5723921728AA500BDF00F /* TGChatInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C571FA2172880900BDF00F /* TGChatInfo.m */; }; + 09C5723A21728AA500BDF00F /* TGChatTimestamp.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C571FE2172883E00BDF00F /* TGChatTimestamp.m */; }; + 09C5723B21728AA500BDF00F /* TGNeoConversationRowController.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C571A32172860000BDF00F /* TGNeoConversationRowController.m */; }; + 09C5723D21728C0E00BDF00F /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 09C5723C21728C0E00BDF00F /* CoreGraphics.framework */; }; + 09C5727421728D3700BDF00F /* SAtomic.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C5724921728CFD00BDF00F /* SAtomic.m */; }; + 09C5727521728D3700BDF00F /* SBag.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C5725321728CFE00BDF00F /* SBag.m */; }; + 09C5727621728D3700BDF00F /* SBlockDisposable.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C5725921728CFE00BDF00F /* SBlockDisposable.m */; }; + 09C5727721728D3700BDF00F /* SDisposableSet.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C5726C21728D0000BDF00F /* SDisposableSet.m */; }; + 09C5727821728D3700BDF00F /* SMetaDisposable.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C5724E21728CFD00BDF00F /* SMetaDisposable.m */; }; + 09C5727921728D3700BDF00F /* SMulticastSignalManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C5724D21728CFD00BDF00F /* SMulticastSignalManager.m */; }; + 09C5727A21728D3700BDF00F /* SQueue.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C5726721728CFF00BDF00F /* SQueue.m */; }; + 09C5727B21728D3700BDF00F /* SSignal.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C5726D21728D0000BDF00F /* SSignal.m */; }; + 09C5727C21728D3700BDF00F /* SSignal+Accumulate.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C5723E21728CFC00BDF00F /* SSignal+Accumulate.m */; }; + 09C5727D21728D3700BDF00F /* SSignal+Catch.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C5724421728CFD00BDF00F /* SSignal+Catch.m */; }; + 09C5727E21728D3700BDF00F /* SSignal+Combine.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C5724521728CFD00BDF00F /* SSignal+Combine.m */; }; + 09C5727F21728D3700BDF00F /* SSignal+Dispatch.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C5725221728CFE00BDF00F /* SSignal+Dispatch.m */; }; + 09C5728021728D3700BDF00F /* SSignal+Mapping.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C5727121728D0000BDF00F /* SSignal+Mapping.m */; }; + 09C5728121728D3700BDF00F /* SSignal+Meta.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C5727021728D0000BDF00F /* SSignal+Meta.m */; }; + 09C5728221728D3700BDF00F /* SSignal+Multicast.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C5725521728CFE00BDF00F /* SSignal+Multicast.m */; }; + 09C5728321728D3700BDF00F /* SSignal+Pipe.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C5725E21728CFF00BDF00F /* SSignal+Pipe.m */; }; + 09C5728421728D3700BDF00F /* SSignal+SideEffects.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C5725A21728CFE00BDF00F /* SSignal+SideEffects.m */; }; + 09C5728521728D3700BDF00F /* SSignal+Single.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C5726021728CFF00BDF00F /* SSignal+Single.m */; }; + 09C5728621728D3700BDF00F /* SSignal+Take.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C5725721728CFE00BDF00F /* SSignal+Take.m */; }; + 09C5728721728D3700BDF00F /* SSignal+Timing.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C5724321728CFD00BDF00F /* SSignal+Timing.m */; }; + 09C5728821728D3700BDF00F /* SSubscriber.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C5725D21728CFF00BDF00F /* SSubscriber.m */; }; + 09C5728921728D3700BDF00F /* SThreadPool.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C5727321728D0000BDF00F /* SThreadPool.m */; }; + 09C5728A21728D3700BDF00F /* SThreadPoolQueue.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C5724B21728CFD00BDF00F /* SThreadPoolQueue.m */; }; + 09C5728B21728D3700BDF00F /* SThreadPoolTask.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C5725421728CFE00BDF00F /* SThreadPoolTask.m */; }; + 09C5728C21728D3700BDF00F /* STimer.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C5726221728CFF00BDF00F /* STimer.m */; }; + 09C5728D21728D3700BDF00F /* SVariable.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C5725B21728CFE00BDF00F /* SVariable.m */; }; + 09C572D2217293D400BDF00F /* TGBridgeClient.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C572D02172939F00BDF00F /* TGBridgeClient.m */; }; + 09C572D3217293D400BDF00F /* TGBridgeCommon.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C572CE2172939F00BDF00F /* TGBridgeCommon.m */; }; + 09C573162172953800BDF00F /* TGBridgeMessage+TGTableItem.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C572D72172953300BDF00F /* TGBridgeMessage+TGTableItem.m */; }; + 09C573182172953800BDF00F /* TGBridgeBotReplyMarkup.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C572D92172953300BDF00F /* TGBridgeBotReplyMarkup.m */; }; + 09C573192172953800BDF00F /* TGBridgeContactMediaAttachment.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C572DE2172953300BDF00F /* TGBridgeContactMediaAttachment.m */; }; + 09C5731A2172953800BDF00F /* TGBridgeMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C572DF2172953300BDF00F /* TGBridgeMessage.m */; }; + 09C5731B2172953800BDF00F /* TGBridgeAudioMediaAttachment.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C572E02172953300BDF00F /* TGBridgeAudioMediaAttachment.m */; }; + 09C5731C2172953800BDF00F /* TGBridgeActionMediaAttachment.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C572E22172953300BDF00F /* TGBridgeActionMediaAttachment.m */; }; + 09C5731D2172953800BDF00F /* TGBridgeStickerPack.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C572E32172953400BDF00F /* TGBridgeStickerPack.m */; }; + 09C5731E2172953800BDF00F /* TGBridgeVideoMediaAttachment.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C572E42172953400BDF00F /* TGBridgeVideoMediaAttachment.m */; }; + 09C573202172953800BDF00F /* TGBridgeForwardedMessageMediaAttachment.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C572E62172953400BDF00F /* TGBridgeForwardedMessageMediaAttachment.m */; }; + 09C573212172953800BDF00F /* TGBridgeUser+TGTableItem.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C572E92172953400BDF00F /* TGBridgeUser+TGTableItem.m */; }; + 09C573222172953800BDF00F /* TGBridgeUser.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C572ED2172953400BDF00F /* TGBridgeUser.m */; }; + 09C573232172953800BDF00F /* TGBridgeBotCommandInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C572EF2172953500BDF00F /* TGBridgeBotCommandInfo.m */; }; + 09C573242172953800BDF00F /* TGBridgeImageMediaAttachment.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C572F22172953500BDF00F /* TGBridgeImageMediaAttachment.m */; }; + 09C573252172953800BDF00F /* TGBridgeLocationVenue.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C572F32172953500BDF00F /* TGBridgeLocationVenue.m */; }; + 09C573262172953800BDF00F /* TGBridgeMessageEntities.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C572F42172953500BDF00F /* TGBridgeMessageEntities.m */; }; + 09C573272172953800BDF00F /* TGBridgeWebPageMediaAttachment.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C572F52172953500BDF00F /* TGBridgeWebPageMediaAttachment.m */; }; + 09C573282172953800BDF00F /* TGBridgeChat+TGTableItem.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C572F62172953500BDF00F /* TGBridgeChat+TGTableItem.m */; }; + 09C573292172953900BDF00F /* TGBridgeMediaAttachment.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C572F92172953600BDF00F /* TGBridgeMediaAttachment.m */; }; + 09C5732A2172953900BDF00F /* TGBridgeDocumentMediaAttachment.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C572FB2172953600BDF00F /* TGBridgeDocumentMediaAttachment.m */; }; + 09C5732B2172953900BDF00F /* TGBridgePeerNotificationSettings.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C572FC2172953600BDF00F /* TGBridgePeerNotificationSettings.m */; }; + 09C5732C2172953900BDF00F /* TGBridgeReplyMessageMediaAttachment.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C572FD2172953600BDF00F /* TGBridgeReplyMessageMediaAttachment.m */; }; + 09C5732D2172953900BDF00F /* TGBridgeLocationVenue+TGTableItem.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C572FF2172953600BDF00F /* TGBridgeLocationVenue+TGTableItem.m */; }; + 09C5732E2172953900BDF00F /* TGBridgeLocationMediaAttachment.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C573002172953600BDF00F /* TGBridgeLocationMediaAttachment.m */; }; + 09C5732F2172953900BDF00F /* TGBridgeContext.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C573072172953700BDF00F /* TGBridgeContext.m */; }; + 09C573302172953900BDF00F /* TGBridgeChat.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C573092172953700BDF00F /* TGBridgeChat.m */; }; + 09C573312172953900BDF00F /* TGBridgeMessageEntitiesAttachment.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C5730A2172953700BDF00F /* TGBridgeMessageEntitiesAttachment.m */; }; + 09C573322172953900BDF00F /* TGBridgeChatMessages.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C5730C2172953700BDF00F /* TGBridgeChatMessages.m */; }; + 09C573332172953900BDF00F /* TGBridgeReplyMarkupMediaAttachment.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C5730D2172953800BDF00F /* TGBridgeReplyMarkupMediaAttachment.m */; }; + 09C573342172953900BDF00F /* TGBridgeUnsupportedMediaAttachment.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C5730F2172953800BDF00F /* TGBridgeUnsupportedMediaAttachment.m */; }; + 09C573352172953900BDF00F /* TGBridgeBotInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C573102172953800BDF00F /* TGBridgeBotInfo.m */; }; + 09CFB212217299E80083F7A3 /* CoreLocation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 09CFB211217299E80083F7A3 /* CoreLocation.framework */; }; + 09D30420217418EC00C00567 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = D09DCBB51D0C856B00F51FFE /* Localizable.strings */; }; + 09D304222174335F00C00567 /* WatchBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09D304212174335F00C00567 /* WatchBridge.swift */; }; + 09D304232174340900C00567 /* TGBridgeUser.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C572ED2172953400BDF00F /* TGBridgeUser.m */; }; + 09D304242174340E00C00567 /* TGBridgeMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C572DF2172953300BDF00F /* TGBridgeMessage.m */; }; + 09D304252174341200C00567 /* TGBridgeMediaAttachment.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C572F92172953600BDF00F /* TGBridgeMediaAttachment.m */; }; + 09D304262174341A00C00567 /* TGBridgeLocationVenue.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C572F32172953500BDF00F /* TGBridgeLocationVenue.m */; }; + 09D304272174341E00C00567 /* TGBridgeChatMessages.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C5730C2172953700BDF00F /* TGBridgeChatMessages.m */; }; + 09D304282174342E00C00567 /* TGBridgeChat.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C573092172953700BDF00F /* TGBridgeChat.m */; }; + 09D304292174343300C00567 /* TGBridgeBotInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C573102172953800BDF00F /* TGBridgeBotInfo.m */; }; + 09D3042A2174343B00C00567 /* TGBridgeBotCommandInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C572EF2172953500BDF00F /* TGBridgeBotCommandInfo.m */; }; + 09D3042C2174344900C00567 /* TGBridgeActionMediaAttachment.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C572E22172953300BDF00F /* TGBridgeActionMediaAttachment.m */; }; + 09D3042D2174344900C00567 /* TGBridgeAudioMediaAttachment.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C572E02172953300BDF00F /* TGBridgeAudioMediaAttachment.m */; }; + 09D3042E2174344900C00567 /* TGBridgeContactMediaAttachment.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C572DE2172953300BDF00F /* TGBridgeContactMediaAttachment.m */; }; + 09D3042F2174344900C00567 /* TGBridgeDocumentMediaAttachment.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C572FB2172953600BDF00F /* TGBridgeDocumentMediaAttachment.m */; }; + 09D304302174344900C00567 /* TGBridgeForwardedMessageMediaAttachment.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C572E62172953400BDF00F /* TGBridgeForwardedMessageMediaAttachment.m */; }; + 09D304312174344900C00567 /* TGBridgeImageMediaAttachment.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C572F22172953500BDF00F /* TGBridgeImageMediaAttachment.m */; }; + 09D304322174344900C00567 /* TGBridgeLocationMediaAttachment.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C573002172953600BDF00F /* TGBridgeLocationMediaAttachment.m */; }; + 09D304332174344900C00567 /* TGBridgeMessageEntitiesAttachment.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C5730A2172953700BDF00F /* TGBridgeMessageEntitiesAttachment.m */; }; + 09D304342174344900C00567 /* TGBridgeReplyMarkupMediaAttachment.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C5730D2172953800BDF00F /* TGBridgeReplyMarkupMediaAttachment.m */; }; + 09D304352174344900C00567 /* TGBridgeReplyMessageMediaAttachment.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C572FD2172953600BDF00F /* TGBridgeReplyMessageMediaAttachment.m */; }; + 09D304362174344900C00567 /* TGBridgeUnsupportedMediaAttachment.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C5730F2172953800BDF00F /* TGBridgeUnsupportedMediaAttachment.m */; }; + 09D304372174344900C00567 /* TGBridgeVideoMediaAttachment.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C572E42172953400BDF00F /* TGBridgeVideoMediaAttachment.m */; }; + 09D304382174344900C00567 /* TGBridgeWebPageMediaAttachment.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C572F52172953500BDF00F /* TGBridgeWebPageMediaAttachment.m */; }; + 09D304392174344900C00567 /* TGBridgeMessageEntities.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C572F42172953500BDF00F /* TGBridgeMessageEntities.m */; }; + 09FDAEE62140477F00BF856F /* MtProtoKitDynamic.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 09FDAEE52140477F00BF856F /* MtProtoKitDynamic.framework */; }; + D001D5AA1F878DA300DF975A /* PhoneCountries.txt in Resources */ = {isa = PBXBuildFile; fileRef = D001D5A91F878DA300DF975A /* PhoneCountries.txt */; }; + D00859A21B28189D00EAF753 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00859A11B28189D00EAF753 /* AppDelegate.swift */; }; + D00859A91B28189D00EAF753 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D00859A81B28189D00EAF753 /* Images.xcassets */; }; + D00859AC1B28189D00EAF753 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = D00859AA1B28189D00EAF753 /* LaunchScreen.xib */; }; + D00859B81B28189D00EAF753 /* Telegram_iOSTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00859B71B28189D00EAF753 /* Telegram_iOSTests.swift */; }; + D00ED75A1FE94630001F38BD /* AppIntentVocabulary.plist in Resources */ = {isa = PBXBuildFile; fileRef = D00ED7581FE94630001F38BD /* AppIntentVocabulary.plist */; }; + D00ED75D1FE95287001F38BD /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = D00ED75B1FE95287001F38BD /* InfoPlist.strings */; }; + D01A47551F4DBED700383CC1 /* HockeySDK.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D01A47541F4DBED700383CC1 /* HockeySDK.framework */; }; + D02CF5FD215D9ABF00E0F56A /* UserNotifications.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0AA1A671D568BA400152314 /* UserNotifications.framework */; }; + D02CF5FE215D9ABF00E0F56A /* UserNotificationsUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0AA1A691D568BA400152314 /* UserNotificationsUI.framework */; }; + D02CF601215D9ABF00E0F56A /* NotificationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02CF600215D9ABF00E0F56A /* NotificationViewController.swift */; }; + D02CF604215D9ABF00E0F56A /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D02CF602215D9ABF00E0F56A /* MainInterface.storyboard */; }; + D02CF608215D9ABF00E0F56A /* NotificationContent.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = D02CF5FC215D9ABE00E0F56A /* NotificationContent.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + D02CF615215DA24900E0F56A /* Display.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D02CF614215DA24900E0F56A /* Display.framework */; }; + D02CF617215DA24900E0F56A /* Postbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D02CF616215DA24900E0F56A /* Postbox.framework */; }; + D02CF619215DA24900E0F56A /* SwiftSignalKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D02CF618215DA24900E0F56A /* SwiftSignalKit.framework */; }; + D02CF61B215DA24900E0F56A /* TelegramCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D02CF61A215DA24900E0F56A /* TelegramCore.framework */; }; + D02CF61C215E51D500E0F56A /* BuildConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = D09250011FE52D2A003F693F /* BuildConfig.m */; }; + D02E31231BD803E800CD3F01 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = D02E31221BD803E800CD3F01 /* main.m */; }; + D039FB172170F06A00BD1BAD /* PreFetchedLegacyResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D039FB162170F06A00BD1BAD /* PreFetchedLegacyResource.swift */; }; + D03B0E7B1D63484500955575 /* ShareRootController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03B0E7A1D63484500955575 /* ShareRootController.swift */; }; + D03B0E821D63484500955575 /* Share.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = D03B0E781D63484500955575 /* Share.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + D03B0E8A1D634B1100955575 /* Display.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D03B0E871D634B1100955575 /* Display.framework */; }; + D03B0E8B1D634B1100955575 /* SwiftSignalKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D03B0E881D634B1100955575 /* SwiftSignalKit.framework */; }; + D03B0E8C1D634B1100955575 /* TelegramCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D03B0E891D634B1100955575 /* TelegramCore.framework */; }; + D03BCCCA1C6EBD670097A291 /* ListViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03BCCC91C6EBD670097A291 /* ListViewTests.swift */; }; + D04DCC211F71C80000B021D7 /* 0.m4a in Resources */ = {isa = PBXBuildFile; fileRef = D04DCC0B1F71C80000B021D7 /* 0.m4a */; }; + D04DCC221F71C80000B021D7 /* 1.m4a in Resources */ = {isa = PBXBuildFile; fileRef = D04DCC0C1F71C80000B021D7 /* 1.m4a */; }; + D04DCC231F71C80000B021D7 /* 100.m4a in Resources */ = {isa = PBXBuildFile; fileRef = D04DCC0D1F71C80000B021D7 /* 100.m4a */; }; + D04DCC241F71C80000B021D7 /* 101.m4a in Resources */ = {isa = PBXBuildFile; fileRef = D04DCC0E1F71C80000B021D7 /* 101.m4a */; }; + D04DCC251F71C80000B021D7 /* 102.m4a in Resources */ = {isa = PBXBuildFile; fileRef = D04DCC0F1F71C80000B021D7 /* 102.m4a */; }; + D04DCC261F71C80000B021D7 /* 103.m4a in Resources */ = {isa = PBXBuildFile; fileRef = D04DCC101F71C80000B021D7 /* 103.m4a */; }; + D04DCC271F71C80000B021D7 /* 104.m4a in Resources */ = {isa = PBXBuildFile; fileRef = D04DCC111F71C80000B021D7 /* 104.m4a */; }; + D04DCC281F71C80000B021D7 /* 105.m4a in Resources */ = {isa = PBXBuildFile; fileRef = D04DCC121F71C80000B021D7 /* 105.m4a */; }; + D04DCC291F71C80000B021D7 /* 106.m4a in Resources */ = {isa = PBXBuildFile; fileRef = D04DCC131F71C80000B021D7 /* 106.m4a */; }; + D04DCC2A1F71C80000B021D7 /* 107.m4a in Resources */ = {isa = PBXBuildFile; fileRef = D04DCC141F71C80000B021D7 /* 107.m4a */; }; + D04DCC2B1F71C80000B021D7 /* 108.m4a in Resources */ = {isa = PBXBuildFile; fileRef = D04DCC151F71C80000B021D7 /* 108.m4a */; }; + D04DCC2C1F71C80000B021D7 /* 109.m4a in Resources */ = {isa = PBXBuildFile; fileRef = D04DCC161F71C80000B021D7 /* 109.m4a */; }; + D04DCC2D1F71C80000B021D7 /* 110.m4a in Resources */ = {isa = PBXBuildFile; fileRef = D04DCC171F71C80000B021D7 /* 110.m4a */; }; + D04DCC2E1F71C80000B021D7 /* 111.m4a in Resources */ = {isa = PBXBuildFile; fileRef = D04DCC181F71C80000B021D7 /* 111.m4a */; }; + D04DCC2F1F71C80000B021D7 /* 2.m4a in Resources */ = {isa = PBXBuildFile; fileRef = D04DCC191F71C80000B021D7 /* 2.m4a */; }; + D04DCC301F71C80000B021D7 /* 3.m4a in Resources */ = {isa = PBXBuildFile; fileRef = D04DCC1A1F71C80000B021D7 /* 3.m4a */; }; + D04DCC311F71C80000B021D7 /* 4.m4a in Resources */ = {isa = PBXBuildFile; fileRef = D04DCC1B1F71C80000B021D7 /* 4.m4a */; }; + D04DCC321F71C80000B021D7 /* 5.m4a in Resources */ = {isa = PBXBuildFile; fileRef = D04DCC1C1F71C80000B021D7 /* 5.m4a */; }; + D04DCC331F71C80000B021D7 /* 6.m4a in Resources */ = {isa = PBXBuildFile; fileRef = D04DCC1D1F71C80000B021D7 /* 6.m4a */; }; + D04DCC341F71C80000B021D7 /* 7.m4a in Resources */ = {isa = PBXBuildFile; fileRef = D04DCC1E1F71C80000B021D7 /* 7.m4a */; }; + D04DCC351F71C80000B021D7 /* 8.m4a in Resources */ = {isa = PBXBuildFile; fileRef = D04DCC1F1F71C80000B021D7 /* 8.m4a */; }; + D04DCC361F71C80000B021D7 /* 9.m4a in Resources */ = {isa = PBXBuildFile; fileRef = D04DCC201F71C80000B021D7 /* 9.m4a */; }; + D04FA1C82145E3810006EF45 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = D04FA1AF2145E37F0006EF45 /* Localizable.strings */; }; + D04FA1C92145E3810006EF45 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = D04FA1B12145E37F0006EF45 /* InfoPlist.strings */; }; + D04FA1CA2145E3810006EF45 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = D04FA1B42145E37F0006EF45 /* InfoPlist.strings */; }; + D04FA1CB2145E3810006EF45 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = D04FA1B72145E3800006EF45 /* InfoPlist.strings */; }; + D04FA1CC2145E3810006EF45 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = D04FA1BA2145E3800006EF45 /* InfoPlist.strings */; }; + D04FA1CD2145E3810006EF45 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = D04FA1BD2145E3800006EF45 /* InfoPlist.strings */; }; + D04FA1CE2145E3810006EF45 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = D04FA1C02145E3800006EF45 /* InfoPlist.strings */; }; + D04FA1CF2145E3810006EF45 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = D04FA1C32145E3810006EF45 /* InfoPlist.strings */; }; + D04FA1D02145E3810006EF45 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = D04FA1C62145E3810006EF45 /* InfoPlist.strings */; }; + D051DB0B215E5D1C00F30F92 /* TelegramUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0400ED81D5B8F97007931CE /* TelegramUI.framework */; }; + D051DB5D21602D6E00F30F92 /* LegacyDataImportSplash.swift in Sources */ = {isa = PBXBuildFile; fileRef = D051DB5C21602D6E00F30F92 /* LegacyDataImportSplash.swift */; }; + D053DAD32018ED2B00993D32 /* LockedWindowCoveringView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D053DAD22018ED2B00993D32 /* LockedWindowCoveringView.swift */; }; + D055BD441B7E216400F06C0A /* MapKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D055BD431B7E216400F06C0A /* MapKit.framework */; }; + D05B37F51FEA5F6E0041D2A5 /* SnapshotEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05B37F41FEA5F6E0041D2A5 /* SnapshotEnvironment.swift */; }; + D05B37F71FEA8C640041D2A5 /* SnapshotSecretChat.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05B37F61FEA8C640041D2A5 /* SnapshotSecretChat.swift */; }; + D05B37F91FEA8CF00041D2A5 /* SnapshotSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05B37F81FEA8CF00041D2A5 /* SnapshotSettings.swift */; }; + D05B37FB1FEA8D020041D2A5 /* SnapshotAppearanceSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05B37FA1FEA8D020041D2A5 /* SnapshotAppearanceSettings.swift */; }; + D05B37FD1FEA8D870041D2A5 /* SnapshotResources.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05B37FC1FEA8D870041D2A5 /* SnapshotResources.swift */; }; + D05B380A1FEA8E3D0041D2A5 /* Bitmap2.png in Resources */ = {isa = PBXBuildFile; fileRef = D05B37FF1FEA8E3D0041D2A5 /* Bitmap2.png */; }; + D05B380B1FEA8E3D0041D2A5 /* Bitmap3.png in Resources */ = {isa = PBXBuildFile; fileRef = D05B38001FEA8E3D0041D2A5 /* Bitmap3.png */; }; + D05B380C1FEA8E3D0041D2A5 /* Bitmap1.png in Resources */ = {isa = PBXBuildFile; fileRef = D05B38011FEA8E3D0041D2A5 /* Bitmap1.png */; }; + D05B380D1FEA8E3D0041D2A5 /* Bitmap5.png in Resources */ = {isa = PBXBuildFile; fileRef = D05B38021FEA8E3D0041D2A5 /* Bitmap5.png */; }; + D05B380E1FEA8E3D0041D2A5 /* Bitmap7.png in Resources */ = {isa = PBXBuildFile; fileRef = D05B38031FEA8E3D0041D2A5 /* Bitmap7.png */; }; + D05B380F1FEA8E3D0041D2A5 /* Bitmap6.png in Resources */ = {isa = PBXBuildFile; fileRef = D05B38041FEA8E3D0041D2A5 /* Bitmap6.png */; }; + D05B38101FEA8E3D0041D2A5 /* Bitmap8.png in Resources */ = {isa = PBXBuildFile; fileRef = D05B38051FEA8E3D0041D2A5 /* Bitmap8.png */; }; + D05B38111FEA8E3D0041D2A5 /* Bitmap9.png in Resources */ = {isa = PBXBuildFile; fileRef = D05B38061FEA8E3D0041D2A5 /* Bitmap9.png */; }; + D05B38121FEA8E3D0041D2A5 /* Bitmap12.png in Resources */ = {isa = PBXBuildFile; fileRef = D05B38071FEA8E3D0041D2A5 /* Bitmap12.png */; }; + D05B38131FEA8E3D0041D2A5 /* Bitmap10.png in Resources */ = {isa = PBXBuildFile; fileRef = D05B38081FEA8E3D0041D2A5 /* Bitmap10.png */; }; + D05B38141FEA8E3D0041D2A5 /* Bitmap11.png in Resources */ = {isa = PBXBuildFile; fileRef = D05B38091FEA8E3D0041D2A5 /* Bitmap11.png */; }; + D05F63721EC124D90004BE28 /* TelegramUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0400ED81D5B8F97007931CE /* TelegramUI.framework */; }; + D0612E491D58B478000C8F02 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0612E481D58B478000C8F02 /* Application.swift */; }; + D06706611D51185400DED3E3 /* TelegramCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D06706601D51185400DED3E3 /* TelegramCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + D06706621D5118F500DED3E3 /* TelegramCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D06706601D51185400DED3E3 /* TelegramCore.framework */; }; + D06E4C2F21347D9200088087 /* UIImage+ImageEffects.m in Sources */ = {isa = PBXBuildFile; fileRef = D06E4C2E21347D9200088087 /* UIImage+ImageEffects.m */; }; + D084023220E1883500065674 /* ApplicationShortcutItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D084023120E1883500065674 /* ApplicationShortcutItem.swift */; }; + D08410451FABDC5D008FFE92 /* TGItemProviderSignals.m in Sources */ = {isa = PBXBuildFile; fileRef = D08410441FABDC5C008FFE92 /* TGItemProviderSignals.m */; }; + D08410481FABDCF2008FFE92 /* LegacyComponents.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D08410491FABDCF2008FFE92 /* LegacyComponents.framework */; }; + D084104E1FABDCFD008FFE92 /* TGContactModel.m in Sources */ = {isa = PBXBuildFile; fileRef = D084104C1FABDCFD008FFE92 /* TGContactModel.m */; }; + D084104F1FABDCFD008FFE92 /* TGMimeTypeMap.m in Sources */ = {isa = PBXBuildFile; fileRef = D084104D1FABDCFD008FFE92 /* TGMimeTypeMap.m */; }; + D08410501FABDD54008FFE92 /* MtProtoKitDynamic.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D08410511FABDD54008FFE92 /* MtProtoKitDynamic.framework */; }; + D08410541FABE428008FFE92 /* ShareItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08410531FABE428008FFE92 /* ShareItems.swift */; }; + D08611B21F5711080047111E /* HockeySDK.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D01A47541F4DBED700383CC1 /* HockeySDK.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + D08984FE2118B3F100918162 /* MtProtoKitDynamic.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D08984FD2118B3F100918162 /* MtProtoKitDynamic.framework */; }; + D08985002118B3F100918162 /* Postbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D08984FF2118B3F100918162 /* Postbox.framework */; }; + D08985022118B3F100918162 /* TelegramCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D08985012118B3F100918162 /* TelegramCore.framework */; }; + D08985042118B46F00918162 /* SwiftSignalKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D08985032118B46F00918162 /* SwiftSignalKit.framework */; }; + D08985072119B7FE00918162 /* IntentContacts.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08985062119B7FE00918162 /* IntentContacts.swift */; }; + D08DB0A4213F42F400F2ADBF /* TodayViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E41A3B1D65A69C00FBFC00 /* TodayViewController.swift */; }; + D08DB0A7213F4D1D00F2ADBF /* fast_arrow@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D050F21C1E49DEDE00988324 /* fast_arrow@2x.png */; }; + D08DB0A8213F4D1D00F2ADBF /* fast_arrow_shadow@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D050F21D1E49DEDE00988324 /* fast_arrow_shadow@2x.png */; }; + D08DB0A9213F4D1D00F2ADBF /* fast_body@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D050F21E1E49DEDE00988324 /* fast_body@2x.png */; }; + D08DB0AA213F4D1D00F2ADBF /* fast_spiral@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D050F21F1E49DEDE00988324 /* fast_spiral@2x.png */; }; + D08DB0AB213F4D1D00F2ADBF /* ic_bubble@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D050F2201E49DEDE00988324 /* ic_bubble@2x.png */; }; + D08DB0AC213F4D1D00F2ADBF /* ic_bubble_dot@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D050F2211E49DEDE00988324 /* ic_bubble_dot@2x.png */; }; + D08DB0AD213F4D1D00F2ADBF /* ic_cam@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D050F2221E49DEDE00988324 /* ic_cam@2x.png */; }; + D08DB0AE213F4D1D00F2ADBF /* ic_cam_lens@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D050F2231E49DEDE00988324 /* ic_cam_lens@2x.png */; }; + D08DB0AF213F4D1D00F2ADBF /* ic_pencil@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D050F2241E49DEDE00988324 /* ic_pencil@2x.png */; }; + D08DB0B0213F4D1D00F2ADBF /* ic_pin@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D050F2251E49DEDE00988324 /* ic_pin@2x.png */; }; + D08DB0B1213F4D1D00F2ADBF /* ic_smile@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D050F2261E49DEDE00988324 /* ic_smile@2x.png */; }; + D08DB0B2213F4D1D00F2ADBF /* ic_smile_eye@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D050F2271E49DEDE00988324 /* ic_smile_eye@2x.png */; }; + D08DB0B3213F4D1D00F2ADBF /* ic_videocam@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D050F2281E49DEDE00988324 /* ic_videocam@2x.png */; }; + D08DB0B4213F4D1D00F2ADBF /* knot_down@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D050F2291E49DEDE00988324 /* knot_down@2x.png */; }; + D08DB0B5213F4D1D00F2ADBF /* knot_up1@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D050F22A1E49DEDE00988324 /* knot_up1@2x.png */; }; + D08DB0B6213F4D1D00F2ADBF /* powerful_infinity@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D050F22B1E49DEDE00988324 /* powerful_infinity@2x.png */; }; + D08DB0B7213F4D1D00F2ADBF /* powerful_infinity_white@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D050F22C1E49DEDE00988324 /* powerful_infinity_white@2x.png */; }; + D08DB0B8213F4D1D00F2ADBF /* powerful_mask@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D050F22D1E49DEDE00988324 /* powerful_mask@2x.png */; }; + D08DB0B9213F4D1D00F2ADBF /* powerful_star@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D050F22E1E49DEDE00988324 /* powerful_star@2x.png */; }; + D08DB0BA213F4D1D00F2ADBF /* private_door@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D050F22F1E49DEDE00988324 /* private_door@2x.png */; }; + D08DB0BB213F4D1D00F2ADBF /* private_screw@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D050F2301E49DEDE00988324 /* private_screw@2x.png */; }; + D08DB0BC213F4D1D00F2ADBF /* start_arrow@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D050F2311E49DEDE00988324 /* start_arrow@2x.png */; }; + D08DB0BD213F4D1D00F2ADBF /* start_arrow_ipad.png in Resources */ = {isa = PBXBuildFile; fileRef = D050F2321E49DEDE00988324 /* start_arrow_ipad.png */; }; + D08DB0BE213F4D1D00F2ADBF /* start_arrow_ipad@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D050F2331E49DEDE00988324 /* start_arrow_ipad@2x.png */; }; + D08DB0BF213F4D1D00F2ADBF /* telegram_plane1@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D050F2341E49DEDE00988324 /* telegram_plane1@2x.png */; }; + D08DB0C0213F4D1D00F2ADBF /* telegram_sphere@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D050F2351E49DEDE00988324 /* telegram_sphere@2x.png */; }; + D08F7858213F3E5600225975 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D0E41A3D1D65A69C00FBFC00 /* MainInterface.storyboard */; }; + D09250021FE52D2A003F693F /* BuildConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = D09250011FE52D2A003F693F /* BuildConfig.m */; }; + D096C2BE1CC3C021006D814E /* Display.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D096C2BD1CC3C021006D814E /* Display.framework */; }; + D096C2BF1CC3C021006D814E /* Display.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D096C2BD1CC3C021006D814E /* Display.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + D096C2C21CC3C104006D814E /* Postbox.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D096C2C01CC3C104006D814E /* Postbox.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + D096C2C51CC3C11A006D814E /* SwiftSignalKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D096C2C31CC3C11A006D814E /* SwiftSignalKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + D09A59601B5858DB00FC3724 /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D09A595F1B5858DB00FC3724 /* SystemConfiguration.framework */; }; + D09DCBB71D0C856B00F51FFE /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = D09DCBB51D0C856B00F51FFE /* Localizable.strings */; }; + D0A18D631E149043004C6734 /* PushKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0A18D621E149043004C6734 /* PushKit.framework */; }; + D0A18D651E15C020004C6734 /* WakeupManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A18D641E15C020004C6734 /* WakeupManager.swift */; }; + D0A18D691E16AC9D004C6734 /* NotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A18D681E16AC9D004C6734 /* NotificationManager.swift */; }; + D0ADF958212B56DC00310BBC /* LegacyUserDataImport.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ADF957212B56DC00310BBC /* LegacyUserDataImport.swift */; }; + D0ADF95A212B5AC600310BBC /* LegacyResourceImport.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ADF959212B5AC600310BBC /* LegacyResourceImport.swift */; }; + D0ADF95C212B636D00310BBC /* LegacyChatImport.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ADF95B212B636D00310BBC /* LegacyChatImport.swift */; }; + D0ADF95E212C818F00310BBC /* LegacyPreferencesImport.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ADF95D212C818F00310BBC /* LegacyPreferencesImport.swift */; }; + D0ADF961212C8DF600310BBC /* TGAutoDownloadPreferences.m in Sources */ = {isa = PBXBuildFile; fileRef = D0ADF95F212C8DF600310BBC /* TGAutoDownloadPreferences.m */; }; + D0ADF964212C9AA900310BBC /* TGProxyItem.m in Sources */ = {isa = PBXBuildFile; fileRef = D0ADF963212C9AA900310BBC /* TGProxyItem.m */; }; + D0AF32291FACA1920097362B /* Accelerate.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0D17E891CAAD66600C4750B /* Accelerate.framework */; }; + D0AF322C1FACA1B00097362B /* libc++.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = D0B8445F1DACF561005F29E1 /* libc++.tbd */; }; + D0AF322F1FACBA280097362B /* TGShareLocationSignals.m in Sources */ = {isa = PBXBuildFile; fileRef = D0AF322D1FACBA270097362B /* TGShareLocationSignals.m */; }; + D0B2F738204F4C9900D3BFB9 /* NotificationCenter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0E41A381D65A69C00FBFC00 /* NotificationCenter.framework */; }; + D0B2F742204F4C9900D3BFB9 /* Widget.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = D0B2F737204F4C9900D3BFB9 /* Widget.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + D0B2F74A204F4D6100D3BFB9 /* Postbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0B2F74F204F4D6100D3BFB9 /* Postbox.framework */; }; + D0B2F74B204F4D6100D3BFB9 /* SwiftSignalKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0B2F750204F4D6100D3BFB9 /* SwiftSignalKit.framework */; }; + D0B2F74C204F4D6100D3BFB9 /* TelegramCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0B2F751204F4D6100D3BFB9 /* TelegramCore.framework */; }; + D0B2F755204F4EAF00D3BFB9 /* BuildConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = D09250011FE52D2A003F693F /* BuildConfig.m */; }; + D0B2F7602050102600D3BFB9 /* PeerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B2F75F2050102600D3BFB9 /* PeerNode.swift */; }; + D0B3B53B21666C0000FC60A0 /* LegacyFileImport.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B3B53A21666C0000FC60A0 /* LegacyFileImport.swift */; }; + D0B4AF8F1EC122A700D51FF6 /* TelegramUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0400ED81D5B8F97007931CE /* TelegramUI.framework */; }; + D0B4AF901EC122A700D51FF6 /* TelegramUI.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D0400ED81D5B8F97007931CE /* TelegramUI.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + D0B844601DACF561005F29E1 /* libc++.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = D0B8445F1DACF561005F29E1 /* libc++.tbd */; }; + D0BEAF731E54C9A900BD963D /* ApplicationContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEAF721E54C9A900BD963D /* ApplicationContext.swift */; }; + D0C2DFF81CC4D1BA0044FF83 /* MobileCoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0C2DFF71CC4D1BA0044FF83 /* MobileCoreServices.framework */; }; + D0CAF3191D763B230011F558 /* MtProtoKitDynamic.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D0CAF2F21D75FFAB0011F558 /* MtProtoKitDynamic.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + D0CD17B51CC3AE14007C5650 /* AsyncDisplayKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0CD17B41CC3AE14007C5650 /* AsyncDisplayKit.framework */; }; + D0CD17B61CC3AE14007C5650 /* AsyncDisplayKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D0CD17B41CC3AE14007C5650 /* AsyncDisplayKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + D0CE6F1C213ED11100BCD44B /* TGPresentationAutoNightPreferences.m in Sources */ = {isa = PBXBuildFile; fileRef = D0CE6F1B213ED11100BCD44B /* TGPresentationAutoNightPreferences.m */; }; + D0CE6F55213EDA4400BCD44B /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = D0CE6F1E213EDA4200BCD44B /* Localizable.strings */; }; + D0CE6F56213EDA4400BCD44B /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = D0CE6F20213EDA4200BCD44B /* InfoPlist.strings */; }; + D0CE6F57213EDA4400BCD44B /* AppIntentVocabulary.plist in Resources */ = {isa = PBXBuildFile; fileRef = D0CE6F22213EDA4200BCD44B /* AppIntentVocabulary.plist */; }; + D0CE6F58213EDA4400BCD44B /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = D0CE6F25213EDA4300BCD44B /* Localizable.strings */; }; + D0CE6F59213EDA4400BCD44B /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = D0CE6F27213EDA4300BCD44B /* InfoPlist.strings */; }; + D0CE6F5A213EDA4400BCD44B /* AppIntentVocabulary.plist in Resources */ = {isa = PBXBuildFile; fileRef = D0CE6F29213EDA4300BCD44B /* AppIntentVocabulary.plist */; }; + D0CE6F5B213EDA4400BCD44B /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = D0CE6F2C213EDA4300BCD44B /* Localizable.strings */; }; + D0CE6F5C213EDA4400BCD44B /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = D0CE6F2E213EDA4300BCD44B /* InfoPlist.strings */; }; + D0CE6F5D213EDA4400BCD44B /* AppIntentVocabulary.plist in Resources */ = {isa = PBXBuildFile; fileRef = D0CE6F30213EDA4300BCD44B /* AppIntentVocabulary.plist */; }; + D0CE6F5E213EDA4400BCD44B /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = D0CE6F33213EDA4300BCD44B /* Localizable.strings */; }; + D0CE6F5F213EDA4400BCD44B /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = D0CE6F35213EDA4300BCD44B /* InfoPlist.strings */; }; + D0CE6F60213EDA4400BCD44B /* AppIntentVocabulary.plist in Resources */ = {isa = PBXBuildFile; fileRef = D0CE6F37213EDA4300BCD44B /* AppIntentVocabulary.plist */; }; + D0CE6F61213EDA4400BCD44B /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = D0CE6F3A213EDA4300BCD44B /* Localizable.strings */; }; + D0CE6F62213EDA4400BCD44B /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = D0CE6F3C213EDA4300BCD44B /* InfoPlist.strings */; }; + D0CE6F63213EDA4400BCD44B /* AppIntentVocabulary.plist in Resources */ = {isa = PBXBuildFile; fileRef = D0CE6F3E213EDA4300BCD44B /* AppIntentVocabulary.plist */; }; + D0CE6F64213EDA4400BCD44B /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = D0CE6F41213EDA4400BCD44B /* Localizable.strings */; }; + D0CE6F65213EDA4400BCD44B /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = D0CE6F43213EDA4400BCD44B /* InfoPlist.strings */; }; + D0CE6F66213EDA4400BCD44B /* AppIntentVocabulary.plist in Resources */ = {isa = PBXBuildFile; fileRef = D0CE6F45213EDA4400BCD44B /* AppIntentVocabulary.plist */; }; + D0CE6F67213EDA4400BCD44B /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = D0CE6F48213EDA4400BCD44B /* Localizable.strings */; }; + D0CE6F68213EDA4400BCD44B /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = D0CE6F4A213EDA4400BCD44B /* InfoPlist.strings */; }; + D0CE6F69213EDA4400BCD44B /* AppIntentVocabulary.plist in Resources */ = {isa = PBXBuildFile; fileRef = D0CE6F4C213EDA4400BCD44B /* AppIntentVocabulary.plist */; }; + D0CE6F6A213EDA4400BCD44B /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = D0CE6F4F213EDA4400BCD44B /* Localizable.strings */; }; + D0CE6F6B213EDA4400BCD44B /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = D0CE6F51213EDA4400BCD44B /* InfoPlist.strings */; }; + D0CE6F6C213EDA4400BCD44B /* AppIntentVocabulary.plist in Resources */ = {isa = PBXBuildFile; fileRef = D0CE6F53213EDA4400BCD44B /* AppIntentVocabulary.plist */; }; + D0CFBB931FD88C2900B65C0D /* begin_record.caf in Resources */ = {isa = PBXBuildFile; fileRef = D0CFBB921FD88C2900B65C0D /* begin_record.caf */; }; + D0D17E8A1CAAD66600C4750B /* Accelerate.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0D17E891CAAD66600C4750B /* Accelerate.framework */; }; + D0D2276F212739120028F943 /* LegacyDataImport.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D2276E212739120028F943 /* LegacyDataImport.swift */; }; + D0D268791D79A70A00C422DA /* IntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D268781D79A70A00C422DA /* IntentHandler.swift */; }; + D0D2688E1D79A70B00C422DA /* SiriIntents.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = D0D268761D79A70A00C422DA /* SiriIntents.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + D0E8B8AD2044496C00605593 /* voip_connecting.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = D0E8B8A82044496B00605593 /* voip_connecting.mp3 */; }; + D0E8B8AE2044496C00605593 /* voip_end.caf in Resources */ = {isa = PBXBuildFile; fileRef = D0E8B8A92044496C00605593 /* voip_end.caf */; }; + D0E8B8AF2044496C00605593 /* voip_fail.caf in Resources */ = {isa = PBXBuildFile; fileRef = D0E8B8AA2044496C00605593 /* voip_fail.caf */; }; + D0E8B8B02044496C00605593 /* voip_ringback.caf in Resources */ = {isa = PBXBuildFile; fileRef = D0E8B8AB2044496C00605593 /* voip_ringback.caf */; }; + D0E8B8B12044496C00605593 /* voip_busy.caf in Resources */ = {isa = PBXBuildFile; fileRef = D0E8B8AC2044496C00605593 /* voip_busy.caf */; }; + D0EA97941FE84F2D00792DD6 /* BuildConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = D09250011FE52D2A003F693F /* BuildConfig.m */; }; + D0EA97951FE84F2E00792DD6 /* BuildConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = D09250011FE52D2A003F693F /* BuildConfig.m */; }; + D0EB243B201B77C400F6CC13 /* ClearNotificationsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0EB243A201B77C400F6CC13 /* ClearNotificationsManager.swift */; }; + D0ECCB7F1FE9C38500609802 /* Telegram_iOS_UITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ECCB7E1FE9C38500609802 /* Telegram_iOS_UITests.swift */; }; + D0ECCB8A1FE9C4AC00609802 /* SnapshotHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ECCB891FE9C4AC00609802 /* SnapshotHelper.swift */; }; + D0ECCB8D1FE9CE3F00609802 /* SnapshotChatList.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ECCB8C1FE9CE3F00609802 /* SnapshotChatList.swift */; }; + D0F575132083B96B00F1C1E1 /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0F575122083B96B00F1C1E1 /* CloudKit.framework */; }; + D0FC1948201D2DA800FEDBB2 /* SFCompactRounded-Semibold.otf in Resources */ = {isa = PBXBuildFile; fileRef = D0FC1947201D2DA700FEDBB2 /* SFCompactRounded-Semibold.otf */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 09C56F992172797500BDF00F /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D00859941B28189D00EAF753 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 09C56F962172797400BDF00F; + remoteInfo = "Watch Extension"; + }; + 09C56FA32172797500BDF00F /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D00859941B28189D00EAF753 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 09C56F8A2172797200BDF00F; + remoteInfo = Watch; + }; + D00859B21B28189D00EAF753 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D00859941B28189D00EAF753 /* Project object */; + proxyType = 1; + remoteGlobalIDString = D008599B1B28189D00EAF753; + remoteInfo = "Telegram-iOS"; + }; + D02CF606215D9ABF00E0F56A /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D00859941B28189D00EAF753 /* Project object */; + proxyType = 1; + remoteGlobalIDString = D02CF5FB215D9ABE00E0F56A; + remoteInfo = NotificationContent; + }; + D03B0E801D63484500955575 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D00859941B28189D00EAF753 /* Project object */; + proxyType = 1; + remoteGlobalIDString = D03B0E771D63484500955575; + remoteInfo = Share; + }; + D0B2F740204F4C9900D3BFB9 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D00859941B28189D00EAF753 /* Project object */; + proxyType = 1; + remoteGlobalIDString = D0B2F736204F4C9900D3BFB9; + remoteInfo = Widget; + }; + D0D2688C1D79A70B00C422DA /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D00859941B28189D00EAF753 /* Project object */; + proxyType = 1; + remoteGlobalIDString = D0D268751D79A70A00C422DA; + remoteInfo = SiriIntents; + }; + D0ECCB811FE9C38500609802 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D00859941B28189D00EAF753 /* Project object */; + proxyType = 1; + remoteGlobalIDString = D008599B1B28189D00EAF753; + remoteInfo = "Telegram-iOS"; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 09C56FB52172797500BDF00F /* Embed App Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + 09C56F982172797500BDF00F /* Watch Extension.appex in Embed App Extensions */, + ); + name = "Embed App Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; + 09C56FB72172797500BDF00F /* Embed Watch Content */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = "$(CONTENTS_FOLDER_PATH)/Watch"; + dstSubfolderSpec = 16; + files = ( + 09C56FA52172797500BDF00F /* Watch.app in Embed Watch Content */, + ); + name = "Embed Watch Content"; + runOnlyForDeploymentPostprocessing = 0; + }; + D0289FA81CBFFC8700A12E82 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + D08611B21F5711080047111E /* HockeySDK.framework in Embed Frameworks */, + D06706611D51185400DED3E3 /* TelegramCore.framework in Embed Frameworks */, + D0CD17B61CC3AE14007C5650 /* AsyncDisplayKit.framework in Embed Frameworks */, + D096C2BF1CC3C021006D814E /* Display.framework in Embed Frameworks */, + D0B4AF901EC122A700D51FF6 /* TelegramUI.framework in Embed Frameworks */, + D096C2C21CC3C104006D814E /* Postbox.framework in Embed Frameworks */, + D096C2C51CC3C11A006D814E /* SwiftSignalKit.framework in Embed Frameworks */, + D0CAF3191D763B230011F558 /* MtProtoKitDynamic.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; + D0AA1A791D568BA500152314 /* Embed App Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + D03B0E821D63484500955575 /* Share.appex in Embed App Extensions */, + D0B2F742204F4C9900D3BFB9 /* Widget.appex in Embed App Extensions */, + D0D2688E1D79A70B00C422DA /* SiriIntents.appex in Embed App Extensions */, + D02CF608215D9ABF00E0F56A /* NotificationContent.appex in Embed App Extensions */, + ); + name = "Embed App Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 092F368421542D6C001A9F49 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = Share/en.lproj/Localizable.strings; sourceTree = SOURCE_ROOT; }; + 0956AF2B217B4642008106D0 /* WatchCommunicationManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WatchCommunicationManager.swift; sourceTree = ""; }; + 0956AF2D217B8109008106D0 /* TGNeoUnsupportedMessageViewModel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TGNeoUnsupportedMessageViewModel.h; sourceTree = ""; }; + 0956AF2E217B8109008106D0 /* TGNeoUnsupportedMessageViewModel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TGNeoUnsupportedMessageViewModel.m; sourceTree = ""; }; + 0972C6DF21791D950069E98A /* UserNotifications.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UserNotifications.framework; path = Platforms/WatchOS.platform/Developer/SDKs/WatchOS5.0.sdk/System/Library/Frameworks/UserNotifications.framework; sourceTree = DEVELOPER_DIR; }; + 0972C6E321792D120069E98A /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = InfoPlist.strings; sourceTree = ""; }; + 09C50E7921738178009E676F /* TGBridgeServer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGBridgeServer.h; sourceTree = ""; }; + 09C50E7A21738178009E676F /* TGBridgeServer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGBridgeServer.m; sourceTree = ""; }; + 09C50E852173854D009E676F /* WatchKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WatchKit.framework; path = System/Library/Frameworks/WatchKit.framework; sourceTree = SDKROOT; }; + 09C50E87217385CF009E676F /* WatchConnectivity.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WatchConnectivity.framework; path = System/Library/Frameworks/WatchConnectivity.framework; sourceTree = SDKROOT; }; + 09C50E892173AEDB009E676F /* WatchRequestHandlers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchRequestHandlers.swift; sourceTree = ""; }; + 09C50E8F2173B247009E676F /* TGBridgeSubscriptions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TGBridgeSubscriptions.m; path = Bridge/TGBridgeSubscriptions.m; sourceTree = ""; }; + 09C50E902173B247009E676F /* TGBridgeSubscriptions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TGBridgeSubscriptions.h; path = Bridge/TGBridgeSubscriptions.h; sourceTree = ""; }; + 09C56F8B2172797200BDF00F /* Watch.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Watch.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 09C56F8E2172797200BDF00F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Interface.storyboard; sourceTree = ""; }; + 09C56F902172797400BDF00F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 09C56F922172797400BDF00F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 09C56F972172797400BDF00F /* Watch Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Watch Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; + 09C56FA22172797500BDF00F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 09C5712021727BF800BDF00F /* TGInterfaceController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGInterfaceController.m; sourceTree = ""; }; + 09C5712121727BF800BDF00F /* TGExtensionDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGExtensionDelegate.h; sourceTree = ""; }; + 09C5712221727BF800BDF00F /* TGExtensionDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGExtensionDelegate.m; sourceTree = ""; }; + 09C5712321727BF800BDF00F /* TGInterfaceController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGInterfaceController.h; sourceTree = ""; }; + 09C5712821727CFB00BDF00F /* TGStringUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TGStringUtils.h; path = Watch/Extension/TGStringUtils.h; sourceTree = SOURCE_ROOT; }; + 09C5712921727CFB00BDF00F /* WKInterfaceImage+Signals.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "WKInterfaceImage+Signals.h"; path = "Watch/Extension/WKInterfaceImage+Signals.h"; sourceTree = SOURCE_ROOT; }; + 09C5712A21727CFC00BDF00F /* WKInterfaceTable+TGDataDrivenTable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "WKInterfaceTable+TGDataDrivenTable.h"; path = "Watch/Extension/WKInterfaceTable+TGDataDrivenTable.h"; sourceTree = SOURCE_ROOT; }; + 09C5712B21727CFC00BDF00F /* WKInterfaceImage+Signals.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "WKInterfaceImage+Signals.m"; path = "Watch/Extension/WKInterfaceImage+Signals.m"; sourceTree = SOURCE_ROOT; }; + 09C5712C21727CFC00BDF00F /* WKInterfaceGroup+Signals.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "WKInterfaceGroup+Signals.m"; path = "Watch/Extension/WKInterfaceGroup+Signals.m"; sourceTree = SOURCE_ROOT; }; + 09C5712D21727CFC00BDF00F /* TGGeometry.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TGGeometry.h; path = Watch/Extension/TGGeometry.h; sourceTree = SOURCE_ROOT; }; + 09C5712E21727CFC00BDF00F /* TGLocationUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TGLocationUtils.m; path = Watch/Extension/TGLocationUtils.m; sourceTree = SOURCE_ROOT; }; + 09C5712F21727CFC00BDF00F /* TGGeometry.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TGGeometry.m; path = Watch/Extension/TGGeometry.m; sourceTree = SOURCE_ROOT; }; + 09C5713021727CFC00BDF00F /* TGIndexPath.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TGIndexPath.h; path = Watch/Extension/TGIndexPath.h; sourceTree = SOURCE_ROOT; }; + 09C5713121727CFC00BDF00F /* WKInterfaceTable+TGDataDrivenTable.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "WKInterfaceTable+TGDataDrivenTable.m"; path = "Watch/Extension/WKInterfaceTable+TGDataDrivenTable.m"; sourceTree = SOURCE_ROOT; }; + 09C5713221727CFC00BDF00F /* TGWatchColor.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TGWatchColor.m; path = Watch/Extension/TGWatchColor.m; sourceTree = SOURCE_ROOT; }; + 09C5713321727CFC00BDF00F /* TGLocationUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TGLocationUtils.h; path = Watch/Extension/TGLocationUtils.h; sourceTree = SOURCE_ROOT; }; + 09C5713421727CFC00BDF00F /* TGWatchCommon.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TGWatchCommon.h; path = Watch/Extension/TGWatchCommon.h; sourceTree = SOURCE_ROOT; }; + 09C5713521727CFC00BDF00F /* TGWatchCommon.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TGWatchCommon.m; path = Watch/Extension/TGWatchCommon.m; sourceTree = SOURCE_ROOT; }; + 09C5713621727CFC00BDF00F /* TGDateUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TGDateUtils.h; path = Watch/Extension/TGDateUtils.h; sourceTree = SOURCE_ROOT; }; + 09C5713721727CFD00BDF00F /* TGWatchColor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TGWatchColor.h; path = Watch/Extension/TGWatchColor.h; sourceTree = SOURCE_ROOT; }; + 09C5713821727CFD00BDF00F /* WKInterface+TGInterface.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "WKInterface+TGInterface.h"; path = "Watch/Extension/WKInterface+TGInterface.h"; sourceTree = SOURCE_ROOT; }; + 09C5713921727CFD00BDF00F /* TGDateUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TGDateUtils.m; path = Watch/Extension/TGDateUtils.m; sourceTree = SOURCE_ROOT; }; + 09C5713A21727CFD00BDF00F /* TGStringUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TGStringUtils.m; path = Watch/Extension/TGStringUtils.m; sourceTree = SOURCE_ROOT; }; + 09C5713B21727CFD00BDF00F /* WKInterface+TGInterface.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "WKInterface+TGInterface.m"; path = "Watch/Extension/WKInterface+TGInterface.m"; sourceTree = SOURCE_ROOT; }; + 09C5713C21727CFD00BDF00F /* WKInterfaceGroup+Signals.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "WKInterfaceGroup+Signals.h"; path = "Watch/Extension/WKInterfaceGroup+Signals.h"; sourceTree = SOURCE_ROOT; }; + 09C5713D21727CFD00BDF00F /* TGIndexPath.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TGIndexPath.m; path = Watch/Extension/TGIndexPath.m; sourceTree = SOURCE_ROOT; }; + 09C5714B21727DD900BDF00F /* MediaAudio@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "MediaAudio@2x.png"; sourceTree = ""; }; + 09C5714C21727DD900BDF00F /* MediaPhoto@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "MediaPhoto@2x.png"; sourceTree = ""; }; + 09C5714D21727DD900BDF00F /* Location@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Location@2x.png"; sourceTree = ""; }; + 09C5714E21727DD900BDF00F /* File@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "File@2x.png"; sourceTree = ""; }; + 09C5714F21727DD900BDF00F /* MediaDocument@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "MediaDocument@2x.png"; sourceTree = ""; }; + 09C5715021727DD900BDF00F /* MediaLocation@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "MediaLocation@2x.png"; sourceTree = ""; }; + 09C5715121727DD900BDF00F /* MediaVideo@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "MediaVideo@2x.png"; sourceTree = ""; }; + 09C5715221727DD900BDF00F /* VerifiedList@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "VerifiedList@2x.png"; sourceTree = ""; }; + 09C5715C21727EE600BDF00F /* TGBridgeUserCache.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGBridgeUserCache.h; sourceTree = ""; }; + 09C5715D21727EE700BDF00F /* TGFileCache.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGFileCache.h; sourceTree = ""; }; + 09C5715E21727EE700BDF00F /* TGBridgeUserCache.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGBridgeUserCache.m; sourceTree = ""; }; + 09C5715F21727EE700BDF00F /* TGFileCache.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGFileCache.m; sourceTree = ""; }; + 09C5716221727F1500BDF00F /* TGTableDeltaUpdater.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGTableDeltaUpdater.m; sourceTree = ""; }; + 09C5716321727F1500BDF00F /* TGInputController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGInputController.m; sourceTree = ""; }; + 09C5716421727F1500BDF00F /* TGInputController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGInputController.h; sourceTree = ""; }; + 09C5716521727F1500BDF00F /* TGInterfaceMenu.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGInterfaceMenu.h; sourceTree = ""; }; + 09C5716621727F1500BDF00F /* TGInterfaceMenu.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGInterfaceMenu.m; sourceTree = ""; }; + 09C5716721727F1500BDF00F /* TGTableDeltaUpdater.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGTableDeltaUpdater.h; sourceTree = ""; }; + 09C5717721727FE900BDF00F /* TGAudioMicAlertController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGAudioMicAlertController.h; sourceTree = ""; }; + 09C5717821727FE900BDF00F /* TGAudioMicAlertController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGAudioMicAlertController.m; sourceTree = ""; }; + 09C571792172800800BDF00F /* TGNotificationController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGNotificationController.h; sourceTree = ""; }; + 09C5717A2172800800BDF00F /* TGComplicationController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGComplicationController.h; sourceTree = ""; }; + 09C5717B2172800800BDF00F /* TGNotificationController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGNotificationController.m; sourceTree = ""; }; + 09C5717C2172800800BDF00F /* TGComplicationController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGComplicationController.m; sourceTree = ""; }; + 09C5717D2172802200BDF00F /* TGUserRowController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGUserRowController.m; sourceTree = ""; }; + 09C5717E2172802200BDF00F /* TGUserRowController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGUserRowController.h; sourceTree = ""; }; + 09C571802172805700BDF00F /* TGNeoLabelViewModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGNeoLabelViewModel.h; sourceTree = ""; }; + 09C571812172805700BDF00F /* TGNeoRenderableViewModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGNeoRenderableViewModel.h; sourceTree = ""; }; + 09C571822172805700BDF00F /* TGNeoAttachmentViewModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGNeoAttachmentViewModel.m; sourceTree = ""; }; + 09C571832172805700BDF00F /* TGNeoLabelViewModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGNeoLabelViewModel.m; sourceTree = ""; }; + 09C571842172805700BDF00F /* TGMessageViewModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGMessageViewModel.h; sourceTree = ""; }; + 09C571852172805700BDF00F /* TGAvatarViewModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGAvatarViewModel.h; sourceTree = ""; }; + 09C571862172805700BDF00F /* TGMessageViewModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGMessageViewModel.m; sourceTree = ""; }; + 09C571872172805700BDF00F /* TGNeoViewModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGNeoViewModel.m; sourceTree = ""; }; + 09C571882172805700BDF00F /* TGAvatarViewModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGAvatarViewModel.m; sourceTree = ""; }; + 09C571892172805700BDF00F /* TGNeoRenderableViewModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGNeoRenderableViewModel.m; sourceTree = ""; }; + 09C5718A2172805700BDF00F /* TGNeoAttachmentViewModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGNeoAttachmentViewModel.h; sourceTree = ""; }; + 09C5718B2172805800BDF00F /* TGNeoViewModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGNeoViewModel.h; sourceTree = ""; }; + 09C5718C2172805800BDF00F /* TGNeoImageViewModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGNeoImageViewModel.h; sourceTree = ""; }; + 09C5718D2172805800BDF00F /* TGNeoImageViewModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGNeoImageViewModel.m; sourceTree = ""; }; + 09C57199217280E500BDF00F /* TGNeoChatsController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGNeoChatsController.h; sourceTree = ""; }; + 09C5719A217280E500BDF00F /* TGNeoChatsController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGNeoChatsController.m; sourceTree = ""; }; + 09C5719E217285CE00BDF00F /* TGNeoChatViewModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGNeoChatViewModel.h; sourceTree = ""; }; + 09C5719F217285CE00BDF00F /* TGNeoChatViewModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGNeoChatViewModel.m; sourceTree = ""; }; + 09C571A0217285CE00BDF00F /* TGNeoChatRowController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGNeoChatRowController.m; sourceTree = ""; }; + 09C571A1217285CE00BDF00F /* TGNeoChatRowController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGNeoChatRowController.h; sourceTree = ""; }; + 09C571A22172860000BDF00F /* TGNeoConversationRowController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGNeoConversationRowController.h; sourceTree = ""; }; + 09C571A32172860000BDF00F /* TGNeoConversationRowController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGNeoConversationRowController.m; sourceTree = ""; }; + 09C571A42172861600BDF00F /* TGNeoConversationController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGNeoConversationController.m; sourceTree = ""; }; + 09C571A52172861600BDF00F /* TGNeoConversationController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGNeoConversationController.h; sourceTree = ""; }; + 09C571A62172863D00BDF00F /* TGComposeController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGComposeController.m; sourceTree = ""; }; + 09C571A72172863D00BDF00F /* TGComposeController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGComposeController.h; sourceTree = ""; }; + 09C571A82172865300BDF00F /* TGContactsController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGContactsController.h; sourceTree = ""; }; + 09C571A92172865400BDF00F /* TGContactsController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGContactsController.m; sourceTree = ""; }; + 09C571AB2172867400BDF00F /* TGLocationController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGLocationController.h; sourceTree = ""; }; + 09C571AC2172867400BDF00F /* TGLocationController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGLocationController.m; sourceTree = ""; }; + 09C571AD2172869500BDF00F /* TGLocationVenueRowController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGLocationVenueRowController.m; sourceTree = ""; }; + 09C571AE2172869500BDF00F /* TGLocationMapHeaderController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGLocationMapHeaderController.m; sourceTree = ""; }; + 09C571AF2172869600BDF00F /* TGLocationMapHeaderController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGLocationMapHeaderController.h; sourceTree = ""; }; + 09C571B02172869600BDF00F /* TGLocationVenueRowController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGLocationVenueRowController.h; sourceTree = ""; }; + 09C571B3217286BA00BDF00F /* TGMessageViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGMessageViewController.m; sourceTree = ""; }; + 09C571B4217286BA00BDF00F /* TGMessageViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGMessageViewController.h; sourceTree = ""; }; + 09C571B5217286D700BDF00F /* TGMessageViewMessageRowController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGMessageViewMessageRowController.m; sourceTree = ""; }; + 09C571B6217286D700BDF00F /* TGMessageViewWebPageRowController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGMessageViewWebPageRowController.m; sourceTree = ""; }; + 09C571B7217286D700BDF00F /* TGMessageViewFooterController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGMessageViewFooterController.h; sourceTree = ""; }; + 09C571B8217286D700BDF00F /* TGMessageViewWebPageRowController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGMessageViewWebPageRowController.h; sourceTree = ""; }; + 09C571B9217286D700BDF00F /* TGMessageViewFooterController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGMessageViewFooterController.m; sourceTree = ""; }; + 09C571BA217286D700BDF00F /* TGMessageViewMessageRowController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGMessageViewMessageRowController.h; sourceTree = ""; }; + 09C571BD2172872B00BDF00F /* TGGroupInfoController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGGroupInfoController.m; sourceTree = ""; }; + 09C571BE2172872B00BDF00F /* TGGroupInfoController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGGroupInfoController.h; sourceTree = ""; }; + 09C571BF2172872C00BDF00F /* TGProfilePhotoController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGProfilePhotoController.h; sourceTree = ""; }; + 09C571C02172872C00BDF00F /* TGUserInfoController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGUserInfoController.m; sourceTree = ""; }; + 09C571C12172872C00BDF00F /* TGProfilePhotoController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGProfilePhotoController.m; sourceTree = ""; }; + 09C571C22172872C00BDF00F /* TGUserInfoController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGUserInfoController.h; sourceTree = ""; }; + 09C571C32172874500BDF00F /* TGUserHandleRowController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGUserHandleRowController.h; sourceTree = ""; }; + 09C571C42172874500BDF00F /* TGGroupInfoHeaderController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGGroupInfoHeaderController.m; sourceTree = ""; }; + 09C571C52172874500BDF00F /* TGGroupInfoHeaderController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGGroupInfoHeaderController.h; sourceTree = ""; }; + 09C571C62172874500BDF00F /* TGUserHandle.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGUserHandle.h; sourceTree = ""; }; + 09C571C72172874500BDF00F /* TGGroupInfoFooterController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGGroupInfoFooterController.m; sourceTree = ""; }; + 09C571C82172874500BDF00F /* TGUserInfoHeaderController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGUserInfoHeaderController.h; sourceTree = ""; }; + 09C571C92172874500BDF00F /* TGUserHandle.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGUserHandle.m; sourceTree = ""; }; + 09C571CA2172874500BDF00F /* TGUserHandleRowController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGUserHandleRowController.m; sourceTree = ""; }; + 09C571CB2172874500BDF00F /* TGGroupInfoFooterController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGGroupInfoFooterController.h; sourceTree = ""; }; + 09C571CC2172874600BDF00F /* TGUserInfoHeaderController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGUserInfoHeaderController.m; sourceTree = ""; }; + 09C571DF2172878800BDF00F /* TGStickersHeaderController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGStickersHeaderController.m; sourceTree = ""; }; + 09C571E02172878800BDF00F /* TGStickersRowController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGStickersRowController.h; sourceTree = ""; }; + 09C571E12172878800BDF00F /* TGStickerPacksController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGStickerPacksController.h; sourceTree = ""; }; + 09C571E22172878800BDF00F /* TGStickerPackRowController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGStickerPackRowController.m; sourceTree = ""; }; + 09C571E32172878900BDF00F /* TGStickerPacksController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGStickerPacksController.m; sourceTree = ""; }; + 09C571E42172878900BDF00F /* TGStickersRowController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGStickersRowController.m; sourceTree = ""; }; + 09C571E52172878900BDF00F /* TGStickersSectionHeaderController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGStickersSectionHeaderController.m; sourceTree = ""; }; + 09C571E62172878900BDF00F /* TGStickersController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGStickersController.m; sourceTree = ""; }; + 09C571E72172878900BDF00F /* TGStickerPackRowController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGStickerPackRowController.h; sourceTree = ""; }; + 09C571E82172878900BDF00F /* TGStickersController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGStickersController.h; sourceTree = ""; }; + 09C571E92172878900BDF00F /* TGStickersSectionHeaderController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGStickersSectionHeaderController.h; sourceTree = ""; }; + 09C571EA2172878900BDF00F /* TGStickersHeaderController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGStickersHeaderController.h; sourceTree = ""; }; + 09C571F3217287E500BDF00F /* TGBotKeyboardButtonController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGBotKeyboardButtonController.h; sourceTree = ""; }; + 09C571F4217287E500BDF00F /* TGBotKeyboardButtonController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGBotKeyboardButtonController.m; sourceTree = ""; }; + 09C571F5217287EF00BDF00F /* TGBotCommandController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGBotCommandController.h; sourceTree = ""; }; + 09C571F6217287EF00BDF00F /* TGBotCommandController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGBotCommandController.m; sourceTree = ""; }; + 09C571F7217287F000BDF00F /* TGBotKeyboardController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGBotKeyboardController.h; sourceTree = ""; }; + 09C571F8217287F000BDF00F /* TGBotKeyboardController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGBotKeyboardController.m; sourceTree = ""; }; + 09C571F92172880900BDF00F /* TGChatInfo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGChatInfo.h; sourceTree = ""; }; + 09C571FA2172880900BDF00F /* TGChatInfo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGChatInfo.m; sourceTree = ""; }; + 09C571FB2172882D00BDF00F /* TGConversationFooterController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGConversationFooterController.h; sourceTree = ""; }; + 09C571FC2172882D00BDF00F /* TGConversationFooterController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGConversationFooterController.m; sourceTree = ""; }; + 09C571FD2172883E00BDF00F /* TGChatTimestamp.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGChatTimestamp.h; sourceTree = ""; }; + 09C571FE2172883E00BDF00F /* TGChatTimestamp.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGChatTimestamp.m; sourceTree = ""; }; + 09C571FF2172888E00BDF00F /* TGNeoBackgroundViewModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGNeoBackgroundViewModel.h; sourceTree = ""; }; + 09C572002172888E00BDF00F /* TGNeoConversationStaticRowController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGNeoConversationStaticRowController.m; sourceTree = ""; }; + 09C572012172888E00BDF00F /* TGNeoFileMessageViewModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGNeoFileMessageViewModel.m; sourceTree = ""; }; + 09C572022172888F00BDF00F /* TGNeoStickerMessageViewModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGNeoStickerMessageViewModel.h; sourceTree = ""; }; + 09C572032172888F00BDF00F /* TGNeoForwardHeaderViewModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGNeoForwardHeaderViewModel.h; sourceTree = ""; }; + 09C572042172888F00BDF00F /* TGNeoConversationSimpleRowController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGNeoConversationSimpleRowController.m; sourceTree = ""; }; + 09C572052172888F00BDF00F /* TGNeoMessageViewModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGNeoMessageViewModel.h; sourceTree = ""; }; + 09C572062172888F00BDF00F /* TGNeoStickerMessageViewModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGNeoStickerMessageViewModel.m; sourceTree = ""; }; + 09C572072172888F00BDF00F /* TGNeoBubbleMessageViewModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGNeoBubbleMessageViewModel.m; sourceTree = ""; }; + 09C572082172888F00BDF00F /* TGNeoVenueMessageViewModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGNeoVenueMessageViewModel.m; sourceTree = ""; }; + 09C572092172888F00BDF00F /* TGNeoConversationStaticRowController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGNeoConversationStaticRowController.h; sourceTree = ""; }; + 09C5720A2172888F00BDF00F /* TGNeoRowController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGNeoRowController.m; sourceTree = ""; }; + 09C5720B2172889000BDF00F /* TGNeoMediaMessageViewModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGNeoMediaMessageViewModel.h; sourceTree = ""; }; + 09C5720C2172889000BDF00F /* TGNeoServiceMessageViewModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGNeoServiceMessageViewModel.m; sourceTree = ""; }; + 09C5720D2172889000BDF00F /* TGNeoContactMessageViewModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGNeoContactMessageViewModel.m; sourceTree = ""; }; + 09C5720E2172889000BDF00F /* TGNeoReplyHeaderViewModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGNeoReplyHeaderViewModel.m; sourceTree = ""; }; + 09C5720F2172889000BDF00F /* TGNeoMediaMessageViewModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGNeoMediaMessageViewModel.m; sourceTree = ""; }; + 09C572102172889000BDF00F /* TGNeoConversationTimeRowController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGNeoConversationTimeRowController.m; sourceTree = ""; }; + 09C572112172889000BDF00F /* TGNeoFileMessageViewModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGNeoFileMessageViewModel.h; sourceTree = ""; }; + 09C572122172889000BDF00F /* TGNeoBubbleMessageViewModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGNeoBubbleMessageViewModel.h; sourceTree = ""; }; + 09C572132172889000BDF00F /* TGNeoSmiliesMessageViewModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGNeoSmiliesMessageViewModel.m; sourceTree = ""; }; + 09C572142172889100BDF00F /* TGNeoConversationTimeRowController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGNeoConversationTimeRowController.h; sourceTree = ""; }; + 09C572152172889100BDF00F /* TGNeoTextMessageViewModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGNeoTextMessageViewModel.m; sourceTree = ""; }; + 09C572162172889100BDF00F /* TGNeoConversationMediaRowController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGNeoConversationMediaRowController.m; sourceTree = ""; }; + 09C572172172889100BDF00F /* TGNeoMessageViewModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGNeoMessageViewModel.m; sourceTree = ""; }; + 09C572182172889100BDF00F /* TGNeoReplyHeaderViewModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGNeoReplyHeaderViewModel.h; sourceTree = ""; }; + 09C572192172889100BDF00F /* TGNeoServiceMessageViewModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGNeoServiceMessageViewModel.h; sourceTree = ""; }; + 09C5721A2172889100BDF00F /* TGNeoConversationSimpleRowController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGNeoConversationSimpleRowController.h; sourceTree = ""; }; + 09C5721B2172889100BDF00F /* TGNeoForwardHeaderViewModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGNeoForwardHeaderViewModel.m; sourceTree = ""; }; + 09C5721C2172889100BDF00F /* TGNeoConversationMediaRowController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGNeoConversationMediaRowController.h; sourceTree = ""; }; + 09C5721D2172889100BDF00F /* TGNeoContactMessageViewModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGNeoContactMessageViewModel.h; sourceTree = ""; }; + 09C5721E2172889200BDF00F /* TGNeoVenueMessageViewModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGNeoVenueMessageViewModel.h; sourceTree = ""; }; + 09C5721F2172889200BDF00F /* TGNeoAudioMessageViewModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGNeoAudioMessageViewModel.m; sourceTree = ""; }; + 09C572202172889200BDF00F /* TGNeoBackgroundViewModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGNeoBackgroundViewModel.m; sourceTree = ""; }; + 09C572212172889200BDF00F /* TGNeoRowController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGNeoRowController.h; sourceTree = ""; }; + 09C572222172889200BDF00F /* TGNeoTextMessageViewModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGNeoTextMessageViewModel.h; sourceTree = ""; }; + 09C572232172889200BDF00F /* TGNeoAudioMessageViewModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGNeoAudioMessageViewModel.h; sourceTree = ""; }; + 09C572242172889200BDF00F /* TGNeoSmiliesMessageViewModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGNeoSmiliesMessageViewModel.h; sourceTree = ""; }; + 09C5723C21728C0E00BDF00F /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = Platforms/WatchOS.platform/Developer/SDKs/WatchOS5.0.sdk/System/Library/Frameworks/CoreGraphics.framework; sourceTree = DEVELOPER_DIR; }; + 09C5723E21728CFC00BDF00F /* SSignal+Accumulate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "SSignal+Accumulate.m"; path = "submodules/SSignalKit/SSignalKit/SSignal+Accumulate.m"; sourceTree = SOURCE_ROOT; }; + 09C5723F21728CFC00BDF00F /* SBag.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SBag.h; path = submodules/SSignalKit/SSignalKit/SBag.h; sourceTree = SOURCE_ROOT; }; + 09C5724021728CFC00BDF00F /* SSignal+Pipe.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "SSignal+Pipe.h"; path = "submodules/SSignalKit/SSignalKit/SSignal+Pipe.h"; sourceTree = SOURCE_ROOT; }; + 09C5724121728CFC00BDF00F /* SThreadPoolQueue.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SThreadPoolQueue.h; path = submodules/SSignalKit/SSignalKit/SThreadPoolQueue.h; sourceTree = SOURCE_ROOT; }; + 09C5724221728CFC00BDF00F /* SSignal+Catch.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "SSignal+Catch.h"; path = "submodules/SSignalKit/SSignalKit/SSignal+Catch.h"; sourceTree = SOURCE_ROOT; }; + 09C5724321728CFD00BDF00F /* SSignal+Timing.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "SSignal+Timing.m"; path = "submodules/SSignalKit/SSignalKit/SSignal+Timing.m"; sourceTree = SOURCE_ROOT; }; + 09C5724421728CFD00BDF00F /* SSignal+Catch.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "SSignal+Catch.m"; path = "submodules/SSignalKit/SSignalKit/SSignal+Catch.m"; sourceTree = SOURCE_ROOT; }; + 09C5724521728CFD00BDF00F /* SSignal+Combine.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "SSignal+Combine.m"; path = "submodules/SSignalKit/SSignalKit/SSignal+Combine.m"; sourceTree = SOURCE_ROOT; }; + 09C5724621728CFD00BDF00F /* SDisposable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SDisposable.h; path = submodules/SSignalKit/SSignalKit/SDisposable.h; sourceTree = SOURCE_ROOT; }; + 09C5724721728CFD00BDF00F /* SVariable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SVariable.h; path = submodules/SSignalKit/SSignalKit/SVariable.h; sourceTree = SOURCE_ROOT; }; + 09C5724821728CFD00BDF00F /* SBlockDisposable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SBlockDisposable.h; path = submodules/SSignalKit/SSignalKit/SBlockDisposable.h; sourceTree = SOURCE_ROOT; }; + 09C5724921728CFD00BDF00F /* SAtomic.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SAtomic.m; path = submodules/SSignalKit/SSignalKit/SAtomic.m; sourceTree = SOURCE_ROOT; }; + 09C5724A21728CFD00BDF00F /* SSignal+Take.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "SSignal+Take.h"; path = "submodules/SSignalKit/SSignalKit/SSignal+Take.h"; sourceTree = SOURCE_ROOT; }; + 09C5724B21728CFD00BDF00F /* SThreadPoolQueue.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SThreadPoolQueue.m; path = submodules/SSignalKit/SSignalKit/SThreadPoolQueue.m; sourceTree = SOURCE_ROOT; }; + 09C5724C21728CFD00BDF00F /* SSignal+Combine.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "SSignal+Combine.h"; path = "submodules/SSignalKit/SSignalKit/SSignal+Combine.h"; sourceTree = SOURCE_ROOT; }; + 09C5724D21728CFD00BDF00F /* SMulticastSignalManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SMulticastSignalManager.m; path = submodules/SSignalKit/SSignalKit/SMulticastSignalManager.m; sourceTree = SOURCE_ROOT; }; + 09C5724E21728CFD00BDF00F /* SMetaDisposable.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SMetaDisposable.m; path = submodules/SSignalKit/SSignalKit/SMetaDisposable.m; sourceTree = SOURCE_ROOT; }; + 09C5724F21728CFD00BDF00F /* SSignal+Accumulate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "SSignal+Accumulate.h"; path = "submodules/SSignalKit/SSignalKit/SSignal+Accumulate.h"; sourceTree = SOURCE_ROOT; }; + 09C5725021728CFD00BDF00F /* SSignal+Mapping.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "SSignal+Mapping.h"; path = "submodules/SSignalKit/SSignalKit/SSignal+Mapping.h"; sourceTree = SOURCE_ROOT; }; + 09C5725121728CFD00BDF00F /* SThreadPoolTask.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SThreadPoolTask.h; path = submodules/SSignalKit/SSignalKit/SThreadPoolTask.h; sourceTree = SOURCE_ROOT; }; + 09C5725221728CFE00BDF00F /* SSignal+Dispatch.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "SSignal+Dispatch.m"; path = "submodules/SSignalKit/SSignalKit/SSignal+Dispatch.m"; sourceTree = SOURCE_ROOT; }; + 09C5725321728CFE00BDF00F /* SBag.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SBag.m; path = submodules/SSignalKit/SSignalKit/SBag.m; sourceTree = SOURCE_ROOT; }; + 09C5725421728CFE00BDF00F /* SThreadPoolTask.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SThreadPoolTask.m; path = submodules/SSignalKit/SSignalKit/SThreadPoolTask.m; sourceTree = SOURCE_ROOT; }; + 09C5725521728CFE00BDF00F /* SSignal+Multicast.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "SSignal+Multicast.m"; path = "submodules/SSignalKit/SSignalKit/SSignal+Multicast.m"; sourceTree = SOURCE_ROOT; }; + 09C5725621728CFE00BDF00F /* SSignalKit.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SSignalKit.h; path = submodules/SSignalKit/SSignalKit/SSignalKit.h; sourceTree = SOURCE_ROOT; }; + 09C5725721728CFE00BDF00F /* SSignal+Take.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "SSignal+Take.m"; path = "submodules/SSignalKit/SSignalKit/SSignal+Take.m"; sourceTree = SOURCE_ROOT; }; + 09C5725821728CFE00BDF00F /* SSignal+Multicast.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "SSignal+Multicast.h"; path = "submodules/SSignalKit/SSignalKit/SSignal+Multicast.h"; sourceTree = SOURCE_ROOT; }; + 09C5725921728CFE00BDF00F /* SBlockDisposable.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SBlockDisposable.m; path = submodules/SSignalKit/SSignalKit/SBlockDisposable.m; sourceTree = SOURCE_ROOT; }; + 09C5725A21728CFE00BDF00F /* SSignal+SideEffects.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "SSignal+SideEffects.m"; path = "submodules/SSignalKit/SSignalKit/SSignal+SideEffects.m"; sourceTree = SOURCE_ROOT; }; + 09C5725B21728CFE00BDF00F /* SVariable.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SVariable.m; path = submodules/SSignalKit/SSignalKit/SVariable.m; sourceTree = SOURCE_ROOT; }; + 09C5725C21728CFE00BDF00F /* SMetaDisposable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SMetaDisposable.h; path = submodules/SSignalKit/SSignalKit/SMetaDisposable.h; sourceTree = SOURCE_ROOT; }; + 09C5725D21728CFF00BDF00F /* SSubscriber.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SSubscriber.m; path = submodules/SSignalKit/SSignalKit/SSubscriber.m; sourceTree = SOURCE_ROOT; }; + 09C5725E21728CFF00BDF00F /* SSignal+Pipe.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "SSignal+Pipe.m"; path = "submodules/SSignalKit/SSignalKit/SSignal+Pipe.m"; sourceTree = SOURCE_ROOT; }; + 09C5725F21728CFF00BDF00F /* STimer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = STimer.h; path = submodules/SSignalKit/SSignalKit/STimer.h; sourceTree = SOURCE_ROOT; }; + 09C5726021728CFF00BDF00F /* SSignal+Single.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "SSignal+Single.m"; path = "submodules/SSignalKit/SSignalKit/SSignal+Single.m"; sourceTree = SOURCE_ROOT; }; + 09C5726121728CFF00BDF00F /* SMulticastSignalManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SMulticastSignalManager.h; path = submodules/SSignalKit/SSignalKit/SMulticastSignalManager.h; sourceTree = SOURCE_ROOT; }; + 09C5726221728CFF00BDF00F /* STimer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = STimer.m; path = submodules/SSignalKit/SSignalKit/STimer.m; sourceTree = SOURCE_ROOT; }; + 09C5726321728CFF00BDF00F /* SSignal+Timing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "SSignal+Timing.h"; path = "submodules/SSignalKit/SSignalKit/SSignal+Timing.h"; sourceTree = SOURCE_ROOT; }; + 09C5726421728CFF00BDF00F /* SSignal+SideEffects.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "SSignal+SideEffects.h"; path = "submodules/SSignalKit/SSignalKit/SSignal+SideEffects.h"; sourceTree = SOURCE_ROOT; }; + 09C5726521728CFF00BDF00F /* SQueue.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SQueue.h; path = submodules/SSignalKit/SSignalKit/SQueue.h; sourceTree = SOURCE_ROOT; }; + 09C5726621728CFF00BDF00F /* SSignal+Single.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "SSignal+Single.h"; path = "submodules/SSignalKit/SSignalKit/SSignal+Single.h"; sourceTree = SOURCE_ROOT; }; + 09C5726721728CFF00BDF00F /* SQueue.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SQueue.m; path = submodules/SSignalKit/SSignalKit/SQueue.m; sourceTree = SOURCE_ROOT; }; + 09C5726821728CFF00BDF00F /* SSubscriber.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SSubscriber.h; path = submodules/SSignalKit/SSignalKit/SSubscriber.h; sourceTree = SOURCE_ROOT; }; + 09C5726921728D0000BDF00F /* SSignal+Meta.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "SSignal+Meta.h"; path = "submodules/SSignalKit/SSignalKit/SSignal+Meta.h"; sourceTree = SOURCE_ROOT; }; + 09C5726A21728D0000BDF00F /* SSignal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SSignal.h; path = submodules/SSignalKit/SSignalKit/SSignal.h; sourceTree = SOURCE_ROOT; }; + 09C5726B21728D0000BDF00F /* SThreadPool.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SThreadPool.h; path = submodules/SSignalKit/SSignalKit/SThreadPool.h; sourceTree = SOURCE_ROOT; }; + 09C5726C21728D0000BDF00F /* SDisposableSet.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SDisposableSet.m; path = submodules/SSignalKit/SSignalKit/SDisposableSet.m; sourceTree = SOURCE_ROOT; }; + 09C5726D21728D0000BDF00F /* SSignal.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SSignal.m; path = submodules/SSignalKit/SSignalKit/SSignal.m; sourceTree = SOURCE_ROOT; }; + 09C5726E21728D0000BDF00F /* SSignal+Dispatch.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "SSignal+Dispatch.h"; path = "submodules/SSignalKit/SSignalKit/SSignal+Dispatch.h"; sourceTree = SOURCE_ROOT; }; + 09C5726F21728D0000BDF00F /* SDisposableSet.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SDisposableSet.h; path = submodules/SSignalKit/SSignalKit/SDisposableSet.h; sourceTree = SOURCE_ROOT; }; + 09C5727021728D0000BDF00F /* SSignal+Meta.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "SSignal+Meta.m"; path = "submodules/SSignalKit/SSignalKit/SSignal+Meta.m"; sourceTree = SOURCE_ROOT; }; + 09C5727121728D0000BDF00F /* SSignal+Mapping.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "SSignal+Mapping.m"; path = "submodules/SSignalKit/SSignalKit/SSignal+Mapping.m"; sourceTree = SOURCE_ROOT; }; + 09C5727221728D0000BDF00F /* SAtomic.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SAtomic.h; path = submodules/SSignalKit/SSignalKit/SAtomic.h; sourceTree = SOURCE_ROOT; }; + 09C5727321728D0000BDF00F /* SThreadPool.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SThreadPool.m; path = submodules/SSignalKit/SSignalKit/SThreadPool.m; sourceTree = SOURCE_ROOT; }; + 09C572AD217292B800BDF00F /* TGBridgeRemoteSignals.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TGBridgeRemoteSignals.m; path = Bridge/TGBridgeRemoteSignals.m; sourceTree = ""; }; + 09C572AE217292B900BDF00F /* TGBridgePresetsSignals.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TGBridgePresetsSignals.m; path = Bridge/TGBridgePresetsSignals.m; sourceTree = ""; }; + 09C572AF217292B900BDF00F /* TGBridgeChatListSignals.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TGBridgeChatListSignals.m; path = Bridge/TGBridgeChatListSignals.m; sourceTree = ""; }; + 09C572B0217292B900BDF00F /* TGBridgeConversationSignals.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TGBridgeConversationSignals.m; path = Bridge/TGBridgeConversationSignals.m; sourceTree = ""; }; + 09C572B1217292B900BDF00F /* TGBridgeAudioSignals.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TGBridgeAudioSignals.h; path = Bridge/TGBridgeAudioSignals.h; sourceTree = ""; }; + 09C572B2217292B900BDF00F /* TGBridgeBotSignals.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TGBridgeBotSignals.h; path = Bridge/TGBridgeBotSignals.h; sourceTree = ""; }; + 09C572B3217292B900BDF00F /* TGBridgeStickersSignals.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TGBridgeStickersSignals.h; path = Bridge/TGBridgeStickersSignals.h; sourceTree = ""; }; + 09C572B4217292B900BDF00F /* TGBridgePeerSettingsSignals.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TGBridgePeerSettingsSignals.h; path = Bridge/TGBridgePeerSettingsSignals.h; sourceTree = ""; }; + 09C572B5217292B900BDF00F /* TGBridgeSendMessageSignals.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TGBridgeSendMessageSignals.m; path = Bridge/TGBridgeSendMessageSignals.m; sourceTree = ""; }; + 09C572B6217292B900BDF00F /* TGBridgeStateSignal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TGBridgeStateSignal.h; path = Bridge/TGBridgeStateSignal.h; sourceTree = ""; }; + 09C572B7217292B900BDF00F /* TGBridgeContactsSignals.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TGBridgeContactsSignals.h; path = Bridge/TGBridgeContactsSignals.h; sourceTree = ""; }; + 09C572B8217292B900BDF00F /* TGBridgeStickersSignals.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TGBridgeStickersSignals.m; path = Bridge/TGBridgeStickersSignals.m; sourceTree = ""; }; + 09C572B9217292B900BDF00F /* TGBridgeChatMessageListSignals.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TGBridgeChatMessageListSignals.h; path = Bridge/TGBridgeChatMessageListSignals.h; sourceTree = ""; }; + 09C572BA217292B900BDF00F /* TGBridgeSendMessageSignals.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TGBridgeSendMessageSignals.h; path = Bridge/TGBridgeSendMessageSignals.h; sourceTree = ""; }; + 09C572BB217292B900BDF00F /* TGBridgeBotSignals.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TGBridgeBotSignals.m; path = Bridge/TGBridgeBotSignals.m; sourceTree = ""; }; + 09C572BC217292B900BDF00F /* TGBridgeLocationSignals.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TGBridgeLocationSignals.h; path = Bridge/TGBridgeLocationSignals.h; sourceTree = ""; }; + 09C572BD217292BA00BDF00F /* TGBridgeUserInfoSignals.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TGBridgeUserInfoSignals.h; path = Bridge/TGBridgeUserInfoSignals.h; sourceTree = ""; }; + 09C572BE217292BA00BDF00F /* TGBridgeContactsSignals.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TGBridgeContactsSignals.m; path = Bridge/TGBridgeContactsSignals.m; sourceTree = ""; }; + 09C572BF217292BA00BDF00F /* TGBridgeChatListSignals.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TGBridgeChatListSignals.h; path = Bridge/TGBridgeChatListSignals.h; sourceTree = ""; }; + 09C572C0217292BA00BDF00F /* TGBridgeRemoteSignals.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TGBridgeRemoteSignals.h; path = Bridge/TGBridgeRemoteSignals.h; sourceTree = ""; }; + 09C572C1217292BA00BDF00F /* TGBridgeAudioSignals.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TGBridgeAudioSignals.m; path = Bridge/TGBridgeAudioSignals.m; sourceTree = ""; }; + 09C572C2217292BA00BDF00F /* TGBridgePresetsSignals.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TGBridgePresetsSignals.h; path = Bridge/TGBridgePresetsSignals.h; sourceTree = ""; }; + 09C572C3217292BA00BDF00F /* TGBridgePeerSettingsSignals.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TGBridgePeerSettingsSignals.m; path = Bridge/TGBridgePeerSettingsSignals.m; sourceTree = ""; }; + 09C572C4217292BA00BDF00F /* TGBridgeLocationSignals.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TGBridgeLocationSignals.m; path = Bridge/TGBridgeLocationSignals.m; sourceTree = ""; }; + 09C572C5217292BA00BDF00F /* TGBridgeMediaSignals.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TGBridgeMediaSignals.m; path = Bridge/TGBridgeMediaSignals.m; sourceTree = ""; }; + 09C572C6217292BA00BDF00F /* TGBridgeChatMessageListSignals.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TGBridgeChatMessageListSignals.m; path = Bridge/TGBridgeChatMessageListSignals.m; sourceTree = ""; }; + 09C572C7217292BA00BDF00F /* TGBridgeMediaSignals.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TGBridgeMediaSignals.h; path = Bridge/TGBridgeMediaSignals.h; sourceTree = ""; }; + 09C572C8217292BA00BDF00F /* TGBridgeConversationSignals.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TGBridgeConversationSignals.h; path = Bridge/TGBridgeConversationSignals.h; sourceTree = ""; }; + 09C572C9217292BB00BDF00F /* TGBridgeStateSignal.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TGBridgeStateSignal.m; path = Bridge/TGBridgeStateSignal.m; sourceTree = ""; }; + 09C572CA217292BB00BDF00F /* TGBridgeUserInfoSignals.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TGBridgeUserInfoSignals.m; path = Bridge/TGBridgeUserInfoSignals.m; sourceTree = ""; }; + 09C572CC2172939F00BDF00F /* TGBridgeClient.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TGBridgeClient.h; path = Bridge/TGBridgeClient.h; sourceTree = ""; }; + 09C572CE2172939F00BDF00F /* TGBridgeCommon.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TGBridgeCommon.m; path = Bridge/TGBridgeCommon.m; sourceTree = ""; }; + 09C572CF2172939F00BDF00F /* TGBridgeCommon.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TGBridgeCommon.h; path = Bridge/TGBridgeCommon.h; sourceTree = ""; }; + 09C572D02172939F00BDF00F /* TGBridgeClient.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TGBridgeClient.m; path = Bridge/TGBridgeClient.m; sourceTree = ""; }; + 09C572D62172953200BDF00F /* TGBridgeMediaAttachment.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TGBridgeMediaAttachment.h; path = Bridge/TGBridgeMediaAttachment.h; sourceTree = ""; }; + 09C572D72172953300BDF00F /* TGBridgeMessage+TGTableItem.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "TGBridgeMessage+TGTableItem.m"; path = "Bridge/TGBridgeMessage+TGTableItem.m"; sourceTree = ""; }; + 09C572D92172953300BDF00F /* TGBridgeBotReplyMarkup.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TGBridgeBotReplyMarkup.m; path = Bridge/TGBridgeBotReplyMarkup.m; sourceTree = ""; }; + 09C572DA2172953300BDF00F /* TGBridgeContactMediaAttachment.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TGBridgeContactMediaAttachment.h; path = Bridge/TGBridgeContactMediaAttachment.h; sourceTree = ""; }; + 09C572DB2172953300BDF00F /* TGBridgeChat+TGTableItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "TGBridgeChat+TGTableItem.h"; path = "Bridge/TGBridgeChat+TGTableItem.h"; sourceTree = ""; }; + 09C572DC2172953300BDF00F /* TGBridgeActionMediaAttachment.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TGBridgeActionMediaAttachment.h; path = Bridge/TGBridgeActionMediaAttachment.h; sourceTree = ""; }; + 09C572DD2172953300BDF00F /* TGBridgeBotReplyMarkup.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TGBridgeBotReplyMarkup.h; path = Bridge/TGBridgeBotReplyMarkup.h; sourceTree = ""; }; + 09C572DE2172953300BDF00F /* TGBridgeContactMediaAttachment.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TGBridgeContactMediaAttachment.m; path = Bridge/TGBridgeContactMediaAttachment.m; sourceTree = ""; }; + 09C572DF2172953300BDF00F /* TGBridgeMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TGBridgeMessage.m; path = Bridge/TGBridgeMessage.m; sourceTree = ""; }; + 09C572E02172953300BDF00F /* TGBridgeAudioMediaAttachment.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TGBridgeAudioMediaAttachment.m; path = Bridge/TGBridgeAudioMediaAttachment.m; sourceTree = ""; }; + 09C572E12172953300BDF00F /* TGBridgeReplyMarkupMediaAttachment.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TGBridgeReplyMarkupMediaAttachment.h; path = Bridge/TGBridgeReplyMarkupMediaAttachment.h; sourceTree = ""; }; + 09C572E22172953300BDF00F /* TGBridgeActionMediaAttachment.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TGBridgeActionMediaAttachment.m; path = Bridge/TGBridgeActionMediaAttachment.m; sourceTree = ""; }; + 09C572E32172953400BDF00F /* TGBridgeStickerPack.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TGBridgeStickerPack.m; path = Bridge/TGBridgeStickerPack.m; sourceTree = ""; }; + 09C572E42172953400BDF00F /* TGBridgeVideoMediaAttachment.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TGBridgeVideoMediaAttachment.m; path = Bridge/TGBridgeVideoMediaAttachment.m; sourceTree = ""; }; + 09C572E62172953400BDF00F /* TGBridgeForwardedMessageMediaAttachment.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TGBridgeForwardedMessageMediaAttachment.m; path = Bridge/TGBridgeForwardedMessageMediaAttachment.m; sourceTree = ""; }; + 09C572E72172953400BDF00F /* TGBridgeLocationMediaAttachment.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TGBridgeLocationMediaAttachment.h; path = Bridge/TGBridgeLocationMediaAttachment.h; sourceTree = ""; }; + 09C572E82172953400BDF00F /* TGBridgeBotInfo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TGBridgeBotInfo.h; path = Bridge/TGBridgeBotInfo.h; sourceTree = ""; }; + 09C572E92172953400BDF00F /* TGBridgeUser+TGTableItem.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "TGBridgeUser+TGTableItem.m"; path = "Bridge/TGBridgeUser+TGTableItem.m"; sourceTree = ""; }; + 09C572EA2172953400BDF00F /* TGBridgeBotCommandInfo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TGBridgeBotCommandInfo.h; path = Bridge/TGBridgeBotCommandInfo.h; sourceTree = ""; }; + 09C572EB2172953400BDF00F /* TGBridgeUser.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TGBridgeUser.h; path = Bridge/TGBridgeUser.h; sourceTree = ""; }; + 09C572EC2172953400BDF00F /* TGBridgeReplyMessageMediaAttachment.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TGBridgeReplyMessageMediaAttachment.h; path = Bridge/TGBridgeReplyMessageMediaAttachment.h; sourceTree = ""; }; + 09C572ED2172953400BDF00F /* TGBridgeUser.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TGBridgeUser.m; path = Bridge/TGBridgeUser.m; sourceTree = ""; }; + 09C572EE2172953400BDF00F /* TGBridgeImageMediaAttachment.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TGBridgeImageMediaAttachment.h; path = Bridge/TGBridgeImageMediaAttachment.h; sourceTree = ""; }; + 09C572EF2172953500BDF00F /* TGBridgeBotCommandInfo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TGBridgeBotCommandInfo.m; path = Bridge/TGBridgeBotCommandInfo.m; sourceTree = ""; }; + 09C572F02172953500BDF00F /* TGBridgeDocumentMediaAttachment.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TGBridgeDocumentMediaAttachment.h; path = Bridge/TGBridgeDocumentMediaAttachment.h; sourceTree = ""; }; + 09C572F12172953500BDF00F /* TGBridgeChat.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TGBridgeChat.h; path = Bridge/TGBridgeChat.h; sourceTree = ""; }; + 09C572F22172953500BDF00F /* TGBridgeImageMediaAttachment.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TGBridgeImageMediaAttachment.m; path = Bridge/TGBridgeImageMediaAttachment.m; sourceTree = ""; }; + 09C572F32172953500BDF00F /* TGBridgeLocationVenue.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TGBridgeLocationVenue.m; path = Bridge/TGBridgeLocationVenue.m; sourceTree = ""; }; + 09C572F42172953500BDF00F /* TGBridgeMessageEntities.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TGBridgeMessageEntities.m; path = Bridge/TGBridgeMessageEntities.m; sourceTree = ""; }; + 09C572F52172953500BDF00F /* TGBridgeWebPageMediaAttachment.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TGBridgeWebPageMediaAttachment.m; path = Bridge/TGBridgeWebPageMediaAttachment.m; sourceTree = ""; }; + 09C572F62172953500BDF00F /* TGBridgeChat+TGTableItem.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "TGBridgeChat+TGTableItem.m"; path = "Bridge/TGBridgeChat+TGTableItem.m"; sourceTree = ""; }; + 09C572F72172953500BDF00F /* TGBridgeMessage+TGTableItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "TGBridgeMessage+TGTableItem.h"; path = "Bridge/TGBridgeMessage+TGTableItem.h"; sourceTree = ""; }; + 09C572F82172953500BDF00F /* TGBridgeMessageEntitiesAttachment.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TGBridgeMessageEntitiesAttachment.h; path = Bridge/TGBridgeMessageEntitiesAttachment.h; sourceTree = ""; }; + 09C572F92172953600BDF00F /* TGBridgeMediaAttachment.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TGBridgeMediaAttachment.m; path = Bridge/TGBridgeMediaAttachment.m; sourceTree = ""; }; + 09C572FA2172953600BDF00F /* TGBridgeMessageEntities.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TGBridgeMessageEntities.h; path = Bridge/TGBridgeMessageEntities.h; sourceTree = ""; }; + 09C572FB2172953600BDF00F /* TGBridgeDocumentMediaAttachment.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TGBridgeDocumentMediaAttachment.m; path = Bridge/TGBridgeDocumentMediaAttachment.m; sourceTree = ""; }; + 09C572FC2172953600BDF00F /* TGBridgePeerNotificationSettings.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TGBridgePeerNotificationSettings.m; path = Bridge/TGBridgePeerNotificationSettings.m; sourceTree = ""; }; + 09C572FD2172953600BDF00F /* TGBridgeReplyMessageMediaAttachment.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TGBridgeReplyMessageMediaAttachment.m; path = Bridge/TGBridgeReplyMessageMediaAttachment.m; sourceTree = ""; }; + 09C572FE2172953600BDF00F /* TGBridgeForwardedMessageMediaAttachment.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TGBridgeForwardedMessageMediaAttachment.h; path = Bridge/TGBridgeForwardedMessageMediaAttachment.h; sourceTree = ""; }; + 09C572FF2172953600BDF00F /* TGBridgeLocationVenue+TGTableItem.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "TGBridgeLocationVenue+TGTableItem.m"; path = "Bridge/TGBridgeLocationVenue+TGTableItem.m"; sourceTree = ""; }; + 09C573002172953600BDF00F /* TGBridgeLocationMediaAttachment.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TGBridgeLocationMediaAttachment.m; path = Bridge/TGBridgeLocationMediaAttachment.m; sourceTree = ""; }; + 09C573022172953600BDF00F /* TGBridgePeerNotificationSettings.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TGBridgePeerNotificationSettings.h; path = Bridge/TGBridgePeerNotificationSettings.h; sourceTree = ""; }; + 09C573032172953600BDF00F /* TGBridgeContext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TGBridgeContext.h; path = Bridge/TGBridgeContext.h; sourceTree = ""; }; + 09C573042172953700BDF00F /* TGBridgeMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TGBridgeMessage.h; path = Bridge/TGBridgeMessage.h; sourceTree = ""; }; + 09C573052172953700BDF00F /* TGBridgeUser+TGTableItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "TGBridgeUser+TGTableItem.h"; path = "Bridge/TGBridgeUser+TGTableItem.h"; sourceTree = ""; }; + 09C573062172953700BDF00F /* TGBridgeVideoMediaAttachment.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TGBridgeVideoMediaAttachment.h; path = Bridge/TGBridgeVideoMediaAttachment.h; sourceTree = ""; }; + 09C573072172953700BDF00F /* TGBridgeContext.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TGBridgeContext.m; path = Bridge/TGBridgeContext.m; sourceTree = ""; }; + 09C573082172953700BDF00F /* TGBridgeAudioMediaAttachment.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TGBridgeAudioMediaAttachment.h; path = Bridge/TGBridgeAudioMediaAttachment.h; sourceTree = ""; }; + 09C573092172953700BDF00F /* TGBridgeChat.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TGBridgeChat.m; path = Bridge/TGBridgeChat.m; sourceTree = ""; }; + 09C5730A2172953700BDF00F /* TGBridgeMessageEntitiesAttachment.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TGBridgeMessageEntitiesAttachment.m; path = Bridge/TGBridgeMessageEntitiesAttachment.m; sourceTree = ""; }; + 09C5730B2172953700BDF00F /* TGBridgeLocationVenue.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TGBridgeLocationVenue.h; path = Bridge/TGBridgeLocationVenue.h; sourceTree = ""; }; + 09C5730C2172953700BDF00F /* TGBridgeChatMessages.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TGBridgeChatMessages.m; path = Bridge/TGBridgeChatMessages.m; sourceTree = ""; }; + 09C5730D2172953800BDF00F /* TGBridgeReplyMarkupMediaAttachment.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TGBridgeReplyMarkupMediaAttachment.m; path = Bridge/TGBridgeReplyMarkupMediaAttachment.m; sourceTree = ""; }; + 09C5730F2172953800BDF00F /* TGBridgeUnsupportedMediaAttachment.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TGBridgeUnsupportedMediaAttachment.m; path = Bridge/TGBridgeUnsupportedMediaAttachment.m; sourceTree = ""; }; + 09C573102172953800BDF00F /* TGBridgeBotInfo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TGBridgeBotInfo.m; path = Bridge/TGBridgeBotInfo.m; sourceTree = ""; }; + 09C573112172953800BDF00F /* TGBridgeUnsupportedMediaAttachment.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TGBridgeUnsupportedMediaAttachment.h; path = Bridge/TGBridgeUnsupportedMediaAttachment.h; sourceTree = ""; }; + 09C573122172953800BDF00F /* TGBridgeWebPageMediaAttachment.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TGBridgeWebPageMediaAttachment.h; path = Bridge/TGBridgeWebPageMediaAttachment.h; sourceTree = ""; }; + 09C573132172953800BDF00F /* TGBridgeStickerPack.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TGBridgeStickerPack.h; path = Bridge/TGBridgeStickerPack.h; sourceTree = ""; }; + 09C573142172953800BDF00F /* TGBridgeChatMessages.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TGBridgeChatMessages.h; path = Bridge/TGBridgeChatMessages.h; sourceTree = ""; }; + 09C573152172953800BDF00F /* TGBridgeLocationVenue+TGTableItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "TGBridgeLocationVenue+TGTableItem.h"; path = "Bridge/TGBridgeLocationVenue+TGTableItem.h"; sourceTree = ""; }; + 09C573362172974E00BDF00F /* TGBridgePeerIdAdapter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TGBridgePeerIdAdapter.h; path = Bridge/TGBridgePeerIdAdapter.h; sourceTree = ""; }; + 09CFB211217299E80083F7A3 /* CoreLocation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreLocation.framework; path = Platforms/WatchOS.platform/Developer/SDKs/WatchOS5.0.sdk/System/Library/Frameworks/CoreLocation.framework; sourceTree = DEVELOPER_DIR; }; + 09D304212174335F00C00567 /* WatchBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchBridge.swift; sourceTree = ""; }; + 09FDAEE52140477F00BF856F /* MtProtoKitDynamic.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = MtProtoKitDynamic.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D001D5A91F878DA300DF975A /* PhoneCountries.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = PhoneCountries.txt; path = "Telegram-iOS/Resources/PhoneCountries.txt"; sourceTree = ""; }; + D008599C1B28189D00EAF753 /* Telegram X.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Telegram X.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + D00859A01B28189D00EAF753 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + D00859A11B28189D00EAF753 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + D00859A81B28189D00EAF753 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; + D00859AB1B28189D00EAF753 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; }; + D00859B11B28189D00EAF753 /* Telegram-iOSTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Telegram-iOSTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + D00859B61B28189D00EAF753 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + D00859B71B28189D00EAF753 /* Telegram_iOSTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Telegram_iOSTests.swift; sourceTree = ""; }; + D00ED7591FE94630001F38BD /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = en; path = en.lproj/AppIntentVocabulary.plist; sourceTree = ""; }; + D00ED75C1FE95287001F38BD /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; + D01A47521F4DBEB100383CC1 /* libHockeySDK.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libHockeySDK.a; path = "../../build/HockeySDK-iOS/Support/build/Debug-iphoneos/libHockeySDK.a"; sourceTree = ""; }; + D01A47541F4DBED700383CC1 /* HockeySDK.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = HockeySDK.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D02CF5FC215D9ABE00E0F56A /* NotificationContent.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = NotificationContent.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + D02CF600215D9ABF00E0F56A /* NotificationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationViewController.swift; sourceTree = ""; }; + D02CF603215D9ABF00E0F56A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = ""; }; + D02CF605215D9ABF00E0F56A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + D02CF611215DA1C900E0F56A /* NotificationContent-HockeyApp.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = "NotificationContent-HockeyApp.entitlements"; sourceTree = ""; }; + D02CF612215DA1C900E0F56A /* NotificationContent-AppStore.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = "NotificationContent-AppStore.entitlements"; sourceTree = ""; }; + D02CF613215DA1C900E0F56A /* NotificationContent-AppStoreLLC.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = "NotificationContent-AppStoreLLC.entitlements"; sourceTree = ""; }; + D02CF614215DA24900E0F56A /* Display.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Display.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D02CF616215DA24900E0F56A /* Postbox.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Postbox.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D02CF618215DA24900E0F56A /* SwiftSignalKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SwiftSignalKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D02CF61A215DA24900E0F56A /* TelegramCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = TelegramCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D02CF61D215E522400E0F56A /* NotificationContent-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NotificationContent-Bridging-Header.h"; sourceTree = ""; }; + D02E31221BD803E800CD3F01 /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + D0369C8B1D3E2C9500D91AFC /* libiconv.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libiconv.tbd; path = usr/lib/libiconv.tbd; sourceTree = SDKROOT; }; + D0369C8D1D3E2E4800D91AFC /* VideoToolbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = VideoToolbox.framework; path = System/Library/Frameworks/VideoToolbox.framework; sourceTree = SDKROOT; }; + D0369C8F1D3E2E5000D91AFC /* libbz2.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libbz2.tbd; path = usr/lib/libbz2.tbd; sourceTree = SDKROOT; }; + D039FB162170F06A00BD1BAD /* PreFetchedLegacyResource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreFetchedLegacyResource.swift; sourceTree = ""; }; + D03B0E781D63484500955575 /* Share.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = Share.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + D03B0E7A1D63484500955575 /* ShareRootController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareRootController.swift; sourceTree = ""; }; + D03B0E7F1D63484500955575 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + D03B0E871D634B1100955575 /* Display.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Display.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D03B0E881D634B1100955575 /* SwiftSignalKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = SwiftSignalKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D03B0E891D634B1100955575 /* TelegramCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = TelegramCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D03B0E951D637A0500955575 /* AsyncDisplayKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = AsyncDisplayKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D03BCCC91C6EBD670097A291 /* ListViewTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListViewTests.swift; sourceTree = ""; }; + D0400ED81D5B8F97007931CE /* TelegramUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = TelegramUI.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D0400EE41D5B912E007931CE /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; }; + D0400EE61D5B912E007931CE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + D0400EEE1D5B9540007931CE /* NotificationService.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NotificationService.entitlements; sourceTree = ""; }; + D04DCC0B1F71C80000B021D7 /* 0.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = 0.m4a; sourceTree = ""; }; + D04DCC0C1F71C80000B021D7 /* 1.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = 1.m4a; sourceTree = ""; }; + D04DCC0D1F71C80000B021D7 /* 100.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = 100.m4a; sourceTree = ""; }; + D04DCC0E1F71C80000B021D7 /* 101.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = 101.m4a; sourceTree = ""; }; + D04DCC0F1F71C80000B021D7 /* 102.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = 102.m4a; sourceTree = ""; }; + D04DCC101F71C80000B021D7 /* 103.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = 103.m4a; sourceTree = ""; }; + D04DCC111F71C80000B021D7 /* 104.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = 104.m4a; sourceTree = ""; }; + D04DCC121F71C80000B021D7 /* 105.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = 105.m4a; sourceTree = ""; }; + D04DCC131F71C80000B021D7 /* 106.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = 106.m4a; sourceTree = ""; }; + D04DCC141F71C80000B021D7 /* 107.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = 107.m4a; sourceTree = ""; }; + D04DCC151F71C80000B021D7 /* 108.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = 108.m4a; sourceTree = ""; }; + D04DCC161F71C80000B021D7 /* 109.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = 109.m4a; sourceTree = ""; }; + D04DCC171F71C80000B021D7 /* 110.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = 110.m4a; sourceTree = ""; }; + D04DCC181F71C80000B021D7 /* 111.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = 111.m4a; sourceTree = ""; }; + D04DCC191F71C80000B021D7 /* 2.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = 2.m4a; sourceTree = ""; }; + D04DCC1A1F71C80000B021D7 /* 3.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = 3.m4a; sourceTree = ""; }; + D04DCC1B1F71C80000B021D7 /* 4.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = 4.m4a; sourceTree = ""; }; + D04DCC1C1F71C80000B021D7 /* 5.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = 5.m4a; sourceTree = ""; }; + D04DCC1D1F71C80000B021D7 /* 6.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = 6.m4a; sourceTree = ""; }; + D04DCC1E1F71C80000B021D7 /* 7.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = 7.m4a; sourceTree = ""; }; + D04DCC1F1F71C80000B021D7 /* 8.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = 8.m4a; sourceTree = ""; }; + D04DCC201F71C80000B021D7 /* 9.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = 9.m4a; sourceTree = ""; }; + D04FA1B02145E37F0006EF45 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = Localizable.strings; sourceTree = ""; }; + D04FA1B22145E37F0006EF45 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = InfoPlist.strings; sourceTree = ""; }; + D04FA1B52145E37F0006EF45 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = InfoPlist.strings; sourceTree = ""; }; + D04FA1B82145E3800006EF45 /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = InfoPlist.strings; sourceTree = ""; }; + D04FA1BB2145E3800006EF45 /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = InfoPlist.strings; sourceTree = ""; }; + D04FA1BE2145E3800006EF45 /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = InfoPlist.strings; sourceTree = ""; }; + D04FA1C12145E3800006EF45 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = InfoPlist.strings; sourceTree = ""; }; + D04FA1C42145E3810006EF45 /* pt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pt; path = InfoPlist.strings; sourceTree = ""; }; + D04FA1C72145E3810006EF45 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = InfoPlist.strings; sourceTree = ""; }; + D050F21C1E49DEDE00988324 /* fast_arrow@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "fast_arrow@2x.png"; sourceTree = ""; }; + D050F21D1E49DEDE00988324 /* fast_arrow_shadow@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "fast_arrow_shadow@2x.png"; sourceTree = ""; }; + D050F21E1E49DEDE00988324 /* fast_body@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "fast_body@2x.png"; sourceTree = ""; }; + D050F21F1E49DEDE00988324 /* fast_spiral@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "fast_spiral@2x.png"; sourceTree = ""; }; + D050F2201E49DEDE00988324 /* ic_bubble@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "ic_bubble@2x.png"; sourceTree = ""; }; + D050F2211E49DEDE00988324 /* ic_bubble_dot@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "ic_bubble_dot@2x.png"; sourceTree = ""; }; + D050F2221E49DEDE00988324 /* ic_cam@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "ic_cam@2x.png"; sourceTree = ""; }; + D050F2231E49DEDE00988324 /* ic_cam_lens@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "ic_cam_lens@2x.png"; sourceTree = ""; }; + D050F2241E49DEDE00988324 /* ic_pencil@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "ic_pencil@2x.png"; sourceTree = ""; }; + D050F2251E49DEDE00988324 /* ic_pin@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "ic_pin@2x.png"; sourceTree = ""; }; + D050F2261E49DEDE00988324 /* ic_smile@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "ic_smile@2x.png"; sourceTree = ""; }; + D050F2271E49DEDE00988324 /* ic_smile_eye@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "ic_smile_eye@2x.png"; sourceTree = ""; }; + D050F2281E49DEDE00988324 /* ic_videocam@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "ic_videocam@2x.png"; sourceTree = ""; }; + D050F2291E49DEDE00988324 /* knot_down@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "knot_down@2x.png"; sourceTree = ""; }; + D050F22A1E49DEDE00988324 /* knot_up1@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "knot_up1@2x.png"; sourceTree = ""; }; + D050F22B1E49DEDE00988324 /* powerful_infinity@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "powerful_infinity@2x.png"; sourceTree = ""; }; + D050F22C1E49DEDE00988324 /* powerful_infinity_white@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "powerful_infinity_white@2x.png"; sourceTree = ""; }; + D050F22D1E49DEDE00988324 /* powerful_mask@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "powerful_mask@2x.png"; sourceTree = ""; }; + D050F22E1E49DEDE00988324 /* powerful_star@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "powerful_star@2x.png"; sourceTree = ""; }; + D050F22F1E49DEDE00988324 /* private_door@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "private_door@2x.png"; sourceTree = ""; }; + D050F2301E49DEDE00988324 /* private_screw@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "private_screw@2x.png"; sourceTree = ""; }; + D050F2311E49DEDE00988324 /* start_arrow@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "start_arrow@2x.png"; sourceTree = ""; }; + D050F2321E49DEDE00988324 /* start_arrow_ipad.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = start_arrow_ipad.png; sourceTree = ""; }; + D050F2331E49DEDE00988324 /* start_arrow_ipad@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "start_arrow_ipad@2x.png"; sourceTree = ""; }; + D050F2341E49DEDE00988324 /* telegram_plane1@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "telegram_plane1@2x.png"; sourceTree = ""; }; + D050F2351E49DEDE00988324 /* telegram_sphere@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "telegram_sphere@2x.png"; sourceTree = ""; }; + D051DB0C215E5E2300F30F92 /* NotificationContent.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NotificationContent.entitlements; sourceTree = ""; }; + D051DB5C21602D6E00F30F92 /* LegacyDataImportSplash.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyDataImportSplash.swift; sourceTree = ""; }; + D053DAD22018ED2B00993D32 /* LockedWindowCoveringView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockedWindowCoveringView.swift; sourceTree = ""; }; + D055BD431B7E216400F06C0A /* MapKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MapKit.framework; path = System/Library/Frameworks/MapKit.framework; sourceTree = SDKROOT; }; + D05B37F41FEA5F6E0041D2A5 /* SnapshotEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnapshotEnvironment.swift; sourceTree = ""; }; + D05B37F61FEA8C640041D2A5 /* SnapshotSecretChat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnapshotSecretChat.swift; sourceTree = ""; }; + D05B37F81FEA8CF00041D2A5 /* SnapshotSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnapshotSettings.swift; sourceTree = ""; }; + D05B37FA1FEA8D020041D2A5 /* SnapshotAppearanceSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnapshotAppearanceSettings.swift; sourceTree = ""; }; + D05B37FC1FEA8D870041D2A5 /* SnapshotResources.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnapshotResources.swift; sourceTree = ""; }; + D05B37FF1FEA8E3D0041D2A5 /* Bitmap2.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = Bitmap2.png; sourceTree = ""; }; + D05B38001FEA8E3D0041D2A5 /* Bitmap3.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = Bitmap3.png; sourceTree = ""; }; + D05B38011FEA8E3D0041D2A5 /* Bitmap1.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = Bitmap1.png; sourceTree = ""; }; + D05B38021FEA8E3D0041D2A5 /* Bitmap5.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = Bitmap5.png; sourceTree = ""; }; + D05B38031FEA8E3D0041D2A5 /* Bitmap7.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = Bitmap7.png; sourceTree = ""; }; + D05B38041FEA8E3D0041D2A5 /* Bitmap6.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = Bitmap6.png; sourceTree = ""; }; + D05B38051FEA8E3D0041D2A5 /* Bitmap8.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = Bitmap8.png; sourceTree = ""; }; + D05B38061FEA8E3D0041D2A5 /* Bitmap9.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = Bitmap9.png; sourceTree = ""; }; + D05B38071FEA8E3D0041D2A5 /* Bitmap12.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = Bitmap12.png; sourceTree = ""; }; + D05B38081FEA8E3D0041D2A5 /* Bitmap10.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = Bitmap10.png; sourceTree = ""; }; + D05B38091FEA8E3D0041D2A5 /* Bitmap11.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = Bitmap11.png; sourceTree = ""; }; + D0612E481D58B478000C8F02 /* Application.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = ""; }; + D06706601D51185400DED3E3 /* TelegramCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = TelegramCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D06E4C2D21347D9200088087 /* UIImage+ImageEffects.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIImage+ImageEffects.h"; sourceTree = ""; }; + D06E4C2E21347D9200088087 /* UIImage+ImageEffects.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIImage+ImageEffects.m"; sourceTree = ""; }; + D079FD001F06BBD10038FADE /* Telegram-iOS-AppStore.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = "Telegram-iOS-AppStore.entitlements"; sourceTree = ""; }; + D079FD011F06BBD60038FADE /* Config-Hockeyapp.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Config-Hockeyapp.xcconfig"; sourceTree = ""; }; + D079FD021F06BBD60038FADE /* Config-AppStore.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Config-AppStore.xcconfig"; sourceTree = ""; }; + D084023120E1883500065674 /* ApplicationShortcutItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationShortcutItem.swift; sourceTree = ""; }; + D08410431FABDC5B008FFE92 /* TGItemProviderSignals.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGItemProviderSignals.h; sourceTree = ""; }; + D08410441FABDC5C008FFE92 /* TGItemProviderSignals.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGItemProviderSignals.m; sourceTree = ""; }; + D08410471FABDC7A008FFE92 /* SSignalKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SSignalKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D08410491FABDCF2008FFE92 /* LegacyComponents.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = LegacyComponents.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D084104A1FABDCFD008FFE92 /* TGContactModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGContactModel.h; sourceTree = ""; }; + D084104B1FABDCFD008FFE92 /* TGMimeTypeMap.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGMimeTypeMap.h; sourceTree = ""; }; + D084104C1FABDCFD008FFE92 /* TGContactModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGContactModel.m; sourceTree = ""; }; + D084104D1FABDCFD008FFE92 /* TGMimeTypeMap.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGMimeTypeMap.m; sourceTree = ""; }; + D08410511FABDD54008FFE92 /* MtProtoKitDynamic.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = MtProtoKitDynamic.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D08410521FABDEC8008FFE92 /* Share-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Share-Bridging-Header.h"; sourceTree = ""; }; + D08410531FABE428008FFE92 /* ShareItems.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareItems.swift; sourceTree = ""; }; + D08984FD2118B3F100918162 /* MtProtoKitDynamic.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = MtProtoKitDynamic.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D08984FF2118B3F100918162 /* Postbox.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Postbox.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D08985012118B3F100918162 /* TelegramCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = TelegramCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D08985032118B46F00918162 /* SwiftSignalKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SwiftSignalKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D08985052118B62400918162 /* SiriIntents-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SiriIntents-Bridging-Header.h"; sourceTree = ""; }; + D08985062119B7FE00918162 /* IntentContacts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntentContacts.swift; sourceTree = ""; }; + D0924FFE1FE52C75003F693F /* Config-Hockeyapp Internal.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Config-Hockeyapp Internal.xcconfig"; sourceTree = ""; }; + D09250001FE52D2A003F693F /* BuildConfig.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BuildConfig.h; sourceTree = ""; }; + D09250011FE52D2A003F693F /* BuildConfig.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BuildConfig.m; sourceTree = ""; }; + D096C2BD1CC3C021006D814E /* Display.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Display.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D096C2C01CC3C104006D814E /* Postbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Postbox.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D096C2C31CC3C11A006D814E /* SwiftSignalKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = SwiftSignalKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D09A595F1B5858DB00FC3724 /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = System/Library/Frameworks/SystemConfiguration.framework; sourceTree = SDKROOT; }; + D09A59B71B5876B600FC3724 /* Telegram-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Telegram-Bridging-Header.h"; sourceTree = ""; }; + D09DCBB61D0C856B00F51FFE /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; + D0A18D621E149043004C6734 /* PushKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PushKit.framework; path = System/Library/Frameworks/PushKit.framework; sourceTree = SDKROOT; }; + D0A18D641E15C020004C6734 /* WakeupManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WakeupManager.swift; sourceTree = ""; }; + D0A18D681E16AC9D004C6734 /* NotificationManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = ""; }; + D0AA1A671D568BA400152314 /* UserNotifications.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UserNotifications.framework; path = System/Library/Frameworks/UserNotifications.framework; sourceTree = SDKROOT; }; + D0AA1A691D568BA400152314 /* UserNotificationsUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UserNotificationsUI.framework; path = System/Library/Frameworks/UserNotificationsUI.framework; sourceTree = SDKROOT; }; + D0AB0B9F1D6708B9002C78E7 /* Postbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Postbox.framework; path = "submodules/Postbox/build/Debug-iphoneos/Postbox.framework"; sourceTree = ""; }; + D0AB0BA01D6708B9002C78E7 /* TelegramCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = TelegramCore.framework; path = /TelegramCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D0AC49461D7095A100AA55DA /* MiniAccount.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MiniAccount.swift; sourceTree = ""; }; + D0ADF913212B398000310BBC /* Telegram-iOS-AppStoreLLC.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = "Telegram-iOS-AppStoreLLC.entitlements"; sourceTree = ""; }; + D0ADF914212B399A00310BBC /* Config-AppStoreLLC.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Config-AppStoreLLC.xcconfig"; sourceTree = ""; }; + D0ADF953212B3B4700310BBC /* Share-AppStoreLLC.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = "Share-AppStoreLLC.entitlements"; sourceTree = ""; }; + D0ADF954212B3B5200310BBC /* SiriIntents-AppStoreLLC.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = "SiriIntents-AppStoreLLC.entitlements"; sourceTree = ""; }; + D0ADF955212B3B6400310BBC /* Widget-AppStoreLLC.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = "Widget-AppStoreLLC.entitlements"; sourceTree = ""; }; + D0ADF957212B56DC00310BBC /* LegacyUserDataImport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyUserDataImport.swift; sourceTree = ""; }; + D0ADF959212B5AC600310BBC /* LegacyResourceImport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyResourceImport.swift; sourceTree = ""; }; + D0ADF95B212B636D00310BBC /* LegacyChatImport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyChatImport.swift; sourceTree = ""; }; + D0ADF95D212C818F00310BBC /* LegacyPreferencesImport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyPreferencesImport.swift; sourceTree = ""; }; + D0ADF95F212C8DF600310BBC /* TGAutoDownloadPreferences.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGAutoDownloadPreferences.m; sourceTree = ""; }; + D0ADF960212C8DF600310BBC /* TGAutoDownloadPreferences.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGAutoDownloadPreferences.h; sourceTree = ""; }; + D0ADF962212C9AA900310BBC /* TGProxyItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGProxyItem.h; sourceTree = ""; }; + D0ADF963212C9AA900310BBC /* TGProxyItem.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGProxyItem.m; sourceTree = ""; }; + D0AF322A1FACA1A80097362B /* libstdc++.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = "libstdc++.tbd"; path = "usr/lib/libstdc++.tbd"; sourceTree = SDKROOT; }; + D0AF322D1FACBA270097362B /* TGShareLocationSignals.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGShareLocationSignals.m; sourceTree = ""; }; + D0AF322E1FACBA270097362B /* TGShareLocationSignals.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGShareLocationSignals.h; sourceTree = ""; }; + D0B2F737204F4C9900D3BFB9 /* Widget.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = Widget.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + D0B2F74E204F4D6100D3BFB9 /* Display.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Display.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D0B2F74F204F4D6100D3BFB9 /* Postbox.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Postbox.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D0B2F750204F4D6100D3BFB9 /* SwiftSignalKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SwiftSignalKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D0B2F751204F4D6100D3BFB9 /* TelegramCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = TelegramCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D0B2F752204F4D6100D3BFB9 /* TelegramUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = TelegramUI.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D0B2F754204F4DF300D3BFB9 /* AsyncDisplayKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = AsyncDisplayKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D0B2F759204F4EF400D3BFB9 /* Widget-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Widget-Bridging-Header.h"; sourceTree = ""; }; + D0B2F75A204F51E400D3BFB9 /* Widget-AppStore.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = "Widget-AppStore.entitlements"; sourceTree = ""; }; + D0B2F75B204F51E500D3BFB9 /* Widget-HockeyApp.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = "Widget-HockeyApp.entitlements"; sourceTree = ""; }; + D0B2F75F2050102600D3BFB9 /* PeerNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeerNode.swift; sourceTree = ""; }; + D0B3B53A21666C0000FC60A0 /* LegacyFileImport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyFileImport.swift; sourceTree = ""; }; + D0B844591DACF507005F29E1 /* HockeySDK.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = HockeySDK.framework; path = "third-party/HockeySDK.framework"; sourceTree = ""; }; + D0B8445A1DACF507005F29E1 /* HockeySDKResources.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; name = HockeySDKResources.bundle; path = "third-party/HockeySDKResources.bundle"; sourceTree = ""; }; + D0B8445F1DACF561005F29E1 /* libc++.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = "libc++.tbd"; path = "usr/lib/libc++.tbd"; sourceTree = SDKROOT; }; + D0BEAF721E54C9A900BD963D /* ApplicationContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ApplicationContext.swift; sourceTree = ""; }; + D0C2DFF51CC4D1B20044FF83 /* AssetsLibrary.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AssetsLibrary.framework; path = System/Library/Frameworks/AssetsLibrary.framework; sourceTree = SDKROOT; }; + D0C2DFF71CC4D1BA0044FF83 /* MobileCoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MobileCoreServices.framework; path = System/Library/Frameworks/MobileCoreServices.framework; sourceTree = SDKROOT; }; + D0C2DFF91CC4D1C90044FF83 /* QuickLook.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuickLook.framework; path = System/Library/Frameworks/QuickLook.framework; sourceTree = SDKROOT; }; + D0C50E451E9459BF00F62E39 /* libopus.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libopus.a; path = "submodules/TelegramUI/third-party/opus/lib/libopus.a"; sourceTree = ""; }; + D0CAF2F21D75FFAB0011F558 /* MtProtoKitDynamic.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = MtProtoKitDynamic.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D0CAF3171D76394C0011F558 /* TelegramCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = TelegramCore.framework; path = "submodules/TelegramCore/build/Debug-iphoneos/TelegramCore.framework"; sourceTree = ""; }; + D0CD17B41CC3AE14007C5650 /* AsyncDisplayKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = AsyncDisplayKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D0CE6F1A213ED11100BCD44B /* TGPresentationAutoNightPreferences.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TGPresentationAutoNightPreferences.h; sourceTree = ""; }; + D0CE6F1B213ED11100BCD44B /* TGPresentationAutoNightPreferences.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TGPresentationAutoNightPreferences.m; sourceTree = ""; }; + D0CE6F1F213EDA4200BCD44B /* pt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pt; path = Localizable.strings; sourceTree = ""; }; + D0CE6F21213EDA4200BCD44B /* pt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pt; path = InfoPlist.strings; sourceTree = ""; }; + D0CE6F23213EDA4200BCD44B /* pt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = pt; path = AppIntentVocabulary.plist; sourceTree = ""; }; + D0CE6F26213EDA4300BCD44B /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = Localizable.strings; sourceTree = ""; }; + D0CE6F28213EDA4300BCD44B /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = InfoPlist.strings; sourceTree = ""; }; + D0CE6F2A213EDA4300BCD44B /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = it; path = AppIntentVocabulary.plist; sourceTree = ""; }; + D0CE6F2D213EDA4300BCD44B /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = Localizable.strings; sourceTree = ""; }; + D0CE6F2F213EDA4300BCD44B /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = InfoPlist.strings; sourceTree = ""; }; + D0CE6F31213EDA4300BCD44B /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = ko; path = AppIntentVocabulary.plist; sourceTree = ""; }; + D0CE6F34213EDA4300BCD44B /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = Localizable.strings; sourceTree = ""; }; + D0CE6F36213EDA4300BCD44B /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = InfoPlist.strings; sourceTree = ""; }; + D0CE6F38213EDA4300BCD44B /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = nl; path = AppIntentVocabulary.plist; sourceTree = ""; }; + D0CE6F3B213EDA4300BCD44B /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = Localizable.strings; sourceTree = ""; }; + D0CE6F3D213EDA4300BCD44B /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = InfoPlist.strings; sourceTree = ""; }; + D0CE6F3F213EDA4300BCD44B /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = ru; path = AppIntentVocabulary.plist; sourceTree = ""; }; + D0CE6F42213EDA4400BCD44B /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = Localizable.strings; sourceTree = ""; }; + D0CE6F44213EDA4400BCD44B /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = InfoPlist.strings; sourceTree = ""; }; + D0CE6F46213EDA4400BCD44B /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = es; path = AppIntentVocabulary.plist; sourceTree = ""; }; + D0CE6F49213EDA4400BCD44B /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = Localizable.strings; sourceTree = ""; }; + D0CE6F4B213EDA4400BCD44B /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = InfoPlist.strings; sourceTree = ""; }; + D0CE6F4D213EDA4400BCD44B /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = ar; path = AppIntentVocabulary.plist; sourceTree = ""; }; + D0CE6F50213EDA4400BCD44B /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = Localizable.strings; sourceTree = ""; }; + D0CE6F52213EDA4400BCD44B /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = InfoPlist.strings; sourceTree = ""; }; + D0CE6F54213EDA4400BCD44B /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = de; path = AppIntentVocabulary.plist; sourceTree = ""; }; + D0CFBB921FD88C2900B65C0D /* begin_record.caf */ = {isa = PBXFileReference; lastKnownFileType = file; name = begin_record.caf; path = "Telegram-iOS/Resources/begin_record.caf"; sourceTree = ""; }; + D0D17E891CAAD66600C4750B /* Accelerate.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Accelerate.framework; path = System/Library/Frameworks/Accelerate.framework; sourceTree = SDKROOT; }; + D0D2276E212739120028F943 /* LegacyDataImport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyDataImport.swift; sourceTree = ""; }; + D0D268761D79A70A00C422DA /* SiriIntents.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = SiriIntents.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + D0D268781D79A70A00C422DA /* IntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntentHandler.swift; sourceTree = ""; }; + D0D2687A1D79A70A00C422DA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + D0D268801D79A70A00C422DA /* IntentsUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = IntentsUI.framework; path = System/Library/Frameworks/IntentsUI.framework; sourceTree = SDKROOT; }; + D0D268831D79A70A00C422DA /* IntentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntentViewController.swift; sourceTree = ""; }; + D0D268861D79A70A00C422DA /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = ""; }; + D0D268881D79A70A00C422DA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + D0D268971D79AF1B00C422DA /* SiriIntents-AppStore.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "SiriIntents-AppStore.entitlements"; sourceTree = ""; }; + D0D268981D79AF3900C422DA /* SiriIntentsUI.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SiriIntentsUI.entitlements; sourceTree = ""; }; + D0E3A7071B285B5000A402D9 /* Telegram-iOS-Hockeyapp.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Telegram-iOS-Hockeyapp.entitlements"; sourceTree = ""; }; + D0E41A381D65A69C00FBFC00 /* NotificationCenter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NotificationCenter.framework; path = System/Library/Frameworks/NotificationCenter.framework; sourceTree = SDKROOT; }; + D0E41A3B1D65A69C00FBFC00 /* TodayViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodayViewController.swift; sourceTree = ""; }; + D0E41A3E1D65A69C00FBFC00 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = ""; }; + D0E41A401D65A69C00FBFC00 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + D0E8B8A82044496B00605593 /* voip_connecting.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; name = voip_connecting.mp3; path = "Telegram-iOS/Resources/voip_connecting.mp3"; sourceTree = ""; }; + D0E8B8A92044496C00605593 /* voip_end.caf */ = {isa = PBXFileReference; lastKnownFileType = file; name = voip_end.caf; path = "Telegram-iOS/Resources/voip_end.caf"; sourceTree = ""; }; + D0E8B8AA2044496C00605593 /* voip_fail.caf */ = {isa = PBXFileReference; lastKnownFileType = file; name = voip_fail.caf; path = "Telegram-iOS/Resources/voip_fail.caf"; sourceTree = ""; }; + D0E8B8AB2044496C00605593 /* voip_ringback.caf */ = {isa = PBXFileReference; lastKnownFileType = file; name = voip_ringback.caf; path = "Telegram-iOS/Resources/voip_ringback.caf"; sourceTree = ""; }; + D0E8B8AC2044496C00605593 /* voip_busy.caf */ = {isa = PBXFileReference; lastKnownFileType = file; name = voip_busy.caf; path = "Telegram-iOS/Resources/voip_busy.caf"; sourceTree = ""; }; + D0EA97961FE8536900792DD6 /* SiriIntents-Hockeyapp.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = "SiriIntents-Hockeyapp.entitlements"; sourceTree = ""; }; + D0EA97971FE8537000792DD6 /* Share-HockeyApp.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = "Share-HockeyApp.entitlements"; sourceTree = ""; }; + D0EA97981FE8537000792DD6 /* Share-AppStore.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = "Share-AppStore.entitlements"; sourceTree = ""; }; + D0EB243A201B77C400F6CC13 /* ClearNotificationsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClearNotificationsManager.swift; sourceTree = ""; }; + D0ECCB7C1FE9C38500609802 /* Telegram-iOS UITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Telegram-iOS UITests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + D0ECCB7E1FE9C38500609802 /* Telegram_iOS_UITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Telegram_iOS_UITests.swift; sourceTree = ""; }; + D0ECCB801FE9C38500609802 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + D0ECCB891FE9C4AC00609802 /* SnapshotHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnapshotHelper.swift; sourceTree = ""; }; + D0ECCB8C1FE9CE3F00609802 /* SnapshotChatList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnapshotChatList.swift; sourceTree = ""; }; + D0F575122083B96B00F1C1E1 /* CloudKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CloudKit.framework; path = System/Library/Frameworks/CloudKit.framework; sourceTree = SDKROOT; }; + D0FC1947201D2DA700FEDBB2 /* SFCompactRounded-Semibold.otf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "SFCompactRounded-Semibold.otf"; path = "Telegram-iOS/Resources/SFCompactRounded-Semibold.otf"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 09C56F942172797400BDF00F /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 0972C6E021791D950069E98A /* UserNotifications.framework in Frameworks */, + 09CFB212217299E80083F7A3 /* CoreLocation.framework in Frameworks */, + 09C5723D21728C0E00BDF00F /* CoreGraphics.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D00859991B28189D00EAF753 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 09C50E88217385CF009E676F /* WatchConnectivity.framework in Frameworks */, + 09FDAEE62140477F00BF856F /* MtProtoKitDynamic.framework in Frameworks */, + D01A47551F4DBED700383CC1 /* HockeySDK.framework in Frameworks */, + D0A18D631E149043004C6734 /* PushKit.framework in Frameworks */, + D0B844601DACF561005F29E1 /* libc++.tbd in Frameworks */, + D06706621D5118F500DED3E3 /* TelegramCore.framework in Frameworks */, + D0C2DFF81CC4D1BA0044FF83 /* MobileCoreServices.framework in Frameworks */, + D0CD17B51CC3AE14007C5650 /* AsyncDisplayKit.framework in Frameworks */, + D0D17E8A1CAAD66600C4750B /* Accelerate.framework in Frameworks */, + D0F575132083B96B00F1C1E1 /* CloudKit.framework in Frameworks */, + D0B4AF8F1EC122A700D51FF6 /* TelegramUI.framework in Frameworks */, + D096C2BE1CC3C021006D814E /* Display.framework in Frameworks */, + D055BD441B7E216400F06C0A /* MapKit.framework in Frameworks */, + D09A59601B5858DB00FC3724 /* SystemConfiguration.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D00859AE1B28189D00EAF753 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D02CF5F9215D9ABE00E0F56A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D051DB0B215E5D1C00F30F92 /* TelegramUI.framework in Frameworks */, + D02CF615215DA24900E0F56A /* Display.framework in Frameworks */, + D02CF617215DA24900E0F56A /* Postbox.framework in Frameworks */, + D02CF619215DA24900E0F56A /* SwiftSignalKit.framework in Frameworks */, + D02CF61B215DA24900E0F56A /* TelegramCore.framework in Frameworks */, + D02CF5FE215D9ABF00E0F56A /* UserNotificationsUI.framework in Frameworks */, + D02CF5FD215D9ABF00E0F56A /* UserNotifications.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D03B0E751D63484500955575 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D0AF322C1FACA1B00097362B /* libc++.tbd in Frameworks */, + D0AF32291FACA1920097362B /* Accelerate.framework in Frameworks */, + D08410501FABDD54008FFE92 /* MtProtoKitDynamic.framework in Frameworks */, + D08410481FABDCF2008FFE92 /* LegacyComponents.framework in Frameworks */, + D05F63721EC124D90004BE28 /* TelegramUI.framework in Frameworks */, + D03B0E8A1D634B1100955575 /* Display.framework in Frameworks */, + D03B0E8B1D634B1100955575 /* SwiftSignalKit.framework in Frameworks */, + D03B0E8C1D634B1100955575 /* TelegramCore.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D0B2F734204F4C9900D3BFB9 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D0B2F74A204F4D6100D3BFB9 /* Postbox.framework in Frameworks */, + D0B2F74B204F4D6100D3BFB9 /* SwiftSignalKit.framework in Frameworks */, + D0B2F74C204F4D6100D3BFB9 /* TelegramCore.framework in Frameworks */, + D0B2F738204F4C9900D3BFB9 /* NotificationCenter.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D0D268731D79A70A00C422DA /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D08985042118B46F00918162 /* SwiftSignalKit.framework in Frameworks */, + D08984FE2118B3F100918162 /* MtProtoKitDynamic.framework in Frameworks */, + D08985002118B3F100918162 /* Postbox.framework in Frameworks */, + D08985022118B3F100918162 /* TelegramCore.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D0ECCB791FE9C38500609802 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 092F368121542CE4001A9F49 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 092F368221542CF2001A9F49 /* en.lproj */, + ); + path = "Supporting Files"; + sourceTree = ""; + }; + 092F368221542CF2001A9F49 /* en.lproj */ = { + isa = PBXGroup; + children = ( + 092F368321542D6C001A9F49 /* Localizable.strings */, + ); + path = en.lproj; + sourceTree = ""; + }; + 0972C6E121792CED0069E98A /* ru.lproj */ = { + isa = PBXGroup; + children = ( + 0972C6E221792D120069E98A /* InfoPlist.strings */, + ); + path = ru.lproj; + sourceTree = ""; + }; + 09C50E7821738150009E676F /* Watch */ = { + isa = PBXGroup; + children = ( + 0956AF2B217B4642008106D0 /* WatchCommunicationManager.swift */, + 09C50E892173AEDB009E676F /* WatchRequestHandlers.swift */, + 09D304212174335F00C00567 /* WatchBridge.swift */, + 09C50E7921738178009E676F /* TGBridgeServer.h */, + 09C50E7A21738178009E676F /* TGBridgeServer.m */, + ); + name = Watch; + sourceTree = ""; + }; + 09C56F8C2172797200BDF00F /* Watch */ = { + isa = PBXGroup; + children = ( + 09C5711E21727B5E00BDF00F /* External */, + 09C56F9B2172797500BDF00F /* Extension */, + 09C5711F21727B7200BDF00F /* Bridge */, + 09C56FB821727A1200BDF00F /* App */, + ); + path = Watch; + sourceTree = ""; + }; + 09C56F9B2172797500BDF00F /* Extension */ = { + isa = PBXGroup; + children = ( + 09C5712421727C0500BDF00F /* Controllers */, + 09C5712521727C0900BDF00F /* Components */, + 09C5712621727C1600BDF00F /* Utils */, + 09C5712721727C2000BDF00F /* Resources */, + 09C5712121727BF800BDF00F /* TGExtensionDelegate.h */, + 09C5712221727BF800BDF00F /* TGExtensionDelegate.m */, + 09C56FA22172797500BDF00F /* Info.plist */, + ); + path = Extension; + sourceTree = ""; + }; + 09C56FB821727A1200BDF00F /* App */ = { + isa = PBXGroup; + children = ( + 09C56F922172797400BDF00F /* Info.plist */, + 09C56F8D2172797200BDF00F /* Interface.storyboard */, + 09C56F902172797400BDF00F /* Assets.xcassets */, + ); + path = App; + sourceTree = ""; + }; + 09C5711E21727B5E00BDF00F /* External */ = { + isa = PBXGroup; + children = ( + 09C5727221728D0000BDF00F /* SAtomic.h */, + 09C5724921728CFD00BDF00F /* SAtomic.m */, + 09C5723F21728CFC00BDF00F /* SBag.h */, + 09C5725321728CFE00BDF00F /* SBag.m */, + 09C5724821728CFD00BDF00F /* SBlockDisposable.h */, + 09C5725921728CFE00BDF00F /* SBlockDisposable.m */, + 09C5724621728CFD00BDF00F /* SDisposable.h */, + 09C5726F21728D0000BDF00F /* SDisposableSet.h */, + 09C5726C21728D0000BDF00F /* SDisposableSet.m */, + 09C5725C21728CFE00BDF00F /* SMetaDisposable.h */, + 09C5724E21728CFD00BDF00F /* SMetaDisposable.m */, + 09C5726121728CFF00BDF00F /* SMulticastSignalManager.h */, + 09C5724D21728CFD00BDF00F /* SMulticastSignalManager.m */, + 09C5726521728CFF00BDF00F /* SQueue.h */, + 09C5726721728CFF00BDF00F /* SQueue.m */, + 09C5726A21728D0000BDF00F /* SSignal.h */, + 09C5726D21728D0000BDF00F /* SSignal.m */, + 09C5724F21728CFD00BDF00F /* SSignal+Accumulate.h */, + 09C5723E21728CFC00BDF00F /* SSignal+Accumulate.m */, + 09C5724221728CFC00BDF00F /* SSignal+Catch.h */, + 09C5724421728CFD00BDF00F /* SSignal+Catch.m */, + 09C5724C21728CFD00BDF00F /* SSignal+Combine.h */, + 09C5724521728CFD00BDF00F /* SSignal+Combine.m */, + 09C5726E21728D0000BDF00F /* SSignal+Dispatch.h */, + 09C5725221728CFE00BDF00F /* SSignal+Dispatch.m */, + 09C5725021728CFD00BDF00F /* SSignal+Mapping.h */, + 09C5727121728D0000BDF00F /* SSignal+Mapping.m */, + 09C5726921728D0000BDF00F /* SSignal+Meta.h */, + 09C5727021728D0000BDF00F /* SSignal+Meta.m */, + 09C5725821728CFE00BDF00F /* SSignal+Multicast.h */, + 09C5725521728CFE00BDF00F /* SSignal+Multicast.m */, + 09C5724021728CFC00BDF00F /* SSignal+Pipe.h */, + 09C5725E21728CFF00BDF00F /* SSignal+Pipe.m */, + 09C5726421728CFF00BDF00F /* SSignal+SideEffects.h */, + 09C5725A21728CFE00BDF00F /* SSignal+SideEffects.m */, + 09C5726621728CFF00BDF00F /* SSignal+Single.h */, + 09C5726021728CFF00BDF00F /* SSignal+Single.m */, + 09C5724A21728CFD00BDF00F /* SSignal+Take.h */, + 09C5725721728CFE00BDF00F /* SSignal+Take.m */, + 09C5726321728CFF00BDF00F /* SSignal+Timing.h */, + 09C5724321728CFD00BDF00F /* SSignal+Timing.m */, + 09C5725621728CFE00BDF00F /* SSignalKit.h */, + 09C5726821728CFF00BDF00F /* SSubscriber.h */, + 09C5725D21728CFF00BDF00F /* SSubscriber.m */, + 09C5726B21728D0000BDF00F /* SThreadPool.h */, + 09C5727321728D0000BDF00F /* SThreadPool.m */, + 09C5724121728CFC00BDF00F /* SThreadPoolQueue.h */, + 09C5724B21728CFD00BDF00F /* SThreadPoolQueue.m */, + 09C5725121728CFD00BDF00F /* SThreadPoolTask.h */, + 09C5725421728CFE00BDF00F /* SThreadPoolTask.m */, + 09C5725F21728CFF00BDF00F /* STimer.h */, + 09C5726221728CFF00BDF00F /* STimer.m */, + 09C5724721728CFD00BDF00F /* SVariable.h */, + 09C5725B21728CFE00BDF00F /* SVariable.m */, + ); + path = External; + sourceTree = ""; + }; + 09C5711F21727B7200BDF00F /* Bridge */ = { + isa = PBXGroup; + children = ( + 09C57290217291BC00BDF00F /* Model */, + 09C5728E217291AC00BDF00F /* Signals */, + 09C573362172974E00BDF00F /* TGBridgePeerIdAdapter.h */, + 09C572CC2172939F00BDF00F /* TGBridgeClient.h */, + 09C572D02172939F00BDF00F /* TGBridgeClient.m */, + 09C572CF2172939F00BDF00F /* TGBridgeCommon.h */, + 09C572CE2172939F00BDF00F /* TGBridgeCommon.m */, + 09C50E902173B247009E676F /* TGBridgeSubscriptions.h */, + 09C50E8F2173B247009E676F /* TGBridgeSubscriptions.m */, + ); + name = Bridge; + sourceTree = ""; + }; + 09C5712421727C0500BDF00F /* Controllers */ = { + isa = PBXGroup; + children = ( + 09C5716B21727F8000BDF00F /* Audio */, + 09C5716C21727F8A00BDF00F /* External */, + 09C5716D21727F9300BDF00F /* Common */, + 09C5717021727FB600BDF00F /* Chat */, + 09C5716F21727FAF00BDF00F /* Chat List */, + 09C5717321727FC500BDF00F /* Message */, + 09C5717521727FD600BDF00F /* Profile */, + 09C5717121727FBA00BDF00F /* Compose */, + 09C5716E21727FA700BDF00F /* Contacts */, + 09C5717221727FC100BDF00F /* Location */, + 09C5717621727FDA00BDF00F /* Stickers */, + 09C5717421727FD100BDF00F /* Bots */, + ); + name = Controllers; + sourceTree = ""; + }; + 09C5712521727C0900BDF00F /* Components */ = { + isa = PBXGroup; + children = ( + 09C5715B21727ED400BDF00F /* Interface */, + 09C5715C21727EE600BDF00F /* TGBridgeUserCache.h */, + 09C5715E21727EE700BDF00F /* TGBridgeUserCache.m */, + 09C5715D21727EE700BDF00F /* TGFileCache.h */, + 09C5715F21727EE700BDF00F /* TGFileCache.m */, + ); + name = Components; + sourceTree = ""; + }; + 09C5712621727C1600BDF00F /* Utils */ = { + isa = PBXGroup; + children = ( + 09C5713621727CFC00BDF00F /* TGDateUtils.h */, + 09C5713921727CFD00BDF00F /* TGDateUtils.m */, + 09C5712D21727CFC00BDF00F /* TGGeometry.h */, + 09C5712F21727CFC00BDF00F /* TGGeometry.m */, + 09C5713021727CFC00BDF00F /* TGIndexPath.h */, + 09C5713D21727CFD00BDF00F /* TGIndexPath.m */, + 09C5713321727CFC00BDF00F /* TGLocationUtils.h */, + 09C5712E21727CFC00BDF00F /* TGLocationUtils.m */, + 09C5712821727CFB00BDF00F /* TGStringUtils.h */, + 09C5713A21727CFD00BDF00F /* TGStringUtils.m */, + 09C5713721727CFD00BDF00F /* TGWatchColor.h */, + 09C5713221727CFC00BDF00F /* TGWatchColor.m */, + 09C5713421727CFC00BDF00F /* TGWatchCommon.h */, + 09C5713521727CFC00BDF00F /* TGWatchCommon.m */, + 09C5713821727CFD00BDF00F /* WKInterface+TGInterface.h */, + 09C5713B21727CFD00BDF00F /* WKInterface+TGInterface.m */, + 09C5713C21727CFD00BDF00F /* WKInterfaceGroup+Signals.h */, + 09C5712C21727CFC00BDF00F /* WKInterfaceGroup+Signals.m */, + 09C5712921727CFB00BDF00F /* WKInterfaceImage+Signals.h */, + 09C5712B21727CFC00BDF00F /* WKInterfaceImage+Signals.m */, + ); + name = Utils; + sourceTree = ""; + }; + 09C5712721727C2000BDF00F /* Resources */ = { + isa = PBXGroup; + children = ( + 09C5714E21727DD900BDF00F /* File@2x.png */, + 09C5714D21727DD900BDF00F /* Location@2x.png */, + 09C5714B21727DD900BDF00F /* MediaAudio@2x.png */, + 09C5714F21727DD900BDF00F /* MediaDocument@2x.png */, + 09C5715021727DD900BDF00F /* MediaLocation@2x.png */, + 09C5714C21727DD900BDF00F /* MediaPhoto@2x.png */, + 09C5715121727DD900BDF00F /* MediaVideo@2x.png */, + 09C5715221727DD900BDF00F /* VerifiedList@2x.png */, + ); + path = Resources; + sourceTree = ""; + }; + 09C5715B21727ED400BDF00F /* Interface */ = { + isa = PBXGroup; + children = ( + 09C5712321727BF800BDF00F /* TGInterfaceController.h */, + 09C5712021727BF800BDF00F /* TGInterfaceController.m */, + 09C5716421727F1500BDF00F /* TGInputController.h */, + 09C5716321727F1500BDF00F /* TGInputController.m */, + 09C5716521727F1500BDF00F /* TGInterfaceMenu.h */, + 09C5716621727F1500BDF00F /* TGInterfaceMenu.m */, + 09C5716721727F1500BDF00F /* TGTableDeltaUpdater.h */, + 09C5716221727F1500BDF00F /* TGTableDeltaUpdater.m */, + 09C5712A21727CFC00BDF00F /* WKInterfaceTable+TGDataDrivenTable.h */, + 09C5713121727CFC00BDF00F /* WKInterfaceTable+TGDataDrivenTable.m */, + ); + name = Interface; + sourceTree = ""; + }; + 09C5716B21727F8000BDF00F /* Audio */ = { + isa = PBXGroup; + children = ( + 09C5717721727FE900BDF00F /* TGAudioMicAlertController.h */, + 09C5717821727FE900BDF00F /* TGAudioMicAlertController.m */, + ); + name = Audio; + sourceTree = ""; + }; + 09C5716C21727F8A00BDF00F /* External */ = { + isa = PBXGroup; + children = ( + 09C5717A2172800800BDF00F /* TGComplicationController.h */, + 09C5717C2172800800BDF00F /* TGComplicationController.m */, + 09C571792172800800BDF00F /* TGNotificationController.h */, + 09C5717B2172800800BDF00F /* TGNotificationController.m */, + ); + name = External; + sourceTree = ""; + }; + 09C5716D21727F9300BDF00F /* Common */ = { + isa = PBXGroup; + children = ( + 09C5717F2172802400BDF00F /* Views */, + 09C5717E2172802200BDF00F /* TGUserRowController.h */, + 09C5717D2172802200BDF00F /* TGUserRowController.m */, + ); + name = Common; + sourceTree = ""; + }; + 09C5716E21727FA700BDF00F /* Contacts */ = { + isa = PBXGroup; + children = ( + 09C571A82172865300BDF00F /* TGContactsController.h */, + 09C571A92172865400BDF00F /* TGContactsController.m */, + ); + name = Contacts; + sourceTree = ""; + }; + 09C5716F21727FAF00BDF00F /* Chat List */ = { + isa = PBXGroup; + children = ( + 09C5719D217285AD00BDF00F /* Views */, + 09C57199217280E500BDF00F /* TGNeoChatsController.h */, + 09C5719A217280E500BDF00F /* TGNeoChatsController.m */, + ); + name = "Chat List"; + sourceTree = ""; + }; + 09C5717021727FB600BDF00F /* Chat */ = { + isa = PBXGroup; + children = ( + 09C5719C2172859F00BDF00F /* Views */, + 09C571A52172861600BDF00F /* TGNeoConversationController.h */, + 09C571A42172861600BDF00F /* TGNeoConversationController.m */, + ); + name = Chat; + sourceTree = ""; + }; + 09C5717121727FBA00BDF00F /* Compose */ = { + isa = PBXGroup; + children = ( + 09C571A72172863D00BDF00F /* TGComposeController.h */, + 09C571A62172863D00BDF00F /* TGComposeController.m */, + ); + name = Compose; + sourceTree = ""; + }; + 09C5717221727FC100BDF00F /* Location */ = { + isa = PBXGroup; + children = ( + 09C571AA2172866B00BDF00F /* Views */, + 09C571AB2172867400BDF00F /* TGLocationController.h */, + 09C571AC2172867400BDF00F /* TGLocationController.m */, + ); + name = Location; + sourceTree = ""; + }; + 09C5717321727FC500BDF00F /* Message */ = { + isa = PBXGroup; + children = ( + 09C571B2217286AF00BDF00F /* Views */, + 09C571B4217286BA00BDF00F /* TGMessageViewController.h */, + 09C571B3217286BA00BDF00F /* TGMessageViewController.m */, + ); + name = Message; + sourceTree = ""; + }; + 09C5717421727FD100BDF00F /* Bots */ = { + isa = PBXGroup; + children = ( + 09C571B1217286A300BDF00F /* Views */, + 09C571F5217287EF00BDF00F /* TGBotCommandController.h */, + 09C571F6217287EF00BDF00F /* TGBotCommandController.m */, + 09C571F7217287F000BDF00F /* TGBotKeyboardController.h */, + 09C571F8217287F000BDF00F /* TGBotKeyboardController.m */, + ); + name = Bots; + sourceTree = ""; + }; + 09C5717521727FD600BDF00F /* Profile */ = { + isa = PBXGroup; + children = ( + 09C571BB2172870E00BDF00F /* Views */, + 09C571BE2172872B00BDF00F /* TGGroupInfoController.h */, + 09C571BD2172872B00BDF00F /* TGGroupInfoController.m */, + 09C571BF2172872C00BDF00F /* TGProfilePhotoController.h */, + 09C571C12172872C00BDF00F /* TGProfilePhotoController.m */, + 09C571C22172872C00BDF00F /* TGUserInfoController.h */, + 09C571C02172872C00BDF00F /* TGUserInfoController.m */, + ); + name = Profile; + sourceTree = ""; + }; + 09C5717621727FDA00BDF00F /* Stickers */ = { + isa = PBXGroup; + children = ( + 09C571E72172878900BDF00F /* TGStickerPackRowController.h */, + 09C571E22172878800BDF00F /* TGStickerPackRowController.m */, + 09C571E12172878800BDF00F /* TGStickerPacksController.h */, + 09C571E32172878900BDF00F /* TGStickerPacksController.m */, + 09C571E82172878900BDF00F /* TGStickersController.h */, + 09C571E62172878900BDF00F /* TGStickersController.m */, + 09C571EA2172878900BDF00F /* TGStickersHeaderController.h */, + 09C571DF2172878800BDF00F /* TGStickersHeaderController.m */, + 09C571E02172878800BDF00F /* TGStickersRowController.h */, + 09C571E42172878900BDF00F /* TGStickersRowController.m */, + 09C571E92172878900BDF00F /* TGStickersSectionHeaderController.h */, + 09C571E52172878900BDF00F /* TGStickersSectionHeaderController.m */, + ); + name = Stickers; + sourceTree = ""; + }; + 09C5717F2172802400BDF00F /* Views */ = { + isa = PBXGroup; + children = ( + 09C571852172805700BDF00F /* TGAvatarViewModel.h */, + 09C571882172805700BDF00F /* TGAvatarViewModel.m */, + 09C571842172805700BDF00F /* TGMessageViewModel.h */, + 09C571862172805700BDF00F /* TGMessageViewModel.m */, + 09C5718A2172805700BDF00F /* TGNeoAttachmentViewModel.h */, + 09C571822172805700BDF00F /* TGNeoAttachmentViewModel.m */, + 09C5718C2172805800BDF00F /* TGNeoImageViewModel.h */, + 09C5718D2172805800BDF00F /* TGNeoImageViewModel.m */, + 09C571802172805700BDF00F /* TGNeoLabelViewModel.h */, + 09C571832172805700BDF00F /* TGNeoLabelViewModel.m */, + 09C571812172805700BDF00F /* TGNeoRenderableViewModel.h */, + 09C571892172805700BDF00F /* TGNeoRenderableViewModel.m */, + 09C5718B2172805800BDF00F /* TGNeoViewModel.h */, + 09C571872172805700BDF00F /* TGNeoViewModel.m */, + ); + name = Views; + sourceTree = ""; + }; + 09C5719C2172859F00BDF00F /* Views */ = { + isa = PBXGroup; + children = ( + 09C572232172889200BDF00F /* TGNeoAudioMessageViewModel.h */, + 09C5721F2172889200BDF00F /* TGNeoAudioMessageViewModel.m */, + 09C571FF2172888E00BDF00F /* TGNeoBackgroundViewModel.h */, + 09C572202172889200BDF00F /* TGNeoBackgroundViewModel.m */, + 09C572122172889000BDF00F /* TGNeoBubbleMessageViewModel.h */, + 09C572072172888F00BDF00F /* TGNeoBubbleMessageViewModel.m */, + 09C5721D2172889100BDF00F /* TGNeoContactMessageViewModel.h */, + 09C5720D2172889000BDF00F /* TGNeoContactMessageViewModel.m */, + 09C5721C2172889100BDF00F /* TGNeoConversationMediaRowController.h */, + 09C572162172889100BDF00F /* TGNeoConversationMediaRowController.m */, + 09C5721A2172889100BDF00F /* TGNeoConversationSimpleRowController.h */, + 09C572042172888F00BDF00F /* TGNeoConversationSimpleRowController.m */, + 09C572092172888F00BDF00F /* TGNeoConversationStaticRowController.h */, + 09C572002172888E00BDF00F /* TGNeoConversationStaticRowController.m */, + 09C572142172889100BDF00F /* TGNeoConversationTimeRowController.h */, + 09C572102172889000BDF00F /* TGNeoConversationTimeRowController.m */, + 09C572112172889000BDF00F /* TGNeoFileMessageViewModel.h */, + 09C572012172888E00BDF00F /* TGNeoFileMessageViewModel.m */, + 09C572032172888F00BDF00F /* TGNeoForwardHeaderViewModel.h */, + 09C5721B2172889100BDF00F /* TGNeoForwardHeaderViewModel.m */, + 09C5720B2172889000BDF00F /* TGNeoMediaMessageViewModel.h */, + 09C5720F2172889000BDF00F /* TGNeoMediaMessageViewModel.m */, + 09C572052172888F00BDF00F /* TGNeoMessageViewModel.h */, + 09C572172172889100BDF00F /* TGNeoMessageViewModel.m */, + 09C572182172889100BDF00F /* TGNeoReplyHeaderViewModel.h */, + 09C5720E2172889000BDF00F /* TGNeoReplyHeaderViewModel.m */, + 09C572212172889200BDF00F /* TGNeoRowController.h */, + 09C5720A2172888F00BDF00F /* TGNeoRowController.m */, + 09C572192172889100BDF00F /* TGNeoServiceMessageViewModel.h */, + 09C5720C2172889000BDF00F /* TGNeoServiceMessageViewModel.m */, + 09C572242172889200BDF00F /* TGNeoSmiliesMessageViewModel.h */, + 09C572132172889000BDF00F /* TGNeoSmiliesMessageViewModel.m */, + 09C572022172888F00BDF00F /* TGNeoStickerMessageViewModel.h */, + 09C572062172888F00BDF00F /* TGNeoStickerMessageViewModel.m */, + 09C572222172889200BDF00F /* TGNeoTextMessageViewModel.h */, + 09C572152172889100BDF00F /* TGNeoTextMessageViewModel.m */, + 09C5721E2172889200BDF00F /* TGNeoVenueMessageViewModel.h */, + 09C572082172888F00BDF00F /* TGNeoVenueMessageViewModel.m */, + 09C571FB2172882D00BDF00F /* TGConversationFooterController.h */, + 09C571FC2172882D00BDF00F /* TGConversationFooterController.m */, + 09C571F92172880900BDF00F /* TGChatInfo.h */, + 09C571FA2172880900BDF00F /* TGChatInfo.m */, + 09C571FD2172883E00BDF00F /* TGChatTimestamp.h */, + 09C571FE2172883E00BDF00F /* TGChatTimestamp.m */, + 09C571A22172860000BDF00F /* TGNeoConversationRowController.h */, + 09C571A32172860000BDF00F /* TGNeoConversationRowController.m */, + 0956AF2D217B8109008106D0 /* TGNeoUnsupportedMessageViewModel.h */, + 0956AF2E217B8109008106D0 /* TGNeoUnsupportedMessageViewModel.m */, + ); + name = Views; + sourceTree = ""; + }; + 09C5719D217285AD00BDF00F /* Views */ = { + isa = PBXGroup; + children = ( + 09C571A1217285CE00BDF00F /* TGNeoChatRowController.h */, + 09C571A0217285CE00BDF00F /* TGNeoChatRowController.m */, + 09C5719E217285CE00BDF00F /* TGNeoChatViewModel.h */, + 09C5719F217285CE00BDF00F /* TGNeoChatViewModel.m */, + ); + name = Views; + sourceTree = ""; + }; + 09C571AA2172866B00BDF00F /* Views */ = { + isa = PBXGroup; + children = ( + 09C571AF2172869600BDF00F /* TGLocationMapHeaderController.h */, + 09C571AE2172869500BDF00F /* TGLocationMapHeaderController.m */, + 09C571B02172869600BDF00F /* TGLocationVenueRowController.h */, + 09C571AD2172869500BDF00F /* TGLocationVenueRowController.m */, + ); + name = Views; + sourceTree = ""; + }; + 09C571B1217286A300BDF00F /* Views */ = { + isa = PBXGroup; + children = ( + 09C571F3217287E500BDF00F /* TGBotKeyboardButtonController.h */, + 09C571F4217287E500BDF00F /* TGBotKeyboardButtonController.m */, + ); + name = Views; + sourceTree = ""; + }; + 09C571B2217286AF00BDF00F /* Views */ = { + isa = PBXGroup; + children = ( + 09C571B7217286D700BDF00F /* TGMessageViewFooterController.h */, + 09C571B9217286D700BDF00F /* TGMessageViewFooterController.m */, + 09C571BA217286D700BDF00F /* TGMessageViewMessageRowController.h */, + 09C571B5217286D700BDF00F /* TGMessageViewMessageRowController.m */, + 09C571B8217286D700BDF00F /* TGMessageViewWebPageRowController.h */, + 09C571B6217286D700BDF00F /* TGMessageViewWebPageRowController.m */, + ); + name = Views; + sourceTree = ""; + }; + 09C571BB2172870E00BDF00F /* Views */ = { + isa = PBXGroup; + children = ( + 09C571CB2172874500BDF00F /* TGGroupInfoFooterController.h */, + 09C571C72172874500BDF00F /* TGGroupInfoFooterController.m */, + 09C571C52172874500BDF00F /* TGGroupInfoHeaderController.h */, + 09C571C42172874500BDF00F /* TGGroupInfoHeaderController.m */, + 09C571C62172874500BDF00F /* TGUserHandle.h */, + 09C571C92172874500BDF00F /* TGUserHandle.m */, + 09C571C32172874500BDF00F /* TGUserHandleRowController.h */, + 09C571CA2172874500BDF00F /* TGUserHandleRowController.m */, + 09C571C82172874500BDF00F /* TGUserInfoHeaderController.h */, + 09C571CC2172874600BDF00F /* TGUserInfoHeaderController.m */, + ); + name = Views; + sourceTree = ""; + }; + 09C5728E217291AC00BDF00F /* Signals */ = { + isa = PBXGroup; + children = ( + 09C572B1217292B900BDF00F /* TGBridgeAudioSignals.h */, + 09C572C1217292BA00BDF00F /* TGBridgeAudioSignals.m */, + 09C572B2217292B900BDF00F /* TGBridgeBotSignals.h */, + 09C572BB217292B900BDF00F /* TGBridgeBotSignals.m */, + 09C572BF217292BA00BDF00F /* TGBridgeChatListSignals.h */, + 09C572AF217292B900BDF00F /* TGBridgeChatListSignals.m */, + 09C572B9217292B900BDF00F /* TGBridgeChatMessageListSignals.h */, + 09C572C6217292BA00BDF00F /* TGBridgeChatMessageListSignals.m */, + 09C572B7217292B900BDF00F /* TGBridgeContactsSignals.h */, + 09C572BE217292BA00BDF00F /* TGBridgeContactsSignals.m */, + 09C572C8217292BA00BDF00F /* TGBridgeConversationSignals.h */, + 09C572B0217292B900BDF00F /* TGBridgeConversationSignals.m */, + 09C572BC217292B900BDF00F /* TGBridgeLocationSignals.h */, + 09C572C4217292BA00BDF00F /* TGBridgeLocationSignals.m */, + 09C572C7217292BA00BDF00F /* TGBridgeMediaSignals.h */, + 09C572C5217292BA00BDF00F /* TGBridgeMediaSignals.m */, + 09C572B4217292B900BDF00F /* TGBridgePeerSettingsSignals.h */, + 09C572C3217292BA00BDF00F /* TGBridgePeerSettingsSignals.m */, + 09C572C2217292BA00BDF00F /* TGBridgePresetsSignals.h */, + 09C572AE217292B900BDF00F /* TGBridgePresetsSignals.m */, + 09C572C0217292BA00BDF00F /* TGBridgeRemoteSignals.h */, + 09C572AD217292B800BDF00F /* TGBridgeRemoteSignals.m */, + 09C572BA217292B900BDF00F /* TGBridgeSendMessageSignals.h */, + 09C572B5217292B900BDF00F /* TGBridgeSendMessageSignals.m */, + 09C572B6217292B900BDF00F /* TGBridgeStateSignal.h */, + 09C572C9217292BB00BDF00F /* TGBridgeStateSignal.m */, + 09C572B3217292B900BDF00F /* TGBridgeStickersSignals.h */, + 09C572B8217292B900BDF00F /* TGBridgeStickersSignals.m */, + 09C572BD217292BA00BDF00F /* TGBridgeUserInfoSignals.h */, + 09C572CA217292BB00BDF00F /* TGBridgeUserInfoSignals.m */, + ); + name = Signals; + sourceTree = ""; + }; + 09C57290217291BC00BDF00F /* Model */ = { + isa = PBXGroup; + children = ( + 09C572CB2172938100BDF00F /* Media */, + 09C572EA2172953400BDF00F /* TGBridgeBotCommandInfo.h */, + 09C572EF2172953500BDF00F /* TGBridgeBotCommandInfo.m */, + 09C572E82172953400BDF00F /* TGBridgeBotInfo.h */, + 09C573102172953800BDF00F /* TGBridgeBotInfo.m */, + 09C572DD2172953300BDF00F /* TGBridgeBotReplyMarkup.h */, + 09C572D92172953300BDF00F /* TGBridgeBotReplyMarkup.m */, + 09C572F12172953500BDF00F /* TGBridgeChat.h */, + 09C573092172953700BDF00F /* TGBridgeChat.m */, + 09C572DB2172953300BDF00F /* TGBridgeChat+TGTableItem.h */, + 09C572F62172953500BDF00F /* TGBridgeChat+TGTableItem.m */, + 09C573142172953800BDF00F /* TGBridgeChatMessages.h */, + 09C5730C2172953700BDF00F /* TGBridgeChatMessages.m */, + 09C573032172953600BDF00F /* TGBridgeContext.h */, + 09C573072172953700BDF00F /* TGBridgeContext.m */, + 09C5730B2172953700BDF00F /* TGBridgeLocationVenue.h */, + 09C572F32172953500BDF00F /* TGBridgeLocationVenue.m */, + 09C573152172953800BDF00F /* TGBridgeLocationVenue+TGTableItem.h */, + 09C572FF2172953600BDF00F /* TGBridgeLocationVenue+TGTableItem.m */, + 09C572D62172953200BDF00F /* TGBridgeMediaAttachment.h */, + 09C572F92172953600BDF00F /* TGBridgeMediaAttachment.m */, + 09C573042172953700BDF00F /* TGBridgeMessage.h */, + 09C572DF2172953300BDF00F /* TGBridgeMessage.m */, + 09C572F72172953500BDF00F /* TGBridgeMessage+TGTableItem.h */, + 09C572D72172953300BDF00F /* TGBridgeMessage+TGTableItem.m */, + 09C573022172953600BDF00F /* TGBridgePeerNotificationSettings.h */, + 09C572FC2172953600BDF00F /* TGBridgePeerNotificationSettings.m */, + 09C573132172953800BDF00F /* TGBridgeStickerPack.h */, + 09C572E32172953400BDF00F /* TGBridgeStickerPack.m */, + 09C572EB2172953400BDF00F /* TGBridgeUser.h */, + 09C572ED2172953400BDF00F /* TGBridgeUser.m */, + 09C573052172953700BDF00F /* TGBridgeUser+TGTableItem.h */, + 09C572E92172953400BDF00F /* TGBridgeUser+TGTableItem.m */, + ); + name = Model; + sourceTree = ""; + }; + 09C572CB2172938100BDF00F /* Media */ = { + isa = PBXGroup; + children = ( + 09C572DC2172953300BDF00F /* TGBridgeActionMediaAttachment.h */, + 09C572E22172953300BDF00F /* TGBridgeActionMediaAttachment.m */, + 09C573082172953700BDF00F /* TGBridgeAudioMediaAttachment.h */, + 09C572E02172953300BDF00F /* TGBridgeAudioMediaAttachment.m */, + 09C572DA2172953300BDF00F /* TGBridgeContactMediaAttachment.h */, + 09C572DE2172953300BDF00F /* TGBridgeContactMediaAttachment.m */, + 09C572F02172953500BDF00F /* TGBridgeDocumentMediaAttachment.h */, + 09C572FB2172953600BDF00F /* TGBridgeDocumentMediaAttachment.m */, + 09C572FE2172953600BDF00F /* TGBridgeForwardedMessageMediaAttachment.h */, + 09C572E62172953400BDF00F /* TGBridgeForwardedMessageMediaAttachment.m */, + 09C572EE2172953400BDF00F /* TGBridgeImageMediaAttachment.h */, + 09C572F22172953500BDF00F /* TGBridgeImageMediaAttachment.m */, + 09C572E72172953400BDF00F /* TGBridgeLocationMediaAttachment.h */, + 09C573002172953600BDF00F /* TGBridgeLocationMediaAttachment.m */, + 09C572F82172953500BDF00F /* TGBridgeMessageEntitiesAttachment.h */, + 09C5730A2172953700BDF00F /* TGBridgeMessageEntitiesAttachment.m */, + 09C572E12172953300BDF00F /* TGBridgeReplyMarkupMediaAttachment.h */, + 09C5730D2172953800BDF00F /* TGBridgeReplyMarkupMediaAttachment.m */, + 09C572EC2172953400BDF00F /* TGBridgeReplyMessageMediaAttachment.h */, + 09C572FD2172953600BDF00F /* TGBridgeReplyMessageMediaAttachment.m */, + 09C573112172953800BDF00F /* TGBridgeUnsupportedMediaAttachment.h */, + 09C5730F2172953800BDF00F /* TGBridgeUnsupportedMediaAttachment.m */, + 09C573062172953700BDF00F /* TGBridgeVideoMediaAttachment.h */, + 09C572E42172953400BDF00F /* TGBridgeVideoMediaAttachment.m */, + 09C573122172953800BDF00F /* TGBridgeWebPageMediaAttachment.h */, + 09C572F52172953500BDF00F /* TGBridgeWebPageMediaAttachment.m */, + 09C572FA2172953600BDF00F /* TGBridgeMessageEntities.h */, + 09C572F42172953500BDF00F /* TGBridgeMessageEntities.m */, + ); + name = Media; + sourceTree = ""; + }; + D00859931B28189D00EAF753 = { + isa = PBXGroup; + children = ( + D023EBB31DDB2F0E00BD496D /* Resources */, + D03B0E791D63484500955575 /* Share */, + D0D268771D79A70A00C422DA /* SiriIntents */, + D0D268821D79A70A00C422DA /* SiriIntentsUI */, + D02CF5FF215D9ABF00E0F56A /* NotificationContent */, + 09C56F8C2172797200BDF00F /* Watch */, + D00859C21B281E0000EAF753 /* Frameworks */, + D008599E1B28189D00EAF753 /* Telegram-iOS */, + D0400EE31D5B912E007931CE /* NotificationService */, + D0E41A3A1D65A69C00FBFC00 /* Widget */, + D0ECCB7D1FE9C38500609802 /* Telegram-iOS UITests */, + D00859B41B28189D00EAF753 /* Telegram-iOSTests */, + D008599D1B28189D00EAF753 /* Products */, + ); + sourceTree = ""; + }; + D008599D1B28189D00EAF753 /* Products */ = { + isa = PBXGroup; + children = ( + D008599C1B28189D00EAF753 /* Telegram X.app */, + D00859B11B28189D00EAF753 /* Telegram-iOSTests.xctest */, + D03B0E781D63484500955575 /* Share.appex */, + D0D268761D79A70A00C422DA /* SiriIntents.appex */, + D0ECCB7C1FE9C38500609802 /* Telegram-iOS UITests.xctest */, + D0B2F737204F4C9900D3BFB9 /* Widget.appex */, + D02CF5FC215D9ABE00E0F56A /* NotificationContent.appex */, + 09C56F8B2172797200BDF00F /* Watch.app */, + 09C56F972172797400BDF00F /* Watch Extension.appex */, + ); + name = Products; + sourceTree = ""; + }; + D008599E1B28189D00EAF753 /* Telegram-iOS */ = { + isa = PBXGroup; + children = ( + 09C50E7821738150009E676F /* Watch */, + D0ADF914212B399A00310BBC /* Config-AppStoreLLC.xcconfig */, + D079FD021F06BBD60038FADE /* Config-AppStore.xcconfig */, + D0924FFE1FE52C75003F693F /* Config-Hockeyapp Internal.xcconfig */, + D079FD011F06BBD60038FADE /* Config-Hockeyapp.xcconfig */, + D0ADF913212B398000310BBC /* Telegram-iOS-AppStoreLLC.entitlements */, + D079FD001F06BBD10038FADE /* Telegram-iOS-AppStore.entitlements */, + D0E3A7071B285B5000A402D9 /* Telegram-iOS-Hockeyapp.entitlements */, + D00859A11B28189D00EAF753 /* AppDelegate.swift */, + D0BEAF721E54C9A900BD963D /* ApplicationContext.swift */, + D053DAD22018ED2B00993D32 /* LockedWindowCoveringView.swift */, + D084023120E1883500065674 /* ApplicationShortcutItem.swift */, + D0A18D641E15C020004C6734 /* WakeupManager.swift */, + D0A18D681E16AC9D004C6734 /* NotificationManager.swift */, + D0EB243A201B77C400F6CC13 /* ClearNotificationsManager.swift */, + D00859A81B28189D00EAF753 /* Images.xcassets */, + D00859AA1B28189D00EAF753 /* LaunchScreen.xib */, + D0ADF956212B56C200310BBC /* Legacy Data Import */, + D0ECCB8B1FE9CE2B00609802 /* Snapshots */, + D008599F1B28189D00EAF753 /* Supporting Files */, + ); + path = "Telegram-iOS"; + sourceTree = ""; + }; + D008599F1B28189D00EAF753 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + D06E4C2D21347D9200088087 /* UIImage+ImageEffects.h */, + D06E4C2E21347D9200088087 /* UIImage+ImageEffects.m */, + D0612E481D58B478000C8F02 /* Application.swift */, + D09250001FE52D2A003F693F /* BuildConfig.h */, + D09250011FE52D2A003F693F /* BuildConfig.m */, + D09DCBB41D0C854D00F51FFE /* en.lproj */, + D0CE6F47213EDA4400BCD44B /* ar.lproj */, + D0CE6F4E213EDA4400BCD44B /* de.lproj */, + D0CE6F40213EDA4400BCD44B /* es.lproj */, + D0CE6F24213EDA4300BCD44B /* it.lproj */, + D0CE6F2B213EDA4300BCD44B /* ko.lproj */, + D0CE6F32213EDA4300BCD44B /* nl.lproj */, + D0CE6F1D213EDA4200BCD44B /* pt.lproj */, + D0CE6F39213EDA4300BCD44B /* ru.lproj */, + D00859A01B28189D00EAF753 /* Info.plist */, + D09A59B71B5876B600FC3724 /* Telegram-Bridging-Header.h */, + D02E31221BD803E800CD3F01 /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + D00859B41B28189D00EAF753 /* Telegram-iOSTests */ = { + isa = PBXGroup; + children = ( + D00859B71B28189D00EAF753 /* Telegram_iOSTests.swift */, + D03BCCC91C6EBD670097A291 /* ListViewTests.swift */, + D00859B51B28189D00EAF753 /* Supporting Files */, + ); + path = "Telegram-iOSTests"; + sourceTree = ""; + }; + D00859B51B28189D00EAF753 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + D00859B61B28189D00EAF753 /* Info.plist */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + D00859C21B281E0000EAF753 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 0972C6DF21791D950069E98A /* UserNotifications.framework */, + 09C50E87217385CF009E676F /* WatchConnectivity.framework */, + 09C50E852173854D009E676F /* WatchKit.framework */, + 09CFB211217299E80083F7A3 /* CoreLocation.framework */, + 09C5723C21728C0E00BDF00F /* CoreGraphics.framework */, + D02CF614215DA24900E0F56A /* Display.framework */, + D02CF616215DA24900E0F56A /* Postbox.framework */, + D02CF618215DA24900E0F56A /* SwiftSignalKit.framework */, + D02CF61A215DA24900E0F56A /* TelegramCore.framework */, + 09FDAEE52140477F00BF856F /* MtProtoKitDynamic.framework */, + D08985032118B46F00918162 /* SwiftSignalKit.framework */, + D08984FD2118B3F100918162 /* MtProtoKitDynamic.framework */, + D08984FF2118B3F100918162 /* Postbox.framework */, + D08985012118B3F100918162 /* TelegramCore.framework */, + D0F575122083B96B00F1C1E1 /* CloudKit.framework */, + D0B2F754204F4DF300D3BFB9 /* AsyncDisplayKit.framework */, + D0B2F74E204F4D6100D3BFB9 /* Display.framework */, + D0B2F74F204F4D6100D3BFB9 /* Postbox.framework */, + D0B2F750204F4D6100D3BFB9 /* SwiftSignalKit.framework */, + D0B2F751204F4D6100D3BFB9 /* TelegramCore.framework */, + D0B2F752204F4D6100D3BFB9 /* TelegramUI.framework */, + D0AF322A1FACA1A80097362B /* libstdc++.tbd */, + D08410511FABDD54008FFE92 /* MtProtoKitDynamic.framework */, + D08410491FABDCF2008FFE92 /* LegacyComponents.framework */, + D08410471FABDC7A008FFE92 /* SSignalKit.framework */, + D01A47541F4DBED700383CC1 /* HockeySDK.framework */, + D01A47521F4DBEB100383CC1 /* libHockeySDK.a */, + D0C50E451E9459BF00F62E39 /* libopus.a */, + D0A18D621E149043004C6734 /* PushKit.framework */, + D0B8445F1DACF561005F29E1 /* libc++.tbd */, + D0B844591DACF507005F29E1 /* HockeySDK.framework */, + D0B8445A1DACF507005F29E1 /* HockeySDKResources.bundle */, + D0CAF3171D76394C0011F558 /* TelegramCore.framework */, + D0CAF2F21D75FFAB0011F558 /* MtProtoKitDynamic.framework */, + D0AB0B9F1D6708B9002C78E7 /* Postbox.framework */, + D0AB0BA01D6708B9002C78E7 /* TelegramCore.framework */, + D06706601D51185400DED3E3 /* TelegramCore.framework */, + D096C2C31CC3C11A006D814E /* SwiftSignalKit.framework */, + D096C2C01CC3C104006D814E /* Postbox.framework */, + D096C2BD1CC3C021006D814E /* Display.framework */, + D0CD17B41CC3AE14007C5650 /* AsyncDisplayKit.framework */, + D0400ED81D5B8F97007931CE /* TelegramUI.framework */, + D03B0E951D637A0500955575 /* AsyncDisplayKit.framework */, + D03B0E871D634B1100955575 /* Display.framework */, + D03B0E881D634B1100955575 /* SwiftSignalKit.framework */, + D03B0E891D634B1100955575 /* TelegramCore.framework */, + D0369C8F1D3E2E5000D91AFC /* libbz2.tbd */, + D0369C8D1D3E2E4800D91AFC /* VideoToolbox.framework */, + D0369C8B1D3E2C9500D91AFC /* libiconv.tbd */, + D0C2DFF91CC4D1C90044FF83 /* QuickLook.framework */, + D0C2DFF71CC4D1BA0044FF83 /* MobileCoreServices.framework */, + D0C2DFF51CC4D1B20044FF83 /* AssetsLibrary.framework */, + D0D17E891CAAD66600C4750B /* Accelerate.framework */, + D055BD431B7E216400F06C0A /* MapKit.framework */, + D09A595F1B5858DB00FC3724 /* SystemConfiguration.framework */, + D0AA1A671D568BA400152314 /* UserNotifications.framework */, + D0AA1A691D568BA400152314 /* UserNotificationsUI.framework */, + D0E41A381D65A69C00FBFC00 /* NotificationCenter.framework */, + D0D268801D79A70A00C422DA /* IntentsUI.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + D023EBB31DDB2F0E00BD496D /* Resources */ = { + isa = PBXGroup; + children = ( + D0E8B8AC2044496C00605593 /* voip_busy.caf */, + D0E8B8A82044496B00605593 /* voip_connecting.mp3 */, + D0E8B8A92044496C00605593 /* voip_end.caf */, + D0E8B8AA2044496C00605593 /* voip_fail.caf */, + D0E8B8AB2044496C00605593 /* voip_ringback.caf */, + D0FC1947201D2DA700FEDBB2 /* SFCompactRounded-Semibold.otf */, + D0CFBB921FD88C2900B65C0D /* begin_record.caf */, + D001D5A91F878DA300DF975A /* PhoneCountries.txt */, + D04DCC0A1F71C80000B021D7 /* notifications */, + D050F21B1E49DEDE00988324 /* intro */, + ); + name = Resources; + sourceTree = ""; + }; + D02CF5FF215D9ABF00E0F56A /* NotificationContent */ = { + isa = PBXGroup; + children = ( + D051DB0C215E5E2300F30F92 /* NotificationContent.entitlements */, + D02CF612215DA1C900E0F56A /* NotificationContent-AppStore.entitlements */, + D02CF613215DA1C900E0F56A /* NotificationContent-AppStoreLLC.entitlements */, + D02CF611215DA1C900E0F56A /* NotificationContent-HockeyApp.entitlements */, + D02CF600215D9ABF00E0F56A /* NotificationViewController.swift */, + D02CF602215D9ABF00E0F56A /* MainInterface.storyboard */, + D02CF61D215E522400E0F56A /* NotificationContent-Bridging-Header.h */, + D02CF605215D9ABF00E0F56A /* Info.plist */, + ); + path = NotificationContent; + sourceTree = ""; + }; + D03B0E791D63484500955575 /* Share */ = { + isa = PBXGroup; + children = ( + 092F368121542CE4001A9F49 /* Supporting Files */, + D0ADF953212B3B4700310BBC /* Share-AppStoreLLC.entitlements */, + D0EA97981FE8537000792DD6 /* Share-AppStore.entitlements */, + D0EA97971FE8537000792DD6 /* Share-HockeyApp.entitlements */, + D084104A1FABDCFD008FFE92 /* TGContactModel.h */, + D084104C1FABDCFD008FFE92 /* TGContactModel.m */, + D084104B1FABDCFD008FFE92 /* TGMimeTypeMap.h */, + D084104D1FABDCFD008FFE92 /* TGMimeTypeMap.m */, + D08410431FABDC5B008FFE92 /* TGItemProviderSignals.h */, + D08410441FABDC5C008FFE92 /* TGItemProviderSignals.m */, + D0AF322E1FACBA270097362B /* TGShareLocationSignals.h */, + D0AF322D1FACBA270097362B /* TGShareLocationSignals.m */, + D03B0E7A1D63484500955575 /* ShareRootController.swift */, + D08410521FABDEC8008FFE92 /* Share-Bridging-Header.h */, + D03B0E7F1D63484500955575 /* Info.plist */, + D08410531FABE428008FFE92 /* ShareItems.swift */, + ); + path = Share; + sourceTree = ""; + }; + D0400EE31D5B912E007931CE /* NotificationService */ = { + isa = PBXGroup; + children = ( + D0400EEE1D5B9540007931CE /* NotificationService.entitlements */, + D0400EE41D5B912E007931CE /* NotificationService.swift */, + D0400EE61D5B912E007931CE /* Info.plist */, + D0AC49461D7095A100AA55DA /* MiniAccount.swift */, + ); + path = NotificationService; + sourceTree = ""; + }; + D04DCC0A1F71C80000B021D7 /* notifications */ = { + isa = PBXGroup; + children = ( + D04DCC0B1F71C80000B021D7 /* 0.m4a */, + D04DCC0C1F71C80000B021D7 /* 1.m4a */, + D04DCC0D1F71C80000B021D7 /* 100.m4a */, + D04DCC0E1F71C80000B021D7 /* 101.m4a */, + D04DCC0F1F71C80000B021D7 /* 102.m4a */, + D04DCC101F71C80000B021D7 /* 103.m4a */, + D04DCC111F71C80000B021D7 /* 104.m4a */, + D04DCC121F71C80000B021D7 /* 105.m4a */, + D04DCC131F71C80000B021D7 /* 106.m4a */, + D04DCC141F71C80000B021D7 /* 107.m4a */, + D04DCC151F71C80000B021D7 /* 108.m4a */, + D04DCC161F71C80000B021D7 /* 109.m4a */, + D04DCC171F71C80000B021D7 /* 110.m4a */, + D04DCC181F71C80000B021D7 /* 111.m4a */, + D04DCC191F71C80000B021D7 /* 2.m4a */, + D04DCC1A1F71C80000B021D7 /* 3.m4a */, + D04DCC1B1F71C80000B021D7 /* 4.m4a */, + D04DCC1C1F71C80000B021D7 /* 5.m4a */, + D04DCC1D1F71C80000B021D7 /* 6.m4a */, + D04DCC1E1F71C80000B021D7 /* 7.m4a */, + D04DCC1F1F71C80000B021D7 /* 8.m4a */, + D04DCC201F71C80000B021D7 /* 9.m4a */, + ); + name = notifications; + path = "Telegram-iOS/Resources/notifications"; + sourceTree = ""; + }; + D04FA1AE2145E37F0006EF45 /* en.lproj */ = { + isa = PBXGroup; + children = ( + D04FA1AF2145E37F0006EF45 /* Localizable.strings */, + D04FA1B12145E37F0006EF45 /* InfoPlist.strings */, + ); + path = en.lproj; + sourceTree = ""; + }; + D04FA1B32145E37F0006EF45 /* de.lproj */ = { + isa = PBXGroup; + children = ( + D04FA1B42145E37F0006EF45 /* InfoPlist.strings */, + ); + path = de.lproj; + sourceTree = ""; + }; + D04FA1B62145E3800006EF45 /* ar.lproj */ = { + isa = PBXGroup; + children = ( + D04FA1B72145E3800006EF45 /* InfoPlist.strings */, + ); + path = ar.lproj; + sourceTree = ""; + }; + D04FA1B92145E3800006EF45 /* nl.lproj */ = { + isa = PBXGroup; + children = ( + D04FA1BA2145E3800006EF45 /* InfoPlist.strings */, + ); + path = nl.lproj; + sourceTree = ""; + }; + D04FA1BC2145E3800006EF45 /* ko.lproj */ = { + isa = PBXGroup; + children = ( + D04FA1BD2145E3800006EF45 /* InfoPlist.strings */, + ); + path = ko.lproj; + sourceTree = ""; + }; + D04FA1BF2145E3800006EF45 /* it.lproj */ = { + isa = PBXGroup; + children = ( + D04FA1C02145E3800006EF45 /* InfoPlist.strings */, + ); + path = it.lproj; + sourceTree = ""; + }; + D04FA1C22145E3810006EF45 /* pt.lproj */ = { + isa = PBXGroup; + children = ( + D04FA1C32145E3810006EF45 /* InfoPlist.strings */, + ); + path = pt.lproj; + sourceTree = ""; + }; + D04FA1C52145E3810006EF45 /* es.lproj */ = { + isa = PBXGroup; + children = ( + D04FA1C62145E3810006EF45 /* InfoPlist.strings */, + ); + path = es.lproj; + sourceTree = ""; + }; + D050F21B1E49DEDE00988324 /* intro */ = { + isa = PBXGroup; + children = ( + D050F21C1E49DEDE00988324 /* fast_arrow@2x.png */, + D050F21D1E49DEDE00988324 /* fast_arrow_shadow@2x.png */, + D050F21E1E49DEDE00988324 /* fast_body@2x.png */, + D050F21F1E49DEDE00988324 /* fast_spiral@2x.png */, + D050F2201E49DEDE00988324 /* ic_bubble@2x.png */, + D050F2211E49DEDE00988324 /* ic_bubble_dot@2x.png */, + D050F2221E49DEDE00988324 /* ic_cam@2x.png */, + D050F2231E49DEDE00988324 /* ic_cam_lens@2x.png */, + D050F2241E49DEDE00988324 /* ic_pencil@2x.png */, + D050F2251E49DEDE00988324 /* ic_pin@2x.png */, + D050F2261E49DEDE00988324 /* ic_smile@2x.png */, + D050F2271E49DEDE00988324 /* ic_smile_eye@2x.png */, + D050F2281E49DEDE00988324 /* ic_videocam@2x.png */, + D050F2291E49DEDE00988324 /* knot_down@2x.png */, + D050F22A1E49DEDE00988324 /* knot_up1@2x.png */, + D050F22B1E49DEDE00988324 /* powerful_infinity@2x.png */, + D050F22C1E49DEDE00988324 /* powerful_infinity_white@2x.png */, + D050F22D1E49DEDE00988324 /* powerful_mask@2x.png */, + D050F22E1E49DEDE00988324 /* powerful_star@2x.png */, + D050F22F1E49DEDE00988324 /* private_door@2x.png */, + D050F2301E49DEDE00988324 /* private_screw@2x.png */, + D050F2311E49DEDE00988324 /* start_arrow@2x.png */, + D050F2321E49DEDE00988324 /* start_arrow_ipad.png */, + D050F2331E49DEDE00988324 /* start_arrow_ipad@2x.png */, + D050F2341E49DEDE00988324 /* telegram_plane1@2x.png */, + D050F2351E49DEDE00988324 /* telegram_sphere@2x.png */, + ); + name = intro; + path = "Telegram-iOS/Resources/intro"; + sourceTree = ""; + }; + D05B37FE1FEA8E3D0041D2A5 /* Images */ = { + isa = PBXGroup; + children = ( + D05B37FF1FEA8E3D0041D2A5 /* Bitmap2.png */, + D05B38001FEA8E3D0041D2A5 /* Bitmap3.png */, + D05B38011FEA8E3D0041D2A5 /* Bitmap1.png */, + D05B38021FEA8E3D0041D2A5 /* Bitmap5.png */, + D05B38031FEA8E3D0041D2A5 /* Bitmap7.png */, + D05B38041FEA8E3D0041D2A5 /* Bitmap6.png */, + D05B38051FEA8E3D0041D2A5 /* Bitmap8.png */, + D05B38061FEA8E3D0041D2A5 /* Bitmap9.png */, + D05B38071FEA8E3D0041D2A5 /* Bitmap12.png */, + D05B38081FEA8E3D0041D2A5 /* Bitmap10.png */, + D05B38091FEA8E3D0041D2A5 /* Bitmap11.png */, + ); + path = Images; + sourceTree = ""; + }; + D09DCBB41D0C854D00F51FFE /* en.lproj */ = { + isa = PBXGroup; + children = ( + D00ED7581FE94630001F38BD /* AppIntentVocabulary.plist */, + D09DCBB51D0C856B00F51FFE /* Localizable.strings */, + D00ED75B1FE95287001F38BD /* InfoPlist.strings */, + ); + name = en.lproj; + sourceTree = ""; + }; + D0ADF956212B56C200310BBC /* Legacy Data Import */ = { + isa = PBXGroup; + children = ( + D0D2276E212739120028F943 /* LegacyDataImport.swift */, + D0ADF957212B56DC00310BBC /* LegacyUserDataImport.swift */, + D0ADF95B212B636D00310BBC /* LegacyChatImport.swift */, + D0B3B53A21666C0000FC60A0 /* LegacyFileImport.swift */, + D0ADF959212B5AC600310BBC /* LegacyResourceImport.swift */, + D0ADF95D212C818F00310BBC /* LegacyPreferencesImport.swift */, + D0ADF960212C8DF600310BBC /* TGAutoDownloadPreferences.h */, + D0ADF95F212C8DF600310BBC /* TGAutoDownloadPreferences.m */, + D0CE6F1A213ED11100BCD44B /* TGPresentationAutoNightPreferences.h */, + D0CE6F1B213ED11100BCD44B /* TGPresentationAutoNightPreferences.m */, + D0ADF962212C9AA900310BBC /* TGProxyItem.h */, + D0ADF963212C9AA900310BBC /* TGProxyItem.m */, + D051DB5C21602D6E00F30F92 /* LegacyDataImportSplash.swift */, + D039FB162170F06A00BD1BAD /* PreFetchedLegacyResource.swift */, + ); + name = "Legacy Data Import"; + sourceTree = ""; + }; + D0CE6F1D213EDA4200BCD44B /* pt.lproj */ = { + isa = PBXGroup; + children = ( + D0CE6F1E213EDA4200BCD44B /* Localizable.strings */, + D0CE6F20213EDA4200BCD44B /* InfoPlist.strings */, + D0CE6F22213EDA4200BCD44B /* AppIntentVocabulary.plist */, + ); + path = pt.lproj; + sourceTree = ""; + }; + D0CE6F24213EDA4300BCD44B /* it.lproj */ = { + isa = PBXGroup; + children = ( + D0CE6F25213EDA4300BCD44B /* Localizable.strings */, + D0CE6F27213EDA4300BCD44B /* InfoPlist.strings */, + D0CE6F29213EDA4300BCD44B /* AppIntentVocabulary.plist */, + ); + path = it.lproj; + sourceTree = ""; + }; + D0CE6F2B213EDA4300BCD44B /* ko.lproj */ = { + isa = PBXGroup; + children = ( + D0CE6F2C213EDA4300BCD44B /* Localizable.strings */, + D0CE6F2E213EDA4300BCD44B /* InfoPlist.strings */, + D0CE6F30213EDA4300BCD44B /* AppIntentVocabulary.plist */, + ); + path = ko.lproj; + sourceTree = ""; + }; + D0CE6F32213EDA4300BCD44B /* nl.lproj */ = { + isa = PBXGroup; + children = ( + D0CE6F33213EDA4300BCD44B /* Localizable.strings */, + D0CE6F35213EDA4300BCD44B /* InfoPlist.strings */, + D0CE6F37213EDA4300BCD44B /* AppIntentVocabulary.plist */, + ); + path = nl.lproj; + sourceTree = ""; + }; + D0CE6F39213EDA4300BCD44B /* ru.lproj */ = { + isa = PBXGroup; + children = ( + D0CE6F3A213EDA4300BCD44B /* Localizable.strings */, + D0CE6F3C213EDA4300BCD44B /* InfoPlist.strings */, + D0CE6F3E213EDA4300BCD44B /* AppIntentVocabulary.plist */, + ); + path = ru.lproj; + sourceTree = ""; + }; + D0CE6F40213EDA4400BCD44B /* es.lproj */ = { + isa = PBXGroup; + children = ( + D0CE6F41213EDA4400BCD44B /* Localizable.strings */, + D0CE6F43213EDA4400BCD44B /* InfoPlist.strings */, + D0CE6F45213EDA4400BCD44B /* AppIntentVocabulary.plist */, + ); + path = es.lproj; + sourceTree = ""; + }; + D0CE6F47213EDA4400BCD44B /* ar.lproj */ = { + isa = PBXGroup; + children = ( + D0CE6F48213EDA4400BCD44B /* Localizable.strings */, + D0CE6F4A213EDA4400BCD44B /* InfoPlist.strings */, + D0CE6F4C213EDA4400BCD44B /* AppIntentVocabulary.plist */, + ); + path = ar.lproj; + sourceTree = ""; + }; + D0CE6F4E213EDA4400BCD44B /* de.lproj */ = { + isa = PBXGroup; + children = ( + D0CE6F4F213EDA4400BCD44B /* Localizable.strings */, + D0CE6F51213EDA4400BCD44B /* InfoPlist.strings */, + D0CE6F53213EDA4400BCD44B /* AppIntentVocabulary.plist */, + ); + path = de.lproj; + sourceTree = ""; + }; + D0D268771D79A70A00C422DA /* SiriIntents */ = { + isa = PBXGroup; + children = ( + D0ADF954212B3B5200310BBC /* SiriIntents-AppStoreLLC.entitlements */, + D0EA97961FE8536900792DD6 /* SiriIntents-Hockeyapp.entitlements */, + D0D268971D79AF1B00C422DA /* SiriIntents-AppStore.entitlements */, + D0D268781D79A70A00C422DA /* IntentHandler.swift */, + D08985062119B7FE00918162 /* IntentContacts.swift */, + D0D2687A1D79A70A00C422DA /* Info.plist */, + D08985052118B62400918162 /* SiriIntents-Bridging-Header.h */, + ); + path = SiriIntents; + sourceTree = ""; + }; + D0D268821D79A70A00C422DA /* SiriIntentsUI */ = { + isa = PBXGroup; + children = ( + D0D268981D79AF3900C422DA /* SiriIntentsUI.entitlements */, + D0D268831D79A70A00C422DA /* IntentViewController.swift */, + D0D268851D79A70A00C422DA /* MainInterface.storyboard */, + D0D268881D79A70A00C422DA /* Info.plist */, + ); + path = SiriIntentsUI; + sourceTree = ""; + }; + D0E41A3A1D65A69C00FBFC00 /* Widget */ = { + isa = PBXGroup; + children = ( + 0972C6E121792CED0069E98A /* ru.lproj */, + D04FA1B62145E3800006EF45 /* ar.lproj */, + D04FA1B32145E37F0006EF45 /* de.lproj */, + D04FA1AE2145E37F0006EF45 /* en.lproj */, + D04FA1C52145E3810006EF45 /* es.lproj */, + D04FA1BF2145E3800006EF45 /* it.lproj */, + D04FA1BC2145E3800006EF45 /* ko.lproj */, + D04FA1B92145E3800006EF45 /* nl.lproj */, + D04FA1C22145E3810006EF45 /* pt.lproj */, + D0ADF955212B3B6400310BBC /* Widget-AppStoreLLC.entitlements */, + D0B2F75A204F51E400D3BFB9 /* Widget-AppStore.entitlements */, + D0B2F75B204F51E500D3BFB9 /* Widget-HockeyApp.entitlements */, + D0E41A3B1D65A69C00FBFC00 /* TodayViewController.swift */, + D0E41A3D1D65A69C00FBFC00 /* MainInterface.storyboard */, + D0E41A401D65A69C00FBFC00 /* Info.plist */, + D0B2F759204F4EF400D3BFB9 /* Widget-Bridging-Header.h */, + D0B2F75F2050102600D3BFB9 /* PeerNode.swift */, + ); + path = Widget; + sourceTree = ""; + }; + D0ECCB7D1FE9C38500609802 /* Telegram-iOS UITests */ = { + isa = PBXGroup; + children = ( + D05B37FE1FEA8E3D0041D2A5 /* Images */, + D0ECCB891FE9C4AC00609802 /* SnapshotHelper.swift */, + D0ECCB7E1FE9C38500609802 /* Telegram_iOS_UITests.swift */, + D0ECCB801FE9C38500609802 /* Info.plist */, + ); + path = "Telegram-iOS UITests"; + sourceTree = ""; + }; + D0ECCB8B1FE9CE2B00609802 /* Snapshots */ = { + isa = PBXGroup; + children = ( + D05B37F41FEA5F6E0041D2A5 /* SnapshotEnvironment.swift */, + D05B37FC1FEA8D870041D2A5 /* SnapshotResources.swift */, + D0ECCB8C1FE9CE3F00609802 /* SnapshotChatList.swift */, + D05B37F61FEA8C640041D2A5 /* SnapshotSecretChat.swift */, + D05B37F81FEA8CF00041D2A5 /* SnapshotSettings.swift */, + D05B37FA1FEA8D020041D2A5 /* SnapshotAppearanceSettings.swift */, + ); + name = Snapshots; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 09C56F8A2172797200BDF00F /* Watch */ = { + isa = PBXNativeTarget; + buildConfigurationList = 09C56FB62172797500BDF00F /* Build configuration list for PBXNativeTarget "Watch" */; + buildPhases = ( + 09C56F892172797200BDF00F /* Resources */, + 09C56FB52172797500BDF00F /* Embed App Extensions */, + ); + buildRules = ( + ); + dependencies = ( + 09C56F9A2172797500BDF00F /* PBXTargetDependency */, + ); + name = Watch; + productName = Watch; + productReference = 09C56F8B2172797200BDF00F /* Watch.app */; + productType = "com.apple.product-type.application.watchapp2"; + }; + 09C56F962172797400BDF00F /* Watch Extension */ = { + isa = PBXNativeTarget; + buildConfigurationList = 09C56FB42172797500BDF00F /* Build configuration list for PBXNativeTarget "Watch Extension" */; + buildPhases = ( + 09C56F932172797400BDF00F /* Sources */, + 09C56F942172797400BDF00F /* Frameworks */, + 09C56F952172797400BDF00F /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "Watch Extension"; + productName = "Watch Extension"; + productReference = 09C56F972172797400BDF00F /* Watch Extension.appex */; + productType = "com.apple.product-type.watchkit2-extension"; + }; + D008599B1B28189D00EAF753 /* Telegram-iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = D00859BB1B28189D00EAF753 /* Build configuration list for PBXNativeTarget "Telegram-iOS" */; + buildPhases = ( + D00859981B28189D00EAF753 /* Sources */, + D00859991B28189D00EAF753 /* Frameworks */, + D008599A1B28189D00EAF753 /* Resources */, + D0289FA81CBFFC8700A12E82 /* Embed Frameworks */, + D0AA1A791D568BA500152314 /* Embed App Extensions */, + 09C56FB72172797500BDF00F /* Embed Watch Content */, + ); + buildRules = ( + ); + dependencies = ( + D03B0E811D63484500955575 /* PBXTargetDependency */, + D0D2688D1D79A70B00C422DA /* PBXTargetDependency */, + D0B2F741204F4C9900D3BFB9 /* PBXTargetDependency */, + D02CF607215D9ABF00E0F56A /* PBXTargetDependency */, + 09C56FA42172797500BDF00F /* PBXTargetDependency */, + ); + name = "Telegram-iOS"; + productName = "Telegram-iOS"; + productReference = D008599C1B28189D00EAF753 /* Telegram X.app */; + productType = "com.apple.product-type.application"; + }; + D00859B01B28189D00EAF753 /* Telegram-iOSTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = D00859BE1B28189D00EAF753 /* Build configuration list for PBXNativeTarget "Telegram-iOSTests" */; + buildPhases = ( + D00859AD1B28189D00EAF753 /* Sources */, + D00859AE1B28189D00EAF753 /* Frameworks */, + D00859AF1B28189D00EAF753 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + D00859B31B28189D00EAF753 /* PBXTargetDependency */, + ); + name = "Telegram-iOSTests"; + productName = "Telegram-iOSTests"; + productReference = D00859B11B28189D00EAF753 /* Telegram-iOSTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + D02CF5FB215D9ABE00E0F56A /* NotificationContent */ = { + isa = PBXNativeTarget; + buildConfigurationList = D02CF610215D9ABF00E0F56A /* Build configuration list for PBXNativeTarget "NotificationContent" */; + buildPhases = ( + D02CF5F8215D9ABE00E0F56A /* Sources */, + D02CF5F9215D9ABE00E0F56A /* Frameworks */, + D02CF5FA215D9ABE00E0F56A /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = NotificationContent; + productName = NotificationContent; + productReference = D02CF5FC215D9ABE00E0F56A /* NotificationContent.appex */; + productType = "com.apple.product-type.app-extension"; + }; + D03B0E771D63484500955575 /* Share */ = { + isa = PBXNativeTarget; + buildConfigurationList = D03B0E831D63484500955575 /* Build configuration list for PBXNativeTarget "Share" */; + buildPhases = ( + D03B0E741D63484500955575 /* Sources */, + D03B0E751D63484500955575 /* Frameworks */, + D03B0E761D63484500955575 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Share; + productName = Share; + productReference = D03B0E781D63484500955575 /* Share.appex */; + productType = "com.apple.product-type.app-extension"; + }; + D0B2F736204F4C9900D3BFB9 /* Widget */ = { + isa = PBXNativeTarget; + buildConfigurationList = D0B2F748204F4C9900D3BFB9 /* Build configuration list for PBXNativeTarget "Widget" */; + buildPhases = ( + D0B2F733204F4C9900D3BFB9 /* Sources */, + D0B2F734204F4C9900D3BFB9 /* Frameworks */, + D0B2F735204F4C9900D3BFB9 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Widget; + productName = Widget; + productReference = D0B2F737204F4C9900D3BFB9 /* Widget.appex */; + productType = "com.apple.product-type.app-extension"; + }; + D0D268751D79A70A00C422DA /* SiriIntents */ = { + isa = PBXNativeTarget; + buildConfigurationList = D0D268961D79A70B00C422DA /* Build configuration list for PBXNativeTarget "SiriIntents" */; + buildPhases = ( + D0D268721D79A70A00C422DA /* Sources */, + D0D268731D79A70A00C422DA /* Frameworks */, + D0D268741D79A70A00C422DA /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = SiriIntents; + productName = SiriIntents; + productReference = D0D268761D79A70A00C422DA /* SiriIntents.appex */; + productType = "com.apple.product-type.app-extension"; + }; + D0ECCB7B1FE9C38500609802 /* Telegram-iOS UITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = D0ECCB881FE9C38500609802 /* Build configuration list for PBXNativeTarget "Telegram-iOS UITests" */; + buildPhases = ( + D0ECCB781FE9C38500609802 /* Sources */, + D0ECCB791FE9C38500609802 /* Frameworks */, + D0ECCB7A1FE9C38500609802 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + D0ECCB821FE9C38500609802 /* PBXTargetDependency */, + ); + name = "Telegram-iOS UITests"; + productName = "Telegram-iOS UITests"; + productReference = D0ECCB7C1FE9C38500609802 /* Telegram-iOS UITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + D00859941B28189D00EAF753 /* Project object */ = { + isa = PBXProject; + attributes = { + DefaultBuildSystemTypeForWorkspace = Latest; + LastSwiftMigration = 0700; + LastSwiftUpdateCheck = 1000; + LastUpgradeCheck = 0820; + ORGANIZATIONNAME = Telegram; + TargetAttributes = { + 09C56F8A2172797200BDF00F = { + CreatedOnToolsVersion = 10.0; + DevelopmentTeam = X834Q8SBVP; + ProvisioningStyle = Manual; + }; + 09C56F962172797400BDF00F = { + CreatedOnToolsVersion = 10.0; + DevelopmentTeam = X834Q8SBVP; + ProvisioningStyle = Manual; + }; + D008599B1B28189D00EAF753 = { + CreatedOnToolsVersion = 6.3.1; + DevelopmentTeam = X834Q8SBVP; + SystemCapabilities = { + com.apple.ApplicationGroups.iOS = { + enabled = 1; + }; + com.apple.BackgroundModes = { + enabled = 1; + }; + com.apple.Push = { + enabled = 1; + }; + com.apple.Siri = { + enabled = 1; + }; + com.apple.iCloud = { + enabled = 1; + }; + }; + }; + D00859B01B28189D00EAF753 = { + CreatedOnToolsVersion = 6.3.1; + ProvisioningStyle = Manual; + TestTargetID = D008599B1B28189D00EAF753; + }; + D02CF5FB215D9ABE00E0F56A = { + CreatedOnToolsVersion = 10.0; + DevelopmentTeam = X834Q8SBVP; + ProvisioningStyle = Manual; + SystemCapabilities = { + com.apple.ApplicationGroups.iOS = { + enabled = 1; + }; + }; + }; + D03B0E771D63484500955575 = { + CreatedOnToolsVersion = 8.0; + DevelopmentTeam = X834Q8SBVP; + LastSwiftMigration = 0910; + ProvisioningStyle = Manual; + SystemCapabilities = { + com.apple.ApplicationGroups.iOS = { + enabled = 1; + }; + }; + }; + D0B2F736204F4C9900D3BFB9 = { + CreatedOnToolsVersion = 9.2; + DevelopmentTeam = X834Q8SBVP; + ProvisioningStyle = Manual; + SystemCapabilities = { + com.apple.ApplicationGroups.iOS = { + enabled = 1; + }; + }; + }; + D0D268751D79A70A00C422DA = { + CreatedOnToolsVersion = 8.0; + DevelopmentTeam = X834Q8SBVP; + ProvisioningStyle = Manual; + SystemCapabilities = { + com.apple.ApplicationGroups.iOS = { + enabled = 1; + }; + }; + }; + D0ECCB7B1FE9C38500609802 = { + CreatedOnToolsVersion = 9.2; + DevelopmentTeam = X834Q8SBVP; + ProvisioningStyle = Automatic; + TestTargetID = D008599B1B28189D00EAF753; + }; + }; + }; + buildConfigurationList = D00859971B28189D00EAF753 /* Build configuration list for PBXProject "Telegram-iOS" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + pt, + it, + ko, + nl, + ru, + es, + ar, + de, + ); + mainGroup = D00859931B28189D00EAF753; + productRefGroup = D008599D1B28189D00EAF753 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + D008599B1B28189D00EAF753 /* Telegram-iOS */, + D00859B01B28189D00EAF753 /* Telegram-iOSTests */, + D03B0E771D63484500955575 /* Share */, + D0D268751D79A70A00C422DA /* SiriIntents */, + D0ECCB7B1FE9C38500609802 /* Telegram-iOS UITests */, + D0B2F736204F4C9900D3BFB9 /* Widget */, + D02CF5FB215D9ABE00E0F56A /* NotificationContent */, + 09C56F8A2172797200BDF00F /* Watch */, + 09C56F962172797400BDF00F /* Watch Extension */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 09C56F892172797200BDF00F /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 09C56F912172797400BDF00F /* Assets.xcassets in Resources */, + 09C56F8F2172797200BDF00F /* Interface.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 09C56F952172797400BDF00F /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 09C5715921727DD900BDF00F /* MediaVideo@2x.png in Resources */, + 09C5715821727DD900BDF00F /* MediaLocation@2x.png in Resources */, + 09C5715A21727DD900BDF00F /* VerifiedList@2x.png in Resources */, + 09C5715621727DD900BDF00F /* File@2x.png in Resources */, + 09D30420217418EC00C00567 /* Localizable.strings in Resources */, + 09C5715521727DD900BDF00F /* Location@2x.png in Resources */, + 09C5715721727DD900BDF00F /* MediaDocument@2x.png in Resources */, + 09C5715421727DD900BDF00F /* MediaPhoto@2x.png in Resources */, + 09C5715321727DD900BDF00F /* MediaAudio@2x.png in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D008599A1B28189D00EAF753 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D0CE6F6B213EDA4400BCD44B /* InfoPlist.strings in Resources */, + D08DB0AC213F4D1D00F2ADBF /* ic_bubble_dot@2x.png in Resources */, + D08DB0B0213F4D1D00F2ADBF /* ic_pin@2x.png in Resources */, + D04DCC211F71C80000B021D7 /* 0.m4a in Resources */, + D04DCC261F71C80000B021D7 /* 103.m4a in Resources */, + D0CE6F69213EDA4400BCD44B /* AppIntentVocabulary.plist in Resources */, + D08DB0C0213F4D1D00F2ADBF /* telegram_sphere@2x.png in Resources */, + D08DB0AB213F4D1D00F2ADBF /* ic_bubble@2x.png in Resources */, + D04DCC341F71C80000B021D7 /* 7.m4a in Resources */, + D0CE6F60213EDA4400BCD44B /* AppIntentVocabulary.plist in Resources */, + D0CE6F63213EDA4400BCD44B /* AppIntentVocabulary.plist in Resources */, + D00ED75D1FE95287001F38BD /* InfoPlist.strings in Resources */, + D04DCC361F71C80000B021D7 /* 9.m4a in Resources */, + D0CE6F5A213EDA4400BCD44B /* AppIntentVocabulary.plist in Resources */, + D04DCC301F71C80000B021D7 /* 3.m4a in Resources */, + D0E8B8AE2044496C00605593 /* voip_end.caf in Resources */, + D0CE6F68213EDA4400BCD44B /* InfoPlist.strings in Resources */, + D0CE6F6A213EDA4400BCD44B /* Localizable.strings in Resources */, + D09DCBB71D0C856B00F51FFE /* Localizable.strings in Resources */, + D0CE6F66213EDA4400BCD44B /* AppIntentVocabulary.plist in Resources */, + D08DB0B8213F4D1D00F2ADBF /* powerful_mask@2x.png in Resources */, + D08DB0B4213F4D1D00F2ADBF /* knot_down@2x.png in Resources */, + D08DB0BC213F4D1D00F2ADBF /* start_arrow@2x.png in Resources */, + D08DB0B6213F4D1D00F2ADBF /* powerful_infinity@2x.png in Resources */, + D0CE6F5B213EDA4400BCD44B /* Localizable.strings in Resources */, + D0CE6F62213EDA4400BCD44B /* InfoPlist.strings in Resources */, + D08DB0A8213F4D1D00F2ADBF /* fast_arrow_shadow@2x.png in Resources */, + D0CFBB931FD88C2900B65C0D /* begin_record.caf in Resources */, + D04DCC2C1F71C80000B021D7 /* 109.m4a in Resources */, + D08DB0BD213F4D1D00F2ADBF /* start_arrow_ipad.png in Resources */, + D0CE6F64213EDA4400BCD44B /* Localizable.strings in Resources */, + D0CE6F6C213EDA4400BCD44B /* AppIntentVocabulary.plist in Resources */, + D04DCC231F71C80000B021D7 /* 100.m4a in Resources */, + D04DCC281F71C80000B021D7 /* 105.m4a in Resources */, + D08DB0BB213F4D1D00F2ADBF /* private_screw@2x.png in Resources */, + D0CE6F5F213EDA4400BCD44B /* InfoPlist.strings in Resources */, + D04DCC2D1F71C80000B021D7 /* 110.m4a in Resources */, + D04DCC2B1F71C80000B021D7 /* 108.m4a in Resources */, + D00859AC1B28189D00EAF753 /* LaunchScreen.xib in Resources */, + D08DB0B5213F4D1D00F2ADBF /* knot_up1@2x.png in Resources */, + D0E8B8AD2044496C00605593 /* voip_connecting.mp3 in Resources */, + D08DB0BE213F4D1D00F2ADBF /* start_arrow_ipad@2x.png in Resources */, + D08DB0A9213F4D1D00F2ADBF /* fast_body@2x.png in Resources */, + D04DCC321F71C80000B021D7 /* 5.m4a in Resources */, + D04DCC241F71C80000B021D7 /* 101.m4a in Resources */, + D04DCC351F71C80000B021D7 /* 8.m4a in Resources */, + D08DB0B1213F4D1D00F2ADBF /* ic_smile@2x.png in Resources */, + D0CE6F57213EDA4400BCD44B /* AppIntentVocabulary.plist in Resources */, + D0CE6F59213EDA4400BCD44B /* InfoPlist.strings in Resources */, + D08DB0B9213F4D1D00F2ADBF /* powerful_star@2x.png in Resources */, + D04DCC271F71C80000B021D7 /* 104.m4a in Resources */, + D04DCC2A1F71C80000B021D7 /* 107.m4a in Resources */, + D08DB0BA213F4D1D00F2ADBF /* private_door@2x.png in Resources */, + D08DB0AE213F4D1D00F2ADBF /* ic_cam_lens@2x.png in Resources */, + D04DCC2F1F71C80000B021D7 /* 2.m4a in Resources */, + D0CE6F58213EDA4400BCD44B /* Localizable.strings in Resources */, + D08DB0AF213F4D1D00F2ADBF /* ic_pencil@2x.png in Resources */, + D0CE6F67213EDA4400BCD44B /* Localizable.strings in Resources */, + D04DCC291F71C80000B021D7 /* 106.m4a in Resources */, + D0CE6F5E213EDA4400BCD44B /* Localizable.strings in Resources */, + D08DB0B7213F4D1D00F2ADBF /* powerful_infinity_white@2x.png in Resources */, + D00859A91B28189D00EAF753 /* Images.xcassets in Resources */, + D001D5AA1F878DA300DF975A /* PhoneCountries.txt in Resources */, + D0CE6F56213EDA4400BCD44B /* InfoPlist.strings in Resources */, + D0CE6F65213EDA4400BCD44B /* InfoPlist.strings in Resources */, + D0E8B8B12044496C00605593 /* voip_busy.caf in Resources */, + D08DB0A7213F4D1D00F2ADBF /* fast_arrow@2x.png in Resources */, + D0E8B8AF2044496C00605593 /* voip_fail.caf in Resources */, + D0CE6F55213EDA4400BCD44B /* Localizable.strings in Resources */, + D08DB0B2213F4D1D00F2ADBF /* ic_smile_eye@2x.png in Resources */, + D08DB0B3213F4D1D00F2ADBF /* ic_videocam@2x.png in Resources */, + D04DCC2E1F71C80000B021D7 /* 111.m4a in Resources */, + D08DB0AA213F4D1D00F2ADBF /* fast_spiral@2x.png in Resources */, + D0CE6F5C213EDA4400BCD44B /* InfoPlist.strings in Resources */, + D0CE6F5D213EDA4400BCD44B /* AppIntentVocabulary.plist in Resources */, + D0CE6F61213EDA4400BCD44B /* Localizable.strings in Resources */, + D04DCC311F71C80000B021D7 /* 4.m4a in Resources */, + D0FC1948201D2DA800FEDBB2 /* SFCompactRounded-Semibold.otf in Resources */, + D04DCC331F71C80000B021D7 /* 6.m4a in Resources */, + D04DCC251F71C80000B021D7 /* 102.m4a in Resources */, + D08DB0AD213F4D1D00F2ADBF /* ic_cam@2x.png in Resources */, + D08DB0BF213F4D1D00F2ADBF /* telegram_plane1@2x.png in Resources */, + D0E8B8B02044496C00605593 /* voip_ringback.caf in Resources */, + D00ED75A1FE94630001F38BD /* AppIntentVocabulary.plist in Resources */, + D04DCC221F71C80000B021D7 /* 1.m4a in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D00859AF1B28189D00EAF753 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D02CF5FA215D9ABE00E0F56A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D02CF604215D9ABF00E0F56A /* MainInterface.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D03B0E761D63484500955575 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 092F368521542D6C001A9F49 /* Localizable.strings in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D0B2F735204F4C9900D3BFB9 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D08F7858213F3E5600225975 /* MainInterface.storyboard in Resources */, + D04FA1CE2145E3810006EF45 /* InfoPlist.strings in Resources */, + D04FA1CD2145E3810006EF45 /* InfoPlist.strings in Resources */, + D04FA1CC2145E3810006EF45 /* InfoPlist.strings in Resources */, + D04FA1CA2145E3810006EF45 /* InfoPlist.strings in Resources */, + D04FA1C92145E3810006EF45 /* InfoPlist.strings in Resources */, + D04FA1C82145E3810006EF45 /* Localizable.strings in Resources */, + D04FA1CF2145E3810006EF45 /* InfoPlist.strings in Resources */, + D04FA1CB2145E3810006EF45 /* InfoPlist.strings in Resources */, + 0972C6E421792D130069E98A /* InfoPlist.strings in Resources */, + D04FA1D02145E3810006EF45 /* InfoPlist.strings in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D0D268741D79A70A00C422DA /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D0ECCB7A1FE9C38500609802 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D05B38141FEA8E3D0041D2A5 /* Bitmap11.png in Resources */, + D05B38101FEA8E3D0041D2A5 /* Bitmap8.png in Resources */, + D05B380D1FEA8E3D0041D2A5 /* Bitmap5.png in Resources */, + D05B380B1FEA8E3D0041D2A5 /* Bitmap3.png in Resources */, + D05B38131FEA8E3D0041D2A5 /* Bitmap10.png in Resources */, + D05B380A1FEA8E3D0041D2A5 /* Bitmap2.png in Resources */, + D05B38111FEA8E3D0041D2A5 /* Bitmap9.png in Resources */, + D05B380E1FEA8E3D0041D2A5 /* Bitmap7.png in Resources */, + D05B380C1FEA8E3D0041D2A5 /* Bitmap1.png in Resources */, + D05B380F1FEA8E3D0041D2A5 /* Bitmap6.png in Resources */, + D05B38121FEA8E3D0041D2A5 /* Bitmap12.png in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 09C56F932172797400BDF00F /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 09C572D2217293D400BDF00F /* TGBridgeClient.m in Sources */, + 09C5728521728D3700BDF00F /* SSignal+Single.m in Sources */, + 09C5728A21728D3700BDF00F /* SThreadPoolQueue.m in Sources */, + 09C5727821728D3700BDF00F /* SMetaDisposable.m in Sources */, + 09C5732B2172953900BDF00F /* TGBridgePeerNotificationSettings.m in Sources */, + 0956AF2F217B8109008106D0 /* TGNeoUnsupportedMessageViewModel.m in Sources */, + 09C50DE821729D7C009E676F /* TGBridgeBotSignals.m in Sources */, + 09C571952172806900BDF00F /* TGUserRowController.m in Sources */, + 09C50E0321729DB5009E676F /* TGBotCommandController.m in Sources */, + 09C5714421727DAA00BDF00F /* TGStringUtils.m in Sources */, + 09C5722C21728AA500BDF00F /* TGNeoConversationTimeRowController.m in Sources */, + 09C5722E21728AA500BDF00F /* TGNeoForwardHeaderViewModel.m in Sources */, + 09C573212172953800BDF00F /* TGBridgeUser+TGTableItem.m in Sources */, + 09C5731D2172953800BDF00F /* TGBridgeStickerPack.m in Sources */, + 09C50DF321729D7C009E676F /* TGBridgeStateSignal.m in Sources */, + 09C5727421728D3700BDF00F /* SAtomic.m in Sources */, + 09C5731C2172953800BDF00F /* TGBridgeActionMediaAttachment.m in Sources */, + 09C571D52172875500BDF00F /* TGMessageViewController.m in Sources */, + 09C5732F2172953900BDF00F /* TGBridgeContext.m in Sources */, + 09C573192172953800BDF00F /* TGBridgeContactMediaAttachment.m in Sources */, + 09C571EB2172878900BDF00F /* TGStickersHeaderController.m in Sources */, + 09C50DED21729D7C009E676F /* TGBridgeLocationSignals.m in Sources */, + 09C573302172953900BDF00F /* TGBridgeChat.m in Sources */, + 09C571D72172875A00BDF00F /* TGMessageViewMessageRowController.m in Sources */, + 09C571F22172879C00BDF00F /* TGComposeController.m in Sources */, + 09C5727621728D3700BDF00F /* SBlockDisposable.m in Sources */, + 09C5728721728D3700BDF00F /* SSignal+Timing.m in Sources */, + 09C5723021728AA500BDF00F /* TGNeoMessageViewModel.m in Sources */, + 09C5728C21728D3700BDF00F /* STimer.m in Sources */, + 09C5728221728D3700BDF00F /* SSignal+Multicast.m in Sources */, + 09C573342172953900BDF00F /* TGBridgeUnsupportedMediaAttachment.m in Sources */, + 09C573162172953800BDF00F /* TGBridgeMessage+TGTableItem.m in Sources */, + 09C571922172806600BDF00F /* TGNeoLabelViewModel.m in Sources */, + 09C5723521728AA500BDF00F /* TGNeoStickerMessageViewModel.m in Sources */, + 09C50E0421729DB5009E676F /* TGBotKeyboardController.m in Sources */, + 09C5732E2172953900BDF00F /* TGBridgeLocationMediaAttachment.m in Sources */, + 09C5722A21728AA500BDF00F /* TGNeoConversationSimpleRowController.m in Sources */, + 09C573262172953800BDF00F /* TGBridgeMessageEntities.m in Sources */, + 09C5727E21728D3700BDF00F /* SSignal+Combine.m in Sources */, + 09C571D42172875100BDF00F /* TGUserInfoController.m in Sources */, + 09C571932172806600BDF00F /* TGNeoRenderableViewModel.m in Sources */, + 09C5728621728D3700BDF00F /* SSignal+Take.m in Sources */, + 09C571DB2172876700BDF00F /* TGNeoConversationController.m in Sources */, + 09C50DF221729D7C009E676F /* TGBridgeSendMessageSignals.m in Sources */, + 09C571902172806600BDF00F /* TGNeoAttachmentViewModel.m in Sources */, + 09C50DE921729D7C009E676F /* TGBridgeChatListSignals.m in Sources */, + 09C50E922173B247009E676F /* TGBridgeSubscriptions.m in Sources */, + 09C5716A21727F1500BDF00F /* TGInterfaceMenu.m in Sources */, + 09C571D02172874B00BDF00F /* TGUserHandleRowController.m in Sources */, + 09C5727F21728D3700BDF00F /* SSignal+Dispatch.m in Sources */, + 09C573232172953800BDF00F /* TGBridgeBotCommandInfo.m in Sources */, + 09C5723121728AA500BDF00F /* TGNeoReplyHeaderViewModel.m in Sources */, + 09C571F02172878900BDF00F /* TGStickersController.m in Sources */, + 09C5714621727DAA00BDF00F /* TGWatchCommon.m in Sources */, + 09C571972172806D00BDF00F /* TGNotificationController.m in Sources */, + 09C5716821727F1500BDF00F /* TGTableDeltaUpdater.m in Sources */, + 09C571EE2172878900BDF00F /* TGStickersRowController.m in Sources */, + 09C5714721727DAA00BDF00F /* WKInterface+TGInterface.m in Sources */, + 09C571912172806600BDF00F /* TGNeoImageViewModel.m in Sources */, + 09C50DF121729D7C009E676F /* TGBridgeRemoteSignals.m in Sources */, + 09C571ED2172878900BDF00F /* TGStickerPacksController.m in Sources */, + 09C5723A21728AA500BDF00F /* TGChatTimestamp.m in Sources */, + 09C5716021727EE700BDF00F /* TGBridgeUserCache.m in Sources */, + 09C5728B21728D3700BDF00F /* SThreadPoolTask.m in Sources */, + 09C5713E21727D9E00BDF00F /* TGInterfaceController.m in Sources */, + 09C571D22172875100BDF00F /* TGGroupInfoController.m in Sources */, + 09C50DEA21729D7C009E676F /* TGBridgeChatMessageListSignals.m in Sources */, + 09C573332172953900BDF00F /* TGBridgeReplyMarkupMediaAttachment.m in Sources */, + 09C573352172953900BDF00F /* TGBridgeBotInfo.m in Sources */, + 09C571DC2172876C00BDF00F /* TGLocationController.m in Sources */, + 09C5727A21728D3700BDF00F /* SQueue.m in Sources */, + 09C5714521727DAA00BDF00F /* TGWatchColor.m in Sources */, + 09C5718E2172806600BDF00F /* TGAvatarViewModel.m in Sources */, + 09C5728021728D3700BDF00F /* SSignal+Mapping.m in Sources */, + 09C5728D21728D3700BDF00F /* SVariable.m in Sources */, + 09C5714321727DAA00BDF00F /* TGLocationUtils.m in Sources */, + 09C5716121727EE700BDF00F /* TGFileCache.m in Sources */, + 09C573202172953800BDF00F /* TGBridgeForwardedMessageMediaAttachment.m in Sources */, + 09C5723221728AA500BDF00F /* TGNeoRowController.m in Sources */, + 09C5728121728D3700BDF00F /* SSignal+Meta.m in Sources */, + 09C5722521728AA500BDF00F /* TGNeoAudioMessageViewModel.m in Sources */, + 09C5722621728AA500BDF00F /* TGNeoBackgroundViewModel.m in Sources */, + 09C5723721728AA500BDF00F /* TGNeoVenueMessageViewModel.m in Sources */, + 09C5728421728D3700BDF00F /* SSignal+SideEffects.m in Sources */, + 09C5722F21728AA500BDF00F /* TGNeoMediaMessageViewModel.m in Sources */, + 09C50DF021729D7C009E676F /* TGBridgePresetsSignals.m in Sources */, + 09C50DEF21729D7C009E676F /* TGBridgePeerSettingsSignals.m in Sources */, + 09C5714821727DAA00BDF00F /* WKInterfaceGroup+Signals.m in Sources */, + 09C5714921727DAA00BDF00F /* WKInterfaceImage+Signals.m in Sources */, + 09C5728321728D3700BDF00F /* SSignal+Pipe.m in Sources */, + 09C573322172953900BDF00F /* TGBridgeChatMessages.m in Sources */, + 09C5728921728D3700BDF00F /* SThreadPool.m in Sources */, + 09C5722921728AA500BDF00F /* TGNeoConversationMediaRowController.m in Sources */, + 09C571DA2172876300BDF00F /* TGNeoChatViewModel.m in Sources */, + 09C571CD2172874B00BDF00F /* TGGroupInfoFooterController.m in Sources */, + 09C571982172807100BDF00F /* TGAudioMicAlertController.m in Sources */, + 09C571D12172874B00BDF00F /* TGUserInfoHeaderController.m in Sources */, + 09C5723421728AA500BDF00F /* TGNeoSmiliesMessageViewModel.m in Sources */, + 09C5731E2172953800BDF00F /* TGBridgeVideoMediaAttachment.m in Sources */, + 09C50DF421729D7C009E676F /* TGBridgeStickersSignals.m in Sources */, + 09C573252172953800BDF00F /* TGBridgeLocationVenue.m in Sources */, + 09C5727521728D3700BDF00F /* SBag.m in Sources */, + 09C5713F21727DA000BDF00F /* TGExtensionDelegate.m in Sources */, + 09C573292172953900BDF00F /* TGBridgeMediaAttachment.m in Sources */, + 09C50DEE21729D7C009E676F /* TGBridgeMediaSignals.m in Sources */, + 09C571D82172875A00BDF00F /* TGMessageViewWebPageRowController.m in Sources */, + 09C50E0521729DE6009E676F /* TGBotKeyboardButtonController.m in Sources */, + 09C5719B217280E900BDF00F /* TGNeoChatsController.m in Sources */, + 09C5731B2172953800BDF00F /* TGBridgeAudioMediaAttachment.m in Sources */, + 09C5731A2172953800BDF00F /* TGBridgeMessage.m in Sources */, + 09C5728821728D3700BDF00F /* SSubscriber.m in Sources */, + 09C5722D21728AA500BDF00F /* TGNeoFileMessageViewModel.m in Sources */, + 09C5723821728AA500BDF00F /* TGConversationFooterController.m in Sources */, + 09C571DD2172876F00BDF00F /* TGLocationMapHeaderController.m in Sources */, + 09C573312172953900BDF00F /* TGBridgeMessageEntitiesAttachment.m in Sources */, + 09C50DEC21729D7C009E676F /* TGBridgeConversationSignals.m in Sources */, + 09C5727B21728D3700BDF00F /* SSignal.m in Sources */, + 09C573282172953800BDF00F /* TGBridgeChat+TGTableItem.m in Sources */, + 09C5727721728D3700BDF00F /* SDisposableSet.m in Sources */, + 09C573242172953800BDF00F /* TGBridgeImageMediaAttachment.m in Sources */, + 09C5723B21728AA500BDF00F /* TGNeoConversationRowController.m in Sources */, + 09C5714121727DAA00BDF00F /* TGGeometry.m in Sources */, + 09C5714221727DAA00BDF00F /* TGIndexPath.m in Sources */, + 09C5727921728D3700BDF00F /* SMulticastSignalManager.m in Sources */, + 09C571CE2172874B00BDF00F /* TGGroupInfoHeaderController.m in Sources */, + 09C571D32172875100BDF00F /* TGProfilePhotoController.m in Sources */, + 09C571F12172879800BDF00F /* TGContactsController.m in Sources */, + 09C5732D2172953900BDF00F /* TGBridgeLocationVenue+TGTableItem.m in Sources */, + 09C50DF521729D7C009E676F /* TGBridgeUserInfoSignals.m in Sources */, + 09C5723321728AA500BDF00F /* TGNeoServiceMessageViewModel.m in Sources */, + 09C5727D21728D3700BDF00F /* SSignal+Catch.m in Sources */, + 09C50DEB21729D7C009E676F /* TGBridgeContactsSignals.m in Sources */, + 09C571D62172875A00BDF00F /* TGMessageViewFooterController.m in Sources */, + 09C571EC2172878900BDF00F /* TGStickerPackRowController.m in Sources */, + 09C571D92172876300BDF00F /* TGNeoChatRowController.m in Sources */, + 09C573182172953800BDF00F /* TGBridgeBotReplyMarkup.m in Sources */, + 09C50DE721729D7C009E676F /* TGBridgeAudioSignals.m in Sources */, + 09C5722721728AA500BDF00F /* TGNeoBubbleMessageViewModel.m in Sources */, + 09C571CF2172874B00BDF00F /* TGUserHandle.m in Sources */, + 09C572D3217293D400BDF00F /* TGBridgeCommon.m in Sources */, + 09C5722821728AA500BDF00F /* TGNeoContactMessageViewModel.m in Sources */, + 09C573222172953800BDF00F /* TGBridgeUser.m in Sources */, + 09C571EF2172878900BDF00F /* TGStickersSectionHeaderController.m in Sources */, + 09C5714021727DAA00BDF00F /* TGDateUtils.m in Sources */, + 09C5732C2172953900BDF00F /* TGBridgeReplyMessageMediaAttachment.m in Sources */, + 09C571942172806600BDF00F /* TGNeoViewModel.m in Sources */, + 09C5727C21728D3700BDF00F /* SSignal+Accumulate.m in Sources */, + 09C5714A21727DAA00BDF00F /* WKInterfaceTable+TGDataDrivenTable.m in Sources */, + 09C571DE2172876F00BDF00F /* TGLocationVenueRowController.m in Sources */, + 09C5716921727F1500BDF00F /* TGInputController.m in Sources */, + 09C5723921728AA500BDF00F /* TGChatInfo.m in Sources */, + 09C5732A2172953900BDF00F /* TGBridgeDocumentMediaAttachment.m in Sources */, + 09C571962172806D00BDF00F /* TGComplicationController.m in Sources */, + 09C5723621728AA500BDF00F /* TGNeoTextMessageViewModel.m in Sources */, + 09C5722B21728AA500BDF00F /* TGNeoConversationStaticRowController.m in Sources */, + 09C573272172953800BDF00F /* TGBridgeWebPageMediaAttachment.m in Sources */, + 09C5718F2172806600BDF00F /* TGMessageViewModel.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D00859981B28189D00EAF753 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D00859A21B28189D00EAF753 /* AppDelegate.swift in Sources */, + 09C50E7B21738178009E676F /* TGBridgeServer.m in Sources */, + D0ADF95C212B636D00310BBC /* LegacyChatImport.swift in Sources */, + 09D3042E2174344900C00567 /* TGBridgeContactMediaAttachment.m in Sources */, + D05B37F71FEA8C640041D2A5 /* SnapshotSecretChat.swift in Sources */, + 09C50E842173853E009E676F /* TGBridgeCommon.m in Sources */, + 09D3042D2174344900C00567 /* TGBridgeAudioMediaAttachment.m in Sources */, + D05B37F51FEA5F6E0041D2A5 /* SnapshotEnvironment.swift in Sources */, + 0956AF2C217B4642008106D0 /* WatchCommunicationManager.swift in Sources */, + 09D304372174344900C00567 /* TGBridgeVideoMediaAttachment.m in Sources */, + 09C50E912173B247009E676F /* TGBridgeSubscriptions.m in Sources */, + 09D304262174341A00C00567 /* TGBridgeLocationVenue.m in Sources */, + 09D304312174344900C00567 /* TGBridgeImageMediaAttachment.m in Sources */, + D039FB172170F06A00BD1BAD /* PreFetchedLegacyResource.swift in Sources */, + 09D304292174343300C00567 /* TGBridgeBotInfo.m in Sources */, + 09D304272174341E00C00567 /* TGBridgeChatMessages.m in Sources */, + D02E31231BD803E800CD3F01 /* main.m in Sources */, + D05B37FD1FEA8D870041D2A5 /* SnapshotResources.swift in Sources */, + D0EB243B201B77C400F6CC13 /* ClearNotificationsManager.swift in Sources */, + 09D304242174340E00C00567 /* TGBridgeMessage.m in Sources */, + 09D304362174344900C00567 /* TGBridgeUnsupportedMediaAttachment.m in Sources */, + D0ADF95A212B5AC600310BBC /* LegacyResourceImport.swift in Sources */, + D06E4C2F21347D9200088087 /* UIImage+ImageEffects.m in Sources */, + D0B3B53B21666C0000FC60A0 /* LegacyFileImport.swift in Sources */, + D0ADF95E212C818F00310BBC /* LegacyPreferencesImport.swift in Sources */, + 09D304282174342E00C00567 /* TGBridgeChat.m in Sources */, + 09C50E8A2173AEDB009E676F /* WatchRequestHandlers.swift in Sources */, + 09D304302174344900C00567 /* TGBridgeForwardedMessageMediaAttachment.m in Sources */, + D0D2276F212739120028F943 /* LegacyDataImport.swift in Sources */, + D0ADF961212C8DF600310BBC /* TGAutoDownloadPreferences.m in Sources */, + D05B37FB1FEA8D020041D2A5 /* SnapshotAppearanceSettings.swift in Sources */, + D0CE6F1C213ED11100BCD44B /* TGPresentationAutoNightPreferences.m in Sources */, + D0A18D691E16AC9D004C6734 /* NotificationManager.swift in Sources */, + D053DAD32018ED2B00993D32 /* LockedWindowCoveringView.swift in Sources */, + D09250021FE52D2A003F693F /* BuildConfig.m in Sources */, + 09D304392174344900C00567 /* TGBridgeMessageEntities.m in Sources */, + 09D304352174344900C00567 /* TGBridgeReplyMessageMediaAttachment.m in Sources */, + 09D304322174344900C00567 /* TGBridgeLocationMediaAttachment.m in Sources */, + D084023220E1883500065674 /* ApplicationShortcutItem.swift in Sources */, + D0ADF958212B56DC00310BBC /* LegacyUserDataImport.swift in Sources */, + 09D304332174344900C00567 /* TGBridgeMessageEntitiesAttachment.m in Sources */, + 09D304342174344900C00567 /* TGBridgeReplyMarkupMediaAttachment.m in Sources */, + D0A18D651E15C020004C6734 /* WakeupManager.swift in Sources */, + D051DB5D21602D6E00F30F92 /* LegacyDataImportSplash.swift in Sources */, + 09D304382174344900C00567 /* TGBridgeWebPageMediaAttachment.m in Sources */, + 09D3042F2174344900C00567 /* TGBridgeDocumentMediaAttachment.m in Sources */, + 09D3042C2174344900C00567 /* TGBridgeActionMediaAttachment.m in Sources */, + D0ECCB8D1FE9CE3F00609802 /* SnapshotChatList.swift in Sources */, + D0ADF964212C9AA900310BBC /* TGProxyItem.m in Sources */, + 09C50E8321738514009E676F /* TGBridgeContext.m in Sources */, + 09D304222174335F00C00567 /* WatchBridge.swift in Sources */, + 09D304252174341200C00567 /* TGBridgeMediaAttachment.m in Sources */, + 09D304232174340900C00567 /* TGBridgeUser.m in Sources */, + D05B37F91FEA8CF00041D2A5 /* SnapshotSettings.swift in Sources */, + 09D3042A2174343B00C00567 /* TGBridgeBotCommandInfo.m in Sources */, + D0BEAF731E54C9A900BD963D /* ApplicationContext.swift in Sources */, + D0612E491D58B478000C8F02 /* Application.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D00859AD1B28189D00EAF753 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D03BCCCA1C6EBD670097A291 /* ListViewTests.swift in Sources */, + D00859B81B28189D00EAF753 /* Telegram_iOSTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D02CF5F8215D9ABE00E0F56A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D02CF61C215E51D500E0F56A /* BuildConfig.m in Sources */, + D02CF601215D9ABF00E0F56A /* NotificationViewController.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D03B0E741D63484500955575 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D08410451FABDC5D008FFE92 /* TGItemProviderSignals.m in Sources */, + D084104F1FABDCFD008FFE92 /* TGMimeTypeMap.m in Sources */, + D03B0E7B1D63484500955575 /* ShareRootController.swift in Sources */, + D0EA97941FE84F2D00792DD6 /* BuildConfig.m in Sources */, + D084104E1FABDCFD008FFE92 /* TGContactModel.m in Sources */, + D08410541FABE428008FFE92 /* ShareItems.swift in Sources */, + D0AF322F1FACBA280097362B /* TGShareLocationSignals.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D0B2F733204F4C9900D3BFB9 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D0B2F7602050102600D3BFB9 /* PeerNode.swift in Sources */, + D0B2F755204F4EAF00D3BFB9 /* BuildConfig.m in Sources */, + D08DB0A4213F42F400F2ADBF /* TodayViewController.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D0D268721D79A70A00C422DA /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D08985072119B7FE00918162 /* IntentContacts.swift in Sources */, + D0EA97951FE84F2E00792DD6 /* BuildConfig.m in Sources */, + D0D268791D79A70A00C422DA /* IntentHandler.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D0ECCB781FE9C38500609802 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D0ECCB7F1FE9C38500609802 /* Telegram_iOS_UITests.swift in Sources */, + D0ECCB8A1FE9C4AC00609802 /* SnapshotHelper.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 09C56F9A2172797500BDF00F /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 09C56F962172797400BDF00F /* Watch Extension */; + targetProxy = 09C56F992172797500BDF00F /* PBXContainerItemProxy */; + }; + 09C56FA42172797500BDF00F /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 09C56F8A2172797200BDF00F /* Watch */; + targetProxy = 09C56FA32172797500BDF00F /* PBXContainerItemProxy */; + }; + D00859B31B28189D00EAF753 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D008599B1B28189D00EAF753 /* Telegram-iOS */; + targetProxy = D00859B21B28189D00EAF753 /* PBXContainerItemProxy */; + }; + D02CF607215D9ABF00E0F56A /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D02CF5FB215D9ABE00E0F56A /* NotificationContent */; + targetProxy = D02CF606215D9ABF00E0F56A /* PBXContainerItemProxy */; + }; + D03B0E811D63484500955575 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D03B0E771D63484500955575 /* Share */; + targetProxy = D03B0E801D63484500955575 /* PBXContainerItemProxy */; + }; + D0B2F741204F4C9900D3BFB9 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D0B2F736204F4C9900D3BFB9 /* Widget */; + targetProxy = D0B2F740204F4C9900D3BFB9 /* PBXContainerItemProxy */; + }; + D0D2688D1D79A70B00C422DA /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D0D268751D79A70A00C422DA /* SiriIntents */; + targetProxy = D0D2688C1D79A70B00C422DA /* PBXContainerItemProxy */; + }; + D0ECCB821FE9C38500609802 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D008599B1B28189D00EAF753 /* Telegram-iOS */; + targetProxy = D0ECCB811FE9C38500609802 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 092F368321542D6C001A9F49 /* Localizable.strings */ = { + isa = PBXVariantGroup; + children = ( + 092F368421542D6C001A9F49 /* en */, + ); + name = Localizable.strings; + sourceTree = ""; + }; + 0972C6E221792D120069E98A /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + 0972C6E321792D120069E98A /* ru */, + ); + name = InfoPlist.strings; + sourceTree = ""; + }; + 09C56F8D2172797200BDF00F /* Interface.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 09C56F8E2172797200BDF00F /* Base */, + ); + name = Interface.storyboard; + sourceTree = ""; + }; + D00859AA1B28189D00EAF753 /* LaunchScreen.xib */ = { + isa = PBXVariantGroup; + children = ( + D00859AB1B28189D00EAF753 /* Base */, + ); + name = LaunchScreen.xib; + sourceTree = ""; + }; + D00ED7581FE94630001F38BD /* AppIntentVocabulary.plist */ = { + isa = PBXVariantGroup; + children = ( + D00ED7591FE94630001F38BD /* en */, + ); + name = AppIntentVocabulary.plist; + sourceTree = ""; + }; + D00ED75B1FE95287001F38BD /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + D00ED75C1FE95287001F38BD /* en */, + ); + name = InfoPlist.strings; + sourceTree = ""; + }; + D02CF602215D9ABF00E0F56A /* MainInterface.storyboard */ = { + isa = PBXVariantGroup; + children = ( + D02CF603215D9ABF00E0F56A /* Base */, + ); + name = MainInterface.storyboard; + sourceTree = ""; + }; + D04FA1AF2145E37F0006EF45 /* Localizable.strings */ = { + isa = PBXVariantGroup; + children = ( + D04FA1B02145E37F0006EF45 /* en */, + ); + name = Localizable.strings; + sourceTree = ""; + }; + D04FA1B12145E37F0006EF45 /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + D04FA1B22145E37F0006EF45 /* en */, + ); + name = InfoPlist.strings; + sourceTree = ""; + }; + D04FA1B42145E37F0006EF45 /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + D04FA1B52145E37F0006EF45 /* de */, + ); + name = InfoPlist.strings; + sourceTree = ""; + }; + D04FA1B72145E3800006EF45 /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + D04FA1B82145E3800006EF45 /* ar */, + ); + name = InfoPlist.strings; + sourceTree = ""; + }; + D04FA1BA2145E3800006EF45 /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + D04FA1BB2145E3800006EF45 /* nl */, + ); + name = InfoPlist.strings; + sourceTree = ""; + }; + D04FA1BD2145E3800006EF45 /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + D04FA1BE2145E3800006EF45 /* ko */, + ); + name = InfoPlist.strings; + sourceTree = ""; + }; + D04FA1C02145E3800006EF45 /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + D04FA1C12145E3800006EF45 /* it */, + ); + name = InfoPlist.strings; + sourceTree = ""; + }; + D04FA1C32145E3810006EF45 /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + D04FA1C42145E3810006EF45 /* pt */, + ); + name = InfoPlist.strings; + sourceTree = ""; + }; + D04FA1C62145E3810006EF45 /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + D04FA1C72145E3810006EF45 /* es */, + ); + name = InfoPlist.strings; + sourceTree = ""; + }; + D09DCBB51D0C856B00F51FFE /* Localizable.strings */ = { + isa = PBXVariantGroup; + children = ( + D09DCBB61D0C856B00F51FFE /* en */, + ); + name = Localizable.strings; + sourceTree = ""; + }; + D0CE6F1E213EDA4200BCD44B /* Localizable.strings */ = { + isa = PBXVariantGroup; + children = ( + D0CE6F1F213EDA4200BCD44B /* pt */, + ); + name = Localizable.strings; + sourceTree = ""; + }; + D0CE6F20213EDA4200BCD44B /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + D0CE6F21213EDA4200BCD44B /* pt */, + ); + name = InfoPlist.strings; + sourceTree = ""; + }; + D0CE6F22213EDA4200BCD44B /* AppIntentVocabulary.plist */ = { + isa = PBXVariantGroup; + children = ( + D0CE6F23213EDA4200BCD44B /* pt */, + ); + name = AppIntentVocabulary.plist; + sourceTree = ""; + }; + D0CE6F25213EDA4300BCD44B /* Localizable.strings */ = { + isa = PBXVariantGroup; + children = ( + D0CE6F26213EDA4300BCD44B /* it */, + ); + name = Localizable.strings; + sourceTree = ""; + }; + D0CE6F27213EDA4300BCD44B /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + D0CE6F28213EDA4300BCD44B /* it */, + ); + name = InfoPlist.strings; + sourceTree = ""; + }; + D0CE6F29213EDA4300BCD44B /* AppIntentVocabulary.plist */ = { + isa = PBXVariantGroup; + children = ( + D0CE6F2A213EDA4300BCD44B /* it */, + ); + name = AppIntentVocabulary.plist; + sourceTree = ""; + }; + D0CE6F2C213EDA4300BCD44B /* Localizable.strings */ = { + isa = PBXVariantGroup; + children = ( + D0CE6F2D213EDA4300BCD44B /* ko */, + ); + name = Localizable.strings; + sourceTree = ""; + }; + D0CE6F2E213EDA4300BCD44B /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + D0CE6F2F213EDA4300BCD44B /* ko */, + ); + name = InfoPlist.strings; + sourceTree = ""; + }; + D0CE6F30213EDA4300BCD44B /* AppIntentVocabulary.plist */ = { + isa = PBXVariantGroup; + children = ( + D0CE6F31213EDA4300BCD44B /* ko */, + ); + name = AppIntentVocabulary.plist; + sourceTree = ""; + }; + D0CE6F33213EDA4300BCD44B /* Localizable.strings */ = { + isa = PBXVariantGroup; + children = ( + D0CE6F34213EDA4300BCD44B /* nl */, + ); + name = Localizable.strings; + sourceTree = ""; + }; + D0CE6F35213EDA4300BCD44B /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + D0CE6F36213EDA4300BCD44B /* nl */, + ); + name = InfoPlist.strings; + sourceTree = ""; + }; + D0CE6F37213EDA4300BCD44B /* AppIntentVocabulary.plist */ = { + isa = PBXVariantGroup; + children = ( + D0CE6F38213EDA4300BCD44B /* nl */, + ); + name = AppIntentVocabulary.plist; + sourceTree = ""; + }; + D0CE6F3A213EDA4300BCD44B /* Localizable.strings */ = { + isa = PBXVariantGroup; + children = ( + D0CE6F3B213EDA4300BCD44B /* ru */, + ); + name = Localizable.strings; + sourceTree = ""; + }; + D0CE6F3C213EDA4300BCD44B /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + D0CE6F3D213EDA4300BCD44B /* ru */, + ); + name = InfoPlist.strings; + sourceTree = ""; + }; + D0CE6F3E213EDA4300BCD44B /* AppIntentVocabulary.plist */ = { + isa = PBXVariantGroup; + children = ( + D0CE6F3F213EDA4300BCD44B /* ru */, + ); + name = AppIntentVocabulary.plist; + sourceTree = ""; + }; + D0CE6F41213EDA4400BCD44B /* Localizable.strings */ = { + isa = PBXVariantGroup; + children = ( + D0CE6F42213EDA4400BCD44B /* es */, + ); + name = Localizable.strings; + sourceTree = ""; + }; + D0CE6F43213EDA4400BCD44B /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + D0CE6F44213EDA4400BCD44B /* es */, + ); + name = InfoPlist.strings; + sourceTree = ""; + }; + D0CE6F45213EDA4400BCD44B /* AppIntentVocabulary.plist */ = { + isa = PBXVariantGroup; + children = ( + D0CE6F46213EDA4400BCD44B /* es */, + ); + name = AppIntentVocabulary.plist; + sourceTree = ""; + }; + D0CE6F48213EDA4400BCD44B /* Localizable.strings */ = { + isa = PBXVariantGroup; + children = ( + D0CE6F49213EDA4400BCD44B /* ar */, + ); + name = Localizable.strings; + sourceTree = ""; + }; + D0CE6F4A213EDA4400BCD44B /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + D0CE6F4B213EDA4400BCD44B /* ar */, + ); + name = InfoPlist.strings; + sourceTree = ""; + }; + D0CE6F4C213EDA4400BCD44B /* AppIntentVocabulary.plist */ = { + isa = PBXVariantGroup; + children = ( + D0CE6F4D213EDA4400BCD44B /* ar */, + ); + name = AppIntentVocabulary.plist; + sourceTree = ""; + }; + D0CE6F4F213EDA4400BCD44B /* Localizable.strings */ = { + isa = PBXVariantGroup; + children = ( + D0CE6F50213EDA4400BCD44B /* de */, + ); + name = Localizable.strings; + sourceTree = ""; + }; + D0CE6F51213EDA4400BCD44B /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + D0CE6F52213EDA4400BCD44B /* de */, + ); + name = InfoPlist.strings; + sourceTree = ""; + }; + D0CE6F53213EDA4400BCD44B /* AppIntentVocabulary.plist */ = { + isa = PBXVariantGroup; + children = ( + D0CE6F54213EDA4400BCD44B /* de */, + ); + name = AppIntentVocabulary.plist; + sourceTree = ""; + }; + D0D268851D79A70A00C422DA /* MainInterface.storyboard */ = { + isa = PBXVariantGroup; + children = ( + D0D268861D79A70A00C422DA /* Base */, + ); + name = MainInterface.storyboard; + sourceTree = ""; + }; + D0E41A3D1D65A69C00FBFC00 /* MainInterface.storyboard */ = { + isa = PBXVariantGroup; + children = ( + D0E41A3E1D65A69C00FBFC00 /* Base */, + ); + name = MainInterface.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 09C56FA62172797500BDF00F /* Debug Hockeyapp */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D079FD011F06BBD60038FADE /* Config-Hockeyapp.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_STYLE = Manual; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = X834Q8SBVP; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IBSC_MODULE = Watch_Extension; + INFOPLIST_FILE = Watch/App/Info.plist; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = "${APP_BUNDLE_ID}.watchkitapp"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = "match Development org.telegram.Telegram-iOS.watchkitapp"; + SDKROOT = watchos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 4.2; + TARGETED_DEVICE_FAMILY = 4; + WATCHOS_DEPLOYMENT_TARGET = 2.2; + }; + name = "Debug Hockeyapp"; + }; + 09C56FA72172797500BDF00F /* Debug AppStore */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D079FD021F06BBD60038FADE /* Config-AppStore.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer: Peter Iakovlev (88P695A564)"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer: Peter Iakovlev (88P695A564)"; + CODE_SIGN_STYLE = Manual; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = 6N38VWS5BX; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IBSC_MODULE = Watch_Extension; + INFOPLIST_FILE = Watch/App/Info.plist; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = "${APP_BUNDLE_ID}.watchkitapp"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = "match Development org.telegram.TelegramHD.watchkitapp"; + SDKROOT = watchos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 4.2; + TARGETED_DEVICE_FAMILY = 4; + WATCHOS_DEPLOYMENT_TARGET = 2.2; + }; + name = "Debug AppStore"; + }; + 09C56FA82172797500BDF00F /* Debug AppStore LLC */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D0ADF914212B399A00310BBC /* Config-AppStoreLLC.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_STYLE = Manual; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = C67CF9S4VU; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IBSC_MODULE = Watch_Extension; + INFOPLIST_FILE = Watch/App/Info.plist; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = "${APP_BUNDLE_ID}.watchkitapp"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = "match Development ph.telegra.Telegraph.watchkitapp"; + SDKROOT = watchos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 4.2; + TARGETED_DEVICE_FAMILY = 4; + WATCHOS_DEPLOYMENT_TARGET = 2.2; + }; + name = "Debug AppStore LLC"; + }; + 09C56FA92172797500BDF00F /* Release AppStore */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D079FD021F06BBD60038FADE /* Config-AppStore.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Distribution: TELEGRAM MESSENGER LLP (6N38VWS5BX)"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution: TELEGRAM MESSENGER LLP (6N38VWS5BX)"; + CODE_SIGN_STYLE = Manual; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = 6N38VWS5BX; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IBSC_MODULE = Watch_Extension; + INFOPLIST_FILE = Watch/App/Info.plist; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = "${APP_BUNDLE_ID}.watchkitapp"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = "match AppStore org.telegram.TelegramHD.watchkitapp"; + SDKROOT = watchos; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 4.2; + TARGETED_DEVICE_FAMILY = 4; + VALIDATE_PRODUCT = YES; + WATCHOS_DEPLOYMENT_TARGET = 2.2; + }; + name = "Release AppStore"; + }; + 09C56FAA2172797500BDF00F /* Release AppStore LLC */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D0ADF914212B399A00310BBC /* Config-AppStoreLLC.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Distribution: Digital Fortress LLC (C67CF9S4VU)"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution: Digital Fortress LLC (C67CF9S4VU)"; + CODE_SIGN_STYLE = Manual; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = C67CF9S4VU; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IBSC_MODULE = Watch_Extension; + INFOPLIST_FILE = Watch/App/Info.plist; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = "${APP_BUNDLE_ID}.watchkitapp"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = "match AppStore ph.telegra.Telegraph.watchkitapp"; + SDKROOT = watchos; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 4.2; + TARGETED_DEVICE_FAMILY = 4; + VALIDATE_PRODUCT = YES; + WATCHOS_DEPLOYMENT_TARGET = 2.2; + }; + name = "Release AppStore LLC"; + }; + 09C56FAB2172797500BDF00F /* Release Hockeyapp */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D079FD011F06BBD60038FADE /* Config-Hockeyapp.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Distribution: TELEGRAM MESSENGER LLP"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution: TELEGRAM MESSENGER LLP"; + CODE_SIGN_STYLE = Manual; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = X834Q8SBVP; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IBSC_MODULE = Watch_Extension; + INFOPLIST_FILE = Watch/App/Info.plist; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = "${APP_BUNDLE_ID}.watchkitapp"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = "match InHouse org.telegram.Telegram-iOS.watchkitapp"; + SDKROOT = watchos; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 4.2; + TARGETED_DEVICE_FAMILY = 4; + VALIDATE_PRODUCT = YES; + WATCHOS_DEPLOYMENT_TARGET = 2.2; + }; + name = "Release Hockeyapp"; + }; + 09C56FAC2172797500BDF00F /* Release Hockeyapp Internal */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D0924FFE1FE52C75003F693F /* Config-Hockeyapp Internal.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Distribution: TELEGRAM MESSENGER LLP"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution: TELEGRAM MESSENGER LLP"; + CODE_SIGN_STYLE = Manual; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = X834Q8SBVP; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IBSC_MODULE = Watch_Extension; + INFOPLIST_FILE = Watch/App/Info.plist; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = "${APP_BUNDLE_ID}.watchkitapp"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = "match InHouse org.telegram.Telegram-iOS.watchkitapp"; + SDKROOT = watchos; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 4.2; + TARGETED_DEVICE_FAMILY = 4; + VALIDATE_PRODUCT = YES; + WATCHOS_DEPLOYMENT_TARGET = 2.2; + }; + name = "Release Hockeyapp Internal"; + }; + 09C56FAD2172797500BDF00F /* Debug Hockeyapp */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D079FD011F06BBD60038FADE /* Config-Hockeyapp.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_COMPLICATION_NAME = Complication; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Manual; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = X834Q8SBVP; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + HEADER_SEARCH_PATHS = submodules/SSignalKit; + INFOPLIST_FILE = Watch/Extension/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + LIBRARY_SEARCH_PATHS = submodules/SSignalKit; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = "${APP_BUNDLE_ID}.watchkitapp.watchkitextension"; + PRODUCT_NAME = "${TARGET_NAME}"; + PROVISIONING_PROFILE_SPECIFIER = "match Development org.telegram.Telegram-iOS.watchkitapp.watchkitextension"; + SDKROOT = watchos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 4.2; + TARGETED_DEVICE_FAMILY = 4; + WATCHOS_DEPLOYMENT_TARGET = 2.2; + }; + name = "Debug Hockeyapp"; + }; + 09C56FAE2172797500BDF00F /* Debug AppStore */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D079FD021F06BBD60038FADE /* Config-AppStore.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_COMPLICATION_NAME = Complication; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Manual; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = 6N38VWS5BX; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + HEADER_SEARCH_PATHS = submodules/SSignalKit; + INFOPLIST_FILE = Watch/Extension/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + LIBRARY_SEARCH_PATHS = submodules/SSignalKit; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = "${APP_BUNDLE_ID}.watchkitapp.watchkitextension"; + PRODUCT_NAME = "${TARGET_NAME}"; + PROVISIONING_PROFILE_SPECIFIER = "match Development org.telegram.TelegramHD.watchkitapp.watchkitextension"; + SDKROOT = watchos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 4.2; + TARGETED_DEVICE_FAMILY = 4; + WATCHOS_DEPLOYMENT_TARGET = 2.2; + }; + name = "Debug AppStore"; + }; + 09C56FAF2172797500BDF00F /* Debug AppStore LLC */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D0ADF914212B399A00310BBC /* Config-AppStoreLLC.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_COMPLICATION_NAME = Complication; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Manual; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = C67CF9S4VU; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + HEADER_SEARCH_PATHS = submodules/SSignalKit; + INFOPLIST_FILE = Watch/Extension/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + LIBRARY_SEARCH_PATHS = submodules/SSignalKit; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = "${APP_BUNDLE_ID}.watchkitapp.watchkitextension"; + PRODUCT_NAME = "${TARGET_NAME}"; + PROVISIONING_PROFILE_SPECIFIER = "match Development ph.telegra.Telegraph.watchkitapp.watchkitextension"; + SDKROOT = watchos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 4.2; + TARGETED_DEVICE_FAMILY = 4; + WATCHOS_DEPLOYMENT_TARGET = 2.2; + }; + name = "Debug AppStore LLC"; + }; + 09C56FB02172797500BDF00F /* Release AppStore */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D079FD021F06BBD60038FADE /* Config-AppStore.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_COMPLICATION_NAME = Complication; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Distribution"; + CODE_SIGN_STYLE = Manual; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = 6N38VWS5BX; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + HEADER_SEARCH_PATHS = submodules/SSignalKit; + INFOPLIST_FILE = Watch/Extension/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + LIBRARY_SEARCH_PATHS = submodules/SSignalKit; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = "${APP_BUNDLE_ID}.watchkitapp.watchkitextension"; + PRODUCT_NAME = "${TARGET_NAME}"; + PROVISIONING_PROFILE_SPECIFIER = "match AppStore org.telegram.TelegramHD.watchkitapp.watchkitextension"; + SDKROOT = watchos; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 4.2; + TARGETED_DEVICE_FAMILY = 4; + VALIDATE_PRODUCT = YES; + WATCHOS_DEPLOYMENT_TARGET = 2.2; + }; + name = "Release AppStore"; + }; + 09C56FB12172797500BDF00F /* Release AppStore LLC */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D0ADF914212B399A00310BBC /* Config-AppStoreLLC.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_COMPLICATION_NAME = Complication; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Distribution"; + CODE_SIGN_STYLE = Manual; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = C67CF9S4VU; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + HEADER_SEARCH_PATHS = submodules/SSignalKit; + INFOPLIST_FILE = Watch/Extension/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + LIBRARY_SEARCH_PATHS = submodules/SSignalKit; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = "${APP_BUNDLE_ID}.watchkitapp.watchkitextension"; + PRODUCT_NAME = "${TARGET_NAME}"; + PROVISIONING_PROFILE_SPECIFIER = "match AppStore ph.telegra.Telegraph.watchkitapp.watchkitextension"; + SDKROOT = watchos; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 4.2; + TARGETED_DEVICE_FAMILY = 4; + VALIDATE_PRODUCT = YES; + WATCHOS_DEPLOYMENT_TARGET = 2.2; + }; + name = "Release AppStore LLC"; + }; + 09C56FB22172797500BDF00F /* Release Hockeyapp */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D079FD011F06BBD60038FADE /* Config-Hockeyapp.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_COMPLICATION_NAME = Complication; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_STYLE = Manual; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = X834Q8SBVP; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + HEADER_SEARCH_PATHS = submodules/SSignalKit; + INFOPLIST_FILE = Watch/Extension/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + LIBRARY_SEARCH_PATHS = submodules/SSignalKit; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = "${APP_BUNDLE_ID}.watchkitapp.watchkitextension"; + PRODUCT_NAME = "${TARGET_NAME}"; + PROVISIONING_PROFILE_SPECIFIER = "match InHouse org.telegram.Telegram-iOS.watchkitapp.watchkitextension"; + SDKROOT = watchos; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 4.2; + TARGETED_DEVICE_FAMILY = 4; + VALIDATE_PRODUCT = YES; + WATCHOS_DEPLOYMENT_TARGET = 2.2; + }; + name = "Release Hockeyapp"; + }; + 09C56FB32172797500BDF00F /* Release Hockeyapp Internal */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D0924FFE1FE52C75003F693F /* Config-Hockeyapp Internal.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_COMPLICATION_NAME = Complication; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_STYLE = Manual; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = X834Q8SBVP; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + HEADER_SEARCH_PATHS = submodules/SSignalKit; + INFOPLIST_FILE = Watch/Extension/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + LIBRARY_SEARCH_PATHS = submodules/SSignalKit; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = "${APP_BUNDLE_ID}.watchkitapp.watchkitextension"; + PRODUCT_NAME = "${TARGET_NAME}"; + PROVISIONING_PROFILE_SPECIFIER = "match InHouse org.telegram.Telegram-iOS.watchkitapp.watchkitextension"; + SDKROOT = watchos; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 4.2; + TARGETED_DEVICE_FAMILY = 4; + VALIDATE_PRODUCT = YES; + WATCHOS_DEPLOYMENT_TARGET = 2.2; + }; + name = "Release Hockeyapp Internal"; + }; + D00859B91B28189D00EAF753 /* Debug Hockeyapp */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D079FD011F06BBD60038FADE /* Config-Hockeyapp.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer: Peter Iakovlev (9J4EJ3F97G)"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + PROVISIONING_PROFILE = "a4dc3795-99a0-4552-934a-0399f5df77b2"; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = "Debug Hockeyapp"; + }; + D00859BA1B28189D00EAF753 /* Release AppStore */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D079FD021F06BBD60038FADE /* Config-AppStore.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = "Release AppStore"; + }; + D00859BC1B28189D00EAF753 /* Debug Hockeyapp */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D079FD011F06BBD60038FADE /* Config-Hockeyapp.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + BITCODE_GENERATION_MODE = marker; + CLANG_MODULES_AUTOLINK = NO; + CODE_SIGN_ENTITLEMENTS = "Telegram-iOS/Telegram-iOS-Hockeyapp.entitlements"; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CURRENT_PROJECT_VERSION = 624; + DEVELOPMENT_TEAM = X834Q8SBVP; + ENABLE_BITCODE = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(BUILT_PRODUCTS_DIR)", + "$(PROJECT_DIR)/third-party", + ); + HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/Watch/Bridge"; + INFOPLIST_FILE = "Telegram-iOS/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/submodules/TelegramUI/third-party/opus/lib", + ); + OTHER_LDFLAGS = ( + "-ObjC", + "-lz", + ); + OTHER_SWIFT_FLAGS = "-D DEBUG"; + PRODUCT_BUNDLE_IDENTIFIER = "${APP_BUNDLE_ID}"; + PRODUCT_NAME = "Telegram X"; + PROVISIONING_PROFILE = "1e62fceb-1fb5-4804-ad0f-9b9b974393b8"; + PROVISIONING_PROFILE_SPECIFIER = "match Development org.telegram.Telegram-iOS"; + SWIFT_OBJC_BRIDGING_HEADER = "Telegram-iOS/Telegram-Bridging-Header.h"; + SWIFT_VERSION = 4.0; + USER_HEADER_SEARCH_PATHS = ""; + }; + name = "Debug Hockeyapp"; + }; + D00859BD1B28189D00EAF753 /* Release AppStore */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D079FD021F06BBD60038FADE /* Config-AppStore.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + BITCODE_GENERATION_MODE = bitcode; + CLANG_MODULES_AUTOLINK = NO; + CODE_SIGN_ENTITLEMENTS = "Telegram-iOS/Telegram-iOS-AppStore.entitlements"; + CODE_SIGN_IDENTITY = "iPhone Distribution: TELEGRAM MESSENGER LLP (6N38VWS5BX)"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution: TELEGRAM MESSENGER LLP (6N38VWS5BX)"; + CURRENT_PROJECT_VERSION = 624; + DEVELOPMENT_TEAM = 6N38VWS5BX; + ENABLE_BITCODE = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(BUILT_PRODUCTS_DIR)", + "$(PROJECT_DIR)/third-party", + ); + HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/Watch/Bridge"; + INFOPLIST_FILE = "Telegram-iOS/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/submodules/TelegramUI/third-party/opus/lib", + ); + OTHER_LDFLAGS = ( + "-ObjC", + "-lz", + ); + OTHER_SWIFT_FLAGS = ""; + PRODUCT_BUNDLE_IDENTIFIER = "${APP_BUNDLE_ID}"; + PRODUCT_NAME = "Telegram X"; + PROVISIONING_PROFILE = "df24dc18-3fdc-447c-b347-5a110cecd77d"; + PROVISIONING_PROFILE_SPECIFIER = "match AppStore org.telegram.TelegramHD"; + SWIFT_OBJC_BRIDGING_HEADER = "Telegram-iOS/Telegram-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 4.0; + USER_HEADER_SEARCH_PATHS = ""; + }; + name = "Release AppStore"; + }; + D00859BF1B28189D00EAF753 /* Debug Hockeyapp */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D079FD011F06BBD60038FADE /* Config-Hockeyapp.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + FRAMEWORK_SEARCH_PATHS = "$(SDKROOT)/Developer/Library/Frameworks"; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + INFOPLIST_FILE = "Telegram-iOSTests/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "org.telegram.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 3.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Telegram-iOS.app/Telegram-iOS"; + }; + name = "Debug Hockeyapp"; + }; + D00859C01B28189D00EAF753 /* Release AppStore */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D079FD021F06BBD60038FADE /* Config-AppStore.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + FRAMEWORK_SEARCH_PATHS = "$(SDKROOT)/Developer/Library/Frameworks"; + INFOPLIST_FILE = "Telegram-iOSTests/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "org.telegram.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 3.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Telegram-iOS.app/Telegram-iOS"; + }; + name = "Release AppStore"; + }; + D02CF609215D9ABF00E0F56A /* Debug Hockeyapp */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D079FD011F06BBD60038FADE /* Config-Hockeyapp.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_ENTITLEMENTS = "NotificationContent/NotificationContent-HockeyApp.entitlements"; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_STYLE = Manual; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = X834Q8SBVP; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + INFOPLIST_FILE = NotificationContent/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = "${APP_BUNDLE_ID}.NotificationContent"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = "match Development org.telegram.Telegram-iOS.NotificationContent"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OBJC_BRIDGING_HEADER = "NotificationContent/NotificationContent-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 4.2; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = "Debug Hockeyapp"; + }; + D02CF60A215D9ABF00E0F56A /* Debug AppStore */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D079FD021F06BBD60038FADE /* Config-AppStore.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_ENTITLEMENTS = "NotificationContent/NotificationContent-AppStore.entitlements"; + CODE_SIGN_IDENTITY = "iPhone Developer: Peter Iakovlev (88P695A564)"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer: Peter Iakovlev (88P695A564)"; + CODE_SIGN_STYLE = Manual; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = 6N38VWS5BX; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + INFOPLIST_FILE = NotificationContent/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = "${APP_BUNDLE_ID}.NotificationContent"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = "match Development org.telegram.TelegramHD.NotificationContent"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OBJC_BRIDGING_HEADER = "NotificationContent/NotificationContent-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 4.2; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = "Debug AppStore"; + }; + D02CF60B215D9ABF00E0F56A /* Debug AppStore LLC */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D0ADF914212B399A00310BBC /* Config-AppStoreLLC.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_ENTITLEMENTS = "NotificationContent/NotificationContent-AppStoreLLC.entitlements"; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_STYLE = Manual; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = C67CF9S4VU; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + INFOPLIST_FILE = NotificationContent/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = "${APP_BUNDLE_ID}.NotificationContent"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = "match Development ph.telegra.Telegraph.NotificationContent"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OBJC_BRIDGING_HEADER = "NotificationContent/NotificationContent-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 4.2; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = "Debug AppStore LLC"; + }; + D02CF60C215D9ABF00E0F56A /* Release AppStore */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D079FD021F06BBD60038FADE /* Config-AppStore.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_ENTITLEMENTS = "NotificationContent/NotificationContent-AppStore.entitlements"; + CODE_SIGN_IDENTITY = "iPhone Distribution: TELEGRAM MESSENGER LLP (6N38VWS5BX)"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution: TELEGRAM MESSENGER LLP (6N38VWS5BX)"; + CODE_SIGN_STYLE = Manual; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = 6N38VWS5BX; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + INFOPLIST_FILE = NotificationContent/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = "${APP_BUNDLE_ID}.NotificationContent"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = "match AppStore org.telegram.TelegramHD.NotificationContent"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_OBJC_BRIDGING_HEADER = "NotificationContent/NotificationContent-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 4.2; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = "Release AppStore"; + }; + D02CF60D215D9ABF00E0F56A /* Release AppStore LLC */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D0ADF914212B399A00310BBC /* Config-AppStoreLLC.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_ENTITLEMENTS = "NotificationContent/NotificationContent-AppStoreLLC.entitlements"; + CODE_SIGN_IDENTITY = "iPhone Distribution: Digital Fortress LLC (C67CF9S4VU)"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution: Digital Fortress LLC (C67CF9S4VU)"; + CODE_SIGN_STYLE = Manual; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = C67CF9S4VU; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + INFOPLIST_FILE = NotificationContent/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = "${APP_BUNDLE_ID}.NotificationContent"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = "match AppStore ph.telegra.Telegraph.NotificationContent"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_OBJC_BRIDGING_HEADER = "NotificationContent/NotificationContent-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 4.2; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = "Release AppStore LLC"; + }; + D02CF60E215D9ABF00E0F56A /* Release Hockeyapp */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D079FD011F06BBD60038FADE /* Config-Hockeyapp.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_ENTITLEMENTS = "NotificationContent/NotificationContent-HockeyApp.entitlements"; + CODE_SIGN_IDENTITY = "iPhone Distribution: TELEGRAM MESSENGER LLP"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution: TELEGRAM MESSENGER LLP"; + CODE_SIGN_STYLE = Manual; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = X834Q8SBVP; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + INFOPLIST_FILE = NotificationContent/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = "${APP_BUNDLE_ID}.NotificationContent"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = "match InHouse org.telegram.Telegram-iOS.NotificationContent"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_OBJC_BRIDGING_HEADER = "NotificationContent/NotificationContent-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 4.2; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = "Release Hockeyapp"; + }; + D02CF60F215D9ABF00E0F56A /* Release Hockeyapp Internal */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D0924FFE1FE52C75003F693F /* Config-Hockeyapp Internal.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_ENTITLEMENTS = "NotificationContent/NotificationContent-HockeyApp.entitlements"; + CODE_SIGN_IDENTITY = "iPhone Distribution: TELEGRAM MESSENGER LLP"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution: TELEGRAM MESSENGER LLP"; + CODE_SIGN_STYLE = Manual; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = X834Q8SBVP; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + INFOPLIST_FILE = NotificationContent/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = "${APP_BUNDLE_ID}.NotificationContent"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = "match InHouse org.telegram.Telegram-iOS.NotificationContent"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_OBJC_BRIDGING_HEADER = "NotificationContent/NotificationContent-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 4.2; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = "Release Hockeyapp Internal"; + }; + D03B0E841D63484500955575 /* Debug Hockeyapp */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D079FD011F06BBD60038FADE /* Config-Hockeyapp.xcconfig */; + buildSettings = { + BITCODE_GENERATION_MODE = marker; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_SUSPICIOUS_MOVES = YES; + CODE_SIGN_ENTITLEMENTS = "Share/Share-HockeyApp.entitlements"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CURRENT_PROJECT_VERSION = 624; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = X834Q8SBVP; + INFOPLIST_FILE = Share/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "${APP_BUNDLE_ID}.Share"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE = "6a75ec98-1ed3-4485-83ed-3d56de6a6bb1"; + PROVISIONING_PROFILE_SPECIFIER = "match Development org.telegram.Telegram-iOS.Share"; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_INSTALL_OBJC_HEADER = NO; + SWIFT_OBJC_BRIDGING_HEADER = "Share/Share-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 4.0; + }; + name = "Debug Hockeyapp"; + }; + D03B0E851D63484500955575 /* Release AppStore */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D079FD021F06BBD60038FADE /* Config-AppStore.xcconfig */; + buildSettings = { + BITCODE_GENERATION_MODE = bitcode; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_SUSPICIOUS_MOVES = YES; + CODE_SIGN_ENTITLEMENTS = "Share/Share-AppStore.entitlements"; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; + CURRENT_PROJECT_VERSION = 624; + DEVELOPMENT_TEAM = 6N38VWS5BX; + INFOPLIST_FILE = Share/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "${APP_BUNDLE_ID}.Share"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE = "bd6d5558-5513-45ee-aa35-1bf57a1e7d95"; + PROVISIONING_PROFILE_SPECIFIER = "match AppStore org.telegram.TelegramHD.Share"; + SKIP_INSTALL = YES; + SWIFT_INSTALL_OBJC_HEADER = NO; + SWIFT_OBJC_BRIDGING_HEADER = "Share/Share-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 4.0; + }; + name = "Release AppStore"; + }; + D03B0E861D63484500955575 /* Release Hockeyapp */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D079FD011F06BBD60038FADE /* Config-Hockeyapp.xcconfig */; + buildSettings = { + BITCODE_GENERATION_MODE = bitcode; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_SUSPICIOUS_MOVES = YES; + CODE_SIGN_ENTITLEMENTS = "Share/Share-HockeyApp.entitlements"; + CODE_SIGN_IDENTITY = "iPhone Distribution: TELEGRAM MESSENGER LLP"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution: TELEGRAM MESSENGER LLP"; + CURRENT_PROJECT_VERSION = 624; + DEVELOPMENT_TEAM = X834Q8SBVP; + INFOPLIST_FILE = Share/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "${APP_BUNDLE_ID}.Share"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE = "cfe514a2-916a-4247-b5a4-c2c4c6fd2805"; + PROVISIONING_PROFILE_SPECIFIER = "match InHouse org.telegram.Telegram-iOS.Share"; + SKIP_INSTALL = YES; + SWIFT_INSTALL_OBJC_HEADER = NO; + SWIFT_OBJC_BRIDGING_HEADER = "Share/Share-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 4.0; + }; + name = "Release Hockeyapp"; + }; + D079FCF91F06BBA40038FADE /* Debug AppStore */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D079FD021F06BBD60038FADE /* Config-AppStore.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer: Peter Iakovlev (9J4EJ3F97G)"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + PROVISIONING_PROFILE = "a4dc3795-99a0-4552-934a-0399f5df77b2"; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = "Debug AppStore"; + }; + D079FCFA1F06BBA40038FADE /* Debug AppStore */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D079FD021F06BBD60038FADE /* Config-AppStore.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + BITCODE_GENERATION_MODE = marker; + CLANG_MODULES_AUTOLINK = NO; + CODE_SIGN_ENTITLEMENTS = "Telegram-iOS/Telegram-iOS-AppStore.entitlements"; + CODE_SIGN_IDENTITY = "iPhone Developer: Peter Iakovlev (88P695A564)"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer: Peter Iakovlev (88P695A564)"; + CURRENT_PROJECT_VERSION = 624; + DEVELOPMENT_TEAM = 6N38VWS5BX; + ENABLE_BITCODE = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(BUILT_PRODUCTS_DIR)", + "$(PROJECT_DIR)/third-party", + ); + HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/Watch/Bridge"; + INFOPLIST_FILE = "Telegram-iOS/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/submodules/TelegramUI/third-party/opus/lib", + ); + OTHER_LDFLAGS = ( + "-ObjC", + "-lz", + ); + OTHER_SWIFT_FLAGS = "-D DEBUG"; + PRODUCT_BUNDLE_IDENTIFIER = "${APP_BUNDLE_ID}"; + PRODUCT_NAME = "Telegram X"; + PROVISIONING_PROFILE = "954189c1-ae38-447b-a473-7c9b7198aa47"; + PROVISIONING_PROFILE_SPECIFIER = "match Development org.telegram.TelegramHD"; + SWIFT_OBJC_BRIDGING_HEADER = "Telegram-iOS/Telegram-Bridging-Header.h"; + SWIFT_VERSION = 4.0; + USER_HEADER_SEARCH_PATHS = ""; + }; + name = "Debug AppStore"; + }; + D079FCFB1F06BBA40038FADE /* Debug AppStore */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D079FD021F06BBD60038FADE /* Config-AppStore.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + FRAMEWORK_SEARCH_PATHS = "$(SDKROOT)/Developer/Library/Frameworks"; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + INFOPLIST_FILE = "Telegram-iOSTests/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "org.telegram.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 3.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Telegram-iOS.app/Telegram-iOS"; + }; + name = "Debug AppStore"; + }; + D079FCFD1F06BBA40038FADE /* Debug AppStore */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D079FD021F06BBD60038FADE /* Config-AppStore.xcconfig */; + buildSettings = { + BITCODE_GENERATION_MODE = marker; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_SUSPICIOUS_MOVES = YES; + CODE_SIGN_ENTITLEMENTS = "Share/Share-AppStore.entitlements"; + CODE_SIGN_IDENTITY = "iPhone Developer: Peter Iakovlev (88P695A564)"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer: Peter Iakovlev (88P695A564)"; + CURRENT_PROJECT_VERSION = 624; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = 6N38VWS5BX; + INFOPLIST_FILE = Share/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "${APP_BUNDLE_ID}.Share"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE = "388bbf37-24cd-44d4-b38d-9b1ad5006247"; + PROVISIONING_PROFILE_SPECIFIER = "match Development org.telegram.TelegramHD.Share"; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_INSTALL_OBJC_HEADER = NO; + SWIFT_OBJC_BRIDGING_HEADER = "Share/Share-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 4.0; + }; + name = "Debug AppStore"; + }; + D079FCFE1F06BBA40038FADE /* Debug AppStore */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D079FD021F06BBD60038FADE /* Config-AppStore.xcconfig */; + buildSettings = { + BITCODE_GENERATION_MODE = marker; + CLANG_ANALYZER_NONNULL = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_SUSPICIOUS_MOVES = YES; + CODE_SIGN_ENTITLEMENTS = "SiriIntents/SiriIntents-AppStore.entitlements"; + CODE_SIGN_IDENTITY = "iPhone Developer: Peter Iakovlev (88P695A564)"; + CURRENT_PROJECT_VERSION = 624; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = 6N38VWS5BX; + INFOPLIST_FILE = SiriIntents/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "${APP_BUNDLE_ID}.SiriIntents"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE = "e393ec42-d75d-4bc2-a75b-97891ad4cae8"; + PROVISIONING_PROFILE_SPECIFIER = "match Development org.telegram.TelegramHD.SiriIntents"; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OBJC_BRIDGING_HEADER = "SiriIntents/SiriIntents-Bridging-Header.h"; + SWIFT_VERSION = 4.0; + }; + name = "Debug AppStore"; + }; + D0924FF91FE52C46003F693F /* Release Hockeyapp Internal */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D0924FFE1FE52C75003F693F /* Config-Hockeyapp Internal.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Distribution: TELEGRAM MESSENGER LLP"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution: TELEGRAM MESSENGER LLP"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = NO; + PROVISIONING_PROFILE = "f58b31fd-48bf-4eac-b066-a5ebea37dfd7"; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = "Release Hockeyapp Internal"; + }; + D0924FFA1FE52C46003F693F /* Release Hockeyapp Internal */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D0924FFE1FE52C75003F693F /* Config-Hockeyapp Internal.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + BITCODE_GENERATION_MODE = bitcode; + CLANG_MODULES_AUTOLINK = NO; + CODE_SIGN_ENTITLEMENTS = "Telegram-iOS/Telegram-iOS-Hockeyapp.entitlements"; + CODE_SIGN_IDENTITY = "iPhone Distribution: TELEGRAM MESSENGER LLP"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution: TELEGRAM MESSENGER LLP"; + CURRENT_PROJECT_VERSION = 624; + DEVELOPMENT_TEAM = X834Q8SBVP; + ENABLE_BITCODE = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(BUILT_PRODUCTS_DIR)", + "$(PROJECT_DIR)/third-party", + ); + HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/Watch/Bridge"; + INFOPLIST_FILE = "Telegram-iOS/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/submodules/TelegramUI/third-party/opus/lib", + ); + OTHER_LDFLAGS = ( + "-ObjC", + "-lz", + ); + OTHER_SWIFT_FLAGS = ""; + PRODUCT_BUNDLE_IDENTIFIER = "${APP_BUNDLE_ID}"; + PRODUCT_NAME = "Telegram X"; + PROVISIONING_PROFILE = "a453791e-7153-42a4-8b61-d00a130e59d2"; + PROVISIONING_PROFILE_SPECIFIER = "match InHouse org.telegram.Telegram-iOS"; + SWIFT_OBJC_BRIDGING_HEADER = "Telegram-iOS/Telegram-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 4.0; + USER_HEADER_SEARCH_PATHS = ""; + }; + name = "Release Hockeyapp Internal"; + }; + D0924FFB1FE52C46003F693F /* Release Hockeyapp Internal */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D0924FFE1FE52C75003F693F /* Config-Hockeyapp Internal.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + FRAMEWORK_SEARCH_PATHS = "$(SDKROOT)/Developer/Library/Frameworks"; + INFOPLIST_FILE = "Telegram-iOSTests/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "org.telegram.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 3.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Telegram-iOS.app/Telegram-iOS"; + }; + name = "Release Hockeyapp Internal"; + }; + D0924FFC1FE52C46003F693F /* Release Hockeyapp Internal */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D0924FFE1FE52C75003F693F /* Config-Hockeyapp Internal.xcconfig */; + buildSettings = { + BITCODE_GENERATION_MODE = bitcode; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_SUSPICIOUS_MOVES = YES; + CODE_SIGN_ENTITLEMENTS = "Share/Share-HockeyApp.entitlements"; + CODE_SIGN_IDENTITY = "iPhone Distribution: TELEGRAM MESSENGER LLP"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution: TELEGRAM MESSENGER LLP"; + CURRENT_PROJECT_VERSION = 624; + DEVELOPMENT_TEAM = X834Q8SBVP; + INFOPLIST_FILE = Share/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "${APP_BUNDLE_ID}.Share"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE = "cfe514a2-916a-4247-b5a4-c2c4c6fd2805"; + PROVISIONING_PROFILE_SPECIFIER = "match InHouse org.telegram.Telegram-iOS.Share"; + SKIP_INSTALL = YES; + SWIFT_INSTALL_OBJC_HEADER = NO; + SWIFT_OBJC_BRIDGING_HEADER = "Share/Share-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 4.0; + }; + name = "Release Hockeyapp Internal"; + }; + D0924FFD1FE52C46003F693F /* Release Hockeyapp Internal */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D0924FFE1FE52C75003F693F /* Config-Hockeyapp Internal.xcconfig */; + buildSettings = { + BITCODE_GENERATION_MODE = bitcode; + CLANG_ANALYZER_NONNULL = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_SUSPICIOUS_MOVES = YES; + CODE_SIGN_ENTITLEMENTS = "SiriIntents/SiriIntents-Hockeyapp.entitlements"; + CODE_SIGN_IDENTITY = "iPhone Distribution: TELEGRAM MESSENGER LLP"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution: TELEGRAM MESSENGER LLP"; + CURRENT_PROJECT_VERSION = 624; + DEVELOPMENT_TEAM = X834Q8SBVP; + INFOPLIST_FILE = SiriIntents/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "${APP_BUNDLE_ID}.SiriIntents"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE = "94efdcdd-1da2-4ec6-87f3-5e50b55a07f9"; + PROVISIONING_PROFILE_SPECIFIER = "match InHouse org.telegram.Telegram-iOS.SiriIntents"; + SKIP_INSTALL = YES; + SWIFT_OBJC_BRIDGING_HEADER = "SiriIntents/SiriIntents-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 4.0; + }; + name = "Release Hockeyapp Internal"; + }; + D0ADF915212B3A9D00310BBC /* Debug AppStore LLC */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D0ADF914212B399A00310BBC /* Config-AppStoreLLC.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer: Peter Iakovlev (9J4EJ3F97G)"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + PROVISIONING_PROFILE = "a4dc3795-99a0-4552-934a-0399f5df77b2"; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = "Debug AppStore LLC"; + }; + D0ADF916212B3A9D00310BBC /* Debug AppStore LLC */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D0ADF914212B399A00310BBC /* Config-AppStoreLLC.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIconLLC; + BITCODE_GENERATION_MODE = marker; + CLANG_MODULES_AUTOLINK = NO; + CODE_SIGN_ENTITLEMENTS = "Telegram-iOS/Telegram-iOS-AppStoreLLC.entitlements"; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CURRENT_PROJECT_VERSION = 624; + DEVELOPMENT_TEAM = C67CF9S4VU; + ENABLE_BITCODE = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(BUILT_PRODUCTS_DIR)", + "$(PROJECT_DIR)/third-party", + ); + HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/Watch/Bridge"; + INFOPLIST_FILE = "Telegram-iOS/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/submodules/TelegramUI/third-party/opus/lib", + ); + OTHER_LDFLAGS = ( + "-ObjC", + "-lz", + ); + OTHER_SWIFT_FLAGS = "-D DEBUG"; + PRODUCT_BUNDLE_IDENTIFIER = "${APP_BUNDLE_ID}"; + PRODUCT_NAME = "Telegram X"; + PROVISIONING_PROFILE = "954189c1-ae38-447b-a473-7c9b7198aa47"; + PROVISIONING_PROFILE_SPECIFIER = "match Development ph.telegra.Telegraph"; + SWIFT_OBJC_BRIDGING_HEADER = "Telegram-iOS/Telegram-Bridging-Header.h"; + SWIFT_VERSION = 4.0; + USER_HEADER_SEARCH_PATHS = ""; + }; + name = "Debug AppStore LLC"; + }; + D0ADF917212B3A9E00310BBC /* Debug AppStore LLC */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D0ADF914212B399A00310BBC /* Config-AppStoreLLC.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + FRAMEWORK_SEARCH_PATHS = "$(SDKROOT)/Developer/Library/Frameworks"; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + INFOPLIST_FILE = "Telegram-iOSTests/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "org.telegram.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 3.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Telegram-iOS.app/Telegram-iOS"; + }; + name = "Debug AppStore LLC"; + }; + D0ADF918212B3A9E00310BBC /* Debug AppStore LLC */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D0ADF914212B399A00310BBC /* Config-AppStoreLLC.xcconfig */; + buildSettings = { + BITCODE_GENERATION_MODE = marker; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_SUSPICIOUS_MOVES = YES; + CODE_SIGN_ENTITLEMENTS = "Share/Share-AppStoreLLC.entitlements"; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CURRENT_PROJECT_VERSION = 624; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = C67CF9S4VU; + INFOPLIST_FILE = Share/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "${APP_BUNDLE_ID}.Share"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE = "388bbf37-24cd-44d4-b38d-9b1ad5006247"; + PROVISIONING_PROFILE_SPECIFIER = "match Development ph.telegra.Telegraph.Share"; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_INSTALL_OBJC_HEADER = NO; + SWIFT_OBJC_BRIDGING_HEADER = "Share/Share-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 4.0; + }; + name = "Debug AppStore LLC"; + }; + D0ADF919212B3A9E00310BBC /* Debug AppStore LLC */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D0ADF914212B399A00310BBC /* Config-AppStoreLLC.xcconfig */; + buildSettings = { + BITCODE_GENERATION_MODE = marker; + CLANG_ANALYZER_NONNULL = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_SUSPICIOUS_MOVES = YES; + CODE_SIGN_ENTITLEMENTS = "SiriIntents/SiriIntents-AppStoreLLC.entitlements"; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CURRENT_PROJECT_VERSION = 624; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = C67CF9S4VU; + INFOPLIST_FILE = SiriIntents/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "${APP_BUNDLE_ID}.SiriIntents"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE = "e393ec42-d75d-4bc2-a75b-97891ad4cae8"; + PROVISIONING_PROFILE_SPECIFIER = "match Development ph.telegra.Telegraph.SiriIntents"; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OBJC_BRIDGING_HEADER = "SiriIntents/SiriIntents-Bridging-Header.h"; + SWIFT_VERSION = 4.0; + }; + name = "Debug AppStore LLC"; + }; + D0ADF91A212B3A9E00310BBC /* Debug AppStore LLC */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D0ADF914212B399A00310BBC /* Config-AppStoreLLC.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = X834Q8SBVP; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + INFOPLIST_FILE = "Telegram-iOS UITests/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 11.2; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = "org.Telegram.Telegram-iOS-UITests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE = "df24dc18-3fdc-447c-b347-5a110cecd77d"; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = "Telegram-iOS"; + }; + name = "Debug AppStore LLC"; + }; + D0ADF91B212B3A9E00310BBC /* Debug AppStore LLC */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D0ADF914212B399A00310BBC /* Config-AppStoreLLC.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_ENTITLEMENTS = "Widget/Widget-AppStoreLLC.entitlements"; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_STYLE = Manual; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = C67CF9S4VU; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + INFOPLIST_FILE = Widget/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = "${APP_BUNDLE_ID}.Widget"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = "match Development ph.telegra.Telegraph.Widget"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OBJC_BRIDGING_HEADER = "Widget/Widget-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = "Debug AppStore LLC"; + }; + D0B2F743204F4C9900D3BFB9 /* Debug Hockeyapp */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D079FD011F06BBD60038FADE /* Config-Hockeyapp.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_ENTITLEMENTS = "Widget/Widget-HockeyApp.entitlements"; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_STYLE = Manual; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = X834Q8SBVP; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + INFOPLIST_FILE = Widget/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = "${APP_BUNDLE_ID}.Widget"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE = "8a4bf8b9-8fae-4ea6-9b8f-c2ac19f3676d"; + PROVISIONING_PROFILE_SPECIFIER = "match Development org.telegram.Telegram-iOS.Widget"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OBJC_BRIDGING_HEADER = "Widget/Widget-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = "Debug Hockeyapp"; + }; + D0B2F744204F4C9900D3BFB9 /* Debug AppStore */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D079FD021F06BBD60038FADE /* Config-AppStore.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_ENTITLEMENTS = "Widget/Widget-AppStore.entitlements"; + CODE_SIGN_IDENTITY = "iPhone Developer: Peter Iakovlev (88P695A564)"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer: Peter Iakovlev (88P695A564)"; + CODE_SIGN_STYLE = Manual; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = 6N38VWS5BX; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + INFOPLIST_FILE = Widget/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = "${APP_BUNDLE_ID}.Widget"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = "match Development org.telegram.TelegramHD.Widget"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OBJC_BRIDGING_HEADER = "Widget/Widget-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = "Debug AppStore"; + }; + D0B2F745204F4C9900D3BFB9 /* Release AppStore */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D079FD021F06BBD60038FADE /* Config-AppStore.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_ENTITLEMENTS = "Widget/Widget-AppStore.entitlements"; + CODE_SIGN_IDENTITY = "iPhone Distribution: TELEGRAM MESSENGER LLP (6N38VWS5BX)"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution: TELEGRAM MESSENGER LLP (6N38VWS5BX)"; + CODE_SIGN_STYLE = Manual; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = 6N38VWS5BX; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + INFOPLIST_FILE = Widget/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + MTL_ENABLE_DEBUG_INFO = NO; + PRODUCT_BUNDLE_IDENTIFIER = "${APP_BUNDLE_ID}.Widget"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE = "a9943cd7-6b01-4ca7-908e-172cefb0bde2"; + PROVISIONING_PROFILE_SPECIFIER = "match AppStore org.telegram.TelegramHD.Widget"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_OBJC_BRIDGING_HEADER = "Widget/Widget-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = "Release AppStore"; + }; + D0B2F746204F4C9900D3BFB9 /* Release Hockeyapp */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D079FD011F06BBD60038FADE /* Config-Hockeyapp.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_ENTITLEMENTS = "Widget/Widget-HockeyApp.entitlements"; + CODE_SIGN_IDENTITY = "iPhone Distribution: TELEGRAM MESSENGER LLP"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution: TELEGRAM MESSENGER LLP"; + CODE_SIGN_STYLE = Manual; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = X834Q8SBVP; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + INFOPLIST_FILE = Widget/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + MTL_ENABLE_DEBUG_INFO = NO; + PRODUCT_BUNDLE_IDENTIFIER = "${APP_BUNDLE_ID}.Widget"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE = "1a27976e-0845-48e4-ae3d-2ff38089024d"; + PROVISIONING_PROFILE_SPECIFIER = "match InHouse org.telegram.Telegram-iOS.Widget"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_OBJC_BRIDGING_HEADER = "Widget/Widget-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = "Release Hockeyapp"; + }; + D0B2F747204F4C9900D3BFB9 /* Release Hockeyapp Internal */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D0924FFE1FE52C75003F693F /* Config-Hockeyapp Internal.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_ENTITLEMENTS = "Widget/Widget-HockeyApp.entitlements"; + CODE_SIGN_IDENTITY = "iPhone Distribution: TELEGRAM MESSENGER LLP"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution: TELEGRAM MESSENGER LLP"; + CODE_SIGN_STYLE = Manual; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = X834Q8SBVP; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + INFOPLIST_FILE = Widget/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + MTL_ENABLE_DEBUG_INFO = NO; + PRODUCT_BUNDLE_IDENTIFIER = "${APP_BUNDLE_ID}.Widget"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE = "1a27976e-0845-48e4-ae3d-2ff38089024d"; + PROVISIONING_PROFILE_SPECIFIER = "match InHouse org.telegram.Telegram-iOS.Widget"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_OBJC_BRIDGING_HEADER = "Widget/Widget-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = "Release Hockeyapp Internal"; + }; + D0CE6F0D213DC33200BCD44B /* Release AppStore LLC */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D0ADF914212B399A00310BBC /* Config-AppStoreLLC.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = "Release AppStore LLC"; + }; + D0CE6F0E213DC33200BCD44B /* Release AppStore LLC */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D0ADF914212B399A00310BBC /* Config-AppStoreLLC.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIconLLC; + BITCODE_GENERATION_MODE = bitcode; + CLANG_MODULES_AUTOLINK = NO; + CODE_SIGN_ENTITLEMENTS = "Telegram-iOS/Telegram-iOS-AppStoreLLC.entitlements"; + CODE_SIGN_IDENTITY = "iPhone Distribution: Digital Fortress LLC (C67CF9S4VU)"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution: Digital Fortress LLC (C67CF9S4VU)"; + CURRENT_PROJECT_VERSION = 624; + DEVELOPMENT_TEAM = C67CF9S4VU; + ENABLE_BITCODE = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(BUILT_PRODUCTS_DIR)", + "$(PROJECT_DIR)/third-party", + ); + HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/Watch/Bridge"; + INFOPLIST_FILE = "Telegram-iOS/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/submodules/TelegramUI/third-party/opus/lib", + ); + OTHER_LDFLAGS = ( + "-ObjC", + "-lz", + ); + OTHER_SWIFT_FLAGS = ""; + PRODUCT_BUNDLE_IDENTIFIER = "${APP_BUNDLE_ID}"; + PRODUCT_NAME = "Telegram X"; + PROVISIONING_PROFILE = "485ab6ec-94f8-4f05-909c-9e4877e9ce18"; + PROVISIONING_PROFILE_SPECIFIER = "match AppStore ph.telegra.Telegraph"; + SWIFT_OBJC_BRIDGING_HEADER = "Telegram-iOS/Telegram-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 4.0; + USER_HEADER_SEARCH_PATHS = ""; + }; + name = "Release AppStore LLC"; + }; + D0CE6F0F213DC33200BCD44B /* Release AppStore LLC */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D0ADF914212B399A00310BBC /* Config-AppStoreLLC.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + FRAMEWORK_SEARCH_PATHS = "$(SDKROOT)/Developer/Library/Frameworks"; + INFOPLIST_FILE = "Telegram-iOSTests/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "org.telegram.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 3.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Telegram-iOS.app/Telegram-iOS"; + }; + name = "Release AppStore LLC"; + }; + D0CE6F10213DC33200BCD44B /* Release AppStore LLC */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D0ADF914212B399A00310BBC /* Config-AppStoreLLC.xcconfig */; + buildSettings = { + BITCODE_GENERATION_MODE = bitcode; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_SUSPICIOUS_MOVES = YES; + CODE_SIGN_ENTITLEMENTS = "Share/Share-AppStoreLLC.entitlements"; + CODE_SIGN_IDENTITY = "iPhone Distribution: Digital Fortress LLC (C67CF9S4VU)"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution: Digital Fortress LLC (C67CF9S4VU)"; + CURRENT_PROJECT_VERSION = 624; + DEVELOPMENT_TEAM = C67CF9S4VU; + GCC_OPTIMIZATION_LEVEL = s; + INFOPLIST_FILE = Share/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "${APP_BUNDLE_ID}.Share"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE = "57843657-7287-4a3d-b056-3bb7acbd6afb"; + PROVISIONING_PROFILE_SPECIFIER = "match AppStore ph.telegra.Telegraph.Share"; + SKIP_INSTALL = YES; + SWIFT_INSTALL_OBJC_HEADER = NO; + SWIFT_OBJC_BRIDGING_HEADER = "Share/Share-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 4.0; + }; + name = "Release AppStore LLC"; + }; + D0CE6F11213DC33200BCD44B /* Release AppStore LLC */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D0ADF914212B399A00310BBC /* Config-AppStoreLLC.xcconfig */; + buildSettings = { + BITCODE_GENERATION_MODE = bitcode; + CLANG_ANALYZER_NONNULL = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_SUSPICIOUS_MOVES = YES; + CODE_SIGN_ENTITLEMENTS = "SiriIntents/SiriIntents-AppStoreLLC.entitlements"; + CODE_SIGN_IDENTITY = "iPhone Distribution: Digital Fortress LLC (C67CF9S4VU)"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution: Digital Fortress LLC (C67CF9S4VU)"; + CURRENT_PROJECT_VERSION = 624; + DEVELOPMENT_TEAM = C67CF9S4VU; + INFOPLIST_FILE = SiriIntents/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "${APP_BUNDLE_ID}.SiriIntents"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE = "d4d12f50-f43f-49fa-8f14-6e7b73b68d59"; + PROVISIONING_PROFILE_SPECIFIER = "match AppStore ph.telegra.Telegraph.SiriIntents"; + SKIP_INSTALL = YES; + SWIFT_OBJC_BRIDGING_HEADER = "SiriIntents/SiriIntents-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 4.0; + }; + name = "Release AppStore LLC"; + }; + D0CE6F12213DC33200BCD44B /* Release AppStore LLC */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D0ADF914212B399A00310BBC /* Config-AppStoreLLC.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = X834Q8SBVP; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + INFOPLIST_FILE = "Telegram-iOS UITests/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 11.2; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = NO; + PRODUCT_BUNDLE_IDENTIFIER = "org.Telegram.Telegram-iOS-UITests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE = "df24dc18-3fdc-447c-b347-5a110cecd77d"; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = "Telegram-iOS"; + VALIDATE_PRODUCT = YES; + }; + name = "Release AppStore LLC"; + }; + D0CE6F13213DC33200BCD44B /* Release AppStore LLC */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D0ADF914212B399A00310BBC /* Config-AppStoreLLC.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_ENTITLEMENTS = "Widget/Widget-AppStoreLLC.entitlements"; + CODE_SIGN_IDENTITY = "iPhone Distribution: Digital Fortress LLC (C67CF9S4VU)"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution: Digital Fortress LLC (C67CF9S4VU)"; + CODE_SIGN_STYLE = Manual; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = C67CF9S4VU; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + INFOPLIST_FILE = Widget/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + MTL_ENABLE_DEBUG_INFO = NO; + PRODUCT_BUNDLE_IDENTIFIER = "${APP_BUNDLE_ID}.Widget"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE = "17ef978a-2103-47cd-8cd3-9104312ade40"; + PROVISIONING_PROFILE_SPECIFIER = "match AppStore ph.telegra.Telegraph.Widget"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_OBJC_BRIDGING_HEADER = "Widget/Widget-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = "Release AppStore LLC"; + }; + D0D2688F1D79A70B00C422DA /* Debug Hockeyapp */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D079FD011F06BBD60038FADE /* Config-Hockeyapp.xcconfig */; + buildSettings = { + BITCODE_GENERATION_MODE = marker; + CLANG_ANALYZER_NONNULL = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_SUSPICIOUS_MOVES = YES; + CODE_SIGN_ENTITLEMENTS = "SiriIntents/SiriIntents-Hockeyapp.entitlements"; + CURRENT_PROJECT_VERSION = 624; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = X834Q8SBVP; + INFOPLIST_FILE = SiriIntents/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "${APP_BUNDLE_ID}.SiriIntents"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE = "dbf0b895-1e4b-43ee-993f-b4db7ce8cbb8"; + PROVISIONING_PROFILE_SPECIFIER = "match Development org.telegram.Telegram-iOS.SiriIntents"; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OBJC_BRIDGING_HEADER = "SiriIntents/SiriIntents-Bridging-Header.h"; + SWIFT_VERSION = 4.0; + }; + name = "Debug Hockeyapp"; + }; + D0D268901D79A70B00C422DA /* Release AppStore */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D079FD021F06BBD60038FADE /* Config-AppStore.xcconfig */; + buildSettings = { + BITCODE_GENERATION_MODE = bitcode; + CLANG_ANALYZER_NONNULL = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_SUSPICIOUS_MOVES = YES; + CODE_SIGN_ENTITLEMENTS = "SiriIntents/SiriIntents-AppStore.entitlements"; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; + CURRENT_PROJECT_VERSION = 624; + DEVELOPMENT_TEAM = 6N38VWS5BX; + INFOPLIST_FILE = SiriIntents/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "${APP_BUNDLE_ID}.SiriIntents"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE = "733b3708-0d16-4d15-96a0-d287e184ca2b"; + PROVISIONING_PROFILE_SPECIFIER = "match AppStore org.telegram.TelegramHD.SiriIntents"; + SKIP_INSTALL = YES; + SWIFT_OBJC_BRIDGING_HEADER = "SiriIntents/SiriIntents-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 4.0; + }; + name = "Release AppStore"; + }; + D0D268911D79A70B00C422DA /* Release Hockeyapp */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D079FD011F06BBD60038FADE /* Config-Hockeyapp.xcconfig */; + buildSettings = { + BITCODE_GENERATION_MODE = bitcode; + CLANG_ANALYZER_NONNULL = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_SUSPICIOUS_MOVES = YES; + CODE_SIGN_ENTITLEMENTS = "SiriIntents/SiriIntents-Hockeyapp.entitlements"; + CODE_SIGN_IDENTITY = "iPhone Distribution: TELEGRAM MESSENGER LLP"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution: TELEGRAM MESSENGER LLP"; + CURRENT_PROJECT_VERSION = 624; + DEVELOPMENT_TEAM = X834Q8SBVP; + INFOPLIST_FILE = SiriIntents/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "${APP_BUNDLE_ID}.SiriIntents"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE = "94efdcdd-1da2-4ec6-87f3-5e50b55a07f9"; + PROVISIONING_PROFILE_SPECIFIER = "match InHouse org.telegram.Telegram-iOS.SiriIntents"; + SKIP_INSTALL = YES; + SWIFT_OBJC_BRIDGING_HEADER = "SiriIntents/SiriIntents-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 4.0; + }; + name = "Release Hockeyapp"; + }; + D0D7481D1CBBE0AC00B0ED5C /* Release Hockeyapp */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D079FD011F06BBD60038FADE /* Config-Hockeyapp.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Distribution: TELEGRAM MESSENGER LLP"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution: TELEGRAM MESSENGER LLP"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = NO; + PROVISIONING_PROFILE = "f58b31fd-48bf-4eac-b066-a5ebea37dfd7"; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = "Release Hockeyapp"; + }; + D0D7481E1CBBE0AC00B0ED5C /* Release Hockeyapp */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D079FD011F06BBD60038FADE /* Config-Hockeyapp.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + BITCODE_GENERATION_MODE = bitcode; + CLANG_MODULES_AUTOLINK = NO; + CODE_SIGN_ENTITLEMENTS = "Telegram-iOS/Telegram-iOS-Hockeyapp.entitlements"; + CODE_SIGN_IDENTITY = "iPhone Distribution: TELEGRAM MESSENGER LLP"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution: TELEGRAM MESSENGER LLP"; + CURRENT_PROJECT_VERSION = 624; + DEVELOPMENT_TEAM = X834Q8SBVP; + ENABLE_BITCODE = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(BUILT_PRODUCTS_DIR)", + "$(PROJECT_DIR)/third-party", + ); + HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/Watch/Bridge"; + INFOPLIST_FILE = "Telegram-iOS/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/submodules/TelegramUI/third-party/opus/lib", + ); + OTHER_LDFLAGS = ( + "-ObjC", + "-lz", + ); + OTHER_SWIFT_FLAGS = ""; + PRODUCT_BUNDLE_IDENTIFIER = "${APP_BUNDLE_ID}"; + PRODUCT_NAME = "Telegram X"; + PROVISIONING_PROFILE = "a453791e-7153-42a4-8b61-d00a130e59d2"; + PROVISIONING_PROFILE_SPECIFIER = "match InHouse org.telegram.Telegram-iOS"; + SWIFT_OBJC_BRIDGING_HEADER = "Telegram-iOS/Telegram-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 4.0; + USER_HEADER_SEARCH_PATHS = ""; + }; + name = "Release Hockeyapp"; + }; + D0D7481F1CBBE0AC00B0ED5C /* Release Hockeyapp */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D079FD011F06BBD60038FADE /* Config-Hockeyapp.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + FRAMEWORK_SEARCH_PATHS = "$(SDKROOT)/Developer/Library/Frameworks"; + INFOPLIST_FILE = "Telegram-iOSTests/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "org.telegram.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 3.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Telegram-iOS.app/Telegram-iOS"; + }; + name = "Release Hockeyapp"; + }; + D0ECCB831FE9C38500609802 /* Debug Hockeyapp */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D079FD011F06BBD60038FADE /* Config-Hockeyapp.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = X834Q8SBVP; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + INFOPLIST_FILE = "Telegram-iOS UITests/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 11.2; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = "org.Telegram.Telegram-iOS-UITests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE = "df24dc18-3fdc-447c-b347-5a110cecd77d"; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = "Telegram-iOS"; + }; + name = "Debug Hockeyapp"; + }; + D0ECCB841FE9C38500609802 /* Debug AppStore */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D079FD021F06BBD60038FADE /* Config-AppStore.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = X834Q8SBVP; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + INFOPLIST_FILE = "Telegram-iOS UITests/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 11.2; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = "org.Telegram.Telegram-iOS-UITests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE = "df24dc18-3fdc-447c-b347-5a110cecd77d"; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = "Telegram-iOS"; + }; + name = "Debug AppStore"; + }; + D0ECCB851FE9C38500609802 /* Release AppStore */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D079FD021F06BBD60038FADE /* Config-AppStore.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = X834Q8SBVP; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + INFOPLIST_FILE = "Telegram-iOS UITests/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 11.2; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = NO; + PRODUCT_BUNDLE_IDENTIFIER = "org.Telegram.Telegram-iOS-UITests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE = "df24dc18-3fdc-447c-b347-5a110cecd77d"; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = "Telegram-iOS"; + VALIDATE_PRODUCT = YES; + }; + name = "Release AppStore"; + }; + D0ECCB861FE9C38500609802 /* Release Hockeyapp */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D079FD011F06BBD60038FADE /* Config-Hockeyapp.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = X834Q8SBVP; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + INFOPLIST_FILE = "Telegram-iOS UITests/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 11.2; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = NO; + PRODUCT_BUNDLE_IDENTIFIER = "org.Telegram.Telegram-iOS-UITests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE = "df24dc18-3fdc-447c-b347-5a110cecd77d"; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = "Telegram-iOS"; + VALIDATE_PRODUCT = YES; + }; + name = "Release Hockeyapp"; + }; + D0ECCB871FE9C38500609802 /* Release Hockeyapp Internal */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D0924FFE1FE52C75003F693F /* Config-Hockeyapp Internal.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = X834Q8SBVP; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + INFOPLIST_FILE = "Telegram-iOS UITests/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 11.2; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = NO; + PRODUCT_BUNDLE_IDENTIFIER = "org.Telegram.Telegram-iOS-UITests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE = "df24dc18-3fdc-447c-b347-5a110cecd77d"; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = "Telegram-iOS"; + VALIDATE_PRODUCT = YES; + }; + name = "Release Hockeyapp Internal"; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 09C56FB42172797500BDF00F /* Build configuration list for PBXNativeTarget "Watch Extension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 09C56FAD2172797500BDF00F /* Debug Hockeyapp */, + 09C56FAE2172797500BDF00F /* Debug AppStore */, + 09C56FAF2172797500BDF00F /* Debug AppStore LLC */, + 09C56FB02172797500BDF00F /* Release AppStore */, + 09C56FB12172797500BDF00F /* Release AppStore LLC */, + 09C56FB22172797500BDF00F /* Release Hockeyapp */, + 09C56FB32172797500BDF00F /* Release Hockeyapp Internal */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = "Release AppStore"; + }; + 09C56FB62172797500BDF00F /* Build configuration list for PBXNativeTarget "Watch" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 09C56FA62172797500BDF00F /* Debug Hockeyapp */, + 09C56FA72172797500BDF00F /* Debug AppStore */, + 09C56FA82172797500BDF00F /* Debug AppStore LLC */, + 09C56FA92172797500BDF00F /* Release AppStore */, + 09C56FAA2172797500BDF00F /* Release AppStore LLC */, + 09C56FAB2172797500BDF00F /* Release Hockeyapp */, + 09C56FAC2172797500BDF00F /* Release Hockeyapp Internal */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = "Release AppStore"; + }; + D00859971B28189D00EAF753 /* Build configuration list for PBXProject "Telegram-iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D00859B91B28189D00EAF753 /* Debug Hockeyapp */, + D079FCF91F06BBA40038FADE /* Debug AppStore */, + D0ADF915212B3A9D00310BBC /* Debug AppStore LLC */, + D00859BA1B28189D00EAF753 /* Release AppStore */, + D0CE6F0D213DC33200BCD44B /* Release AppStore LLC */, + D0D7481D1CBBE0AC00B0ED5C /* Release Hockeyapp */, + D0924FF91FE52C46003F693F /* Release Hockeyapp Internal */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = "Release AppStore"; + }; + D00859BB1B28189D00EAF753 /* Build configuration list for PBXNativeTarget "Telegram-iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D00859BC1B28189D00EAF753 /* Debug Hockeyapp */, + D079FCFA1F06BBA40038FADE /* Debug AppStore */, + D0ADF916212B3A9D00310BBC /* Debug AppStore LLC */, + D00859BD1B28189D00EAF753 /* Release AppStore */, + D0CE6F0E213DC33200BCD44B /* Release AppStore LLC */, + D0D7481E1CBBE0AC00B0ED5C /* Release Hockeyapp */, + D0924FFA1FE52C46003F693F /* Release Hockeyapp Internal */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = "Release AppStore"; + }; + D00859BE1B28189D00EAF753 /* Build configuration list for PBXNativeTarget "Telegram-iOSTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D00859BF1B28189D00EAF753 /* Debug Hockeyapp */, + D079FCFB1F06BBA40038FADE /* Debug AppStore */, + D0ADF917212B3A9E00310BBC /* Debug AppStore LLC */, + D00859C01B28189D00EAF753 /* Release AppStore */, + D0CE6F0F213DC33200BCD44B /* Release AppStore LLC */, + D0D7481F1CBBE0AC00B0ED5C /* Release Hockeyapp */, + D0924FFB1FE52C46003F693F /* Release Hockeyapp Internal */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = "Release AppStore"; + }; + D02CF610215D9ABF00E0F56A /* Build configuration list for PBXNativeTarget "NotificationContent" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D02CF609215D9ABF00E0F56A /* Debug Hockeyapp */, + D02CF60A215D9ABF00E0F56A /* Debug AppStore */, + D02CF60B215D9ABF00E0F56A /* Debug AppStore LLC */, + D02CF60C215D9ABF00E0F56A /* Release AppStore */, + D02CF60D215D9ABF00E0F56A /* Release AppStore LLC */, + D02CF60E215D9ABF00E0F56A /* Release Hockeyapp */, + D02CF60F215D9ABF00E0F56A /* Release Hockeyapp Internal */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = "Release AppStore"; + }; + D03B0E831D63484500955575 /* Build configuration list for PBXNativeTarget "Share" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D03B0E841D63484500955575 /* Debug Hockeyapp */, + D079FCFD1F06BBA40038FADE /* Debug AppStore */, + D0ADF918212B3A9E00310BBC /* Debug AppStore LLC */, + D03B0E851D63484500955575 /* Release AppStore */, + D0CE6F10213DC33200BCD44B /* Release AppStore LLC */, + D03B0E861D63484500955575 /* Release Hockeyapp */, + D0924FFC1FE52C46003F693F /* Release Hockeyapp Internal */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = "Release AppStore"; + }; + D0B2F748204F4C9900D3BFB9 /* Build configuration list for PBXNativeTarget "Widget" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D0B2F743204F4C9900D3BFB9 /* Debug Hockeyapp */, + D0B2F744204F4C9900D3BFB9 /* Debug AppStore */, + D0ADF91B212B3A9E00310BBC /* Debug AppStore LLC */, + D0B2F745204F4C9900D3BFB9 /* Release AppStore */, + D0CE6F13213DC33200BCD44B /* Release AppStore LLC */, + D0B2F746204F4C9900D3BFB9 /* Release Hockeyapp */, + D0B2F747204F4C9900D3BFB9 /* Release Hockeyapp Internal */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = "Release AppStore"; + }; + D0D268961D79A70B00C422DA /* Build configuration list for PBXNativeTarget "SiriIntents" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D0D2688F1D79A70B00C422DA /* Debug Hockeyapp */, + D079FCFE1F06BBA40038FADE /* Debug AppStore */, + D0ADF919212B3A9E00310BBC /* Debug AppStore LLC */, + D0D268901D79A70B00C422DA /* Release AppStore */, + D0CE6F11213DC33200BCD44B /* Release AppStore LLC */, + D0D268911D79A70B00C422DA /* Release Hockeyapp */, + D0924FFD1FE52C46003F693F /* Release Hockeyapp Internal */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = "Release AppStore"; + }; + D0ECCB881FE9C38500609802 /* Build configuration list for PBXNativeTarget "Telegram-iOS UITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D0ECCB831FE9C38500609802 /* Debug Hockeyapp */, + D0ECCB841FE9C38500609802 /* Debug AppStore */, + D0ADF91A212B3A9E00310BBC /* Debug AppStore LLC */, + D0ECCB851FE9C38500609802 /* Release AppStore */, + D0CE6F12213DC33200BCD44B /* Release AppStore LLC */, + D0ECCB861FE9C38500609802 /* Release Hockeyapp */, + D0ECCB871FE9C38500609802 /* Release Hockeyapp Internal */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = "Release AppStore"; + }; +/* End XCConfigurationList section */ + }; + rootObject = D00859941B28189D00EAF753 /* Project object */; +} diff --git a/Telegram-iOS.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Telegram-iOS.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..e2baac0529 --- /dev/null +++ b/Telegram-iOS.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Telegram-iOS.xcodeproj/xcshareddata/xcschemes/NotificationContent.xcscheme b/Telegram-iOS.xcodeproj/xcshareddata/xcschemes/NotificationContent.xcscheme new file mode 100644 index 0000000000..58e8da4c9c --- /dev/null +++ b/Telegram-iOS.xcodeproj/xcshareddata/xcschemes/NotificationContent.xcscheme @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Telegram-iOS.xcodeproj/xcshareddata/xcschemes/Telegram-iOS AppStore LLC.xcscheme b/Telegram-iOS.xcodeproj/xcshareddata/xcschemes/Telegram-iOS AppStore LLC.xcscheme new file mode 100644 index 0000000000..7c255d0f3f --- /dev/null +++ b/Telegram-iOS.xcodeproj/xcshareddata/xcschemes/Telegram-iOS AppStore LLC.xcscheme @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Telegram-iOS.xcodeproj/xcshareddata/xcschemes/Telegram-iOS AppStore.xcscheme b/Telegram-iOS.xcodeproj/xcshareddata/xcschemes/Telegram-iOS AppStore.xcscheme new file mode 100644 index 0000000000..c8059d579b --- /dev/null +++ b/Telegram-iOS.xcodeproj/xcshareddata/xcschemes/Telegram-iOS AppStore.xcscheme @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Telegram-iOS.xcodeproj/xcshareddata/xcschemes/Telegram-iOS UITests.xcscheme b/Telegram-iOS.xcodeproj/xcshareddata/xcschemes/Telegram-iOS UITests.xcscheme new file mode 100644 index 0000000000..3ddfb17d29 --- /dev/null +++ b/Telegram-iOS.xcodeproj/xcshareddata/xcschemes/Telegram-iOS UITests.xcscheme @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Telegram-iOS.xcworkspace/contents.xcworkspacedata b/Telegram-iOS.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..3c72d64542 --- /dev/null +++ b/Telegram-iOS.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Telegram-iOS.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Telegram-iOS.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000000..18d981003d --- /dev/null +++ b/Telegram-iOS.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Telegram-iOS.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/Telegram-iOS.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000000..3ddf867a10 --- /dev/null +++ b/Telegram-iOS.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + BuildSystemType + Latest + + diff --git a/Telegram-iOS.xcworkspace/xcshareddata/xcschemes/Telegram-iOS AppStore.xcscheme b/Telegram-iOS.xcworkspace/xcshareddata/xcschemes/Telegram-iOS AppStore.xcscheme new file mode 100644 index 0000000000..1b52b0bd31 --- /dev/null +++ b/Telegram-iOS.xcworkspace/xcshareddata/xcschemes/Telegram-iOS AppStore.xcscheme @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Telegram-iOS.xcworkspace/xcshareddata/xcschemes/Telegram-iOS Hockeyapp Internal.xcscheme b/Telegram-iOS.xcworkspace/xcshareddata/xcschemes/Telegram-iOS Hockeyapp Internal.xcscheme new file mode 100644 index 0000000000..c0a612d3f0 --- /dev/null +++ b/Telegram-iOS.xcworkspace/xcshareddata/xcschemes/Telegram-iOS Hockeyapp Internal.xcscheme @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Telegram-iOS.xcworkspace/xcshareddata/xcschemes/Telegram-iOS Hockeyapp.xcscheme b/Telegram-iOS.xcworkspace/xcshareddata/xcschemes/Telegram-iOS Hockeyapp.xcscheme new file mode 100644 index 0000000000..4e283ea9f7 --- /dev/null +++ b/Telegram-iOS.xcworkspace/xcshareddata/xcschemes/Telegram-iOS Hockeyapp.xcscheme @@ -0,0 +1,145 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Telegram-iOS/AppDelegate.swift b/Telegram-iOS/AppDelegate.swift new file mode 100644 index 0000000000..ef2fe58725 --- /dev/null +++ b/Telegram-iOS/AppDelegate.swift @@ -0,0 +1,1632 @@ +import UIKit +import SwiftSignalKit +import Display +import TelegramCore +import TelegramUI +import UserNotifications +import Intents +import HockeySDK +import Postbox +import PushKit +import AsyncDisplayKit +import CloudKit + +private let handleVoipNotifications = false + +private final class TestingCoveringView: WindowCoveringView { + +} + +private func encodeText(_ string: String, _ key: Int) -> String { + var result = "" + for c in string.unicodeScalars { + result.append(Character(UnicodeScalar(UInt32(Int(c.value) + key))!)) + } + return result +} + +private let statusBarRootViewClass: AnyClass = NSClassFromString("UIStatusBar")! +private let cutoutStatusBarForegroundClass: AnyClass? = NSClassFromString("_UIStatusBar") +private let keyboardViewClass: AnyClass? = NSClassFromString(encodeText("VJJoqvuTfuIptuWjfx", -1))! +private let keyboardViewContainerClass: AnyClass? = NSClassFromString(encodeText("VJJoqvuTfuDpoubjofsWjfx", -1))! + +private let keyboardWindowClass: AnyClass? = { + if #available(iOS 9.0, *) { + return NSClassFromString(encodeText("VJSfnpufLfzcpbseXjoepx", -1)) + } else { + return NSClassFromString(encodeText("VJUfyuFggfdutXjoepx", -1)) + } +}() + +private class ApplicationStatusBarHost: StatusBarHost { + private let application = UIApplication.shared + + var statusBarFrame: CGRect { + return self.application.statusBarFrame + } + var statusBarStyle: UIStatusBarStyle { + get { + return self.application.statusBarStyle + } set(value) { + self.application.setStatusBarStyle(value, animated: false) + } + } + var statusBarWindow: UIView? { + return self.application.value(forKey: "statusBarWindow") as? UIView + } + + var statusBarView: UIView? { + guard let containerView = self.statusBarWindow?.subviews.first else { + return nil + } + + if containerView.isKind(of: statusBarRootViewClass) { + return containerView + } + + for subview in containerView.subviews { + if let cutoutStatusBarForegroundClass = cutoutStatusBarForegroundClass, subview.isKind(of: cutoutStatusBarForegroundClass) { + return subview + } + } + return nil + } + + var keyboardWindow: UIWindow? { + guard let keyboardWindowClass = keyboardWindowClass else { + return nil + } + + for window in UIApplication.shared.windows { + if window.isKind(of: keyboardWindowClass) { + return window + } + } + return nil + } + + var keyboardView: UIView? { + guard let keyboardWindow = self.keyboardWindow, let keyboardViewContainerClass = keyboardViewContainerClass, let keyboardViewClass = keyboardViewClass else { + return nil + } + + for view in keyboardWindow.subviews { + if view.isKind(of: keyboardViewContainerClass) { + for subview in view.subviews { + if subview.isKind(of: keyboardViewClass) { + return subview + } + } + } + } + return nil + } + + var handleVolumeControl: Signal { + return MediaManager.globalAudioSession.isPlaybackActive() + } +} + +private func legacyDocumentsPath() -> String { + return NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] + "/legacy" +} + +protocol SupportedStartCallIntent { + @available(iOS 10.0, *) + var contacts: [INPerson]? { get } +} + +@available(iOS 10.0, *) +extension INStartAudioCallIntent: SupportedStartCallIntent {} + +private enum QueuedWakeup: Int32 { + case call + case backgroundLocation +} + +@objc(AppDelegate) class AppDelegate: UIResponder, UIApplicationDelegate, PKPushRegistryDelegate, BITHockeyManagerDelegate, UNUserNotificationCenterDelegate, UIAlertViewDelegate { + @objc var window: UIWindow? + var nativeWindow: (UIWindow & WindowHost)? + var mainWindow: Window1! + private var dataImportSplash: LegacyDataImportSplash? + + let episodeId = arc4random() + + private let isInForegroundPromise = ValuePromise(false, ignoreRepeated: true) + private var isInForegroundValue = false + private let isActivePromise = ValuePromise(false, ignoreRepeated: true) + private var isActiveValue = false + let hasActiveAudioSession = Promise(false) + + private let accountManagerPromise = Promise() + private let watchCommunicationManagerPromise = Promise() + + private var contextValue: ApplicationContext? + private let context = Promise() + private let contextDisposable = MetaDisposable() + + private let openChatWhenReadyDisposable = MetaDisposable() + private let openUrlWhenReadyDisposable = MetaDisposable() + + private let badgeDisposable = MetaDisposable() + private let quickActionsDisposable = MetaDisposable() + + private var pushRegistry: PKPushRegistry? + + private var replyFromNotificationsDisposables = DisposableSet() + + private var replyFromNotificationsTokensValue = Set() { + didSet { + assert(Queue.mainQueue().isCurrent()) + self.replyFromNotificationsTokensPromise.set(.single(self.replyFromNotificationsTokensValue)) + } + } + private let replyFromNotificationsTokensPromise = Promise>(Set()) + private var nextToken: Int32 = 0 + private func takeNextToken() -> Int32 { + let value = self.nextToken + self.nextToken = value + 1 + return value + } + private func addReplyFromNotificationsToken() -> Int32 { + let token = self.takeNextToken() + var value = self.replyFromNotificationsTokensValue + value.insert(token) + self.replyFromNotificationsTokensValue = value + return token + } + private func removeReplyFromNotificationsToken(_ token: Int32) { + var value = self.replyFromNotificationsTokensValue + value.remove(token) + self.replyFromNotificationsTokensValue = value + } + + private var _notificationTokenPromise: Promise? + private let voipTokenPromise = Promise() + + private var notificationTokenPromise: Promise { + if let current = self._notificationTokenPromise { + return current + } else { + let promise = Promise() + self._notificationTokenPromise = promise + + return promise + } + } + + private var queuedNotifications: [PKPushPayload] = [] + private var queuedNotificationRequests: [(String, String, String?, NotificationManagedNotificationRequestId)] = [] + private var queuedMutePolling = false + private var queuedAnnouncements: [String] = [] + private var queuedWakeups = Set() + private var clearNotificationsManager: ClearNotificationsManager? + + private let idleTimerExtensionSubscribers = Bag() + + private var alertActions: (primary: (() -> Void)?, other: (() -> Void)?)? + + func alertView(_ alertView: UIAlertView, clickedButtonAt buttonIndex: Int) { + if buttonIndex == alertView.firstOtherButtonIndex { + self.alertActions?.other?() + } else { + self.alertActions?.primary?() + } + } + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]? = nil) -> Bool { + let statusBarHost = ApplicationStatusBarHost() + let (window, hostView) = nativeWindowHostView() + self.mainWindow = Window1(hostView: hostView, statusBarHost: statusBarHost) + window.backgroundColor = UIColor.white + self.window = window + self.nativeWindow = window + + self.clearNotificationsManager = ClearNotificationsManager(getNotificationIds: { completion in + if #available(iOS 10.0, *) { + UNUserNotificationCenter.current().getDeliveredNotifications(completionHandler: { notifications in + var result: [(String, NotificationManagedNotificationRequestId)] = [] + for notification in notifications { + if let requestId = NotificationManagedNotificationRequestId(string: notification.request.identifier) { + result.append((notification.request.identifier, requestId)) + } else { + let payload = notification.request.content.userInfo + var notificationRequestId: NotificationManagedNotificationRequestId? + + var peerId: PeerId? + if let fromId = payload["from_id"] { + let fromIdValue = fromId as! NSString + peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: Int32(fromIdValue.intValue)) + } else if let fromId = payload["chat_id"] { + let fromIdValue = fromId as! NSString + peerId = PeerId(namespace: Namespaces.Peer.CloudGroup, id: Int32(fromIdValue.intValue)) + } else if let fromId = payload["channel_id"] { + let fromIdValue = fromId as! NSString + peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: Int32(fromIdValue.intValue)) + } + + if let msgId = payload["msg_id"] { + let msgIdValue = msgId as! NSString + if let peerId = peerId { + notificationRequestId = .messageId(MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: Int32(msgIdValue.intValue))) + } + } + + if let notificationRequestId = notificationRequestId { + result.append((notification.request.identifier, notificationRequestId)) + } + } + } + completion.f(result) + }) + } else { + var result: [(String, NotificationManagedNotificationRequestId)] = [] + if let notifications = UIApplication.shared.scheduledLocalNotifications { + for notification in notifications { + if let userInfo = notification.userInfo, let id = userInfo["id"] as? String { + if let requestId = NotificationManagedNotificationRequestId(string: id) { + result.append((id, requestId)) + } + } + } + } + completion.f(result) + } + }, removeNotificationIds: { ids in + if #available(iOS 10.0, *) { + UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: ids) + } else { + if let notifications = UIApplication.shared.scheduledLocalNotifications { + for notification in notifications { + if let userInfo = notification.userInfo, let id = userInfo["id"] as? String { + if ids.contains(id) { + UIApplication.shared.cancelLocalNotification(notification) + } + } + } + } + } + }, getPendingNotificationIds: { completion in + if #available(iOS 10.0, *) { + UNUserNotificationCenter.current().getPendingNotificationRequests(completionHandler: { requests in + var result: [(String, NotificationManagedNotificationRequestId)] = [] + for request in requests { + if let requestId = NotificationManagedNotificationRequestId(string: request.identifier) { + result.append((request.identifier, requestId)) + } + } + completion.f(result) + }) + } else { + var result: [(String, NotificationManagedNotificationRequestId)] = [] + if let notifications = UIApplication.shared.scheduledLocalNotifications { + for notification in notifications { + if let userInfo = notification.userInfo, let id = userInfo["id"] as? String { + if let requestId = NotificationManagedNotificationRequestId(string: id) { + result.append((id, requestId)) + } + } + } + } + completion.f(result) + } + }, removePendingNotificationIds: { ids in + if #available(iOS 10.0, *) { + UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: ids) + } else { + if let notifications = UIApplication.shared.scheduledLocalNotifications { + for notification in notifications { + if let userInfo = notification.userInfo, let id = userInfo["id"] as? String { + if ids.contains(id) { + UIApplication.shared.cancelLocalNotification(notification) + } + } + } + } + } + }) + + #if DEBUG + for argument in ProcessInfo.processInfo.arguments { + if argument.hasPrefix("snapshot:") { + GlobalExperimentalSettings.isAppStoreBuild = true + + guard let dataPath = ProcessInfo.processInfo.environment["snapshot-data-path"] else { + preconditionFailure() + } + setupSnapshotData(dataPath) + switch String(argument[argument.index(argument.startIndex, offsetBy: "snapshot:".count)...]) { + case "chat-list": + snapshotChatList(application: application, mainWindow: self.window!, window: self.mainWindow, statusBarHost: statusBarHost) + case "secret-chat": + snapshotSecretChat(application: application, mainWindow: self.window!, window: self.mainWindow, statusBarHost: statusBarHost) + case "settings": + snapshotSettings(application: application, mainWindow: self.window!, window: self.mainWindow, statusBarHost: statusBarHost) + case "appearance-settings": + snapshotAppearanceSettings(application: application, mainWindow: self.window!, window: self.mainWindow, statusBarHost: statusBarHost) + default: + break + } + self.window?.makeKeyAndVisible() + return true + } + } + #endif + + let apiId: Int32 = BuildConfig.shared().apiId + let languagesCategory = "ios" + + let appVersion = (Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String) ?? "unknown" + + let networkArguments = NetworkInitializationArguments(apiId: apiId, languagesCategory: languagesCategory, appVersion: appVersion) + + let appGroupName = "group.\(Bundle.main.bundleIdentifier!)" + let maybeAppGroupUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupName) + + guard let appGroupUrl = maybeAppGroupUrl else { + UIAlertView(title: nil, message: "Error 1", delegate: nil, cancelButtonTitle: "OK").show() + return true + } + + let rootPath = rootPathForBasePath(appGroupUrl.path) + performAppGroupUpgrades(appGroupPath: appGroupUrl.path, rootPath: rootPath) + + TempBox.initializeShared(basePath: rootPath, processType: "app", launchSpecificId: arc4random64()) + + let logsPath = rootPath + "/logs" + let _ = try? FileManager.default.createDirectory(atPath: logsPath, withIntermediateDirectories: true, attributes: nil) + Logger.setSharedLogger(Logger(basePath: logsPath)) + + ASDisableLogging() + + initializeLegacyComponents(application: application, currentSizeClassGetter: { + return UIUserInterfaceSizeClass.compact + }, currentHorizontalClassGetter: { + return UIUserInterfaceSizeClass.compact + }, documentsPath: legacyDocumentsPath(), currentApplicationBounds: { + return UIScreen.main.bounds + }, canOpenUrl: { url in + return UIApplication.shared.canOpenURL(url) + }, openUrl: { url in + UIApplication.shared.openURL(url) + }) + + if #available(iOS 10.0, *) { + UNUserNotificationCenter.current().delegate = self + } + + telegramUIDeclareEncodables() + + #if DEBUG + LoggingSettings.defaultSettings = LoggingSettings(logToFile: true, logToConsole: false, redactSensitiveData: true) + #else + if BuildConfig.shared().isInternalBuild { + LoggingSettings.defaultSettings = LoggingSettings(logToFile: true, logToConsole: false, redactSensitiveData: true) + } + #endif + + GlobalExperimentalSettings.isAppStoreBuild = BuildConfig.shared().isAppStoreBuild + + GlobalExperimentalSettings.enableFeed = false + #if DEBUG + //GlobalExperimentalSettings.enableFeed = true + #if targetEnvironment(simulator) + //GlobalTelegramCoreConfiguration.readMessages = false + #endif + #endif + + self.window?.makeKeyAndVisible() + + self.hasActiveAudioSession.set(MediaManager.globalAudioSession.isActive()) + + initializeAccountManagement() + self.accountManagerPromise.set(accountManager(basePath: rootPath + "/accounts-metadata") + |> mapToSignal { accountManager -> Signal<(AccountManager, LoggingSettings), NoError> in + return accountManager.transaction { transaction -> (AccountManager, LoggingSettings) in + return (accountManager, transaction.getSharedData(SharedDataKeys.loggingSettings) as? LoggingSettings ?? LoggingSettings.defaultSettings) + } + } + |> mapToSignal { accountManager, loggingSettings -> Signal in + Logger.shared.logToFile = loggingSettings.logToFile + Logger.shared.logToConsole = loggingSettings.logToConsole + Logger.shared.redactSensitiveData = loggingSettings.redactSensitiveData + + return importedLegacyAccount(basePath: appGroupUrl.path, accountManager: accountManager, present: { controller in + self.window?.rootViewController?.present(controller, animated: true, completion: nil) + }) + |> `catch` { _ -> Signal in + return Signal { subscriber in + let alertView = UIAlertView(title: "", message: "An error occured while trying to upgrade application data. Would you like to logout?", delegate: self, cancelButtonTitle: "No", otherButtonTitles: "Yes") + self.alertActions = (primary: { + let statusPath = appGroupUrl.path + "/Documents/importcompleted" + let _ = try? FileManager.default.createDirectory(atPath: appGroupUrl.path + "/Documents", withIntermediateDirectories: true, attributes: nil) + let _ = try? Data().write(to: URL(fileURLWithPath: statusPath)) + subscriber.putNext(.result(nil)) + subscriber.putCompletion() + }, other: { + exit(0) + }) + alertView.show() + + return EmptyDisposable + } |> runOn(Queue.mainQueue()) + } + |> mapToSignal { event -> Signal in + switch event { + case let .progress(type, value): + Queue.mainQueue().async { + if self.dataImportSplash == nil { + self.dataImportSplash = LegacyDataImportSplash() + self.dataImportSplash?.serviceAction = { + self.debugPressed() + } + self.mainWindow.coveringView = self.dataImportSplash + } + self.dataImportSplash?.progress = (type, value) + } + return .complete() + case let .result(temporaryId): + Queue.mainQueue().async { + if let _ = self.dataImportSplash { + self.dataImportSplash = nil + self.mainWindow.coveringView = nil + } + } + if let temporaryId = temporaryId { + Queue.mainQueue().after(1.0, { + let statusPath = appGroupUrl.path + "/Documents/importcompleted" + let _ = try? FileManager.default.createDirectory(atPath: appGroupUrl.path + "/Documents", withIntermediateDirectories: true, attributes: nil) + let _ = try? Data().write(to: URL(fileURLWithPath: statusPath)) + }) + return accountManager.transaction { transaction -> AccountManager in + transaction.setCurrentId(temporaryId) + transaction.updateRecord(temporaryId, { record in + if let record = record { + return AccountRecord(id: record.id, attributes: record.attributes, temporarySessionId: nil) + } + return record + }) + return accountManager + } + } + return .single(accountManager) + } + } + }) + + let _ = (self.accountManagerPromise.get() + |> mapToSignal { manager in + return managedCleanupAccounts(networkArguments: networkArguments, accountManager: manager, rootPath: rootPath, auxiliaryMethods: telegramAccountAuxiliaryMethods) + }).start() + + let applicationBindings = TelegramApplicationBindings(isMainApp: true, openUrl: { url in + var parsedUrl = URL(string: url) + if let parsed = parsedUrl { + if parsed.scheme == nil || parsed.scheme!.isEmpty { + parsedUrl = URL(string: "https://\(url)") + } + if parsed.scheme == "tg" { + return + } + } + + if let parsedUrl = parsedUrl { + UIApplication.shared.openURL(parsedUrl) + } else if let escapedUrl = url.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed), let parsedUrl = URL(string: escapedUrl) { + UIApplication.shared.openURL(parsedUrl) + } + }, openUniversalUrl: { url, completion in + if #available(iOS 10.0, *) { + var parsedUrl = URL(string: url) + if let parsed = parsedUrl { + if parsed.scheme == nil || parsed.scheme!.isEmpty { + parsedUrl = URL(string: "https://\(url)") + } + } + + if let parsedUrl = parsedUrl { + return UIApplication.shared.open(parsedUrl, options: [UIApplicationOpenURLOptionUniversalLinksOnly: true as NSNumber], completionHandler: { value in + completion.completion(value) + }) + } else if let escapedUrl = url.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed), let parsedUrl = URL(string: escapedUrl) { + return UIApplication.shared.open(parsedUrl, options: [UIApplicationOpenURLOptionUniversalLinksOnly: true as NSNumber], completionHandler: { value in + completion.completion(value) + }) + } else { + completion.completion(false) + } + } else { + completion.completion(false) + } + }, canOpenUrl: { url in + var parsedUrl = URL(string: url) + if let parsed = parsedUrl { + if parsed.scheme == nil || parsed.scheme!.isEmpty { + parsedUrl = URL(string: "https://\(url)") + } + } + if let parsedUrl = parsedUrl { + return UIApplication.shared.canOpenURL(parsedUrl) + } else if let escapedUrl = url.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed), let parsedUrl = URL(string: escapedUrl) { + return UIApplication.shared.canOpenURL(parsedUrl) + } else { + return false + } + }, getTopWindow: { + for window in application.windows.reversed() { + if window === self.window || window === statusBarHost.keyboardWindow { + return window + } + } + return application.windows.last + }, displayNotification: { text in + }, applicationInForeground: self.isInForegroundPromise.get(), + applicationIsActive: self.isActivePromise.get(), + clearMessageNotifications: { ids in + for id in ids { + self.clearNotificationsManager?.append(id) + } + }, pushIdleTimerExtension: { + let disposable = MetaDisposable() + Queue.mainQueue().async { + let wasEmpty = self.idleTimerExtensionSubscribers.isEmpty + let index = self.idleTimerExtensionSubscribers.add(Void()) + + if wasEmpty { + application.isIdleTimerDisabled = true + } + + disposable.set(ActionDisposable { + Queue.mainQueue().async { + self.idleTimerExtensionSubscribers.remove(index) + if self.idleTimerExtensionSubscribers.isEmpty { + application.isIdleTimerDisabled = false + } + } + }) + } + + return disposable + }, openSettings: { + if let url = URL(string: UIApplicationOpenSettingsURLString) { + UIApplication.shared.openURL(url) + } + }, openAppStorePage: { + let appStoreId = BuildConfig.shared().appStoreId + if let url = URL(string: "itms-apps://itunes.apple.com/app/id\(appStoreId)") { + UIApplication.shared.openURL(url) + } + }, getWindowHost: { + return self.nativeWindow + }, presentNativeController: { controller in + self.window?.rootViewController?.present(controller, animated: true, completion: nil) + }, dismissNativeController: { + self.window?.rootViewController?.dismiss(animated: true, completion: nil) + }) + + let watchManagerArgumentsPromise = Promise() + + #if DEBUG + let testingEnvironment = false + #else + let testingEnvironment = false + #endif + + self.context.set(self.accountManagerPromise.get() + |> deliverOnMainQueue + |> mapToSignal { accountManager -> Signal in + let replyFromNotificationsActive = self.replyFromNotificationsTokensPromise.get() + |> map { + !$0.isEmpty + } + |> distinctUntilChanged + return applicationContext(networkArguments: networkArguments, applicationBindings: applicationBindings, replyFromNotificationsActive: replyFromNotificationsActive, backgroundAudioActive: self.hasActiveAudioSession.get() |> distinctUntilChanged, watchManagerArguments: watchManagerArgumentsPromise.get(), accountManager: accountManager, rootPath: rootPath, legacyBasePath: appGroupUrl.path, testingEnvironment: testingEnvironment, mainWindow: self.mainWindow, reinitializedNotificationSettings: { + let _ = (self.context.get() + |> take(1) + |> deliverOnMainQueue).start(next: { value in + if let value = value, case let .authorized(context) = value { + self.registerForNotifications(account: context.account) + } + }) + }) + }) + + self.contextDisposable.set(self.context.get().start(next: { context in + assert(Queue.mainQueue().isCurrent()) + Logger.shared.log("App \(self.episodeId)", "received context \(String(describing: context)) account \(String(describing: context?.accountId))") + + if let contextValue = self.contextValue { + (contextValue.account?.applicationContext as? TelegramApplicationContext)?.isCurrent = false + } + self.contextValue = context + if let context = context { + (context.account?.applicationContext as? TelegramApplicationContext)?.isCurrent = true + updateLegacyComponentsAccount(context.account) + self.mainWindow.viewController = context.rootController + self.mainWindow.topLevelOverlayControllers = context.overlayControllers + self.maybeDequeueNotificationPayloads() + self.maybeDequeueNotificationRequests() + self.maybeDequeueWakeups() + switch context { + case let .authorized(context): + self.registerForNotifications(account: context.account) + context.account.notificationToken.set(self.notificationTokenPromise.get()) + context.account.voipToken.set(self.voipTokenPromise.get()) + case .unauthorized: + break + case .upgrading: + break + } + } else { + self.mainWindow.viewController = nil + } + })) + + self.watchCommunicationManagerPromise.set(watchCommunicationManager(context: self.context)) + let _ = self.watchCommunicationManagerPromise.get().start(next: { manager in + if let manager = manager { + watchManagerArgumentsPromise.set(.single(manager.arguments)) + } else { + watchManagerArgumentsPromise.set(.single(nil)) + } + }) + + let pushRegistry = PKPushRegistry(queue: .main) + pushRegistry.desiredPushTypes = Set([.voIP]) + self.pushRegistry = pushRegistry + pushRegistry.delegate = self + + self.badgeDisposable.set((self.context.get() + |> mapToSignal { context -> Signal in + if let context = context { + switch context { + case let .authorized(context): + return context.applicationBadge + case .unauthorized: + return .single(0) + case .upgrading: + return .single(0) + } + } else { + return .never() + } + } + |> deliverOnMainQueue).start(next: { count in + UIApplication.shared.applicationIconBadgeNumber = Int(count) + })) + + if #available(iOS 9.1, *) { + self.quickActionsDisposable.set((self.context.get() + |> mapToSignal { context -> Signal<[ApplicationShortcutItem], NoError> in + if let context = context { + switch context { + case let .authorized(context): + let presentationData = context.account.telegramApplicationContext.currentPresentationData.with { $0 } + return .single(applicationShortcutItems(strings: presentationData.strings)) + case .unauthorized: + return .single([]) + case .upgrading: + return .single([]) + } + } else { + return .never() + } + } + |> distinctUntilChanged + |> deliverOnMainQueue).start(next: { items in + if items.isEmpty { + UIApplication.shared.shortcutItems = nil + } else { + UIApplication.shared.shortcutItems = items.map({ $0.shortcutItem() }) + } + })) + } + + let _ = self.isInForegroundPromise.get().start(next: { value in + Logger.shared.log("App \(self.episodeId)", "isInForeground = \(value)") + }) + let _ = self.isActivePromise.get().start(next: { value in + Logger.shared.log("App \(self.episodeId)", "isActive = \(value)") + }) + + /*if let url = launchOptions?[.url] { + if let url = url as? URL, url.scheme == "tg" { + self.openUrlWhenReady(url: url.absoluteString) + } else if let url = url as? String, url.lowercased().hasPrefix("tg://") { + self.openUrlWhenReady(url: url) + } + }*/ + + if application.applicationState == .active { + self.isInForegroundValue = true + self.isInForegroundPromise.set(true) + self.isActiveValue = true + self.isActivePromise.set(true) + } + + BITHockeyBaseManager.setPresentAlert({ [weak self] alert in + if let strongSelf = self, let alert = alert { + var actions: [TextAlertAction] = [] + for action in alert.actions { + let isDefault = action.style == .default + actions.append(TextAlertAction(type: isDefault ? .defaultAction : .genericAction, title: action.title ?? "", action: { + if let action = action as? BITAlertAction { + action.invokeAction() + } + })) + } + if let contextValue = strongSelf.contextValue { + if case let .authorized(context) = contextValue { + let presentationData = context.applicationContext.currentPresentationData.with { $0 } + strongSelf.mainWindow.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: alert.title, text: alert.message ?? "", actions: actions), on: .root) + } else if case let .unauthorized(context) = contextValue { + strongSelf.mainWindow.present(standardTextAlertController(theme: AlertControllerTheme(authTheme: context.rootController.theme), title: alert.title, text: alert.message ?? "", actions: actions), on: .root) + } + } + } + }) + + BITHockeyBaseManager.setPresentView({ [weak self] controller in + if let strongSelf = self, let controller = controller { + let parent = LegacyController(presentation: .modal(animateIn: true), theme: nil) + let navigationController = UINavigationController(rootViewController: controller) + controller.navigation_setDismiss({ [weak parent] in + parent?.dismiss() + }, rootController: nil) + parent.bind(controller: navigationController) + strongSelf.mainWindow.present(parent, on: .root) + } + }) + + if !BuildConfig.shared().hockeyAppId.isEmpty { + BITHockeyManager.shared().configure(withIdentifier: BuildConfig.shared().hockeyAppId, delegate: self) + BITHockeyManager.shared().crashManager.crashManagerStatus = .autoSend + BITHockeyManager.shared().start() + BITHockeyManager.shared().authenticator.authenticateInstallation() + } + + return true + } + + func applicationWillResignActive(_ application: UIApplication) { + self.isActiveValue = false + self.isActivePromise.set(false) + self.clearNotificationsManager?.commitNow() + } + + func applicationDidEnterBackground(_ application: UIApplication) { + self.isInForegroundValue = false + self.isInForegroundPromise.set(false) + self.isActiveValue = false + self.isActivePromise.set(false) + + var taskId: Int? + taskId = application.beginBackgroundTask(withName: "lock", expirationHandler: { + if let taskId = taskId { + UIApplication.shared.endBackgroundTask(taskId) + } + }) + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 5.0, execute: { + if let taskId = taskId { + UIApplication.shared.endBackgroundTask(taskId) + } + }) + } + + func applicationWillEnterForeground(_ application: UIApplication) { + if self.isActiveValue { + self.isInForegroundValue = true + self.isInForegroundPromise.set(true) + } else { + if #available(iOSApplicationExtension 12.0, *) { + DispatchQueue.main.async { + self.isInForegroundValue = true + self.isInForegroundPromise.set(true) + } + } + } + } + + func applicationDidBecomeActive(_ application: UIApplication) { + self.isInForegroundValue = true + self.isInForegroundPromise.set(true) + self.isActiveValue = true + self.isActivePromise.set(true) + } + + func applicationWillTerminate(_ application: UIApplication) { + Logger.shared.log("App \(self.episodeId)", "terminating") + } + + func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { + self.notificationTokenPromise.set(.single(deviceToken)) + } + + func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { + var redactedPayload = userInfo + if var aps = redactedPayload["aps"] as? [AnyHashable: Any] { + if Logger.shared.redactSensitiveData { + if aps["alert"] != nil { + aps["alert"] = "[[redacted]]" + } + if aps["body"] != nil { + aps["body"] = "[[redacted]]" + } + } + redactedPayload["aps"] = aps + } + + + Logger.shared.log("App \(self.episodeId)", "remoteNotification: \(redactedPayload)") + completionHandler(UIBackgroundFetchResult.noData) + } + + func application(_ application: UIApplication, didReceive notification: UILocalNotification) { + if (application.applicationState == .inactive) { + Logger.shared.log("App \(self.episodeId)", "tap local notification \(String(describing: notification.userInfo)), applicationState \(application.applicationState)") + } + } + + public func pushRegistry(_ registry: PKPushRegistry, didUpdate credentials: PKPushCredentials, for type: PKPushType) { + if case PKPushType.voIP = type { + Logger.shared.log("App \(self.episodeId)", "pushRegistry credentials: \(credentials.token as NSData)") + + self.voipTokenPromise.set(.single(credentials.token)) + } + } + + private var pushCnt = 0 + public func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType) { + if case PKPushType.voIP = type { + Logger.shared.log("App \(self.episodeId)", "pushRegistry payload: \(payload.dictionaryPayload)") + /*#if DEBUG + self.pushCnt += 1 + if self.pushCnt % 2 != 0 { + Logger.shared.log("App \(self.episodeId)", "pushRegistry payload drop") + return + } + #endif*/ + + self.queuedNotifications.append(payload) + self.maybeDequeueNotificationPayloads() + } + } + + private func processPushPayload(_ payload: PKPushPayload, account: Account) { + let decryptedPayload: Signal<[AnyHashable: Any]?, NoError> + if let _ = payload.dictionaryPayload["aps"] as? [AnyHashable: Any] { + decryptedPayload = .single(payload.dictionaryPayload as [AnyHashable: Any]) + } else if var encryptedPayload = payload.dictionaryPayload["p"] as? String { + encryptedPayload = encryptedPayload.replacingOccurrences(of: "-", with: "+") + encryptedPayload = encryptedPayload.replacingOccurrences(of: "_", with: "/") + while encryptedPayload.count % 4 != 0 { + encryptedPayload.append("=") + } + if let data = Data(base64Encoded: encryptedPayload) { + decryptedPayload = decryptedNotificationPayload(account: account, data: data) + |> map { value -> [AnyHashable: Any]? in + if let value = value, let object = try? JSONSerialization.jsonObject(with: value, options: []) { + return object as? [AnyHashable: Any] + } + return nil + } + } else { + decryptedPayload = .single(nil) + } + } else { + decryptedPayload = .single(nil) + } + + let _ = (decryptedPayload + |> deliverOnMainQueue).start(next: { payload in + guard let payload = payload else { + return + } + + var redactedPayload = payload + if var aps = redactedPayload["aps"] as? [AnyHashable: Any] { + if Logger.shared.redactSensitiveData { + if aps["alert"] != nil { + aps["alert"] = "[[redacted]]" + } + if aps["body"] != nil { + aps["body"] = "[[redacted]]" + } + } + redactedPayload["aps"] = aps + } + Logger.shared.log("Apns \(self.episodeId)", "\(redactedPayload)") + + let aps = payload["aps"] as? [AnyHashable: Any] + + if UIApplication.shared.applicationState == .background { + var readMessageId: MessageId? + var isCall = false + var isAnnouncement = false + var isLocationPolling = false + var isMutePolling = false + var title: String = "" + var body: String? + var apnsSound: String? + var configurationUpdate: (Int32, String, Int32, Data?)? + if let aps = aps, let alert = aps["alert"] as? String { + if let range = alert.range(of: ": ") { + title = String(alert[..() + if isCall { + addedWakeups.insert(.call) + } + if isLocationPolling { + addedWakeups.insert(.backgroundLocation) + } + if !addedWakeups.isEmpty { + self.queuedWakeups.formUnion(addedWakeups) + self.maybeDequeueWakeups() + } + if let readMessageId = readMessageId { + self.clearNotificationsManager?.append(readMessageId) + self.clearNotificationsManager?.commitNow() + + let signal = self.currentAuthorizedContext() + |> take(1) + |> mapToSignal { context -> Signal in + if let context = context { + return context.account.postbox.transaction (ignoreDisabled: true, { transaction -> Void in + transaction.applyIncomingReadMaxId(readMessageId) + }) + } else { + return .complete() + } + } + let _ = signal.start() + } + + if let (datacenterId, host, port, secret) = configurationUpdate { + let signal = self.currentAuthorizedContext() + |> take(1) + |> mapToSignal { context -> Signal in + if let context = context { + context.account.network.mergeBackupDatacenterAddress(datacenterId: datacenterId, host: host, port: port, secret: secret) + } + return .complete() + } + let _ = signal.start() + } + } + }) + } + + public func pushRegistry(_ registry: PKPushRegistry, didInvalidatePushTokenFor type: PKPushType) { + Logger.shared.log("App \(self.episodeId)", "invalidated token for \(type)") + } + + private func currentAuthorizedContext() -> Signal { + return self.context.get() + |> take(1) + |> mapToSignal { contextValue -> Signal in + if let contextValue = contextValue, case let .authorized(context) = contextValue { + return .single(context) + } else { + return .single(nil) + } + } + } + + private func authorizedContext() -> Signal { + return self.context.get() + |> mapToSignal { contextValue -> Signal in + if let contextValue = contextValue, case let .authorized(context) = contextValue { + return .single(context) + } else { + return .complete() + } + } + } + + func application(_ application: UIApplication, open url: URL, sourceApplication: String?) -> Bool { + self.openUrl(url: url) + return true + } + + func application(_ application: UIApplication, open url: URL, sourceApplication: String?, annotation: Any) -> Bool { + self.openUrl(url: url) + return true + } + + func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool { + self.openUrl(url: url) + return true + } + + func application(_ application: UIApplication, handleOpen url: URL) -> Bool { + self.openUrl(url: url) + return true + } + + private func openUrl(url: URL) { + let _ = (self.context.get() + |> flatMap { $0 } + |> filter { context in + switch context { + case .authorized, .unauthorized: + return true + default: + return false + } + } + |> take(1) + |> deliverOnMainQueue).start(next: { contextValue in + switch contextValue { + case let .authorized(context): + context.openUrl(url) + case let .unauthorized(context): + if let secureIdData = parseSecureIdUrl(url: url) { + let strings = context.applicationContext.currentPresentationData.with({ $0 }).strings + let theme = context.rootController.theme + context.rootController.currentWindow?.present(standardTextAlertController(theme: AlertControllerTheme(authTheme: theme), title: nil, text: strings.Passport_NotLoggedInMessage, actions: [TextAlertAction(type: .genericAction, title: strings.Calls_NotNow, action: { + if let callbackUrl = URL(string: secureIdCallbackUrl(with: secureIdData.callbackUrl, peerId: secureIdData.peerId, result: .cancel, parameters: [:])) { + UIApplication.shared.openURL(callbackUrl) + } + }), TextAlertAction(type: .defaultAction, title: strings.Common_OK, action: {})]), on: .root, blockInteraction: false) + } + default: + break + } + }) + } + + func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([Any]?) -> Void) -> Bool { + if #available(iOS 10.0, *) { + if let startCallIntent = userActivity.interaction?.intent as? SupportedStartCallIntent { + if let contact = startCallIntent.contacts?.first { + if let handle = contact.personHandle?.value { + if let userId = Int32(handle) { + if let contextValue = self.contextValue, case let .authorized(context) = contextValue { + let callResult = context.applicationContext.callManager?.requestCall(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: userId), endCurrentIfAny: false) + if let callResult = callResult, case let .alreadyInProgress(currentPeerId) = callResult { + + } + } + } + } + } + } + } + + if userActivity.activityType == NSUserActivityTypeBrowsingWeb, let url = userActivity.webpageURL { + self.openUrl(url: url) + } + + return true + } + + @available(iOS 9.0, *) + func application(_ application: UIApplication, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) { + let _ = (self.context.get() + |> mapToSignal { value -> Signal in + if let value = value { + if case let .authorized(context) = value { + return context.unlockedState + |> filter { $0 } + |> take(1) + |> map { _ -> ApplicationContext? in + return value + } + } else { + return .single(nil) + } + } else { + return .complete() + } + } + |> take(1) + |> deliverOnMainQueue).start(next: { contextValue in + if let contextValue = contextValue, case let .authorized(context) = contextValue { + if let type = ApplicationShortcutItemType(rawValue: shortcutItem.type) { + switch type { + case .search: + context.openRootSearch() + case .compose: + context.openRootCompose() + case .camera: + context.openRootCamera() + case .savedMessages: + self.openChatWhenReady(peerId: context.account.peerId) + } + } + } + }) + } + + private func openChatWhenReady(peerId: PeerId, messageId: MessageId? = nil) { + self.openChatWhenReadyDisposable.set((self.authorizedContext() + |> take(1) + |> deliverOnMainQueue).start(next: { context in + context.openChatWithPeerId(peerId: peerId, messageId: messageId) + })) + } + + private func openUrlWhenReady(url: String) { + self.openUrlWhenReadyDisposable.set((self.authorizedContext() + |> take(1) + |> deliverOnMainQueue).start(next: { context in + let presentationData = context.account.telegramApplicationContext.currentPresentationData.with { $0 } + openExternalUrl(account: context.account, url: url, presentationData: presentationData, applicationContext: context.account.telegramApplicationContext, navigationController: context.rootController, dismissInput: { + + }) + })) + } + + @available(iOS 10.0, *) + func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { + if response.actionIdentifier == UNNotificationDefaultActionIdentifier { + if let peerId = peerIdFromNotification(response.notification) { + var messageId: MessageId? = nil + if response.notification.request.content.categoryIdentifier == "watch" { + messageId = messageIdFromNotification(peerId: peerId, notification: response.notification) + } + self.openChatWhenReady(peerId: peerId, messageId: messageId) + } + completionHandler() + } else if response.actionIdentifier == "reply", let peerId = peerIdFromNotification(response.notification) { + if let response = response as? UNTextInputNotificationResponse, !response.userText.isEmpty { + let text = response.userText + let token = addReplyFromNotificationsToken() + + let signal = self.authorizedContext() + |> take(1) + |> mapToSignal { context -> Signal in + if let messageId = messageIdFromNotification(peerId: peerId, notification: response.notification) { + let _ = applyMaxReadIndexInteractively(postbox: context.account.postbox, stateManager: context.account.stateManager, index: MessageIndex(id: messageId, timestamp: 0)).start() + } + return enqueueMessages(account: context.account, peerId: peerId, messages: [EnqueueMessage.message(text: text, attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil)]) + |> map { messageIds -> MessageId? in + if messageIds.isEmpty { + return nil + } else { + return messageIds[0] + } + } + |> mapToSignal { messageId -> Signal in + if let messageId = messageId { + return context.account.postbox.unsentMessageIdsView() + |> filter { view in + return !view.ids.contains(messageId) + } + |> take(1) + |> mapToSignal { _ -> Signal in + return .complete() + } + } else { + return .complete() + } + } + } + |> deliverOnMainQueue + |> timeout(15.0, queue: Queue.mainQueue(), alternate: .complete() |> beforeCompleted { + /*let content = UNMutableNotificationContent() + content.body = "Please open the app to continue sending messages" + content.sound = UNNotificationSound.default() + content.categoryIdentifier = "error" + content.userInfo = ["peerId": peerId as NSNumber] + + let request = UNNotificationRequest(identifier: "reply-error", content: content, trigger: nil) + + let center = UNUserNotificationCenter.current() + center.add(request)*/ + }) + + let disposable = MetaDisposable() + disposable.set((signal + |> afterDisposed { [weak disposable] in + Queue.mainQueue().async { + if let disposable = disposable { + self.replyFromNotificationsDisposables.remove(disposable) + } + self.removeReplyFromNotificationsToken(token) + completionHandler() + } + }).start()) + self.replyFromNotificationsDisposables.add(disposable) + } else { + completionHandler() + } + } else { + completionHandler() + } + } + + private func registerForNotifications(replyString: String, messagePlaceholderString: String, hiddenContentString: String, includeNames: Bool) { + if #available(iOS 10.0, *) { + UNUserNotificationCenter.current().requestAuthorization(options: [.badge, .sound, .alert], completionHandler: { result, _ in + if result { + Queue.mainQueue().async { + let reply = UNTextInputNotificationAction(identifier: "reply", title: replyString, options: [], textInputButtonTitle: replyString, textInputPlaceholder: messagePlaceholderString) + + let unknownMessageCategory: UNNotificationCategory + let replyMessageCategory: UNNotificationCategory + let replyLegacyMessageCategory: UNNotificationCategory + let replyLegacyMediaMessageCategory: UNNotificationCategory + let replyMediaMessageCategory: UNNotificationCategory + let muteMessageCategory: UNNotificationCategory + let muteMediaMessageCategory: UNNotificationCategory + if #available(iOS 11.0, *) { + var options: UNNotificationCategoryOptions = [] + if includeNames { + options.insert(.hiddenPreviewsShowTitle) + } + + unknownMessageCategory = UNNotificationCategory(identifier: "unknown", actions: [], intentIdentifiers: [], hiddenPreviewsBodyPlaceholder: hiddenContentString, options: options) + replyMessageCategory = UNNotificationCategory(identifier: "withReply", actions: [reply], intentIdentifiers: [], hiddenPreviewsBodyPlaceholder: hiddenContentString, options: options) + replyLegacyMessageCategory = UNNotificationCategory(identifier: "r", actions: [reply], intentIdentifiers: [], hiddenPreviewsBodyPlaceholder: hiddenContentString, options: options) + replyLegacyMediaMessageCategory = UNNotificationCategory(identifier: "m", actions: [reply], intentIdentifiers: [], hiddenPreviewsBodyPlaceholder: hiddenContentString, options: options) + replyMediaMessageCategory = UNNotificationCategory(identifier: "withReplyMedia", actions: [reply], intentIdentifiers: [], hiddenPreviewsBodyPlaceholder: hiddenContentString, options: options) + muteMessageCategory = UNNotificationCategory(identifier: "withMute", actions: [], intentIdentifiers: [], hiddenPreviewsBodyPlaceholder: hiddenContentString, options: options) + muteMediaMessageCategory = UNNotificationCategory(identifier: "withMuteMedia", actions: [], intentIdentifiers: [], hiddenPreviewsBodyPlaceholder: hiddenContentString, options: options) + } else { + unknownMessageCategory = UNNotificationCategory(identifier: "unknown", actions: [], intentIdentifiers: [], options: []) + replyMessageCategory = UNNotificationCategory(identifier: "withReply", actions: [reply], intentIdentifiers: [], options: []) + replyLegacyMessageCategory = UNNotificationCategory(identifier: "r", actions: [reply], intentIdentifiers: [], options: []) + replyLegacyMediaMessageCategory = UNNotificationCategory(identifier: "m", actions: [reply], intentIdentifiers: [], options: []) + replyMediaMessageCategory = UNNotificationCategory(identifier: "withReplyMedia", actions: [reply], intentIdentifiers: [], options: []) + muteMessageCategory = UNNotificationCategory(identifier: "withMute", actions: [], intentIdentifiers: [], options: []) + muteMediaMessageCategory = UNNotificationCategory(identifier: "withMuteMedia", actions: [], intentIdentifiers: [], options: []) + } + + UNUserNotificationCenter.current().setNotificationCategories([unknownMessageCategory, replyMessageCategory, replyLegacyMessageCategory, replyLegacyMediaMessageCategory, replyMediaMessageCategory, muteMessageCategory, muteMediaMessageCategory]) + + UIApplication.shared.registerForRemoteNotifications() + } + } + }) + } else { + let settings = UIUserNotificationSettings(types: [.badge, .sound, .alert], categories:[]) + UIApplication.shared.registerUserNotificationSettings(settings) + + UIApplication.shared.registerForRemoteNotifications() + } + } + + private func maybeDequeueNotificationPayloads() { + if let contextValue = self.contextValue, case let .authorized(context) = contextValue, !self.queuedNotifications.isEmpty { + let queuedNotifications = self.queuedNotifications + self.queuedNotifications = [] + for payload in queuedNotifications { + self.processPushPayload(payload, account: context.account) + } + } + } + + private func maybeDequeueNotificationRequests() { + if let contextValue = self.contextValue, case let .authorized(context) = contextValue { + let requests = self.queuedNotificationRequests + self.queuedNotificationRequests = [] + let queuedMutePolling = self.queuedMutePolling + self.queuedMutePolling = false + + let _ = (context.account.postbox.transaction(ignoreDisabled: true, { transaction -> PostboxAccessChallengeData in + return transaction.getAccessChallengeData() + }) + |> deliverOnMainQueue).start(next: { accessChallengeData in + guard let contextValue = self.contextValue, case let .authorized(context) = contextValue else { + Logger.shared.log("App \(self.episodeId)", "Couldn't process remote notification request") + return + } + + let strings = context.account.telegramApplicationContext.currentPresentationData.with({ $0 }).strings + + for (title, body, apnsSound, requestId) in requests { + if handleVoipNotifications { + context.notificationManager.enqueueRemoteNotification(title: title, text: body, apnsSound: apnsSound, requestId: requestId, strings: strings, accessChallengeData: accessChallengeData) + } + + context.wakeupManager.wakeupForIncomingMessages(completion: { messageIds -> Signal in + if let contextValue = self.contextValue, case let .authorized(context) = contextValue { + if handleVoipNotifications { + return context.notificationManager.commitRemoteNotification(originalRequestId: requestId, messageIds: messageIds) + } else { + return context.notificationManager.commitRemoteNotification(originalRequestId: nil, messageIds: []) + } + } else { + Logger.shared.log("App \(self.episodeId)", "Couldn't process remote notifications wakeup result") + return .complete() + } + }) + } + if queuedMutePolling { + context.wakeupManager.wakeupForIncomingMessages(completion: { messageIds -> Signal in + if let contextValue = self.contextValue, case .authorized = contextValue { + return .single(Void()) + } else { + Logger.shared.log("App \(self.episodeId)", "Couldn't process remote notifications wakeup result") + return .single(Void()) + } + }) + } + }) + } else { + Logger.shared.log("App \(self.episodeId)", "maybeDequeueNotificationRequests failed, no active context") + } + } + + private func maybeDequeueAnnouncements() { + if let contextValue = self.contextValue, case let .authorized(context) = contextValue, !self.queuedAnnouncements.isEmpty { + let queuedAnnouncements = self.queuedAnnouncements + self.queuedAnnouncements = [] + let _ = (context.account.postbox.transaction(ignoreDisabled: true, { transaction -> [MessageId: String] in + var result: [MessageId: String] = [:] + let timestamp = Int32(context.account.network.globalTime) + let servicePeer = TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: 777000), accessHash: nil, firstName: "Telegram", lastName: nil, username: nil, phone: "42777", photo: [], botInfo: nil, restrictionInfo: nil, flags: [.isVerified]) + if transaction.getPeer(servicePeer.id) == nil { + transaction.updatePeersInternal([servicePeer], update: { _, updated in + return updated + }) + } + for body in queuedAnnouncements { + let globalId = arc4random64() + var attributes: [MessageAttribute] = [] + let entities = generateTextEntities(body, enabledTypes: .all) + if !entities.isEmpty { + attributes.append(TextEntitiesMessageAttribute(entities: entities)) + } + let message = StoreMessage(id: .Partial(servicePeer.id, Namespaces.Message.Local), globallyUniqueId: globalId, groupingKey: nil, timestamp: timestamp, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, authorId: servicePeer.id, text: body, attributes: attributes, media: []) + let ids = transaction.addMessages([message], location: .Random) + if let id = ids[globalId] { + result[id] = body + } + } + return result + }) |> deliverOnMainQueue).start(next: { result in + if let contextValue = self.contextValue, case let .authorized(context) = contextValue { + for (id, text) in result { + context.notificationManager.enqueueRemoteNotification(title: "", text: text, apnsSound: nil, requestId: .messageId(id), strings: context.account.telegramApplicationContext.currentPresentationData.with({ $0 }).strings, accessChallengeData: .none) + } + } + }) + } + } + + private func maybeDequeueWakeups() { + for wakeup in self.queuedWakeups { + switch wakeup { + case .call: + if let contextValue = self.contextValue, case let .authorized(context) = contextValue { + context.wakeupManager.wakeupForIncomingMessages() + } + case .backgroundLocation: + if UIApplication.shared.applicationState == .background { + if let contextValue = self.contextValue, case let .authorized(context) = contextValue { + context.applicationContext.liveLocationManager?.pollOnce() + } + } + } + } + + self.queuedWakeups.removeAll() + } + + private func registerForNotifications(account: Account) { + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + let _ = (account.postbox.transaction { transaction -> Bool in + let settings = transaction.getPreferencesEntry(key: ApplicationSpecificPreferencesKeys.inAppNotificationSettings) as? InAppNotificationSettings ?? InAppNotificationSettings.defaultSettings + return settings.displayNameOnLockscreen + } + |> deliverOnMainQueue).start(next: { displayNames in + self.registerForNotifications(replyString: presentationData.strings.Notification_Reply, messagePlaceholderString: presentationData.strings.Conversation_InputTextPlaceholder, hiddenContentString: presentationData.strings.Watch_MessageView_Title, includeNames: displayNames) + }) + } + + @available(iOS 10.0, *) + func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { + #if DEBUG + completionHandler([.alert]) + #else + completionHandler([]) + #endif + } + + override var next: UIResponder? { + if let contextValue = self.contextValue, case let .authorized(context) = contextValue, let controller = context.applicationContext.keyShortcutsController { + return controller + } + return super.next + } + + @objc func debugPressed() { + let _ = (Logger.shared.collectLogs() + |> deliverOnMainQueue).start(next: { logs in + var activityItems: [Any] = [] + for (_, path) in logs { + activityItems.append(URL(fileURLWithPath: path)) + } + + let activityController = UIActivityViewController(activityItems: activityItems, applicationActivities: nil) + + self.window?.rootViewController?.present(activityController, animated: true, completion: nil) + }) + } +} + +@available(iOS 10.0, *) +private func peerIdFromNotification(_ notification: UNNotification) -> PeerId? { + if let peerId = notification.request.content.userInfo["peerId"] as? Int64 { + return PeerId(peerId) + } else { + let payload = notification.request.content.userInfo + var peerId: PeerId? + if let fromId = payload["from_id"] { + let fromIdValue = fromId as! NSString + peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: Int32(fromIdValue.intValue)) + } else if let fromId = payload["chat_id"] { + let fromIdValue = fromId as! NSString + peerId = PeerId(namespace: Namespaces.Peer.CloudGroup, id: Int32(fromIdValue.intValue)) + } else if let fromId = payload["channel_id"] { + let fromIdValue = fromId as! NSString + peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: Int32(fromIdValue.intValue)) + } else if let fromId = payload["encryption_id"] { + let fromIdValue = fromId as! NSString + peerId = PeerId(namespace: Namespaces.Peer.SecretChat, id: Int32(fromIdValue.intValue)) + } + return peerId + } +} + +@available(iOS 10.0, *) +private func messageIdFromNotification(peerId: PeerId, notification: UNNotification) -> MessageId? { + let payload = notification.request.content.userInfo + if let messageIdNamespace = payload["messageId.namespace"] as? Int32, let messageIdId = payload["messageId.id"] as? Int32 { + return MessageId(peerId: peerId, namespace: messageIdNamespace, id: messageIdId) + } + + if let msgId = payload["msg_id"] { + let msgIdValue = msgId as! NSString + return MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: Int32(msgIdValue.intValue)) + } + return nil +} diff --git a/Telegram-iOS/Application.swift b/Telegram-iOS/Application.swift new file mode 100644 index 0000000000..10c08975ea --- /dev/null +++ b/Telegram-iOS/Application.swift @@ -0,0 +1,5 @@ +import UIKit + +@objc(Application) class Application: UIApplication { + +} diff --git a/Telegram-iOS/ApplicationContext.swift b/Telegram-iOS/ApplicationContext.swift new file mode 100644 index 0000000000..8c210f592f --- /dev/null +++ b/Telegram-iOS/ApplicationContext.swift @@ -0,0 +1,1077 @@ +import Foundation +import Intents +import TelegramUI +import SwiftSignalKit +import Postbox +import TelegramCore +import Display +import LegacyComponents + +func applicationContext(networkArguments: NetworkInitializationArguments, applicationBindings: TelegramApplicationBindings, replyFromNotificationsActive: Signal, backgroundAudioActive: Signal, watchManagerArguments: Signal, accountManager: AccountManager, rootPath: String, legacyBasePath: String, testingEnvironment: Bool, mainWindow: Window1, reinitializedNotificationSettings: @escaping () -> Void) -> Signal { + return currentAccount(allocateIfNotExists: true, networkArguments: networkArguments, supplementary: false, manager: accountManager, rootPath: rootPath, beginWithTestingEnvironment: testingEnvironment, auxiliaryMethods: telegramAccountAuxiliaryMethods) + |> filter { $0 != nil } + |> deliverOnMainQueue + |> mapToSignal { account -> Signal in + if let account = account { + switch account { + case .upgrading: + return .single(.upgrading(UpgradingApplicationContext())) + case let .unauthorized(account): + return currentPresentationDataAndSettings(postbox: account.postbox) + |> deliverOnMainQueue + |> map { dataAndSettings -> ApplicationContext? in + return .unauthorized(UnauthorizedApplicationContext(applicationContext: TelegramApplicationContext(applicationBindings: applicationBindings, accountManager: accountManager, account: nil, initialPresentationDataAndSettings: dataAndSettings, postbox: account.postbox), account: account)) + } + case let .authorized(account): + return currentPresentationDataAndSettings(postbox: account.postbox) + |> deliverOnMainQueue + |> map { dataAndSettings -> ApplicationContext? in + return .authorized(AuthorizedApplicationContext(mainWindow: mainWindow, applicationContext: TelegramApplicationContext(applicationBindings: applicationBindings, accountManager: accountManager, account: account, initialPresentationDataAndSettings: dataAndSettings, postbox: account.postbox), replyFromNotificationsActive: replyFromNotificationsActive, backgroundAudioActive: backgroundAudioActive, watchManagerArguments: watchManagerArguments, account: account, accountManager: accountManager, legacyBasePath: legacyBasePath, showCallsTab: dataAndSettings.callListSettings.showTab, reinitializedNotificationSettings: reinitializedNotificationSettings)) + } + } + } else { + return .single(nil) + } + } +} + +func isAccessLocked(data: PostboxAccessChallengeData, at timestamp: Int32) -> Bool { + if data.isLockable, let autolockDeadline = data.autolockDeadline, autolockDeadline <= timestamp { + return true + } else { + return false + } +} + +enum ApplicationContext { + case upgrading(UpgradingApplicationContext) + case unauthorized(UnauthorizedApplicationContext) + case authorized(AuthorizedApplicationContext) + + var account: Account? { + switch self { + case .upgrading: + return nil + case .unauthorized: + return nil + case let .authorized(context): + return context.account + } + } + + var accountId: AccountRecordId? { + switch self { + case .upgrading: + return nil + case let .unauthorized(unauthorized): + return unauthorized.account.id + case let .authorized(authorized): + return authorized.account.id + } + } + + var rootController: NavigationController { + switch self { + case let .upgrading(context): + return context.rootController + case let .unauthorized(context): + return context.rootController + case let .authorized(context): + return context.rootController + } + } + + var overlayControllers: [ViewController] { + switch self { + case .upgrading: + return [] + case .unauthorized: + return [] + case let .authorized(context): + return [context.overlayMediaController, context.notificationController] + } + } +} + +final class UpgradingApplicationContext { + let rootController: NavigationController + + init() { + self.rootController = NavigationController(mode: .single, theme: NavigationControllerTheme(navigationBar: NavigationBarTheme(buttonColor: .white, disabledButtonColor: .gray, primaryTextColor: .white, backgroundColor: .black, separatorColor: .white, badgeBackgroundColor: .black, badgeStrokeColor: .black, badgeTextColor: .white), emptyAreaColor: .black, emptyDetailIcon: nil)) + + let noticeController = ViewController(navigationBarPresentationData: nil) + self.rootController.pushViewController(noticeController, animated: false) + } +} + +final class UnauthorizedApplicationContext { + let applicationContext: TelegramApplicationContext + let account: UnauthorizedAccount + + let rootController: AuthorizationSequenceController + + init(applicationContext: TelegramApplicationContext, account: UnauthorizedAccount) { + self.account = account + self.applicationContext = applicationContext + + self.rootController = AuthorizationSequenceController(account: account, strings: (applicationContext.currentPresentationData.with { $0 }).strings, openUrl: { [weak applicationContext] url in + applicationContext?.applicationBindings.openUrl(url) + }, apiId: BuildConfig.shared().apiId, apiHash: BuildConfig.shared().apiHash) + + account.shouldBeServiceTaskMaster.set(applicationContext.applicationBindings.applicationInForeground |> map { value -> AccountServiceTaskMasterMode in + if value { + return .always + } else { + return .never + } + }) + } +} + +private struct PasscodeState: Equatable { + let isActive: Bool + let challengeData: PostboxAccessChallengeData + let autolockTimeout: Int32? + let enableBiometrics: Bool +} + +private enum CallStatusText: Equatable { + case none + case inProgress(Double?) + + static func ==(lhs: CallStatusText, rhs: CallStatusText) -> Bool { + switch lhs { + case .none: + if case .none = rhs { + return true + } else { + return false + } + case let .inProgress(lhsReferenceTime): + if case let .inProgress(rhsReferenceTime) = rhs, lhsReferenceTime == rhsReferenceTime { + return true + } else { + return false + } + + } + } +} + +final class AuthorizedApplicationContext { + let mainWindow: Window1 + let lockedCoveringView: LockedWindowCoveringView + + let applicationContext: TelegramApplicationContext + let account: Account + let replyFromNotificationsActive: Signal + let backgroundAudioActive: Signal + + let rootController: TelegramRootController + let overlayMediaController: OverlayMediaController + let notificationController: NotificationContainerController + + private var scheduledOperChatWithPeerId: PeerId? + private var scheduledOpenExternalUrl: URL? + + let wakeupManager: WakeupManager + let notificationManager: NotificationManager + + private let passcodeStatusDisposable = MetaDisposable() + private let passcodeLockDisposable = MetaDisposable() + private let loggedOutDisposable = MetaDisposable() + private let inAppNotificationSettingsDisposable = MetaDisposable() + private let notificationMessagesDisposable = MetaDisposable() + private let termsOfServiceUpdatesDisposable = MetaDisposable() + private let proccedTOSBotDisposable = MetaDisposable() + private var watchNavigateToMessageDisposable = MetaDisposable() + + private var inAppNotificationSettings: InAppNotificationSettings? + + private var isLocked: Bool = true + private var passcodeController: ViewController? + private var callController: CallController? + private let hasOngoingCall = ValuePromise(false) + private let callState = Promise(nil) + + private var currentTermsOfServiceUpdate: TermsOfServiceUpdate? + private var currentTermsOfServiceUpdateController: TermsOfServiceController? + + private let unlockedStatePromise = Promise() + var unlockedState: Signal { + return self.unlockedStatePromise.get() + } + + var applicationBadge: Signal { + return renderedTotalUnreadCount(postbox: self.account.postbox) + |> map { + $0.0 + } + } + + private var presentationDataDisposable: Disposable? + private var displayAlertsDisposable: Disposable? + private var removeNotificationsDisposable: Disposable? + private var callDisposable: Disposable? + private var callStateDisposable: Disposable? + private var currentCallStatusText: CallStatusText = .none + private var currentCallStatusTextTimer: SwiftSignalKit.Timer? + + private var applicationInForegroundDisposable: Disposable? + + private var showCallsTab: Bool + private var showCallsTabDisposable: Disposable? + private var enablePostboxTransactionsDiposable: Disposable? + + init(mainWindow: Window1, applicationContext: TelegramApplicationContext, replyFromNotificationsActive: Signal, backgroundAudioActive: Signal, watchManagerArguments: Signal, account: Account, accountManager: AccountManager, legacyBasePath: String, showCallsTab: Bool, reinitializedNotificationSettings: @escaping () -> Void) { + setupLegacyComponents(account: account) + let presentationData = applicationContext.currentPresentationData.with { $0 } + + self.mainWindow = mainWindow + self.lockedCoveringView = LockedWindowCoveringView(theme: presentationData.theme) + + self.applicationContext = applicationContext + self.account = account + self.replyFromNotificationsActive = replyFromNotificationsActive + self.backgroundAudioActive = backgroundAudioActive + + let runningBackgroundLocationTasks: Signal + if let liveLocationManager = applicationContext.liveLocationManager { + runningBackgroundLocationTasks = liveLocationManager.isPolling + } else { + runningBackgroundLocationTasks = .single(false) + } + + let runningWatchTasksPromise = Promise(nil) + + self.wakeupManager = WakeupManager(inForeground: applicationContext.applicationBindings.applicationInForeground, runningServiceTasks: account.importantTasksRunning, runningBackgroundLocationTasks: runningBackgroundLocationTasks, runningWatchTasks: runningWatchTasksPromise.get()) + self.wakeupManager.account = account + + self.showCallsTab = showCallsTab + + self.notificationManager = NotificationManager() + self.notificationManager.account = account + self.notificationManager.isApplicationInForeground = false + + self.overlayMediaController = OverlayMediaController() + + applicationContext.attachOverlayMediaController(self.overlayMediaController) + var presentImpl: ((ViewController, Any?) -> Void)? + var openSettingsImpl: (() -> Void)? + let callManager = PresentationCallManager(account: account, getDeviceAccessData: { + return (account.telegramApplicationContext.currentPresentationData.with { $0 }, { c, a in + presentImpl?(c, a) + }, { + openSettingsImpl?() + }) + }, networkType: account.networkType, audioSession: applicationContext.mediaManager!.audioSession, callSessionManager: account.callSessionManager) + applicationContext.callManager = callManager + applicationContext.hasOngoingCall = self.hasOngoingCall.get() + + let shouldBeServiceTaskMaster = combineLatest(applicationContext.applicationBindings.applicationInForeground, self.wakeupManager.isWokenUp, replyFromNotificationsActive, backgroundAudioActive, callManager.hasActiveCalls) + |> map { foreground, wokenUp, replyFromNotificationsActive, backgroundAudioActive, hasActiveCalls -> AccountServiceTaskMasterMode in + if foreground || wokenUp || replyFromNotificationsActive || hasActiveCalls { + return .always + } else { + return .never + } + } + account.shouldBeServiceTaskMaster.set(shouldBeServiceTaskMaster) + self.enablePostboxTransactionsDiposable = (combineLatest(shouldBeServiceTaskMaster, backgroundAudioActive) + |> map { shouldBeServiceTaskMaster, backgroundAudioActive -> Bool in + switch shouldBeServiceTaskMaster { + case .never: + break + default: + return true + } + if backgroundAudioActive { + return true + } + return false + } + |> deliverOnMainQueue).start(next: { [weak account] next in + if let account = account { + Logger.shared.log("ApplicationContext", "setting canBeginTransactions to \(next)") + account.postbox.setCanBeginTransactions(next) + } + }) + account.shouldExplicitelyKeepWorkerConnections.set(backgroundAudioActive) + account.shouldKeepOnlinePresence.set(applicationContext.applicationBindings.applicationInForeground) + + let cache = TGCache(cachesPath: legacyBasePath + "/Caches")! + + setupAccount(account, fetchCachedResourceRepresentation: fetchCachedResourceRepresentation, transformOutgoingMessageMedia: transformOutgoingMessageMedia, preFetchedResourcePath: { resource in + preFetchedLegacyResourcePath(basePath: legacyBasePath, resource: resource, cache: cache) + }) + + account.applicationContext = applicationContext + + self.notificationController = NotificationContainerController(account: account) + + self.mainWindow.previewThemeAccentColor = presentationData.theme.rootController.navigationBar.accentTextColor + self.mainWindow.previewThemeDarkBlur = presentationData.theme.chatList.searchBarKeyboardColor == .dark + + self.rootController = TelegramRootController(account: account) + + if KeyShortcutsController.isAvailable { + let keyShortcutsController = KeyShortcutsController { [weak self] f in + if let strongSelf = self { + if let tabController = strongSelf.rootController.rootTabController { + let controller = tabController.controllers[tabController.selectedIndex] + if !f(controller) { + return + } + if let controller = strongSelf.rootController.topViewController as? ViewController { + if !f(controller) { + return + } + } + } + strongSelf.mainWindow.forEachViewController(f) + } + } + applicationContext.keyShortcutsController = keyShortcutsController + } + + self.applicationInForegroundDisposable = applicationContext.applicationBindings.applicationInForeground.start(next: { [weak self] value in + Queue.mainQueue().async { + self?.notificationManager.isApplicationInForeground = value + } + }) + + self.mainWindow.inCallNavigate = { [weak self] in + if let strongSelf = self, let callController = strongSelf.callController { + if callController.isNodeLoaded && callController.view.superview == nil { + strongSelf.rootController.view.endEditing(true) + strongSelf.mainWindow.present(callController, on: .calls) + } + } + } + + applicationContext.presentGlobalController = { [weak self] c, a in + self?.mainWindow.present(c, on: .root) + } + applicationContext.presentCrossfadeController = { [weak self] in + guard let strongSelf = self else { + return + } + var exists = false + strongSelf.mainWindow.forEachViewController { controller in + if controller is ThemeSettingsCrossfadeController { + exists = true + } + return true + } + + if !exists { + mainWindow.present(ThemeSettingsCrossfadeController(), on: .root) + } + } + + applicationContext.navigateToCurrentCall = { [weak self] in + if let strongSelf = self, let callController = strongSelf.callController { + if callController.isNodeLoaded && callController.view.superview == nil { + strongSelf.rootController.view.endEditing(true) + strongSelf.mainWindow.present(callController, on: .calls) + } + } + } + + presentImpl = { [weak self] c, _ in + self?.mainWindow.present(c, on: .root) + } + openSettingsImpl = { + applicationContext.applicationBindings.openSettings() + } + + let previousPasscodeState = Atomic(value: nil) + + let preferencesKey: PostboxViewKey = .preferences(keys: Set([ApplicationSpecificPreferencesKeys.presentationPasscodeSettings])) + + self.passcodeStatusDisposable.set((combineLatest(queue: Queue.mainQueue(), account.postbox.combinedView(keys: [.accessChallengeData, preferencesKey]), applicationContext.applicationBindings.applicationIsActive) + |> map { view, isActive -> (PostboxAccessChallengeData, PresentationPasscodeSettings?, Bool) in + let accessChallengeData = (view.views[.accessChallengeData] as? AccessChallengeDataView)?.data ?? PostboxAccessChallengeData.none + let passcodeSettings = (view.views[preferencesKey] as! PreferencesView).values[ApplicationSpecificPreferencesKeys.presentationPasscodeSettings] as? PresentationPasscodeSettings + return (accessChallengeData, passcodeSettings, isActive) + } + |> map { accessChallengeData, passcodeSettings, isActive -> PasscodeState in + return PasscodeState(isActive: isActive, challengeData: accessChallengeData, autolockTimeout: passcodeSettings?.autolockTimeout, enableBiometrics: passcodeSettings?.enableBiometrics ?? false) + }).start(next: { [weak self] updatedState in + guard let strongSelf = self else { + return + } + let previousState = previousPasscodeState.swap(updatedState) + + var updatedAutolockDeadline: Int32? + if updatedState.isActive != previousState?.isActive, let autolockTimeout = updatedState.autolockTimeout { + updatedAutolockDeadline = Int32(CFAbsoluteTimeGetCurrent()) + max(10, autolockTimeout) + } + + var effectiveAutolockDeadline = updatedState.challengeData.autolockDeadline + if updatedState.isActive { + } else if previousState != nil && previousState!.autolockTimeout != updatedState.autolockTimeout { + effectiveAutolockDeadline = updatedAutolockDeadline + } + + if let previousState = previousState, previousState.isActive, !updatedState.isActive, effectiveAutolockDeadline != 0 { + effectiveAutolockDeadline = updatedAutolockDeadline + } + + var isLocked = false + if isAccessLocked(data: updatedState.challengeData.withUpdatedAutolockDeadline(effectiveAutolockDeadline), at: Int32(CFAbsoluteTimeGetCurrent())) { + isLocked = true + updatedAutolockDeadline = 0 + } + + let isLockable: Bool + switch updatedState.challengeData { + case .none: + isLockable = false + default: + isLockable = true + } + + if previousState?.isActive != updatedState.isActive || isLocked != strongSelf.isLocked { + if updatedAutolockDeadline != previousState?.challengeData.autolockDeadline { + let _ = (account.postbox.transaction { transaction -> Void in + let data = transaction.getAccessChallengeData().withUpdatedAutolockDeadline(updatedAutolockDeadline) + transaction.setAccessChallengeData(data) + }).start() + } + + strongSelf.isLocked = isLocked + strongSelf.notificationManager.isApplicationLocked = isLocked + + if isLocked { + if updatedState.isActive { + if strongSelf.passcodeController == nil { + var attemptData: TGPasscodeEntryAttemptData? + if let attempts = updatedState.challengeData.attempts { + attemptData = TGPasscodeEntryAttemptData(numberOfInvalidAttempts: Int(attempts.count), dateOfLastInvalidAttempt: Double(attempts.timestamp)) + } + var mode: TGPasscodeEntryControllerMode + switch updatedState.challengeData { + case .none: + mode = TGPasscodeEntryControllerModeVerifySimple + case .numericalPassword: + mode = TGPasscodeEntryControllerModeVerifySimple + case .plaintextPassword: + mode = TGPasscodeEntryControllerModeVerifyComplex + } + let presentationData = strongSelf.account.telegramApplicationContext.currentPresentationData.with { $0 } + let presentAnimated = previousState != nil && previousState!.isActive + let legacyController = LegacyController(presentation: LegacyControllerPresentation.modal(animateIn: presentAnimated), theme: presentationData.theme) + let controller = TGPasscodeEntryController(context: legacyController.context, style: TGPasscodeEntryControllerStyleDefault, mode: mode, cancelEnabled: false, allowTouchId: updatedState.enableBiometrics, attemptData: attemptData, completion: { value in + if value != nil { + let _ = (account.postbox.transaction { transaction -> Void in + let data = transaction.getAccessChallengeData().withUpdatedAutolockDeadline(nil) + transaction.setAccessChallengeData(data) + }).start() + } + })! + controller.checkCurrentPasscode = { value in + if let value = value { + switch updatedState.challengeData { + 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 + strongSelf.passcodeController = legacyController + + strongSelf.unlockedStatePromise.set(.single(false)) + if presentAnimated { + legacyController.presentationCompleted = { + if let strongSelf = self { + strongSelf.rootController.view.isHidden = true + strongSelf.overlayMediaController.view.isHidden = true + strongSelf.notificationController.view.isHidden = true + } + } + } else { + strongSelf.rootController.view.isHidden = true + strongSelf.overlayMediaController.view.isHidden = true + strongSelf.notificationController.view.isHidden = true + } + + strongSelf.mainWindow.present(legacyController, on: .root) + + if !presentAnimated { + controller.refreshTouchId() + } + } else if previousState?.isActive != updatedState.isActive, updatedState.isActive, let passcodeController = strongSelf.passcodeController as? LegacyController { + if let controller = passcodeController.legacyController as? TGPasscodeEntryController { + controller.refreshTouchId() + } + } + strongSelf.updateCoveringViewSnaphot(false) + strongSelf.mainWindow.coveringView = nil + } else { + strongSelf.unlockedStatePromise.set(.single(false)) + strongSelf.updateCoveringViewSnaphot(true) + strongSelf.mainWindow.coveringView = strongSelf.lockedCoveringView + strongSelf.rootController.view.isHidden = true + strongSelf.overlayMediaController.view.isHidden = true + strongSelf.notificationController.view.isHidden = true + } + } else { + if !updatedState.isActive && updatedState.autolockTimeout != nil && isLockable { + strongSelf.updateCoveringViewSnaphot(true) + strongSelf.mainWindow.coveringView = strongSelf.lockedCoveringView + strongSelf.rootController.view.isHidden = true + strongSelf.overlayMediaController.view.isHidden = true + strongSelf.notificationController.view.isHidden = true + } else { + strongSelf.updateCoveringViewSnaphot(false) + strongSelf.mainWindow.coveringView = nil + strongSelf.rootController.view.isHidden = false + strongSelf.overlayMediaController.view.isHidden = false + strongSelf.notificationController.view.isHidden = false + if strongSelf.rootController.rootTabController == nil { + strongSelf.rootController.addRootControllers(showCallsTab: strongSelf.showCallsTab) + if let peerId = strongSelf.scheduledOperChatWithPeerId { + strongSelf.scheduledOperChatWithPeerId = nil + strongSelf.openChatWithPeerId(peerId: peerId) + } + + if let url = strongSelf.scheduledOpenExternalUrl { + strongSelf.scheduledOpenExternalUrl = nil + strongSelf.openUrl(url) + } + + DeviceAccess.authorizeAccess(to: .contacts, presentationData: strongSelf.account.telegramApplicationContext.currentPresentationData.with { $0 }, present: { c, a in + }, openSettings: { + }, { _ in + }) + + if #available(iOS 10.0, *) { + INPreferences.requestSiriAuthorization { _ in + } + } + + if #available(iOS 12.0, *) { + let userActivity = NSUserActivity(activityType: NSStringFromClass(INSendMessageIntent.self)) + userActivity.interaction?.donate(completion: { _ in + + }) + } + + if let passcodeController = strongSelf.passcodeController { + if let chatListController = strongSelf.rootController.chatListController { + let _ = chatListController.ready.get().start(next: { [weak passcodeController] _ in + if let strongSelf = self, let passcodeController = passcodeController, strongSelf.passcodeController === passcodeController { + strongSelf.passcodeController = nil + strongSelf.rootController.chatListController?.displayNode.recursivelyEnsureDisplaySynchronously(true) + passcodeController.dismiss() + } + }) + } else { + strongSelf.passcodeController = nil + strongSelf.rootController.chatListController?.displayNode.recursivelyEnsureDisplaySynchronously(true) + passcodeController.dismiss() + } + } + } else { + if let passcodeController = strongSelf.passcodeController { + strongSelf.passcodeController = nil + passcodeController.dismiss() + } + } + } + strongSelf.unlockedStatePromise.set(.single(true)) + } + }/* else if updatedAutolockDeadline != previousState?.challengeData.autolockDeadline { + let _ = (account.postbox.transaction { transaction -> Void in + let data = transaction.getAccessChallengeData().withUpdatedAutolockDeadline(updatedAutolockDeadline) + transaction.setAccessChallengeData(data) + }).start() + }*/ + })) + + let accountId = account.id + self.loggedOutDisposable.set(account.loggedOut.start(next: { value in + if value { + Logger.shared.log("ApplicationContext", "account logged out") + let _ = logoutFromAccount(id: accountId, accountManager: accountManager).start() + } + })) + + let inAppPreferencesKey = PostboxViewKey.preferences(keys: Set([ApplicationSpecificPreferencesKeys.inAppNotificationSettings])) + self.inAppNotificationSettingsDisposable.set(((account.postbox.combinedView(keys: [inAppPreferencesKey])) |> deliverOnMainQueue).start(next: { [weak self] views in + if let strongSelf = self { + if let view = views.views[inAppPreferencesKey] as? PreferencesView { + if let settings = view.values[ApplicationSpecificPreferencesKeys.inAppNotificationSettings] as? InAppNotificationSettings { + let previousSettings = strongSelf.inAppNotificationSettings + strongSelf.inAppNotificationSettings = settings + if let previousSettings = previousSettings, previousSettings.displayNameOnLockscreen != settings.displayNameOnLockscreen { + reinitializedNotificationSettings() + } + } + } + } + })) + + self.notificationMessagesDisposable.set((account.stateManager.notificationMessages |> deliverOn(Queue.mainQueue())).start(next: { [weak self] messageList in + if let strongSelf = self, let (messages, groupId, notify) = messageList.last, let firstMessage = messages.first { + if UIApplication.shared.applicationState == .active { + var chatIsVisible = false + if let topController = strongSelf.rootController.topViewController as? ChatController, topController.traceVisibility() { + if case .peer(firstMessage.id.peerId) = topController.chatLocation { + chatIsVisible = true + } else if case let .group(topGroupId) = topController.chatLocation, topGroupId == groupId { + chatIsVisible = true + } + } + + if !notify { + chatIsVisible = true + } + + if !chatIsVisible { + strongSelf.mainWindow.forEachViewController({ controller in + if let controller = controller as? ChatController, case .peer(firstMessage.id.peerId) = controller.chatLocation { + chatIsVisible = true + return false + } + return true + }) + } + + let inAppNotificationSettings: InAppNotificationSettings + if let current = strongSelf.inAppNotificationSettings { + inAppNotificationSettings = current + } else { + inAppNotificationSettings = InAppNotificationSettings.defaultSettings + } + + if !strongSelf.isLocked { + if inAppNotificationSettings.playSounds { + serviceSoundManager.playIncomingMessageSound() + } + if inAppNotificationSettings.vibrate { + serviceSoundManager.playVibrationSound() + } + } + + if chatIsVisible { + return + } + + if inAppNotificationSettings.displayPreviews { + let presentationData = strongSelf.applicationContext.currentPresentationData.with { $0 } + strongSelf.notificationController.enqueue(ChatMessageNotificationItem(account: strongSelf.account, strings: presentationData.strings, messages: messages, tapAction: { + if let strongSelf = self { + var foundOverlay = false + strongSelf.mainWindow.forEachViewController({ controller in + if isOverlayControllerForChatNotificationOverlayPresentation(controller) { + foundOverlay = true + return false + } + return true + }) + + if foundOverlay { + return true + } + + if let topController = strongSelf.rootController.topViewController as? ViewController, isInlineControllerForChatNotificationOverlayPresentation(topController) { + return true + } + + if let topController = strongSelf.rootController.topViewController as? ChatController, case .peer(firstMessage.id.peerId) = topController.chatLocation { + strongSelf.notificationController.removeItemsWithGroupingKey(firstMessage.id.peerId) + + return false + } + + for controller in strongSelf.rootController.viewControllers { + if let controller = controller as? ChatController, case .peer(firstMessage.id.peerId) = controller.chatLocation { + return true + } + } + + strongSelf.notificationController.removeItemsWithGroupingKey(firstMessage.id.peerId) + + navigateToChatController(navigationController: strongSelf.rootController, account: strongSelf.account, chatLocation: .peer(firstMessage.id.peerId)) + } + return false + }, expandAction: { expandData in + if let strongSelf = self { + let chatController = ChatController(account: strongSelf.account, chatLocation: .peer(firstMessage.id.peerId), mode: .overlay) + (strongSelf.rootController.viewControllers.last as? ViewController)?.present(chatController, in: .window(.root), with: ChatControllerOverlayPresentationData(expandData: expandData())) + } + })) + } + } + } + })) + + self.termsOfServiceUpdatesDisposable.set((account.stateManager.termsOfServiceUpdate + |> deliverOnMainQueue).start(next: { [weak self] termsOfServiceUpdate in + guard let strongSelf = self else { + return + } + + if strongSelf.currentTermsOfServiceUpdate == termsOfServiceUpdate { + return + } + + strongSelf.currentTermsOfServiceUpdate = termsOfServiceUpdate + strongSelf.currentTermsOfServiceUpdateController?.dismiss() + strongSelf.currentTermsOfServiceUpdateController = nil + if let termsOfServiceUpdate = termsOfServiceUpdate { + let presentationData = strongSelf.applicationContext.currentPresentationData.with { $0 } + var acceptImpl: ((String?) -> Void)? + var declineImpl: (() -> Void)? + let controller = TermsOfServiceController(theme: TermsOfServiceControllerTheme(presentationTheme: presentationData.theme), strings: presentationData.strings, text: termsOfServiceUpdate.text, entities: termsOfServiceUpdate.entities, ageConfirmation: termsOfServiceUpdate.ageConfirmation, signingUp: false, accept: { proccedBot in + acceptImpl?(proccedBot) + }, decline: { + declineImpl?() + }, openUrl: { url in + if let parsedUrl = URL(string: url) { + UIApplication.shared.openURL(parsedUrl) + } + }) + + acceptImpl = { [weak controller] botName in + controller?.inProgress = true + guard let strongSelf = self else { + return + } + let _ = (acceptTermsOfService(account: strongSelf.account, id: termsOfServiceUpdate.id) + |> deliverOnMainQueue).start(completed: { + controller?.dismiss() + if let botName = botName { + self?.proccedTOSBotDisposable.set((resolvePeerByName(account: account, name: botName, ageLimit: 10) |> take(1) |> deliverOnMainQueue).start(next: { peerId in + if let peerId = peerId { + self?.rootController.pushViewController(ChatController(account: account, chatLocation: .peer(peerId), messageId: nil)) + } + })) + } + }) + } + + declineImpl = { + guard let strongSelf = self else { + return + } + let _ = (strongSelf.account.postbox.loadedPeerWithId(strongSelf.account.peerId) + |> deliverOnMainQueue).start(next: { peer in + if let phone = (peer as? TelegramUser)?.phone { + UIApplication.shared.openURL(URL(string: "https://telegram.org/deactivate?phone=\(phone)")!) + } + }) + } + + (strongSelf.rootController.viewControllers.last as? ViewController)?.present(controller, in: .window(.root)) + } + })) + + self.displayAlertsDisposable = (account.stateManager.displayAlerts |> deliverOnMainQueue).start(next: { [weak self] alerts in + if let strongSelf = self{ + for text in alerts { + let presentationData = strongSelf.applicationContext.currentPresentationData.with { $0 } + let controller = standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]) + (strongSelf.rootController.viewControllers.last as? ViewController)?.present(controller, in: .window(.root)) + } + } + }) + + self.removeNotificationsDisposable = (account.stateManager.appliedIncomingReadMessages + |> deliverOnMainQueue).start(next: { [weak self] ids in + if let strongSelf = self { + strongSelf.applicationContext.applicationBindings.clearMessageNotifications(ids) + } + }) + + self.callDisposable = (callManager.currentCallSignal + |> deliverOnMainQueue).start(next: { [weak self] call in + if let strongSelf = self { + if call !== strongSelf.callController?.call { + strongSelf.callController?.dismiss() + strongSelf.callController = nil + strongSelf.hasOngoingCall.set(false) + + if let call = call { + let callController = CallController(account: account, call: call) + strongSelf.callController = callController + strongSelf.rootController.view?.endEditing(true) + strongSelf.mainWindow.present(callController, on: .calls) + strongSelf.callState.set(call.state + |> map(Optional.init)) + strongSelf.hasOngoingCall.set(true) + strongSelf.notificationManager.notificationCall = call + } else { + strongSelf.callState.set(.single(nil)) + strongSelf.hasOngoingCall.set(false) + strongSelf.notificationManager.notificationCall = nil + } + } + } + }) + + self.callStateDisposable = (self.callState.get() + |> deliverOnMainQueue).start(next: { [weak self] state in + if let strongSelf = self { + let resolvedText: CallStatusText + if let state = state { + switch state { + case .connecting, .requesting, .terminating, .ringing, .waiting: + resolvedText = .inProgress(nil) + case .terminated: + resolvedText = .none + case let .active(timestamp, _): + resolvedText = .inProgress(timestamp) + } + } else { + resolvedText = .none + } + + if strongSelf.currentCallStatusText != resolvedText { + strongSelf.currentCallStatusText = resolvedText + + var referenceTimestamp: Double? + if case let .inProgress(timestamp) = resolvedText, let concreteTimestamp = timestamp { + referenceTimestamp = concreteTimestamp + } + + if let _ = referenceTimestamp { + if strongSelf.currentCallStatusTextTimer == nil { + let timer = SwiftSignalKit.Timer(timeout: 0.5, repeat: true, completion: { + if let strongSelf = self { + strongSelf.updateStatusBarText() + } + }, queue: Queue.mainQueue()) + strongSelf.currentCallStatusTextTimer = timer + timer.start() + } + } else { + strongSelf.currentCallStatusTextTimer?.invalidate() + strongSelf.currentCallStatusTextTimer = nil + } + + strongSelf.updateStatusBarText() + } + } + }) + + self.account.resetStateManagement() + let contactSynchronizationPreferencesKey = PostboxViewKey.preferences(keys: Set([ApplicationSpecificPreferencesKeys.contactSynchronizationSettings])) + + let importableContacts = self.applicationContext.contactDataManager.importable() + self.account.importableContacts.set(self.account.postbox.combinedView(keys: [contactSynchronizationPreferencesKey]) + |> mapToSignal { preferences -> Signal<[DeviceContactNormalizedPhoneNumber: ImportableDeviceContactData], NoError> in + let settings: ContactSynchronizationSettings = ((preferences.views[contactSynchronizationPreferencesKey] as? PreferencesView)?.values[ApplicationSpecificPreferencesKeys.contactSynchronizationSettings] as? ContactSynchronizationSettings) ?? .defaultSettings + if settings.synchronizeDeviceContacts { + return importableContacts + } else { + return .single([:]) + } + }) + + let previousTheme = Atomic(value: nil) + self.presentationDataDisposable = (applicationContext.presentationData + |> deliverOnMainQueue).start(next: { [weak self] presentationData in + if let strongSelf = self { + if previousTheme.swap(presentationData.theme) !== presentationData.theme { + strongSelf.mainWindow.previewThemeAccentColor = presentationData.theme.rootController.navigationBar.accentTextColor + strongSelf.mainWindow.previewThemeDarkBlur = presentationData.theme.chatList.searchBarKeyboardColor == .dark + strongSelf.lockedCoveringView.updateTheme(presentationData.theme) + strongSelf.rootController.updateTheme(NavigationControllerTheme(presentationTheme: presentationData.theme)) + } + } + }) + + let showCallsTabSignal = account.postbox.preferencesView(keys: [ApplicationSpecificPreferencesKeys.callListSettings]) + |> map { view -> Bool in + var value = true + if let settings = view.values[ApplicationSpecificPreferencesKeys.callListSettings] as? CallListSettings { + value = settings.showTab + } + return value + } + self.showCallsTabDisposable = (showCallsTabSignal |> deliverOnMainQueue).start(next: { [weak self] value in + if let strongSelf = self { + if strongSelf.showCallsTab != value { + strongSelf.showCallsTab = value + strongSelf.rootController.updateRootControllers(showCallsTab: value) + } + } + }) + + let _ = (watchManagerArguments |> deliverOnMainQueue).start(next: { [weak self] arguments in + guard let strongSelf = self else { + return + } + + let watchManager = WatchManager(arguments: arguments) + strongSelf.applicationContext.watchManager = watchManager + runningWatchTasksPromise.set(watchManager.runningTasks) + + strongSelf.watchNavigateToMessageDisposable.set((strongSelf.applicationContext.applicationBindings.applicationInForeground |> mapToSignal({ applicationInForeground -> Signal<(Bool, MessageId), NoError> in + return watchManager.navigateToMessageRequested + |> map { messageId in + return (applicationInForeground, messageId) + } + |> deliverOnMainQueue + })).start(next: { [weak self] applicationInForeground, messageId in + if let strongSelf = self { + if applicationInForeground { + var chatIsVisible = false + if let controller = strongSelf.rootController.viewControllers.last as? ChatController, case .peer(messageId.peerId) = controller.chatLocation { + chatIsVisible = true + } + + let navigateToMessage = { + navigateToChatController(navigationController: strongSelf.rootController, account: strongSelf.account, chatLocation: .peer(messageId.peerId), messageId: messageId) + } + + if chatIsVisible { + navigateToMessage() + } else { + let presentationData = strongSelf.applicationContext.currentPresentationData.with { $0 } + let controller = standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: presentationData.strings.WatchRemote_AlertTitle, text: presentationData.strings.WatchRemote_AlertText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.WatchRemote_AlertOpen, action:navigateToMessage)]) + (strongSelf.rootController.viewControllers.last as? ViewController)?.present(controller, in: .window(.root)) + } + } else { + strongSelf.notificationManager.presentWatchContinuityNotification(messageId: messageId) + } + } + })) + }) + } + + private func updateStatusBarText() { + if case let .inProgress(timestamp) = self.currentCallStatusText { + let text: String + let presentationData = self.applicationContext.currentPresentationData.with { $0 } + if let timestamp = timestamp { + let duration = Int32(CFAbsoluteTimeGetCurrent() - timestamp) + let durationString: String + if duration > 60 * 60 { + durationString = String(format: "%02d:%02d:%02d", arguments: [duration / 3600, (duration / 60) % 60, duration % 60]) + } else { + durationString = String(format: "%02d:%02d", arguments: [(duration / 60) % 60, duration % 60]) + } + + text = presentationData.strings.Call_StatusBar(durationString).0 + } else { + text = presentationData.strings.Call_StatusBar("").0 + } + + self.mainWindow.setForceInCallStatusBar(text) + } else { + self.mainWindow.setForceInCallStatusBar(nil) + } + } + + deinit { + self.account.postbox.clearCaches() + self.account.shouldKeepOnlinePresence.set(.single(false)) + self.account.shouldBeServiceTaskMaster.set(.single(.never)) + self.loggedOutDisposable.dispose() + self.inAppNotificationSettingsDisposable.dispose() + self.notificationMessagesDisposable.dispose() + self.termsOfServiceUpdatesDisposable.dispose() + self.passcodeLockDisposable.dispose() + self.passcodeStatusDisposable.dispose() + self.displayAlertsDisposable?.dispose() + self.removeNotificationsDisposable?.dispose() + self.callDisposable?.dispose() + self.callStateDisposable?.dispose() + self.currentCallStatusTextTimer?.invalidate() + self.presentationDataDisposable?.dispose() + self.enablePostboxTransactionsDiposable?.dispose() + self.proccedTOSBotDisposable.dispose() + self.watchNavigateToMessageDisposable.dispose() + } + + func openChatWithPeerId(peerId: PeerId, messageId: MessageId? = nil) { + var visiblePeerId: PeerId? + if let controller = self.rootController.topViewController as? ChatController, case let .peer(peerId) = controller.chatLocation { + visiblePeerId = peerId + } + + if visiblePeerId != peerId || messageId != nil { + if self.rootController.rootTabController != nil { + navigateToChatController(navigationController: self.rootController, account: self.account, chatLocation: .peer(peerId), messageId: messageId) + } else { + self.scheduledOperChatWithPeerId = peerId + } + } + } + + func openUrl(_ url: URL) { + if self.rootController.rootTabController != nil { + let presentationData = self.account.telegramApplicationContext.currentPresentationData.with { $0 } + openExternalUrl(account: self.account, url: url.absoluteString, presentationData: presentationData, applicationContext: self.applicationContext, navigationController: self.rootController, dismissInput: { [weak self] in + self?.rootController.view.endEditing(true) + }) + } else { + self.scheduledOpenExternalUrl = url + } + } + + func openRootSearch() { + self.rootController.openChatsSearch() + } + + func openRootCompose() { + self.rootController.openRootCompose() + } + + func openRootCamera() { + self.rootController.openRootCamera() + } + + private func updateCoveringViewSnaphot(_ visible: Bool) { + if visible { + let scale: CGFloat = 0.5 + let unscaledSize = self.mainWindow.hostView.containerView.frame.size + let image = generateImage(CGSize(width: floor(unscaledSize.width * scale), height: floor(unscaledSize.height * scale)), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.scaleBy(x: scale, y: scale) + UIGraphicsPushContext(context) + self.mainWindow.hostView.containerView.drawHierarchy(in: CGRect(origin: CGPoint(), size: unscaledSize), afterScreenUpdates: false) + UIGraphicsPopContext() + })?.applyScreenshotEffect() + self.lockedCoveringView.updateSnapshot(image) + } else { + self.lockedCoveringView.updateSnapshot(nil) + } + } +} diff --git a/Telegram-iOS/ApplicationShortcutItem.swift b/Telegram-iOS/ApplicationShortcutItem.swift new file mode 100644 index 0000000000..d49f139e1c --- /dev/null +++ b/Telegram-iOS/ApplicationShortcutItem.swift @@ -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) + ] +} diff --git a/Telegram-iOS/Base.lproj/LaunchScreen.xib b/Telegram-iOS/Base.lproj/LaunchScreen.xib new file mode 100644 index 0000000000..9584dae552 --- /dev/null +++ b/Telegram-iOS/Base.lproj/LaunchScreen.xib @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/Telegram-iOS/BuildConfig.h b/Telegram-iOS/BuildConfig.h new file mode 100644 index 0000000000..8c298560f1 --- /dev/null +++ b/Telegram-iOS/BuildConfig.h @@ -0,0 +1,14 @@ +#import + +@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 diff --git a/Telegram-iOS/BuildConfig.m b/Telegram-iOS/BuildConfig.m new file mode 100644 index 0000000000..5fb9218805 --- /dev/null +++ b/Telegram-iOS/BuildConfig.m @@ -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 diff --git a/Telegram-iOS/ClearNotificationsManager.swift b/Telegram-iOS/ClearNotificationsManager.swift new file mode 100644 index 0000000000..ef81a3c92f --- /dev/null +++ b/Telegram-iOS/ClearNotificationsManager.swift @@ -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) + } + } + }) + } +} diff --git a/Telegram-iOS/Config-AppStore.xcconfig b/Telegram-iOS/Config-AppStore.xcconfig new file mode 100644 index 0000000000..53d62913df --- /dev/null +++ b/Telegram-iOS/Config-AppStore.xcconfig @@ -0,0 +1 @@ +#include "../../Telegram-iOS-Shared/Config-AppStore.xcconfig" \ No newline at end of file diff --git a/Telegram-iOS/Config-AppStoreLLC.xcconfig b/Telegram-iOS/Config-AppStoreLLC.xcconfig new file mode 100644 index 0000000000..ddba9e8ff5 --- /dev/null +++ b/Telegram-iOS/Config-AppStoreLLC.xcconfig @@ -0,0 +1 @@ +#include "../../Telegram-iOS-Shared/Config-AppStoreLLC.xcconfig" \ No newline at end of file diff --git a/Telegram-iOS/Config-Hockeyapp Internal.xcconfig b/Telegram-iOS/Config-Hockeyapp Internal.xcconfig new file mode 100644 index 0000000000..3f8c245107 --- /dev/null +++ b/Telegram-iOS/Config-Hockeyapp Internal.xcconfig @@ -0,0 +1 @@ +#include "../../Telegram-iOS-Shared/Config-Hockeyapp Internal.xcconfig" \ No newline at end of file diff --git a/Telegram-iOS/Config-Hockeyapp.xcconfig b/Telegram-iOS/Config-Hockeyapp.xcconfig new file mode 100644 index 0000000000..7d6755f1f2 --- /dev/null +++ b/Telegram-iOS/Config-Hockeyapp.xcconfig @@ -0,0 +1 @@ +#include "../../Telegram-iOS-Shared/Config-Hockeyapp.xcconfig" \ No newline at end of file diff --git a/Telegram-iOS/Images.xcassets/AppIcon.appiconset/Contents.json b/Telegram-iOS/Images.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..b1dd0aaddc --- /dev/null +++ b/Telegram-iOS/Images.xcassets/AppIcon.appiconset/Contents.json @@ -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" + } +} \ No newline at end of file diff --git a/Telegram-iOS/Images.xcassets/AppIcon.appiconset/icon@1024px.png b/Telegram-iOS/Images.xcassets/AppIcon.appiconset/icon@1024px.png new file mode 100644 index 0000000000..92875c6983 Binary files /dev/null and b/Telegram-iOS/Images.xcassets/AppIcon.appiconset/icon@1024px.png differ diff --git a/Telegram-iOS/Images.xcassets/AppIcon.appiconset/icon@120px.png b/Telegram-iOS/Images.xcassets/AppIcon.appiconset/icon@120px.png new file mode 100644 index 0000000000..5e863192c3 Binary files /dev/null and b/Telegram-iOS/Images.xcassets/AppIcon.appiconset/icon@120px.png differ diff --git a/Telegram-iOS/Images.xcassets/AppIcon.appiconset/icon@152px.png b/Telegram-iOS/Images.xcassets/AppIcon.appiconset/icon@152px.png new file mode 100644 index 0000000000..733e14018b Binary files /dev/null and b/Telegram-iOS/Images.xcassets/AppIcon.appiconset/icon@152px.png differ diff --git a/Telegram-iOS/Images.xcassets/AppIcon.appiconset/icon@167px.png b/Telegram-iOS/Images.xcassets/AppIcon.appiconset/icon@167px.png new file mode 100644 index 0000000000..6ae90ed3cd Binary files /dev/null and b/Telegram-iOS/Images.xcassets/AppIcon.appiconset/icon@167px.png differ diff --git a/Telegram-iOS/Images.xcassets/AppIcon.appiconset/icon@180px.png b/Telegram-iOS/Images.xcassets/AppIcon.appiconset/icon@180px.png new file mode 100644 index 0000000000..715eb5d04a Binary files /dev/null and b/Telegram-iOS/Images.xcassets/AppIcon.appiconset/icon@180px.png differ diff --git a/Telegram-iOS/Images.xcassets/AppIcon.appiconset/icon@76px.png b/Telegram-iOS/Images.xcassets/AppIcon.appiconset/icon@76px.png new file mode 100644 index 0000000000..6473725462 Binary files /dev/null and b/Telegram-iOS/Images.xcassets/AppIcon.appiconset/icon@76px.png differ diff --git a/Telegram-iOS/Images.xcassets/AppIconLLC.appiconset/Contents.json b/Telegram-iOS/Images.xcassets/AppIconLLC.appiconset/Contents.json new file mode 100644 index 0000000000..e76619997a --- /dev/null +++ b/Telegram-iOS/Images.xcassets/AppIconLLC.appiconset/Contents.json @@ -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" + } +} \ No newline at end of file diff --git a/Telegram-iOS/Images.xcassets/AppIconLLC.appiconset/Icon-120.png b/Telegram-iOS/Images.xcassets/AppIconLLC.appiconset/Icon-120.png new file mode 100644 index 0000000000..b2272bdc09 Binary files /dev/null and b/Telegram-iOS/Images.xcassets/AppIconLLC.appiconset/Icon-120.png differ diff --git a/Telegram-iOS/Images.xcassets/AppIconLLC.appiconset/Icon-167.png b/Telegram-iOS/Images.xcassets/AppIconLLC.appiconset/Icon-167.png new file mode 100644 index 0000000000..3e4f4e5ac3 Binary files /dev/null and b/Telegram-iOS/Images.xcassets/AppIconLLC.appiconset/Icon-167.png differ diff --git a/Telegram-iOS/Images.xcassets/AppIconLLC.appiconset/Icon-29@1x.png b/Telegram-iOS/Images.xcassets/AppIconLLC.appiconset/Icon-29@1x.png new file mode 100644 index 0000000000..e5ebd6703a Binary files /dev/null and b/Telegram-iOS/Images.xcassets/AppIconLLC.appiconset/Icon-29@1x.png differ diff --git a/Telegram-iOS/Images.xcassets/AppIconLLC.appiconset/Icon-40.png b/Telegram-iOS/Images.xcassets/AppIconLLC.appiconset/Icon-40.png new file mode 100644 index 0000000000..ee4e8356d7 Binary files /dev/null and b/Telegram-iOS/Images.xcassets/AppIconLLC.appiconset/Icon-40.png differ diff --git a/Telegram-iOS/Images.xcassets/AppIconLLC.appiconset/Icon-40@2x-1.png b/Telegram-iOS/Images.xcassets/AppIconLLC.appiconset/Icon-40@2x-1.png new file mode 100644 index 0000000000..9dab72dd6b Binary files /dev/null and b/Telegram-iOS/Images.xcassets/AppIconLLC.appiconset/Icon-40@2x-1.png differ diff --git a/Telegram-iOS/Images.xcassets/AppIconLLC.appiconset/Icon-40@2x.png b/Telegram-iOS/Images.xcassets/AppIconLLC.appiconset/Icon-40@2x.png new file mode 100644 index 0000000000..9dab72dd6b Binary files /dev/null and b/Telegram-iOS/Images.xcassets/AppIconLLC.appiconset/Icon-40@2x.png differ diff --git a/Telegram-iOS/Images.xcassets/AppIconLLC.appiconset/Icon-40@3x.png b/Telegram-iOS/Images.xcassets/AppIconLLC.appiconset/Icon-40@3x.png new file mode 100644 index 0000000000..3ac90f1959 Binary files /dev/null and b/Telegram-iOS/Images.xcassets/AppIconLLC.appiconset/Icon-40@3x.png differ diff --git a/Telegram-iOS/Images.xcassets/AppIconLLC.appiconset/Icon-41.png b/Telegram-iOS/Images.xcassets/AppIconLLC.appiconset/Icon-41.png new file mode 100644 index 0000000000..ee4e8356d7 Binary files /dev/null and b/Telegram-iOS/Images.xcassets/AppIconLLC.appiconset/Icon-41.png differ diff --git a/Telegram-iOS/Images.xcassets/AppIconLLC.appiconset/Icon-42.png b/Telegram-iOS/Images.xcassets/AppIconLLC.appiconset/Icon-42.png new file mode 100644 index 0000000000..ee4e8356d7 Binary files /dev/null and b/Telegram-iOS/Images.xcassets/AppIconLLC.appiconset/Icon-42.png differ diff --git a/Telegram-iOS/Images.xcassets/AppIconLLC.appiconset/Icon-60@2x.png b/Telegram-iOS/Images.xcassets/AppIconLLC.appiconset/Icon-60@2x.png new file mode 100644 index 0000000000..ca5ede8209 Binary files /dev/null and b/Telegram-iOS/Images.xcassets/AppIconLLC.appiconset/Icon-60@2x.png differ diff --git a/Telegram-iOS/Images.xcassets/AppIconLLC.appiconset/Icon-60@3x.png b/Telegram-iOS/Images.xcassets/AppIconLLC.appiconset/Icon-60@3x.png new file mode 100644 index 0000000000..6f25ca8c71 Binary files /dev/null and b/Telegram-iOS/Images.xcassets/AppIconLLC.appiconset/Icon-60@3x.png differ diff --git a/Telegram-iOS/Images.xcassets/AppIconLLC.appiconset/Icon-76.png b/Telegram-iOS/Images.xcassets/AppIconLLC.appiconset/Icon-76.png new file mode 100644 index 0000000000..69c2c7541d Binary files /dev/null and b/Telegram-iOS/Images.xcassets/AppIconLLC.appiconset/Icon-76.png differ diff --git a/Telegram-iOS/Images.xcassets/AppIconLLC.appiconset/Icon-76@2x.png b/Telegram-iOS/Images.xcassets/AppIconLLC.appiconset/Icon-76@2x.png new file mode 100644 index 0000000000..d1c7a253a3 Binary files /dev/null and b/Telegram-iOS/Images.xcassets/AppIconLLC.appiconset/Icon-76@2x.png differ diff --git a/Telegram-iOS/Images.xcassets/AppIconLLC.appiconset/Icon-87.png b/Telegram-iOS/Images.xcassets/AppIconLLC.appiconset/Icon-87.png new file mode 100644 index 0000000000..c0bf19fc1d Binary files /dev/null and b/Telegram-iOS/Images.xcassets/AppIconLLC.appiconset/Icon-87.png differ diff --git a/Telegram-iOS/Images.xcassets/AppIconLLC.appiconset/Icon-Small@0s.png b/Telegram-iOS/Images.xcassets/AppIconLLC.appiconset/Icon-Small@0s.png new file mode 100644 index 0000000000..bfc16a04f5 Binary files /dev/null and b/Telegram-iOS/Images.xcassets/AppIconLLC.appiconset/Icon-Small@0s.png differ diff --git a/Telegram-iOS/Images.xcassets/AppIconLLC.appiconset/Icon-Small@2x-1.png b/Telegram-iOS/Images.xcassets/AppIconLLC.appiconset/Icon-Small@2x-1.png new file mode 100644 index 0000000000..e515faf6d6 Binary files /dev/null and b/Telegram-iOS/Images.xcassets/AppIconLLC.appiconset/Icon-Small@2x-1.png differ diff --git a/Telegram-iOS/Images.xcassets/AppIconLLC.appiconset/Icon-Small@2x.png b/Telegram-iOS/Images.xcassets/AppIconLLC.appiconset/Icon-Small@2x.png new file mode 100644 index 0000000000..e515faf6d6 Binary files /dev/null and b/Telegram-iOS/Images.xcassets/AppIconLLC.appiconset/Icon-Small@2x.png differ diff --git a/Telegram-iOS/Images.xcassets/AppIconLLC.appiconset/T-Ipad_1024.png b/Telegram-iOS/Images.xcassets/AppIconLLC.appiconset/T-Ipad_1024.png new file mode 100644 index 0000000000..5441472f3a Binary files /dev/null and b/Telegram-iOS/Images.xcassets/AppIconLLC.appiconset/T-Ipad_1024.png differ diff --git a/Telegram-iOS/Images.xcassets/Contents.json b/Telegram-iOS/Images.xcassets/Contents.json new file mode 100644 index 0000000000..da4a164c91 --- /dev/null +++ b/Telegram-iOS/Images.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-iOS/Images.xcassets/Shortcuts/Contents.json b/Telegram-iOS/Images.xcassets/Shortcuts/Contents.json new file mode 100644 index 0000000000..38f0c81fc2 --- /dev/null +++ b/Telegram-iOS/Images.xcassets/Shortcuts/Contents.json @@ -0,0 +1,9 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "provides-namespace" : true + } +} \ No newline at end of file diff --git a/Telegram-iOS/Images.xcassets/Shortcuts/SavedMessages.imageset/Contents.json b/Telegram-iOS/Images.xcassets/Shortcuts/SavedMessages.imageset/Contents.json new file mode 100644 index 0000000000..97518aad3a --- /dev/null +++ b/Telegram-iOS/Images.xcassets/Shortcuts/SavedMessages.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "SavedMessagesIcon@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "SavedMessagesIcon@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-iOS/Images.xcassets/Shortcuts/SavedMessages.imageset/SavedMessagesIcon@2x.png b/Telegram-iOS/Images.xcassets/Shortcuts/SavedMessages.imageset/SavedMessagesIcon@2x.png new file mode 100644 index 0000000000..de460c24ee Binary files /dev/null and b/Telegram-iOS/Images.xcassets/Shortcuts/SavedMessages.imageset/SavedMessagesIcon@2x.png differ diff --git a/Telegram-iOS/Images.xcassets/Shortcuts/SavedMessages.imageset/SavedMessagesIcon@3x.png b/Telegram-iOS/Images.xcassets/Shortcuts/SavedMessages.imageset/SavedMessagesIcon@3x.png new file mode 100644 index 0000000000..96a7dca662 Binary files /dev/null and b/Telegram-iOS/Images.xcassets/Shortcuts/SavedMessages.imageset/SavedMessagesIcon@3x.png differ diff --git a/Telegram-iOS/Info.plist b/Telegram-iOS/Info.plist new file mode 100644 index 0000000000..e867634fdf --- /dev/null +++ b/Telegram-iOS/Info.plist @@ -0,0 +1,165 @@ + + + + + CFBundleAllowMixedLocalizations + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + ${APP_NAME} + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 5.0.17 + CFBundleSignature + ???? + CFBundleURLTypes + + + CFBundleTypeRole + Viewer + CFBundleURLName + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleURLSchemes + + telegram + + + + CFBundleTypeRole + Editor + CFBundleURLName + $(PRODUCT_BUNDLE_IDENTIFIER).compatibility + CFBundleURLSchemes + + tg + + + + CFBundleTypeRole + Viewer + CFBundleURLName + $(PRODUCT_BUNDLE_IDENTIFIER).dropbox + CFBundleURLSchemes + + db-pa9wtoz9l514anx + + + + CFBundleVersion + 624 + ITSAppUsesNonExemptEncryption + + LSApplicationQueriesSchemes + + dbapi-3 + instagram + googledrive + comgooglemaps-x-callback + foursquare + here-location + yandexmaps + yandexnavi + comgooglemaps + youtube + twitter + vk + waze + googlechrome + googlechromes + firefox + opera-http + opera-https + yandexbrowser-open-url + vimeo + vine + coub + uber + citymapper + lyft + + LSRequiresIPhoneOS + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + NSCameraUsageDescription + We need this so that you can take and share photos and videos. + NSContactsUsageDescription + Telegram stores your contacts heavily encrypted in the cloud to let you connect with your friends across all your devices. + NSFaceIDUsageDescription + You can use Face ID to unlock the app. + NSLocationAlwaysUsageDescription + When you send your location to your friends, Telegram needs access to show them a map. You also need this to send locations from an Apple Watch. + NSLocationWhenInUseUsageDescription + When you send your location to your friends, Telegram needs access to show them a map. + NSMicrophoneUsageDescription + We need this so that you can record and share voice messages and videos with sound. + NSMotionUsageDescription + When you send your location to your friends, Telegram needs access to show them a map. + NSPhotoLibraryAddUsageDescription + We need this so that you can share photos and videos from your photo library. + NSPhotoLibraryUsageDescription + We need this so that you can share photos and videos from your photo library. + NSSiriUsageDescription + You can use Siri to send messages. + NSUserActivityTypes + + INSendMessageIntent + + UIAppFonts + + SFCompactRounded-Semibold.otf + + UIBackgroundModes + + audio + fetch + location + remote-notification + voip + + UIFileSharingEnabled + + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + + armv7 + + UIRequiresPersistentWiFi + + UIStatusBarStyle + UIStatusBarStyleDefault + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + UIViewEdgeAntialiasing + + UIViewGroupOpacity + + + diff --git a/Telegram-iOS/LegacyChatImport.swift b/Telegram-iOS/LegacyChatImport.swift new file mode 100644 index 0000000000..5053d59fb6 --- /dev/null +++ b/Telegram-iOS/LegacyChatImport.swift @@ -0,0 +1,790 @@ +import Foundation +import TelegramCore +import SwiftSignalKit +import Postbox +import TelegramUI + +import LegacyComponents + +private let reportedLayer_hash: Int32 = -717538193 +private let layer_hash: Int32 = 849537378 +private let seq_out_hash: Int32 = -737765753 +private let seq_in_hash: Int32 = -7646011 + +private let defaultPrime: Data = { + let bytes: [UInt8] = [ + 0xc7, 0x1c, 0xae, 0xb9, 0xc6, 0xb1, 0xc9, 0x04, 0x8e, 0x6c, 0x52, 0x2f, + 0x70, 0xf1, 0x3f, 0x73, 0x98, 0x0d, 0x40, 0x23, 0x8e, 0x3e, 0x21, 0xc1, + 0x49, 0x34, 0xd0, 0x37, 0x56, 0x3d, 0x93, 0x0f, 0x48, 0x19, 0x8a, 0x0a, + 0xa7, 0xc1, 0x40, 0x58, 0x22, 0x94, 0x93, 0xd2, 0x25, 0x30, 0xf4, 0xdb, + 0xfa, 0x33, 0x6f, 0x6e, 0x0a, 0xc9, 0x25, 0x13, 0x95, 0x43, 0xae, 0xd4, + 0x4c, 0xce, 0x7c, 0x37, 0x20, 0xfd, 0x51, 0xf6, 0x94, 0x58, 0x70, 0x5a, + 0xc6, 0x8c, 0xd4, 0xfe, 0x6b, 0x6b, 0x13, 0xab, 0xdc, 0x97, 0x46, 0x51, + 0x29, 0x69, 0x32, 0x84, 0x54, 0xf1, 0x8f, 0xaf, 0x8c, 0x59, 0x5f, 0x64, + 0x24, 0x77, 0xfe, 0x96, 0xbb, 0x2a, 0x94, 0x1d, 0x5b, 0xcd, 0x1d, 0x4a, + 0xc8, 0xcc, 0x49, 0x88, 0x07, 0x08, 0xfa, 0x9b, 0x37, 0x8e, 0x3c, 0x4f, + 0x3a, 0x90, 0x60, 0xbe, 0xe6, 0x7c, 0xf9, 0xa4, 0xa4, 0xa6, 0x95, 0x81, + 0x10, 0x51, 0x90, 0x7e, 0x16, 0x27, 0x53, 0xb5, 0x6b, 0x0f, 0x6b, 0x41, + 0x0d, 0xba, 0x74, 0xd8, 0xa8, 0x4b, 0x2a, 0x14, 0xb3, 0x14, 0x4e, 0x0e, + 0xf1, 0x28, 0x47, 0x54, 0xfd, 0x17, 0xed, 0x95, 0x0d, 0x59, 0x65, 0xb4, + 0xb9, 0xdd, 0x46, 0x58, 0x2d, 0xb1, 0x17, 0x8d, 0x16, 0x9c, 0x6b, 0xc4, + 0x65, 0xb0, 0xd6, 0xff, 0x9c, 0xa3, 0x92, 0x8f, 0xef, 0x5b, 0x9a, 0xe4, + 0xe4, 0x18, 0xfc, 0x15, 0xe8, 0x3e, 0xbe, 0xa0, 0xf8, 0x7f, 0xa9, 0xff, + 0x5e, 0xed, 0x70, 0x05, 0x0d, 0xed, 0x28, 0x49, 0xf4, 0x7b, 0xf9, 0x59, + 0xd9, 0x56, 0x85, 0x0c, 0xe9, 0x29, 0x85, 0x1f, 0x0d, 0x81, 0x15, 0xf6, + 0x35, 0xb1, 0x05, 0xee, 0x2e, 0x4e, 0x15, 0xd0, 0x4b, 0x24, 0x54, 0xbf, + 0x6f, 0x4f, 0xad, 0xf0, 0x34, 0xb1, 0x04, 0x03, 0x11, 0x9c, 0xd8, 0xe3, + 0xb9, 0x2f, 0xcc, 0x5b + ] + var data = Data(count: bytes.count) + data.withUnsafeMutableBytes { (dst: UnsafeMutablePointer) -> Void in + for i in 0 ..< bytes.count { + dst.advanced(by: i).pointee = bytes[i] + } + } + return data +}() + +@objc(TGEncryptionKeyData) private final class TGEncryptionKeyData: NSObject, NSCoding { + let keyId: Int64 + let key: Data + let firstSeqOut: Int32 + + init?(coder aDecoder: NSCoder) { + self.keyId = aDecoder.decodeInt64(forKey: "keyId") + self.key = (aDecoder.decodeObject(forKey: "key") as? Data) ?? Data() + self.firstSeqOut = aDecoder.decodeInt32(forKey: "firstSeqOut") + } + + func encode(with aCoder: NSCoder) { + assertionFailure() + } +} + +private struct SecretChatData { + let accessHash: Int64 + let handshakeState: Int32 + let rekeyState: SecretChatRekeySessionState? +} + +private func readSecretChatParticipantData(accountPeerId: PeerId, data: Data) -> (SecretChatRole, PeerId)? { + let reader = BufferReader(Buffer(data: data)) + + guard reader.readInt32() == Int32(bitPattern: 0xabcdef12) else { + return nil + } + guard let formatVersion = reader.readInt32(), formatVersion >= 2 else { + return nil + } + reader.skip(4) + + guard let adminId = reader.readInt32() else { + return nil + } + guard let count = reader.readInt32() else { + return nil + } + var ids: [Int32] = [] + for _ in 0 ..< Int(count) { + guard let id = reader.readInt32() else { + return nil + } + reader.skip(4) + reader.skip(4) + ids.append(id) + } + + guard let otherPeerId = ids.first else { + return nil + } + + return (adminId == accountPeerId.id ? .creator : .participant, PeerId(namespace: Namespaces.Peer.CloudUser, id: otherPeerId)) +} + +private func readSecretChatData(reader: BufferReader) -> SecretChatData? { + guard let version = reader.readBytesAsInt32(1) else { + return nil + } + if version != 3 { + return nil + } + reader.skip(8) + + guard let accessHash = reader.readInt64() else { + return nil + } + guard let keyFingerprint = reader.readInt64() else { + return nil + } + guard let handshakeState = reader.readInt32() else { + return nil + } + guard let currentRekeyExchangeId = reader.readInt64() else { + return nil + } + guard let currentRekeyIsInitiatedByLocalClient = reader.readBytesAsInt32(1) else { + return nil + } + guard let currentRekeyNumberLength = reader.readInt32() else { + return nil + } + var currentRekeyNumber: Data? + if currentRekeyNumberLength > 0 { + guard let value = reader.readBuffer(Int(currentRekeyNumberLength))?.makeData() else { + return nil + } + currentRekeyNumber = value + } + guard let currentRekeyKeyLength = reader.readInt32() else { + return nil + } + var currentRekeyKey: Data? + if currentRekeyKeyLength > 0 { + guard let value = reader.readBuffer(Int(currentRekeyKeyLength))?.makeData() else { + return nil + } + currentRekeyKey = value + } + guard let currentRekeyKeyId = reader.readInt64() else { + return nil + } + + var rekeyState: SecretChatRekeySessionState? + if currentRekeyExchangeId != 0 { + let innerState: SecretChatRekeySessionData? + if currentRekeyIsInitiatedByLocalClient != 0, let currentRekeyNumber = currentRekeyNumber { + innerState = .requested(a: MemoryBuffer(data: currentRekeyNumber), config: SecretChatEncryptionConfig(g: 3, p: MemoryBuffer(data: defaultPrime), version: 0)) + } else if currentRekeyIsInitiatedByLocalClient == 0, let currentRekeyKey = currentRekeyKey, currentRekeyKeyId != 0 { + innerState = .accepted(key: MemoryBuffer(data: currentRekeyKey), keyFingerprint: currentRekeyKeyId) + } else { + innerState = nil + } + if let innerState = innerState { + rekeyState = SecretChatRekeySessionState(id: currentRekeyExchangeId, data: innerState) + } + } + + return SecretChatData(accessHash: accessHash, handshakeState: handshakeState, rekeyState: rekeyState) +} + +let registeredAttachmentParsers: Bool = { + let parsers: [(Int32, TGMediaAttachmentParser)] = [ + (TGActionMediaAttachmentType, TGActionMediaAttachment()), + (TGImageMediaAttachmentType, TGImageMediaAttachment()), + (TGLocationMediaAttachmentType, TGLocationMediaAttachment()), + (TGVideoMediaAttachmentType, TGVideoMediaAttachment()), + (Int32(bitPattern: 0xB90A5663), TGContactMediaAttachment()), + (Int32(bitPattern: 0xE6C64318), TGDocumentMediaAttachment()), + (TGAudioMediaAttachmentType, TGAudioMediaAttachment()), + (Int32(bitPattern: 0x8C2E3CCE), TGMessageEntitiesAttachment()), + (Int32(bitPattern: 0x944DE6B6), TGLocalMessageMetaMediaAttachment()), + (TGAuthorSignatureMediaAttachmentType, TGAuthorSignatureMediaAttachment()), + (TGInvoiceMediaAttachmentType, TGInvoiceMediaAttachment()), + (TGGameAttachmentType, TGGameMediaAttachment()), + (Int32(bitPattern: 0xA3F4C8F5), TGViaUserAttachment()), + (TGBotContextResultAttachmentType, TGBotContextResultAttachment()), + (TGReplyMarkupAttachmentType, TGReplyMarkupAttachment()), + (TGWebPageMediaAttachmentType, TGWebPageMediaAttachment()), + (TGReplyMessageMediaAttachmentType, TGReplyMessageMediaAttachment()), + (TGAudioMediaAttachmentType, TGAudioMediaAttachment()), + (Int32(bitPattern: 0xaa1050c1), TGForwardedMessageMediaAttachment()) + ] + for (id, parser) in parsers { + TGMessage.registerMediaAttachmentParser(id, parser: parser) + } + return true +}() + +private func parseSecretChatData(peerId: PeerId, data: Data, unreadCount: Int32) -> (SecretChatData, [MessageId.Namespace: PeerReadState], Int32)? { + let reader = BufferReader(Buffer(data: data)) + guard let magic = reader.readInt32() else { + return nil + } + var version: Int32 = 1 + if magic == 0x7acde441 { + guard let value = reader.readInt32() else { + return nil + } + version = value + } + + if version < 2 { + return nil + } + + for _ in 0 ..< 3 { + guard let length = reader.readInt32() else { + return nil + } + reader.skip(Int(length)) + } + + guard let hasEncryptedData = reader.readBytesAsInt32(1), hasEncryptedData == 1 else { + return nil + } + guard let secretChatData = readSecretChatData(reader: reader) else { + return nil + } + reader.skip(4) + reader.skip(4) + reader.skip(8) + reader.skip(4) + reader.skip(4) + reader.skip(4) + reader.skip(4) + guard let maxReadDate = reader.readInt32() else { + return nil + } + guard let maxOutgoingReadDate = reader.readInt32() else { + return nil + } + guard let messageDate = reader.readInt32() else { + return nil + } + guard let minMessageDate = reader.readInt32() else { + return nil + } + + let readStates: [MessageId.Namespace: PeerReadState] = [ + Namespaces.Message.SecretIncoming: .indexBased(maxIncomingReadIndex: MessageIndex(id: MessageId(peerId: peerId, namespace: Namespaces.Message.SecretIncoming, id: 1), timestamp: maxReadDate), maxOutgoingReadIndex: MessageIndex.lowerBound(peerId: peerId), count: 0, markedUnread: false), + Namespaces.Message.Local: .indexBased(maxIncomingReadIndex: MessageIndex.lowerBound(peerId: peerId), maxOutgoingReadIndex: MessageIndex(id: MessageId(peerId: peerId, namespace: Namespaces.Message.Local, id: 1), timestamp: maxOutgoingReadDate), count: 0, markedUnread: false) + ] + return (secretChatData, readStates, max(messageDate, minMessageDate)) +} + +private enum CustomPropertyKey { + case string(String) + case hash(Int32) +} + +private func loadLegacyPeerCustomProperyData(database: SqliteInterface, peerId: Int64, key: CustomPropertyKey) -> Data? { + var propertiesData: Data? + database.select("SELECT custom_properties FROM peers_v29 WHERE pid=\(peerId)", { cursor in + propertiesData = cursor.getData(at: 0) + return false + }) + if let propertiesData = propertiesData { + let keyHash: Int32 + switch key { + case let .string(string): + keyHash = murMurHashString32(string) + case let .hash(hash): + keyHash = hash + } + let reader = BufferReader(Buffer(data: propertiesData)) + + guard let _ = reader.readInt32() else { + return nil + } + guard let count = reader.readInt32() else { + return nil + } + for _ in 0 ..< Int(count) { + guard let valueKey = reader.readInt32() else { + return nil + } + guard let valueLength = reader.readInt32() else { + return nil + } + if valueKey == keyHash { + return reader.readBuffer(Int(valueLength))?.makeData() + } + reader.skip(Int(valueLength)) + } + } + return nil +} + +private func loadLegacyPeerCustomProperyInt32(database: SqliteInterface, peerId: Int64, key: CustomPropertyKey) -> Int32? { + guard let data = loadLegacyPeerCustomProperyData(database: database, peerId: peerId, key: key), data.count == 4 else { + return nil + } + var result: Int32 = 0 + withUnsafeMutablePointer(to: &result, { bytes -> Void in + data.copyBytes(to: UnsafeMutableRawPointer(bytes).assumingMemoryBound(to: UInt8.self), from: 0 ..< 4) + }) + return result +} + +private func loadLegacyMessages(account: TemporaryAccount, basePath: String, accountPeerId: PeerId, peerId: PeerId, userPeerId: PeerId, database: SqliteInterface, conversationId: Int64, expectedTotalCount: Int32) -> Signal { + return Signal { subscriber in + subscriber.putNext(0.0) + + var copyLocalFiles: [(MediaResource, String)] = [] + var messages: [StoreMessage] = [] + + Logger.shared.log("loadLegacyMessages", "begin peerId \(peerId) conversationId \(conversationId) count \(expectedTotalCount)") + + database.select("CREATE INDEX IF NOT EXISTS random_ids_mid ON random_ids_v29 (mid)", { _ in + return true + }) + + var messageIndex: Int32 = -1 + let reportBase = max(1, expectedTotalCount / 100) + + database.select("SELECT mid, message, media, from_id, dstate, date, flags, localMid, content_properties FROM messages_v29 WHERE cid=\(conversationId)", { cursor in + messageIndex += 1 + + #if DEBUG + //usleep(500000) + #endif + + if messageIndex % reportBase == 0 { + subscriber.putNext(min(1.0, Float(messageIndex) / Float(expectedTotalCount))) + } + + let messageId = cursor.getInt32(at: 0) + + //Logger.shared.log("loadLegacyMessages", "import message \(messageId)") + + var globallyUniqueId: Int64? + database.select("SELECT random_id FROM random_ids_v29 where mid=\(messageId)", { innerCursor in + globallyUniqueId = innerCursor.getInt64(at: 0) + return false + }) + + let text = cursor.getString(at: 1) + let fromId = cursor.getInt64(at: 3) + let deliveryState = cursor.getInt32(at: 4) + let timestamp = cursor.getInt32(at: 5) + let autoremoveTimeout = cursor.getInt32(at: 7) + let contentPropertiesData = cursor.getData(at: 8) + + let parsedAuthorId: PeerId + let parsedId: StoreMessageId + var parsedFlags: StoreMessageFlags = [] + var parsedAttributes: [MessageAttribute] = [] + var parsedMedia: [Media] = [] + var parsedGroupingKey: Int64? + + if fromId == accountPeerId.id { + parsedAuthorId = accountPeerId + parsedId = .Partial(peerId, Namespaces.Message.Local) + } else { + parsedAuthorId = userPeerId + parsedId = .Partial(peerId, Namespaces.Message.SecretIncoming) + parsedFlags.insert(.Incoming) + } + + if deliveryState != 0 { + return true + } + + if !contentPropertiesData.isEmpty { + if let contentProperties = TGMessage.parseContentProperties(contentPropertiesData) { + for (_, value) in contentProperties { + if let value = value as? TGMessageGroupedIdContentProperty { + parsedGroupingKey = value.groupedId + } + } + } + } + + //Logger.shared.log("loadLegacyMessages", "message \(messageId) read content properties") + + let media = cursor.getData(at: 2) + if let mediaList = TGMessage.parseMediaAttachments(media) { + for item in mediaList { + if let item = item as? TGImageMediaAttachment { + let mediaId = MediaId(namespace: Namespaces.Media.LocalImage, id: arc4random64()) + var representations: [TelegramMediaImageRepresentation] = [] + if let allSizes = item.imageInfo?.allSizes() as? [String: NSValue] { + + for (imageUrl, sizeValue) in allSizes { + var resource: TelegramMediaResource = LocalFileMediaResource(fileId: arc4random64()) + var resourcePath: String? + if let (path, updatedResource) = pathAndResourceFromEncryptedFileUrl(basePath: basePath, url: imageUrl, type: .image) { + resource = updatedResource + copyLocalFiles.append((updatedResource, path)) + resourcePath = path + } else if imageUrl.hasPrefix("file://"), let path = URL(string: imageUrl)?.path { + copyLocalFiles.append((resource, path)) + resourcePath = path + } + + var dimensions = sizeValue.cgSizeValue + if let resourcePath = resourcePath, let image = UIImage(contentsOfFile: resourcePath) { + dimensions = image.size + } + representations.append(TelegramMediaImageRepresentation(dimensions: dimensions, resource: resource)) + } + } + + if item.localImageId != 0 { + let fullSizePath = basePath + "/Documents/files/image-local-\(String(item.localImageId, radix: 16))/image.jpg" + if let image = UIImage(contentsOfFile: fullSizePath) { + let resource: TelegramMediaResource = LocalFileMediaResource(fileId: arc4random64()) + copyLocalFiles.append((resource, fullSizePath)) + representations.append(TelegramMediaImageRepresentation(dimensions: image.size, resource: resource)) + } + } + + parsedMedia.append(TelegramMediaImage(imageId: mediaId, representations: representations, reference: nil, partialReference: nil)) + } else if let item = item as? TGVideoMediaAttachment { + let mediaId = MediaId(namespace: Namespaces.Media.LocalImage, id: arc4random64()) + var representations: [TelegramMediaImageRepresentation] = [] + if let allSizes = item.thumbnailInfo?.allSizes() as? [String: NSValue] { + for (imageUrl, sizeValue) in allSizes { + var resource: TelegramMediaResource = LocalFileMediaResource(fileId: arc4random64()) + if let (path, updatedResource) = pathAndResourceFromEncryptedFileUrl(basePath: basePath, url: imageUrl, type: .image) { + resource = updatedResource + copyLocalFiles.append((updatedResource, path)) + } else if imageUrl.hasPrefix("file://"), let path = URL(string: imageUrl)?.path { + copyLocalFiles.append((resource, path)) + } + representations.append(TelegramMediaImageRepresentation(dimensions: sizeValue.cgSizeValue, resource: resource)) + } + } + + var resource: TelegramMediaResource = LocalFileMediaResource(fileId: arc4random64()) + + var attributes: [TelegramMediaFileAttribute] = [] + attributes.append(.Video(duration: Int(item.duration), size: item.dimensions, flags: item.roundMessage ? .instantRoundVideo : [])) + + var size: Int32 = 0 + if let videoUrl = item.videoInfo?.url(withQuality: 1, actualQuality: nil, actualSize: &size) { + if let path = pathFromLegacyLocalVideoUrl(basePath: basePath, url: videoUrl) { + copyLocalFiles.append((resource, path)) + } else if let (path, updatedResource) = pathAndResourceFromEncryptedFileUrl(basePath: basePath, url: videoUrl, type: .video) { + resource = updatedResource + copyLocalFiles.append((updatedResource, path)) + } else if videoUrl.hasPrefix("file://"), let path = URL(string: videoUrl)?.path { + copyLocalFiles.append((resource, path)) + } + } + parsedMedia.append(TelegramMediaFile(fileId: mediaId, partialReference: nil, resource: resource, previewRepresentations: representations, mimeType: "video/mp4", size: size == 0 ? nil : Int(size), attributes: attributes)) + } else if let item = item as? TGAudioMediaAttachment { + let mediaId = MediaId(namespace: Namespaces.Media.LocalImage, id: arc4random64()) + let representations: [TelegramMediaImageRepresentation] = [] + + var resource: TelegramMediaResource = LocalFileMediaResource(fileId: arc4random64()) + + var attributes: [TelegramMediaFileAttribute] = [] + attributes.append(.Audio(isVoice: true, duration: Int(item.duration), title: nil, performer: nil, waveform: nil)) + + let size: Int32 = item.fileSize + let audioUrl = item.audioUri ?? "" + + if let path = pathFromLegacyLocalVideoUrl(basePath: basePath, url: audioUrl) { + copyLocalFiles.append((resource, path)) + } else if let (path, updatedResource) = pathAndResourceFromEncryptedFileUrl(basePath: basePath, url: audioUrl, type: .audio) { + resource = updatedResource + copyLocalFiles.append((updatedResource, path)) + } else if audioUrl.hasPrefix("file://"), let path = URL(string: audioUrl)?.path { + copyLocalFiles.append((resource, path)) + } + parsedMedia.append(TelegramMediaFile(fileId: mediaId, partialReference: nil, resource: resource, previewRepresentations: representations, mimeType: "audio/ogg", size: size == 0 ? nil : Int(size), attributes: attributes)) + } else if let item = item as? TGDocumentMediaAttachment { + let mediaId = MediaId(namespace: Namespaces.Media.LocalImage, id: arc4random64()) + var representations: [TelegramMediaImageRepresentation] = [] + if let allSizes = (item.thumbnailInfo?.allSizes()) as? [String: NSValue] { + for (imageUrl, sizeValue) in allSizes { + var resource: TelegramMediaResource = LocalFileMediaResource(fileId: arc4random64()) + if let (path, updatedResource) = pathAndResourceFromEncryptedFileUrl(basePath: basePath, url: imageUrl, type: .image) { + resource = updatedResource + copyLocalFiles.append((updatedResource, path)) + } else if imageUrl.hasPrefix("file://"), let path = URL(string: imageUrl)?.path { + copyLocalFiles.append((resource, path)) + } else if let updatedResource = resourceFromLegacyImageUrl(imageUrl) { + resource = updatedResource + copyLocalFiles.append((resource, pathFromLegacyImageUrl(basePath: basePath, url: imageUrl))) + } + representations.append(TelegramMediaImageRepresentation(dimensions: sizeValue.cgSizeValue, resource: resource)) + } + } + + var resource: TelegramMediaResource = LocalFileMediaResource(fileId: arc4random64()) + + var attributes: [TelegramMediaFileAttribute] = [] + var fileName = "file" + if let itemAttributes = item.attributes { + for attribute in itemAttributes { + if let attribute = attribute as? TGDocumentAttributeFilename { + attributes.append(.FileName(fileName: attribute.filename ?? "file")) + fileName = attribute.filename ?? "file" + } else if let attribute = attribute as? TGDocumentAttributeAudio { + let title = attribute.title ?? "" + let performer = attribute.performer ?? "" + var waveform: MemoryBuffer? + if let data = attribute.waveform { + waveform = MemoryBuffer(data: data.bitstream()!) + } + attributes.append(.Audio(isVoice: attribute.isVoice, duration: Int(attribute.duration), title: title.isEmpty ? nil : title, performer: performer.isEmpty ? nil : performer, waveform: waveform)) + } else if let _ = attribute as? TGDocumentAttributeAnimated { + attributes.append(.Animated) + } else if let attribute = attribute as? TGDocumentAttributeVideo { + attributes.append(.Video(duration: Int(attribute.duration), size: attribute.size, flags: attribute.isRoundMessage ? .instantRoundVideo : [])) + } else if let attribute = attribute as? TGDocumentAttributeSticker { + var packReference: StickerPackReference? + if let reference = attribute.packReference as? TGStickerPackIdReference { + packReference = .id(id: reference.packId, accessHash: reference.packAccessHash) + } else if let reference = attribute.packReference as? TGStickerPackShortnameReference { + packReference = .name(reference.shortName ?? "") + } + attributes.append(.Sticker(displayText: attribute.alt ?? "", packReference: packReference, maskData: nil)) + } else if let attribute = attribute as? TGDocumentAttributeImageSize { + attributes.append(.ImageSize(size: attribute.size)) + } + } + } + + let documentUri = item.documentUri ?? "" + + let size: Int32 = item.size + if documentUri.hasPrefix("file://"), let path = URL(string: documentUri)?.path { + copyLocalFiles.append((resource, path)) + } else if let (path, updatedResource) = pathAndResourceFromEncryptedFileUrl(basePath: basePath, url: documentUri, type: .document(fileName: fileName)) { + resource = updatedResource + copyLocalFiles.append((resource, path)) + } else if item.localDocumentId != 0 { + copyLocalFiles.append((resource, pathFromLegacyFile(basePath: basePath, fileId: item.localDocumentId, isLocal: true, fileName: TGDocumentMediaAttachment.safeFileName(forFileName: fileName) ?? ""))) + } else if item.documentId != 0 { + copyLocalFiles.append((resource, pathFromLegacyFile(basePath: basePath, fileId: item.documentId, isLocal: false, fileName: TGDocumentMediaAttachment.safeFileName(forFileName: fileName) ?? ""))) + } + parsedMedia.append(TelegramMediaFile(fileId: mediaId, partialReference: nil, resource: resource, previewRepresentations: representations, mimeType: item.mimeType ?? "application/octet-stream", size: size == 0 ? nil : Int(size), attributes: attributes)) + } else if let item = item as? TGActionMediaAttachment { + if item.actionType == TGMessageActionEncryptedChatMessageLifetime, let actionData = item.actionData, let timeout = actionData["messageLifetime"] as? Int32 { + + parsedMedia.append(TelegramMediaAction(action: .messageAutoremoveTimeoutUpdated(timeout))) + } + } else if let item = item as? TGContactMediaAttachment { + parsedMedia.append(TelegramMediaContact(firstName: item.firstName ?? "", lastName: item.lastName ?? "", phoneNumber: item.phoneNumber ?? "", peerId: nil, vCardData: nil)) + } else if let item = item as? TGLocationMediaAttachment { + var venue: MapVenue? + if let v = item.venue { + venue = MapVenue(title: v.title ?? "", address: v.address ?? "", provider: v.provider == "" ? nil : v.provider, id: v.venueId == "" ? nil : v.venueId, type: v.type == "" ? nil : v.type) + } + parsedMedia.append(TelegramMediaMap(latitude: item.latitude, longitude: item.longitude, geoPlace: nil, venue: venue, liveBroadcastingTimeout: nil)) + } + } + } + + //Logger.shared.log("loadLegacyMessages", "message \(messageId) read media") + + if autoremoveTimeout != 0 { + var countdownBeginTime: Int32? + database.select("SELECT date FROM selfdestruct_v29 where mid=\(messageId)", { innerCursor in + countdownBeginTime = innerCursor.getInt32(at: 0) - autoremoveTimeout + return false + }) + parsedAttributes.append(AutoremoveTimeoutMessageAttribute(timeout: autoremoveTimeout, countdownBeginTime: countdownBeginTime)) + } + + let (parsedTags, parsedGlobalTags) = tagsForStoreMessage(incoming: parsedFlags.contains(.Incoming), attributes: parsedAttributes, media: parsedMedia, textEntities: nil) + messages.append(StoreMessage(id: parsedId, globallyUniqueId: globallyUniqueId, groupingKey: parsedGroupingKey, timestamp: timestamp, flags: parsedFlags, tags: parsedTags, globalTags: parsedGlobalTags, localTags: [], forwardInfo: nil, authorId: parsedAuthorId, text: text, attributes: parsedAttributes, media: parsedMedia)) + + //Logger.shared.log("loadLegacyMessages", "message \(messageId) completed") + + return true + }) + + let disposable = (account.postbox.transaction { transaction -> Void in + //Logger.shared.log("loadLegacyMessages", "conversation \(conversationId) storing messages") + let _ = transaction.addMessages(messages, location: .UpperHistoryBlock) + + //Logger.shared.log("loadLegacyMessages", "conversation \(conversationId) copying \(copyLocalFiles.count) files") + + for (resource, path) in copyLocalFiles { + account.postbox.mediaBox.copyResourceData(resource.id, fromTempPath: path) + } + + Logger.shared.log("loadLegacyMessages", "conversation \(conversationId) done") + }).start(completed: { + subscriber.putCompletion() + }) + + return disposable + } +} + +private func importChannelBroadcastPreferences(account: TemporaryAccount, basePath: String, database: SqliteInterface) -> Signal { + return deferred { () -> Signal in + var peerIds: [Int64] = [] + database.select("SELECT cid FROM channel_conversations_v29", { cursor in + peerIds.append(cursor.getInt64(at: 0)) + return true + }) + var peerIdsWithMutedMessages: [Int64] = [] + for peerId in peerIds { + if let data = loadLegacyPeerCustomProperyData(database: database, peerId: peerId, key: .hash(0x374BF349)), !data.isEmpty { + let reader = BufferReader(Buffer(data: data)) + guard let version = reader.readBytesAsInt32(1) else { + continue + } + guard let _ = reader.readBytesAsInt32(1) else { + continue + } + if version >= 2 { + guard let messagesMuted = reader.readBytesAsInt32(1) else { + continue + } + if messagesMuted == 1 { + peerIdsWithMutedMessages.append(peerId) + } + } + } + } + + return account.postbox.transaction { transaction -> Void in + for peerId in peerIdsWithMutedMessages { + let channelId = Int32(clamping: Int64(Int32.min) &* 2 &- peerId) + transaction.updatePeerChatInterfaceState(PeerId(namespace: Namespaces.Peer.CloudChannel, id: channelId), update: { current in + let state = (current as? ChatInterfaceState ?? ChatInterfaceState()).withUpdatedSilentPosting(true) + return state + }) + } + } + |> ignoreValues + } +} + +func loadLegacySecretChats(account: TemporaryAccount, basePath: String, accountPeerId: PeerId, database: SqliteInterface) -> Signal { + return deferred { () -> Signal in + var peerIdToConversationId: [PeerId: Int64] = [:] + database.select("SELECT encrypted_id, cid FROM encrypted_cids_v29", { cursor in + peerIdToConversationId[PeerId(namespace: Namespaces.Peer.SecretChat, id: cursor.getInt32(at: 0))] = cursor.getInt64(at: 1) + return true + }) + var chatInfos: [(TelegramSecretChat, SecretChatState, Int32?, [MessageId.Namespace: PeerReadState], Int64)] = [] + for (peerId, conversationId) in peerIdToConversationId { + database.select("SELECT chat_photo, unread_count, participants, date FROM convesations_v29 WHERE cid=\(conversationId)", { cursor in + guard let (secretChatData, readStates, minMessageDate) = parseSecretChatData(peerId: peerId, data: cursor.getData(at: 0), unreadCount: cursor.getInt32(at: 1)) else { + return false + } + guard let (role, userPeerId) = readSecretChatParticipantData(accountPeerId: accountPeerId, data: cursor.getData(at: 2)) else { + return false + } + let chatMessageDate = cursor.getInt32(at: 3) + let messageDate = min(minMessageDate, chatMessageDate) + + let messageLifetime = loadLegacyPeerCustomProperyInt32(database: database, peerId: conversationId, key: .string("messageLifetime")) ?? 0 + + let state: SecretChatState + var seqOut: Int32? + + switch secretChatData.handshakeState { + case 1: //requested + guard let a = loadLegacyPeerCustomProperyData(database: database, peerId: conversationId, key: .string("a")), !a.isEmpty else { + return false + } + state = SecretChatState(role: .creator, embeddedState: .handshake(.requested(g: 3, p: MemoryBuffer(data: defaultPrime), a: MemoryBuffer(data: a))), keychain: SecretChatKeychain(keys: []), keyFingerprint: nil, messageAutoremoveTimeout: nil) + case 2: //accepting + return false + case 3: //terminated + state = SecretChatState(role: .creator, embeddedState: .terminated, keychain: SecretChatKeychain(keys: []), keyFingerprint: nil, messageAutoremoveTimeout: nil) + case 4: + guard let sha1Fingerprint = loadLegacyPeerCustomProperyData(database: database, peerId: conversationId, key: .string("encryptionKeySha1")) else { + return false + } + guard let sha256Fingerprint = loadLegacyPeerCustomProperyData(database: database, peerId: conversationId, key: .string("encryptionKeySha256")) else { + return false + } + + guard let keysData = loadLegacyPeerCustomProperyData(database: database, peerId: conversationId, key: .string("encryptionKeys")) else { + return false + } + guard let keysArray = NSKeyedUnarchiver.unarchiveObject(with: keysData) as? [TGEncryptionKeyData] else { + return false + } + let parsedKeys: [SecretChatKey] = keysArray.map({ key in + return SecretChatKey(fingerprint: key.keyId, key: MemoryBuffer(data: key.key), validity: .sequenceBasedIndexRange(fromCanonicalIndex: key.firstSeqOut), useCount: 1) + }) + let requestedLayerValue = loadLegacyPeerCustomProperyInt32(database: database, peerId: conversationId, key: .hash(reportedLayer_hash)) ?? 0 + let appliedSeqInValue = loadLegacyPeerCustomProperyInt32(database: database, peerId: conversationId, key: .hash(seq_in_hash)) ?? 0 + guard let seqOutValue = loadLegacyPeerCustomProperyInt32(database: database, peerId: conversationId, key: .hash(seq_out_hash)) else { + return false + } + seqOut = seqOutValue + guard let activeLayerValue = loadLegacyPeerCustomProperyInt32(database: database, peerId: conversationId, key: .hash(layer_hash)) else { + return false + } + guard let activeLayer = SecretChatSequenceBasedLayer(rawValue: activeLayerValue) else { + return false + } + let rekeyState: SecretChatRekeySessionState? = secretChatData.rekeyState + let embeddedState: SecretChatEmbeddedState = .sequenceBasedLayer(SecretChatSequenceBasedLayerState(layerNegotiationState: SecretChatLayerNegotiationState(activeLayer: activeLayer, locallyRequestedLayer: requestedLayerValue == 0 ? nil : requestedLayerValue, remotelyRequestedLayer: nil), rekeyState: rekeyState, baseIncomingOperationIndex: 0, baseOutgoingOperationIndex: 0, topProcessedCanonicalIncomingOperationIndex: appliedSeqInValue == 0 ? nil : max(0, appliedSeqInValue - 1))) + state = SecretChatState(role: role, embeddedState: embeddedState, keychain: SecretChatKeychain(keys: parsedKeys), keyFingerprint: SecretChatKeyFingerprint(sha1: SecretChatKeySha1Fingerprint(digest: sha1Fingerprint), sha256: SecretChatKeySha256Fingerprint(digest: sha256Fingerprint)), messageAutoremoveTimeout: messageLifetime == 0 ? nil : messageLifetime) + default: + return false + } + + let secretChat = TelegramSecretChat(id: peerId, creationDate: messageDate, regularPeerId: userPeerId, accessHash: secretChatData.accessHash, role: role, embeddedState: state.embeddedState.peerState, messageAutoremoveTimeout: messageLifetime == 0 ? nil : messageLifetime) + + chatInfos.append((secretChat, state, seqOut, readStates, conversationId)) + + return false + }) + } + var userPeers: [PeerId: Peer] = [:] + var presences: [PeerId: PeerPresence] = [:] + for info in chatInfos { + if let (peer, presence) = loadLegacyUser(database: database, id: info.0.regularPeerId.id) { + userPeers[peer.id] = peer + presences[peer.id] = presence + } + } + + let storedChats = account.postbox.transaction { transaction -> Void in + updatePeers(transaction: transaction, peers: Array(userPeers.values), update: { _, updated in + return updated + }) + transaction.updatePeerPresencesInternal(presences) + for (peer, state, seqOutValue, readStates, _) in chatInfos { + if userPeers[peer.regularPeerId] == nil { + continue + } + updatePeers(transaction: transaction, peers: [peer], update: { _, updated in + return updated + }) + transaction.setPeerChatState(peer.id, state: state) + switch state.embeddedState { + case .sequenceBasedLayer: + if let seqOutValue = seqOutValue { + transaction.operationLogResetIndices(peerId: peer.id, tag: OperationLogTags.SecretOutgoing, nextTagLocalIndex: seqOutValue + 1) + } + default: + break + } + transaction.resetIncomingReadStates([peer.id: readStates]) + } + } + |> ignoreValues + + let _ = registeredAttachmentParsers + + var countByConversationId: [Int64: Int32] = [:] + var totalCount: Int32 = 0 + + for info in chatInfos { + database.select("SELECT COUNT(*) FROM messages_v29 WHERE cid=\(info.4)", { cursor in + let count = cursor.getInt32(at: 0) + countByConversationId[info.4] = count + totalCount += count + return true + }) + } + + var storedMessagesSignals: Signal = .single(0.0) + var cumulativeCount: Int32 = 0 + for info in chatInfos { + let localBaseline = cumulativeCount + let localCount = countByConversationId[info.4] ?? 0 + storedMessagesSignals = storedMessagesSignals + |> then( + loadLegacyMessages(account: account, basePath: basePath, accountPeerId: accountPeerId, peerId: info.0.id, userPeerId: info.0.regularPeerId, database: database, conversationId: info.4, expectedTotalCount: localCount) + |> map { localProgress -> Float in + if totalCount <= 0 { + return 0.0 + } + let globalCount = localBaseline + Int32(localProgress * Float(localCount)) + return Float(globalCount) / Float(totalCount) + } + ) + cumulativeCount += countByConversationId[info.4] ?? 0 + } + + return storedChats + |> map { _ -> Float in return 0.0 } + |> then(storedMessagesSignals) + } +} diff --git a/Telegram-iOS/LegacyDataImport.swift b/Telegram-iOS/LegacyDataImport.swift new file mode 100644 index 0000000000..dc93728a22 --- /dev/null +++ b/Telegram-iOS/LegacyDataImport.swift @@ -0,0 +1,242 @@ +import Foundation +import TelegramCore +import SwiftSignalKit +import Postbox +import MtProtoKitDynamic + +enum AccountImportError: Error { + case generic +} + +enum AccountImportProgressType { + case generic + case messages + case media +} + +private func importedAccountData(basePath: String, documentsPath: String, account: TemporaryAccount, database: SqliteInterface) -> Signal<(AccountImportProgressType, Float), AccountImportError> { + return deferred { () -> Signal<(AccountImportProgressType, Float), AccountImportError> in + let keychain = MTFileBasedKeychain(name: "Telegram", documentsPath: documentsPath) + guard let masterDatacenterId = keychain.object(forKey: "defaultDatacenterId", group: "persistent") as? Int else { + return .fail(.generic) + } + let keychainContents = keychain.contents(forGroup: "persistent") + + let importKeychain = account.postbox.transaction { transaction -> Void in + for (key, value) in keychainContents { + let data = NSKeyedArchiver.archivedData(withRootObject: value) + transaction.setKeychainEntry(data, forKey: "persistent" + ":" + key) + } + } + |> ignoreValues + |> introduceError(AccountImportError.self) + + let importData = importPreferencesData(documentsPath: documentsPath, masterDatacenterId: Int32(masterDatacenterId), account: account, database: database) + |> mapToSignal { accountUserId -> Signal<(AccountImportProgressType, Float), AccountImportError> in + return importDatabaseData(account: account, basePath: basePath, database: database, accountUserId: accountUserId) + } + + return importKeychain + |> map { _ -> (AccountImportProgressType, Float) in return (.generic, 0.0) } + |> then(importData) + } +} + +private func importPreferencesData(documentsPath: String, masterDatacenterId: Int32, account: TemporaryAccount, database: SqliteInterface) -> Signal { + return deferred { () -> Signal in + let defaultsPath = documentsPath + "/standard.defaults" + var parsedAccountUserId: Int32? + if let data = try? Data(contentsOf: URL(fileURLWithPath: defaultsPath)), let dict = NSKeyedUnarchiver.unarchiveObject(with: data) as? [String: Any], let id = dict["telegraphUserId"] as? Int { + parsedAccountUserId = Int32(id) + } + if parsedAccountUserId == nil { + if let id = UserDefaults.standard.object(forKey: "telegraphUserId") as? Int { + parsedAccountUserId = Int32(id) + } + } + + if let parsedAccountUserId = parsedAccountUserId { + return account.postbox.transaction { transaction -> Int32 in + transaction.setState(AuthorizedAccountState(isTestingEnvironment: false, masterDatacenterId: masterDatacenterId, peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: parsedAccountUserId), state: nil)) + return parsedAccountUserId + } + |> introduceError(AccountImportError.self) + } else { + return .fail(.generic) + } + } +} + +private func importDatabaseData(account: TemporaryAccount, basePath: String, database: SqliteInterface, accountUserId: Int32) -> Signal<(AccountImportProgressType, Float), AccountImportError> { + return deferred { () -> Signal<(AccountImportProgressType, Float), AccountImportError> in + var importedAccountUser: Signal = .complete() + if let (user, presence) = loadLegacyUser(database: database, id: accountUserId) { + importedAccountUser = account.postbox.transaction { transaction -> Void in + updatePeers(transaction: transaction, peers: [user], update: { _, updated in updated }) + transaction.updatePeerPresencesInternal([user.id: presence]) + } + |> ignoreValues + |> introduceError(AccountImportError.self) + } + + let importedSecretChats = loadLegacySecretChats(account: account, basePath: basePath, accountPeerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: accountUserId), database: database) + |> introduceError(AccountImportError.self) + + /*let importedFiles = loadLegacyFiles(account: account, basePath: basePath, accountPeerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: accountUserId), database: database) + |> introduceError(AccountImportError.self)*/ + + let importedLegacyPreferences = importLegacyPreferences(account: account, documentsPath: basePath + "/Documents", database: database) + |> introduceError(AccountImportError.self) + + return importedAccountUser + |> map { _ -> (AccountImportProgressType, Float) in return (.generic, 0.0) } + |> then( + importedLegacyPreferences + |> map { _ -> (AccountImportProgressType, Float) in return (.generic, 0.0) } + ) + |> then( + importedSecretChats + |> map { value -> (AccountImportProgressType, Float) in return (.messages, value) } + ) + } +} + +enum ImportedLegacyAccountEvent { + case progress(AccountImportProgressType, Float) + case result(AccountRecordId?) +} + +func importedLegacyAccount(basePath: String, accountManager: AccountManager, present: @escaping (UIViewController) -> Void) -> Signal { + let queue = Queue() + return deferred { () -> Signal in + let documentsPath = basePath + "/Documents" + if FileManager.default.fileExists(atPath: documentsPath + "/importcompleted") { + return .single(.result(nil)) + } + + let unlockedDatabasePathAndKey: Signal<(String, Data?)?, AccountImportError> + if FileManager.default.fileExists(atPath: documentsPath + "/tgdata.db.y") { + let databasePath = documentsPath + "/tgdata.db.y" + let unlockDatabase = Signal<(String, Data?)?, AccountImportError> { subscriber in + let alertController = UIAlertController(title: nil, message: "Enter your passcode", preferredStyle: .alert) + + let confirmAction = UIAlertAction(title: "Enter", style: .default) { _ in + let passcode = alertController.textFields?[0].text + + func checkPasscode(_ value: String) -> Bool { + guard let database = SqliteInterface(databasePath: databasePath) else { + return false + } + let key = value.data(using: .utf8)! + if !database.unlock(password: hexString(key).data(using: .utf8)!) { + return false + } + + return true + } + + if checkPasscode(passcode ?? "") { + subscriber.putNext((databasePath, (passcode ?? "").data(using: .utf8)!)) + subscriber.putCompletion() + } else { + let alertController = UIAlertController(title: nil, message: "Invalid passcode. Please try again.", preferredStyle: .alert) + + let confirmAction = UIAlertAction(title: "OK", style: .default) { _ in + subscriber.putCompletion() + } + + alertController.addAction(confirmAction) + + present(alertController) + } + } + + let cancelAction = UIAlertAction(title: "Skip", style: .cancel) { _ in + subscriber.putNext(nil) + subscriber.putCompletion() + } + + alertController.addTextField { textField in + textField.placeholder = "Passcode" + } + + alertController.addAction(confirmAction) + alertController.addAction(cancelAction) + + present(alertController) + return EmptyDisposable + } + |> runOn(Queue.mainQueue()) + + unlockedDatabasePathAndKey = (unlockDatabase + |> mapToSignal { result -> Signal<(String, Data?)?, AccountImportError> in + if let result = result { + return .single(result) + } else { + let askAgain = Signal<(String, Data?)?, AccountImportError> { subscriber in + let alertController = UIAlertController(title: "Warning", message: "If you continue without entering your passcode, all your secret chats will be lost.", preferredStyle: .alert) + + let confirmAction = UIAlertAction(title: "Skip", style: .destructive) { _ in + subscriber.putError(.generic) + } + + let cancelAction = UIAlertAction(title: "Try Again", style: .cancel) { _ in + subscriber.putCompletion() + } + + alertController.addAction(confirmAction) + alertController.addAction(cancelAction) + + present(alertController) + return EmptyDisposable + } + |> runOn(Queue.mainQueue()) + return askAgain + } + }) + |> restart + |> take(1) + } else if FileManager.default.fileExists(atPath: documentsPath + "/tgdata.db") { + unlockedDatabasePathAndKey = .single((documentsPath + "/tgdata.db", nil)) + } else { + return .single(.result(nil)) + } + + return unlockedDatabasePathAndKey + |> mapToSignal { pathAndKey -> Signal in + guard let pathAndKey = pathAndKey else { + return .fail(.generic) + } + + guard let database = SqliteInterface(databasePath: pathAndKey.0) else { + return .fail(.generic) + } + + if let key = pathAndKey.1 { + if !database.unlock(password: hexString(key).data(using: .utf8)!) { + return .fail(.generic) + } + } + + return temporaryAccount(manager: accountManager, rootPath: rootPathForBasePath(basePath)) + |> introduceError(AccountImportError.self) + |> mapToSignal { account -> Signal in + let actions = importedAccountData(basePath: basePath, documentsPath: documentsPath, account: account, database: database) + var result = actions + |> map { typeAndProgress -> ImportedLegacyAccountEvent in + return .progress(typeAndProgress.0, typeAndProgress.1) + } + #if DEBUG + //result = result + //|> then(.never()) + #endif + + result = result + |> then(.single(.result(account.id))) + + return result + } + } + } + |> runOn(queue) +} diff --git a/Telegram-iOS/LegacyDataImportSplash.swift b/Telegram-iOS/LegacyDataImportSplash.swift new file mode 100644 index 0000000000..0cf3713817 --- /dev/null +++ b/Telegram-iOS/LegacyDataImportSplash.swift @@ -0,0 +1,68 @@ +import Foundation +import Display +import AsyncDisplayKit +import TelegramUI + +final class LegacyDataImportSplash: WindowCoveringView { + var progress: (AccountImportProgressType, Float) = (.generic, 0.0) { + didSet { + if self.progress.0 != oldValue.0 { + if let size = self.validSize { + switch self.progress.0 { + case .generic: + self.textNode.attributedText = NSAttributedString(string: "Optimizing", font: Font.regular(17.0), textColor: .black) + case .media: + self.textNode.attributedText = NSAttributedString(string: "Optimizing cache", font: Font.regular(17.0), textColor: .black) + case .messages: + self.textNode.attributedText = NSAttributedString(string: "Optimizing database", font: Font.regular(17.0), textColor: .black) + } + self.updateLayout(size) + } + } + self.progressNode.transitionToState(.progress(color: UIColor(rgb: 0x007ee5), lineWidth: 2.0, value: CGFloat(max(0.025, self.progress.1)), cancelEnabled: false), animated: false, completion: {}) + } + } + + var serviceAction: (() -> Void)? + + private let progressNode: RadialStatusNode + private let textNode: ImmediateTextNode + + private var validSize: CGSize? + + override init(frame: CGRect) { + self.progressNode = RadialStatusNode(backgroundNodeColor: UIColor.white) + self.textNode = ImmediateTextNode() + self.textNode.attributedText = NSAttributedString(string: "Optimizing", font: Font.regular(17.0), textColor: .black) + + super.init(frame: frame) + + self.addSubnode(self.progressNode) + self.progressNode.isUserInteractionEnabled = false + self.addSubnode(self.textNode) + self.textNode.isUserInteractionEnabled = false + + self.addGestureRecognizer(UILongPressGestureRecognizer(target: self, action: #selector(self.longPressGesture(_:)))) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func updateLayout(_ size: CGSize) { + self.validSize = size + + let progressSize = CGSize(width: 60.0, height: 60.0) + let progressFrame = CGRect(origin: CGPoint(x: floor((size.width - progressSize.width) / 2.0), y: floor((size.height - progressSize.height) / 2.0) - 8.0), size: progressSize) + self.progressNode.frame = progressFrame + + let textSize = self.textNode.updateLayout(size) + self.textNode.frame = CGRect(origin: CGPoint(x: floor((size.width - textSize.width) / 2.0), y: progressFrame.maxY + 15.0), size: textSize) + } + + @objc private func longPressGesture(_ recognizer: UILongPressGestureRecognizer) { + if case .began = recognizer.state { + self.serviceAction?() + } + } +} diff --git a/Telegram-iOS/LegacyFileImport.swift b/Telegram-iOS/LegacyFileImport.swift new file mode 100644 index 0000000000..bf0a914b14 --- /dev/null +++ b/Telegram-iOS/LegacyFileImport.swift @@ -0,0 +1,190 @@ +import Foundation +import TelegramCore +import SwiftSignalKit +import Postbox +import TelegramUI + +import LegacyComponents + +private func importMediaFromMessageData(_ data: Data, basePath: String, copyLocalFiles: inout [(MediaResource, String)], cache: TGCache) { + if let message = TGMessage(keyValueCoder: PSKeyValueDecoder(data: data)) { + if let mediaAttachments = message.mediaAttachments { + importMediaFromMediaList(mediaAttachments, basePath: basePath, copyLocalFiles: ©LocalFiles, cache: cache) + } + } +} + +private func importMediaFromMediaData(_ data: Data, basePath: String, copyLocalFiles: inout [(MediaResource, String)], cache: TGCache) { + if let mediaAttachments = TGMessage.parseMediaAttachments(data) { + importMediaFromMediaList(mediaAttachments, basePath: basePath, copyLocalFiles: ©LocalFiles, cache: cache) + } +} + +private func importMediaFromMediaList(_ mediaAttachments: [Any], basePath: String, copyLocalFiles: inout [(MediaResource, String)], cache: TGCache) { + for media in mediaAttachments { + if let media = media as? TGDocumentMediaAttachment { + var fileName = "file" + if let itemAttributes = media.attributes { + for attribute in itemAttributes { + if let attribute = attribute as? TGDocumentAttributeFilename { + fileName = attribute.filename ?? "file" + } + } + } + + if media.documentId != 0 { + let filePath = pathFromLegacyFile(basePath: basePath, fileId: media.documentId, isLocal: false, fileName: TGDocumentMediaAttachment.safeFileName(forFileName: fileName) ?? "") + if FileManager.default.fileExists(atPath: filePath) { + copyLocalFiles.append((CloudDocumentMediaResource(datacenterId: Int(media.datacenterId), fileId: media.documentId, accessHash: media.accessHash, size: nil, fileReference: nil, fileName: nil), filePath)) + } + } + } else if let media = media as? TGVideoMediaAttachment { + if media.videoId != 0, let videoUrl = media.videoInfo?.url(withQuality: 1, actualQuality: nil, actualSize: nil) { + if let (id, accessHash, datacenterId, path) = pathFromLegacyVideoUrl(basePath: basePath, url: videoUrl) { + copyLocalFiles.append((CloudDocumentMediaResource(datacenterId: Int(datacenterId), fileId: id, accessHash: accessHash, size: nil, fileReference: nil, fileName: nil), path)) + } + } + } else if let media = media as? TGImageMediaAttachment { + if let allSizes = media.imageInfo?.allSizes() as? [String: NSValue] { + for (imageUrl, _) in allSizes { + if let path = cache.path(forCachedData: imageUrl), let resource = resourceFromLegacyImageUrl(imageUrl), FileManager.default.fileExists(atPath: path) { + copyLocalFiles.append((resource, path)) + } + } + } + } + } +} + +private func makeMessageSortKey(tag: Int32, conversationId: Int64, space: Int8, timestamp: Int32, messageId: Int32) -> Data { + let key = ValueBoxKey(length: 4 + 8 + 1 + 4 + 4) + key.setInt32(0, value: tag.byteSwapped) + key.setInt64(4, value: conversationId.byteSwapped) + key.setInt8(4 + 8, value: space) + key.setInt32(4 + 8 + 1, value: timestamp) + key.setInt32(4 + 8 + 1 + 4, value: messageId.byteSwapped) + return Data(bytes: key.memory, count: key.length) +} + +func loadLegacyFiles(account: TemporaryAccount, basePath: String, accountPeerId: PeerId, database: SqliteInterface) -> Signal { + return Signal { subscriber in + let _ = registeredAttachmentParsers + + subscriber.putNext(0.0) + + var channelIds: [Int64] = [] + database.select("SELECT DISTINCT cid FROM channel_message_tags_v29", { cursor in + channelIds.append(cursor.getInt64(at: 0)) + return true + }) + print(database.explain("SELECT DISTINCT cid FROM channel_message_tags_v29")) + + var channelMessageIds: [(Int64, Int32)] = [] + + print(database.explain("SELECT mid FROM channel_message_tags_v29 WHERE tag_sort_key<100 AND tag_sort_key>0 ORDER BY tag_sort_key DESC LIMIT 4000")) + + if !channelIds.isEmpty { + /* + TGSharedMediaCacheItemTypePhoto = 0, + TGSharedMediaCacheItemTypeVideo = 1, + TGSharedMediaCacheItemTypeFile = 2, + TGSharedMediaCacheItemTypePhotoVideo = 3, + TGSharedMediaCacheItemTypePhotoVideoFile = 4, + TGSharedMediaCacheItemTypeAudio = 5, + TGSharedMediaCacheItemTypeLink = 6, + TGSharedMediaCacheItemTypeSticker = 7, + TGSharedMediaCacheItemTypeGif = 8, + TGSharedMediaCacheItemTypeVoiceVideoMessage = 9 + */ + let tags: [Int32] = [ + 2, // File + 5, // Audio + 3, // PhotoVideo + ] + database.withStatement("SELECT mid FROM channel_message_tags_v29 WHERE tag_sort_key? ORDER BY tag_sort_key DESC LIMIT 4000", { select in + for channelId in channelIds { + for tag in tags { + select([.data(makeMessageSortKey(tag: tag, conversationId: channelId, space: 0, timestamp: Int32.max - 1, messageId: 0)), .data(makeMessageSortKey(tag: tag, conversationId: channelId, space: 0, timestamp: 0, messageId: 0))], { cursor in + channelMessageIds.append((channelId, cursor.getInt32(at: 0))) + return true + }) + select([.data(makeMessageSortKey(tag: tag, conversationId: channelId, space: 1, timestamp: Int32.max - 1, messageId: 0)), .data(makeMessageSortKey(tag: tag, conversationId: channelId, space: 1, timestamp: 0, messageId: 0))], { cursor in + channelMessageIds.append((channelId, cursor.getInt32(at: 0))) + return true + }) + } + } + }) + } + + var chatMessageIds: [Int32] = [] + let mediaTypes: [Int32] = [ + 1, // video + 2, // image + 3, // file + ] + for type in mediaTypes { + database.select("SELECT mids FROM media_cache_v29 WHERE media_type=\(type) ORDER BY date DESC LIMIT 32000", { cursor in + let midsData = cursor.getData(at: 0) + let reader = BufferReader(Buffer(data: midsData)) + while true { + if let mid = reader.readInt32() { + chatMessageIds.append(mid) + } else { + break + } + } + return true + }) + } + + var copyLocalFiles: [(MediaResource, String)] = [] + + let totalCount = channelMessageIds.count + chatMessageIds.count + let reportBase = max(1, totalCount / 100) + + var itemIndex = -1 + + let cache = TGCache(cachesPath: basePath + "/Caches")! + + if !channelMessageIds.isEmpty { + database.withStatement("SELECT data FROM channel_messages_v29 WHERE cid=? AND mid=?", { select in + for (peerId, messageId) in channelMessageIds { + itemIndex += 1 + if itemIndex % reportBase == 0 { + subscriber.putNext(Float(itemIndex) / Float(totalCount)) + } + select([.int64(peerId), .int32(messageId)], { cursor in + let data = cursor.getData(at: 0) + importMediaFromMessageData(data, basePath: basePath, copyLocalFiles: ©LocalFiles, cache: cache) + return true + }) + } + }) + } + + if !chatMessageIds.isEmpty { + database.withStatement("SELECT media FROM messages_v29 WHERE mid=?", { select in + for messageId in chatMessageIds { + itemIndex += 1 + if itemIndex % reportBase == 0 { + subscriber.putNext(Float(itemIndex) / Float(totalCount)) + } + select([.int32(messageId)], { cursor in + let data = cursor.getData(at: 0) + importMediaFromMediaData(data, basePath: basePath, copyLocalFiles: ©LocalFiles, cache: cache) + return true + }) + } + }) + } + + for (resource, path) in copyLocalFiles { + account.postbox.mediaBox.copyResourceData(resource.id, fromTempPath: path) + } + + subscriber.putCompletion() + + return EmptyDisposable + } +} diff --git a/Telegram-iOS/LegacyPreferencesImport.swift b/Telegram-iOS/LegacyPreferencesImport.swift new file mode 100644 index 0000000000..84fe0c42d2 --- /dev/null +++ b/Telegram-iOS/LegacyPreferencesImport.swift @@ -0,0 +1,497 @@ +import Foundation +import TelegramCore +import SwiftSignalKit +import Postbox +import MtProtoKitDynamic +import TelegramUI + +import LegacyComponents + +@objc(TGPresentationState) private final class TGPresentationState: NSObject, NSCoding { + let pallete: Int32 + let userInfo: Int32 + let fontSize: Int32 + + init?(coder aDecoder: NSCoder) { + self.pallete = aDecoder.decodeInt32(forKey: "p") + self.userInfo = aDecoder.decodeInt32(forKey: "u") + self.fontSize = aDecoder.decodeInt32(forKey: "f") + } + + func encode(with aCoder: NSCoder) { + assertionFailure() + } +} + +private enum PreferencesProvider { + case dict([String: Any]) + case standard(UserDefaults) + + subscript(_ key: String) -> Any? { + get { + switch self { + case let .dict(dict): + return dict[key] + case let .standard(standard): + return standard.object(forKey: key) + } + } + } +} + +private func loadLegacyCustomProperyData(database: SqliteInterface, key: String) -> Data? { + var result: Data? + database.select("SELECT value FROM service_v29 WHERE key=\(murMurHash32(key))", { cursor in + result = cursor.getData(at: 0) + return false + }) + return result +} + +private func convertLegacyProxyPort(_ value: Int) -> Int32 { + if value < 0 { + return Int32(UInt16(bitPattern: Int16(clamping: value))) + } else { + return Int32(clamping: value) + } +} + +func importLegacyPreferences(account: TemporaryAccount, documentsPath: String, database: SqliteInterface) -> Signal { + return deferred { () -> Signal in + var presentationState: TGPresentationState? + if let value = NSKeyedUnarchiver.unarchiveObject(withFile: documentsPath + "/presentation.dat") as? TGPresentationState { + presentationState = value + } + + var autoNightPreferences: TGPresentationAutoNightPreferences? + if let value = NSKeyedUnarchiver.unarchiveObject(withFile: documentsPath + "/autonight.dat") as? TGPresentationAutoNightPreferences { + autoNightPreferences = value + } + + var wallpaperInfo: TGWallpaperInfo? + if let data = UserDefaults.standard.object(forKey: "_currentWallpaperInfo") as? [AnyHashable: Any], let value = TGWallpaperInfo(dictionary: data) { + wallpaperInfo = value + } + + let autoDownloadPreferences: TGAutoDownloadPreferences? = NSKeyedUnarchiver.unarchiveObject(withFile: documentsPath + "/autoDownload.pref") as? TGAutoDownloadPreferences + + let preferencesProvider: PreferencesProvider + let defaultsPath = documentsPath + "/standard.defaults" + + let standardPreferences = PreferencesProvider.standard(UserDefaults.standard) + if let data = try? Data(contentsOf: URL(fileURLWithPath: defaultsPath)), let dict = NSKeyedUnarchiver.unarchiveObject(with: data) as? [String: Any] { + preferencesProvider = .dict(dict) + } else { + preferencesProvider = standardPreferences + } + + var showCallsTab: Bool? + if let data = try? Data(contentsOf: URL(fileURLWithPath: documentsPath + "/enablecalls.tab")), !data.isEmpty { + showCallsTab = data.withUnsafeBytes { (bytes: UnsafePointer) -> Bool in + return bytes.pointee != 0 + } + } + + let parsedAutoplayGifs: Bool? = preferencesProvider["autoPlayAnimations"] as? Bool + let soundEnabled: Bool? = preferencesProvider["soundEnabled"] as? Bool + let vibrationEnabled: Bool? = preferencesProvider["vibrationEnabled"] as? Bool + let bannerEnabled: Bool? = preferencesProvider["bannerEnabled"] as? Bool + let callsDataUsageMode: Int? = preferencesProvider["callsDataUsageMode"] as? Int + let callsDisableP2P: Bool? = preferencesProvider["callsDisableP2P"] as? Bool + let callsDisableCallKit: Bool? = preferencesProvider["callsDisableCallKit"] as? Bool + let callsUseProxy: Bool? = preferencesProvider["callsUseProxy"] as? Bool + let contactsInhibitSync: Bool? = preferencesProvider["contactsInhibitSync"] as? Bool + let stickersSuggestMode: Int? = preferencesProvider["stickersSuggestMode"] as? Int + + let allowSecretWebpages: Bool? = preferencesProvider["allowSecretWebpages"] as? Bool + let allowSecretWebpagesInitialized: Bool? = preferencesProvider["allowSecretWebpagesInitialized"] as? Bool + let secretInlineBotsInitialized: Bool? = preferencesProvider["secretInlineBotsInitialized"] as? Bool + + let musicPlayerOrderType: Int? = standardPreferences["musicPlayerOrderType_v1"] as? Int + let musicPlayerRepeatType: Int? = standardPreferences["musicPlayerRepeatType_v1"] as? Int + + let instantPageFontSize: Float? = standardPreferences["instantPage_fontMultiplier_v0"] as? Float + let instantPageFontSerif: Int? = standardPreferences["instantPage_fontSerif_v0"] as? Int + let instantPageTheme: Int? = standardPreferences["instantPage_theme_v0"] as? Int + let instantPageAutoNightMode: Int? = standardPreferences["instantPage_autoNightTheme_v0"] as? Int + + let proxyList = NSKeyedUnarchiver.unarchiveObject(withFile: documentsPath + "/proxies.data") as? [TGProxyItem] + var selectedProxy: (ProxyServerSettings, Bool)? + if let data = loadLegacyCustomProperyData(database: database, key: "socksProxyData"), let dict = NSKeyedUnarchiver.unarchiveObject(with: data) as? [String: Any], let host = dict["ip"] as? String, let port = dict["port"] as? Int { + let inactive = (dict["inactive"] as? Bool) ?? true + var connection: ProxyServerConnection? + if let secretString = dict["secret"] as? String { + let secret = dataWithHexString(secretString) + var secretIsValid = false + if secret.count == 16 { + secretIsValid = true + } else if secret.count == 17 && MTSocksProxySettings.secretSupportsExtendedPadding(secret) { + secretIsValid = true + } + if secretIsValid { + connection = .mtp(secret: secret) + } + } else { + connection = .socks5(username: (dict["username"] as? String) ?? "", password: (dict["password"] as? String) ?? "") + } + if let connection = connection { + selectedProxy = (ProxyServerSettings(host: host, port: convertLegacyProxyPort(port), connection: connection), !inactive) + } + } + + var passcodeChallenge: PostboxAccessChallengeData? + if let data = try? Data(contentsOf: URL(fileURLWithPath: documentsPath + "/x.y")) { + let reader = BufferReader(Buffer(data: data)) + if let mode = reader.readBytesAsInt32(1), let length = reader.readInt32(), let passwordData = reader.readBuffer(Int(length))?.makeData(), let passwordText = String(data: passwordData, encoding: .utf8) { + var lockTimeout: Int32? + if let value = UserDefaults.standard.object(forKey: "Passcode_lockTimeout") as? Int { + if value == 0 { + lockTimeout = nil + } else { + lockTimeout = max(60, Int32(clamping: value)) + } + } else { + lockTimeout = 1 * 60 * 60 + } + + if mode == 3 { + passcodeChallenge = .numericalPassword(value: passwordText, timeout: lockTimeout, attempts: nil) + } else if mode == 4 { + passcodeChallenge = PostboxAccessChallengeData.plaintextPassword(value: passwordText, timeout: lockTimeout, attempts: nil) + } + } + } + + var passcodeEnableBiometrics: Bool = true + if let value = UserDefaults.standard.object(forKey: "Passcode_useTouchId") as? Bool { + passcodeEnableBiometrics = value + } + + var localization: TGLocalization? + if let nativeDocumentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first { + localization = NSKeyedUnarchiver.unarchiveObject(withFile: nativeDocumentsPath + "/localization") as? TGLocalization + } + + return account.postbox.transaction { transaction -> Void in + transaction.updatePreferencesEntry(key: ApplicationSpecificPreferencesKeys.presentationThemeSettings, { current in + var settings = (current as? PresentationThemeSettings) ?? PresentationThemeSettings.defaultSettings + if let presentationState = presentationState { + switch presentationState.pallete { + case 1: + settings.theme = .builtin(.day) + + if presentationState.userInfo != 0 { + settings.themeAccentColor = presentationState.userInfo + } + settings.chatWallpaper = .color(0xffffff) + case 2: + settings.theme = .builtin(.nightGrayscale) + settings.chatWallpaper = .color(0x00000) + case 3: + settings.theme = .builtin(.nightAccent) + settings.chatWallpaper = .color(0x18222D) + default: + settings.theme = .builtin(.dayClassic) + settings.chatWallpaper = .builtin + } + let fontSizeMap: [Int32: PresentationFontSize] = [ + 14: .extraSmall, + 15: .small, + 16: .medium, + 17: .regular, + 19: .large, + 23: .extraLarge, + 26: .extraLargeX2 + ] + settings.fontSize = fontSizeMap[presentationState.fontSize] ?? .regular + + if presentationState.userInfo != 0 { + settings.themeAccentColor = presentationState.userInfo + } + } + + if let autoNightPreferences = autoNightPreferences { + let nightTheme: PresentationBuiltinThemeReference + switch autoNightPreferences.preferredPalette { + case 1: + nightTheme = .nightGrayscale + default: + nightTheme = .nightAccent + } + switch autoNightPreferences.mode { + case TGPresentationAutoNightModeSunsetSunrise: + settings.automaticThemeSwitchSetting = AutomaticThemeSwitchSetting(trigger: .timeBased(setting: .automatic(latitude: Double(autoNightPreferences.latitude), longitude: Double(autoNightPreferences.longitude), sunset: autoNightPreferences.scheduleStart, sunrise: autoNightPreferences.scheduleEnd, localizedName: autoNightPreferences.cachedLocationName)), theme: nightTheme) + case TGPresentationAutoNightModeScheduled: + settings.automaticThemeSwitchSetting = AutomaticThemeSwitchSetting(trigger: .timeBased(setting: .manual(fromSeconds: autoNightPreferences.scheduleStart, toSeconds: autoNightPreferences.scheduleEnd)), theme: nightTheme) + case TGPresentationAutoNightModeBrightness: + settings.automaticThemeSwitchSetting = AutomaticThemeSwitchSetting(trigger: .brightness(threshold: Double(autoNightPreferences.brightnessThreshold)), theme: nightTheme) + default: + break + } + } + + if let wallpaperInfo = wallpaperInfo as? TGBuiltinWallpaperInfo, wallpaperInfo.isDefault() { + settings.chatWallpaper = .builtin + } else if let wallpaperInfo = wallpaperInfo as? TGRemoteWallpaperInfo, let data = try? Data(contentsOf: URL(fileURLWithPath: documentsPath + "/wallpaper-data/_currentWallpaper.jpg")), let image = UIImage(data: data) { + let url = wallpaperInfo.fullscreenUrl()! + if let resource = resourceFromLegacyImageUrl(url) { + settings.chatWallpaper = .image([TelegramMediaImageRepresentation(dimensions: image.size, resource: resource)]) + account.postbox.mediaBox.storeResourceData(resource.id, data: data) + } + } else if let wallpaperInfo = wallpaperInfo as? TGColorWallpaperInfo { + settings.chatWallpaper = .color(Int32(bitPattern: wallpaperInfo.color)) + } else if let data = try? Data(contentsOf: URL(fileURLWithPath: documentsPath + "/wallpaper-data/_currentWallpaper.jpg")), let image = UIImage(data: data) { + let resource = LocalFileMediaResource(fileId: arc4random64()) + settings.chatWallpaper = .image([TelegramMediaImageRepresentation(dimensions: image.size, resource: resource)]) + account.postbox.mediaBox.storeResourceData(resource.id, data: data) + } + + return settings + }) + + transaction.updatePreferencesEntry(key: ApplicationSpecificPreferencesKeys.automaticMediaDownloadSettings, { current in + var settings: AutomaticMediaDownloadSettings = current as? AutomaticMediaDownloadSettings ?? .defaultSettings + + if let preferences = autoDownloadPreferences, !preferences.isDefaultPreferences() { + settings.masterEnabled = !preferences.disabled + + let peerPaths: [(WritableKeyPath, TGAutoDownloadMode, TGAutoDownloadMode)] = [ + (\AutomaticMediaDownloadPeers.contacts, TGAutoDownloadModeCellularContacts, TGAutoDownloadModeWifiContacts), + (\AutomaticMediaDownloadPeers.channels, TGAutoDownloadModeCellularChannels, TGAutoDownloadModeWifiChannels), + (\AutomaticMediaDownloadPeers.groups, TGAutoDownloadModeCellularGroups, TGAutoDownloadModeWifiGroups), + (\AutomaticMediaDownloadPeers.otherPrivate, TGAutoDownloadModeCellularPrivateChats, TGAutoDownloadModeWifiPrivateChats) + ] + + let categoryPaths: [(WritableKeyPath, TGAutoDownloadMode, Int32)] = [ + (\AutomaticMediaDownloadCategories.photo, preferences.photos, Int32.max), + (\AutomaticMediaDownloadCategories.file, preferences.documents, preferences.maximumDocumentSize), + (\AutomaticMediaDownloadCategories.video, preferences.videos, preferences.maximumVideoSize), + (\AutomaticMediaDownloadCategories.videoMessage, preferences.videoMessages, 1 * 1024 * 1024), + (\AutomaticMediaDownloadCategories.voiceMessage, preferences.voiceMessages, 1 * 1024 * 1024) + ] + + for (categoryPath, category, maxSize) in categoryPaths { + for (peerPath, cellular, wifi) in peerPaths { + let targetPath = peerPath.appending(path: categoryPath) + settings.peers[keyPath: targetPath] = AutomaticMediaDownloadCategory(cellular: (category.rawValue & cellular.rawValue) != 0, wifi: (category.rawValue & wifi.rawValue) != 0, sizeLimit: maxSize) + } + } + } + + if let parsedAutoplayGifs = parsedAutoplayGifs { + settings.autoplayGifs = parsedAutoplayGifs + } + + return settings + }) + + transaction.updatePreferencesEntry(key: ApplicationSpecificPreferencesKeys.inAppNotificationSettings, { current in + var settings: InAppNotificationSettings = current as? InAppNotificationSettings ?? .defaultSettings + if let soundEnabled = soundEnabled { + settings.playSounds = soundEnabled + } + if let vibrationEnabled = vibrationEnabled { + settings.vibrate = vibrationEnabled + } + if let bannerEnabled = bannerEnabled { + settings.displayPreviews = bannerEnabled + } + return settings + }) + + transaction.updatePreferencesEntry(key: ApplicationSpecificPreferencesKeys.voiceCallSettings, { current in + var settings: VoiceCallSettings = current as? VoiceCallSettings ?? .defaultSettings + if let callsDataUsageMode = callsDataUsageMode { + switch callsDataUsageMode { + case 1: + settings.dataSaving = .cellular + case 2: + settings.dataSaving = .always + default: + settings.dataSaving = .never + } + } + if let callsDisableP2P = callsDisableP2P, callsDisableP2P { + settings.legacyP2PMode = .never + } + if let callsDisableCallKit = callsDisableCallKit, callsDisableCallKit { + settings.enableSystemIntegration = false + } + return settings + }) + + transaction.updatePreferencesEntry(key: PreferencesKeys.proxySettings, { current in + var settings: ProxySettings = current as? ProxySettings ?? .defaultSettings + if let callsUseProxy = callsUseProxy { + settings.useForCalls = callsUseProxy + } + + if let proxyList = proxyList { + for item in proxyList { + let connection: ProxyServerConnection? + if item.isMTProxy, let secret = item.secret { + let data = dataWithHexString(secret) + var secretIsValid = false + if data.count == 16 { + secretIsValid = true + } else if data.count == 17 && MTSocksProxySettings.secretSupportsExtendedPadding(data) { + secretIsValid = true + } + if secretIsValid { + connection = .mtp(secret: data) + } else { + connection = nil + } + } else if !item.isMTProxy { + connection = .socks5(username: item.username ?? "", password: item.password ?? "") + } else { + connection = nil + } + if let connection = connection { + settings.servers.append(ProxyServerSettings(host: item.server, port: convertLegacyProxyPort(Int(item.port)), connection: connection)) + } + } + } + + if let (server, active) = selectedProxy { + if !settings.servers.contains(server) { + settings.servers.insert(server, at: 0) + } + settings.activeServer = server + settings.enabled = active + } + + return settings + }) + + transaction.updatePreferencesEntry(key: ApplicationSpecificPreferencesKeys.stickerSettings, { current in + var settings: StickerSettings = current as? StickerSettings ?? .defaultSettings + if let stickersSuggestMode = stickersSuggestMode { + switch stickersSuggestMode { + case 1: + settings.emojiStickerSuggestionMode = .installed + case 2: + settings.emojiStickerSuggestionMode = .none + default: + settings.emojiStickerSuggestionMode = .all + } + } + return settings + }) + + transaction.updatePreferencesEntry(key: ApplicationSpecificPreferencesKeys.contactSynchronizationSettings, { current in + var settings: ContactSynchronizationSettings = current as? ContactSynchronizationSettings ?? .defaultSettings + if let contactsInhibitSync = contactsInhibitSync, contactsInhibitSync { + settings.synchronizeDeviceContacts = false + } + return settings + }) + + transaction.updatePreferencesEntry(key: ApplicationSpecificPreferencesKeys.callListSettings, { current in + var settings: CallListSettings = current as? CallListSettings ?? .defaultSettings + if let showCallsTab = showCallsTab { + settings.showTab = showCallsTab + } + return settings + }) + + transaction.updatePreferencesEntry(key: ApplicationSpecificPreferencesKeys.presentationPasscodeSettings, { current in + var settings: PresentationPasscodeSettings = current as? PresentationPasscodeSettings ?? .defaultSettings + if let passcodeChallenge = passcodeChallenge { + transaction.setAccessChallengeData(passcodeChallenge) + switch passcodeChallenge { + case .none: + break + case let .numericalPassword(_, timeout, _): + settings.autolockTimeout = timeout + case let .plaintextPassword(_, timeout, _): + settings.autolockTimeout = timeout + } + settings.enableBiometrics = passcodeEnableBiometrics + } + return settings + }) + + if let localization = localization { + transaction.updatePreferencesEntry(key: PreferencesKeys.localizationSettings, { _ in + var entries: [LocalizationEntry] = [] + for (key, value) in localization.dict() { + entries.append(LocalizationEntry.string(key: key, value: value)) + } + return LocalizationSettings(primaryComponent: LocalizationComponent(languageCode: localization.code, localizedName: "", localization: Localization(version: 0, entries: entries), customPluralizationCode: nil), secondaryComponent: nil) + }) + } + + if let secretInlineBotsInitialized = secretInlineBotsInitialized, secretInlineBotsInitialized { + ApplicationSpecificNotice.setSecretChatInlineBotUsage(transaction: transaction) + } + + if let allowSecretWebpagesInitialized = allowSecretWebpagesInitialized, allowSecretWebpagesInitialized, let allowSecretWebpages = allowSecretWebpages { + ApplicationSpecificNotice.setSecretChatLinkPreviews(transaction: transaction, value: allowSecretWebpages) + } + + transaction.updatePreferencesEntry(key: ApplicationSpecificPreferencesKeys.musicPlaybackSettings, { current in + var settings: MusicPlaybackSettings = current as? MusicPlaybackSettings ?? .defaultSettings + if let musicPlayerOrderType = musicPlayerOrderType { + switch musicPlayerOrderType { + case 1: + settings.order = .reversed + case 2: + settings.order = .random + default: + settings.order = .regular + } + } + if let musicPlayerRepeatType = musicPlayerRepeatType { + switch musicPlayerRepeatType { + case 1: + settings.looping = .all + case 2: + settings.looping = .item + default: + settings.looping = .none + } + } + return settings + }) + + transaction.updatePreferencesEntry(key: ApplicationSpecificPreferencesKeys.instantPagePresentationSettings, { current in + var settings: InstantPagePresentationSettings = current as? InstantPagePresentationSettings ?? .defaultSettings + if let instantPageFontSize = instantPageFontSize { + switch instantPageFontSize { + case 0.85: + settings.fontSize = .small + case 1.15: + settings.fontSize = .large + case 1.30: + settings.fontSize = .xlarge + case 1.50: + settings.fontSize = .xxlarge + default: + settings.fontSize = .standard + } + } + if let instantPageFontSerif = instantPageFontSerif { + settings.forceSerif = instantPageFontSerif == 1 + } + if let instantPageTheme = instantPageTheme { + switch instantPageTheme { + case 1: + settings.themeType = .sepia + case 2: + settings.themeType = .gray + case 3: + settings.themeType = .dark + default: + settings.themeType = .light + } + } + if let instantPageAutoNightMode = instantPageAutoNightMode { + settings.autoNightMode = instantPageAutoNightMode == 1 + } + return settings + }) + } + |> ignoreValues + } +} diff --git a/Telegram-iOS/LegacyResourceImport.swift b/Telegram-iOS/LegacyResourceImport.swift new file mode 100644 index 0000000000..e8bc22ae67 --- /dev/null +++ b/Telegram-iOS/LegacyResourceImport.swift @@ -0,0 +1,138 @@ +import Foundation +import TelegramCore +import SwiftSignalKit +import Postbox + +import LegacyComponents + +func resourceFromLegacyImageUrl(_ fileRef: String) -> TelegramMediaResource? { + if fileRef.isEmpty { + return nil + } + let components = fileRef.components(separatedBy: "_") + if components.count != 4 { + return nil + } + + guard let datacenterId = Int32(components[0]) else { + return nil + } + guard let volumeId = Int64(components[1]) else { + return nil + } + guard let localId = Int32(components[2]) else { + return nil + } + guard let secret = Int64(components[3]) else { + return nil + } + + return CloudFileMediaResource(datacenterId: Int(datacenterId), volumeId: volumeId, localId: localId, secret: secret, size: nil, fileReference: nil) +} + +func pathFromLegacyImageUrl(basePath: String, url: String) -> String { + let cache = TGCache(cachesPath: basePath + "/Caches")! + return cache.path(forCachedData: url) +} + +func pathFromLegacyVideoUrl(basePath: String, url: String) -> (id: Int64, accessHash: Int64, datacenterId: Int32, path: String)? { + if !url.hasPrefix("video:") { + return nil + } + //[videoInfo addVideoWithQuality:1 url:[[NSString alloc] initWithFormat:@"video:%lld:%lld:%d:%d", videoMedia.videoId, videoMedia.accessHash, concreteResult.document.datacenterId, concreteResult.document.size] size:concreteResult.document.size]; + let components = url.components(separatedBy: ":") + if components.count != 5 { + return nil + } + guard let videoId = Int64(components[1]) else { + return nil + } + guard let accessHash = Int64(components[2]) else { + return nil + } + guard let datacenterId = Int32(components[3]) else { + return nil + } + let documentsPath = basePath + "/Documents" + let videoPath = documentsPath + "/video/remote\(String(videoId, radix: 16)).mov" + return (videoId, accessHash, datacenterId, videoPath) +} + +func pathFromLegacyLocalVideoUrl(basePath: String, url: String) -> String? { + let documentsPath = basePath + "/Documents" + if !url.hasPrefix("local-video:") { + return nil + } + let videoPath = documentsPath + "/video/" + String(url[url.index(url.startIndex, offsetBy: "local-video:".count)...]) + return videoPath +} + +func pathFromLegacyFile(basePath: String, fileId: Int64, isLocal: Bool, fileName: String) -> String { + let documentsPath = basePath + "/Documents" + let filePath = documentsPath + "/files/" + (isLocal ? "local" : "") + "\(String(fileId, radix: 16))/\(fileName)" + return filePath +} + +enum EncryptedFileType { + case image + case video + case document(fileName: String) + case audio +} + +func pathAndResourceFromEncryptedFileUrl(basePath: String, url: String, type: EncryptedFileType) -> (String, TelegramMediaResource)? { + let cache = TGCache(cachesPath: basePath + "/Caches")! + + if url.hasPrefix("encryptedThumbnail:") { + let path = cache.path(forCachedData: url)! + return (path, LocalFileMediaResource(fileId: arc4random64())) + } + + if !url.hasPrefix("mt-encrypted-file://?") { + return nil + } + guard let dict = TGStringUtils.argumentDictionary(inUrlString: String(url[url.index(url.startIndex, offsetBy: "mt-encrypted-file://?".count)...])) else { + return nil + } + guard let idString = dict["id"] as? String, let id = Int64(idString) else { + return nil + } + guard let datacenterIdString = dict["dc"] as? String, let datacenterId = Int32(datacenterIdString) else { + return nil + } + guard let accessHashString = dict["accessHash"] as? String, let accessHash = Int64(accessHashString) else { + return nil + } + guard let sizeString = dict["size"] as? String, let size = Int32(sizeString) else { + return nil + } + guard let decryptedSizeString = dict["decryptedSize"] as? String, let decryptedSize = Int32(decryptedSizeString) else { + return nil + } + guard let keyFingerprintString = dict["fingerprint"] as? String, let _ = Int32(keyFingerprintString) else { + return nil + } + guard let keyString = dict["key"] as? String else { + return nil + } + let keyData = dataWithHexString(keyString) + guard keyData.count == 64 else { + return nil + } + + let resource = SecretFileMediaResource(fileId: id, accessHash: accessHash, containerSize: size, decryptedSize: decryptedSize, datacenterId: Int(datacenterId), key: SecretFileEncryptionKey(aesKey: keyData.subdata(in: 0 ..< 32), aesIv: keyData.subdata(in: 32 ..< 64))) + + let filePath: String + switch type { + case .video: + filePath = basePath + "Documents/video/remote\(String(id, radix: 16)).mov" + case .image: + filePath = cache.path(forCachedData: url) + case let .document(fileName): + filePath = basePath + "Documents/files/\(String(id, radix: 16))/\(TGDocumentMediaAttachment.safeFileName(forFileName: fileName)!)" + case .audio: + filePath = basePath + "Documents/audio/\(String(id, radix: 16))" + } + + return (filePath, resource) +} diff --git a/Telegram-iOS/LegacyUserDataImport.swift b/Telegram-iOS/LegacyUserDataImport.swift new file mode 100644 index 0000000000..da085ea45d --- /dev/null +++ b/Telegram-iOS/LegacyUserDataImport.swift @@ -0,0 +1,46 @@ +import Foundation +import TelegramCore +import SwiftSignalKit +import Postbox + +func loadLegacyUser(database: SqliteInterface, id: Int32) -> (TelegramUser, TelegramUserPresence)? { + var result: (TelegramUser, TelegramUserPresence)? + database.select("SELECT uid, first_name, last_name, phone_number, access_hash, photo_small, photo_big, last_seen, username FROM users_v29 WHERE uid=\(id)", { cursor in + let accessHash = cursor.getInt64(at: 4) + let firstName = cursor.getString(at: 1) + let lastName = cursor.getString(at: 2) + let username = cursor.getString(at: 8) + let phone = cursor.getString(at: 3) + + let photoSmall = cursor.getString(at: 5) + let photoBig = cursor.getString(at: 6) + var photo: [TelegramMediaImageRepresentation] = [] + if let resource = resourceFromLegacyImageUrl(photoSmall) { + photo.append(TelegramMediaImageRepresentation(dimensions: CGSize(width: 80.0, height: 80.0), resource: resource)) + } + if let resource = resourceFromLegacyImageUrl(photoBig) { + photo.append(TelegramMediaImageRepresentation(dimensions: CGSize(width: 600.0, height: 600.0), resource: resource)) + } + + let user = TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: cursor.getInt32(at: 0)), accessHash: accessHash == 0 ? nil : accessHash, firstName: firstName.isEmpty ? nil : firstName, lastName: lastName.isEmpty ? nil : lastName, username: username.isEmpty ? nil : username, phone: phone.isEmpty ? nil : phone, photo: photo, botInfo: nil, restrictionInfo: nil, flags: []) + + let status: UserPresenceStatus + let lastSeen = cursor.getInt32(at: 7) + if lastSeen == -2 { + status = .recently + } else if lastSeen == -3 { + status = .lastWeek + } else if lastSeen == -4 { + status = .lastMonth + } else if lastSeen <= 0 { + status = .none + } else { + status = .present(until: lastSeen) + } + + let presence = TelegramUserPresence(status: status) + result = (user, presence) + return false + }) + return result +} diff --git a/Telegram-iOS/LockedWindowCoveringView.swift b/Telegram-iOS/LockedWindowCoveringView.swift new file mode 100644 index 0000000000..08c55a58a2 --- /dev/null +++ b/Telegram-iOS/LockedWindowCoveringView.swift @@ -0,0 +1,35 @@ +import Foundation +import Display +import TelegramUI +import AsyncDisplayKit + +final class LockedWindowCoveringView: WindowCoveringView { + private let contentView: UIImageView + + init(theme: PresentationTheme) { + self.contentView = UIImageView() + + super.init(frame: CGRect()) + + self.backgroundColor = theme.chatList.backgroundColor + self.addSubview(self.contentView) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func updateTheme(_ theme: PresentationTheme) { + self.backgroundColor = theme.chatList.backgroundColor + } + + func updateSnapshot(_ image: UIImage?) { + if image != nil { + self.contentView.image = image + } + } + + override func updateLayout(_ size: CGSize) { + self.contentView.frame = CGRect(origin: CGPoint(), size: size) + } +} diff --git a/Telegram-iOS/NotificationManager.swift b/Telegram-iOS/NotificationManager.swift new file mode 100644 index 0000000000..c49ed4ee34 --- /dev/null +++ b/Telegram-iOS/NotificationManager.swift @@ -0,0 +1,797 @@ +import Foundation +import Postbox +import TelegramCore +import SwiftSignalKit +import UserNotifications +import TelegramUI + +private func notificationKey(_ requestId: NotificationManagedNotificationRequestId) -> String { + switch requestId { + case let .messageId(id): + return "m\(id.peerId.toInt64()):\(id.namespace):\(id.id)" + case let .globallyUniqueId(id, _): + return "m\(id)" + } +} + +private let messageNotificationKeyExpr = try? NSRegularExpression(pattern: "m([-\\d]+):([-\\d]+):([-\\d]+)_?", options: []) + +enum NotificationManagedNotificationRequestId: Hashable { + case messageId(MessageId) + case globallyUniqueId(Int64, PeerId?) + + init?(string: String) { + if string.hasPrefix("m") { + let matches = messageNotificationKeyExpr!.matches(in: string, options: [], range: NSRange(location: 0, length: string.count)) + if let match = matches.first { + let nsString = string as NSString + let peerIdString = nsString.substring(with: match.range(at: 1)) + let namespaceString = nsString.substring(with: match.range(at: 2)) + let idString = nsString.substring(with: match.range(at: 3)) + + guard let peerId = Int64(peerIdString) else { + return nil + } + guard let namespace = Int32(namespaceString) else { + return nil + } + guard let id = Int32(idString) else { + return nil + } + self = .messageId(MessageId(peerId: PeerId(peerId), namespace: namespace, id: id)) + return + } + } + return nil + } + + var hashValue: Int { + switch self { + case let .messageId(messageId): + return messageId.id.hashValue + case let .globallyUniqueId(id, _): + return id.hashValue + } + } + + static func ==(lhs: NotificationManagedNotificationRequestId, rhs: NotificationManagedNotificationRequestId) -> Bool { + switch lhs { + case let .messageId(id): + if case .messageId(id) = rhs { + return true + } else { + return false + } + case let .globallyUniqueId(id, peerId): + if case .globallyUniqueId(id, peerId) = rhs { + return true + } else { + return false + } + } + } +} + +private func processedSoundName(_ name: String) -> String { + if name.hasSuffix("m4a") { + return name + } else { + return "\(name).m4a" + } +} + +final class NotificationManager { + private var processedMessages = Set() + + var account: Account? { + didSet { + assert(Queue.mainQueue().isCurrent()) + + if let account = self.account { + self.notificationMessagesDisposable.set((account.stateManager.notificationMessages + |> deliverOn(Queue.mainQueue())).start(next: { [weak self] messages in + guard let strongSelf = self else { + return + } + var list: [([Message], PeerMessageSound, Bool, Bool)] = [] + for (messageGroup, _, notify) in messages { + list.append((messageGroup, .default, !strongSelf.isApplicationLocked, false)) + } + //self?.processNotificationMessages(list, isLocked: strongSelf.isApplicationLocked) + })) + } else { + self.notificationMessagesDisposable.set(nil) + } + } + } + + private let notificationCallStateDisposable = MetaDisposable() + var notificationCall: PresentationCall? { + didSet { + if self.notificationCall?.internalId != oldValue?.internalId { + if let notificationCall = self.notificationCall { + let peer = notificationCall.peer + let internalId = notificationCall.internalId + let isIntegratedWithCallKit = notificationCall.isIntegratedWithCallKit + self.notificationCallStateDisposable.set((notificationCall.state + |> map { state -> (Peer?, CallSessionInternalId)? in + if isIntegratedWithCallKit { + return nil + } + if case .ringing = state { + return (peer, internalId) + } else { + return nil + } + } + |> distinctUntilChanged(isEqual: { $0?.1 == $1?.1 })).start(next: { [weak self] peerAndInternalId in + self?.updateNotificationCall(call: peerAndInternalId) + })) + } else { + self.notificationCallStateDisposable.set(nil) + self.updateNotificationCall(call: nil) + } + } + } + } + + private let notificationMessagesDisposable = MetaDisposable() + + private var notificationRequests: [NotificationManagedNotificationRequestId: Double] = [:] + private var processedRequestIds = Set() + + var isApplicationInForeground: Bool = false + var isApplicationLocked: Bool = false + + deinit { + self.notificationMessagesDisposable.dispose() + } + + func enqueueRemoteNotification(title: String, text: String, apnsSound: String?, requestId: NotificationManagedNotificationRequestId, strings: PresentationStrings, accessChallengeData: PostboxAccessChallengeData) { + if notificationRequests[requestId] == nil && !processedRequestIds.contains(requestId) { + var isLocked = false + if isAccessLocked(data: accessChallengeData, at: Int32(CFAbsoluteTimeGetCurrent())) { + isLocked = true + } + + var userInfo: [AnyHashable: Any]? + let category: String + let delay: Bool + var threadIdentifier: String? + switch requestId { + case let .messageId(messageId): + if messageId.namespace == Namespaces.Message.Local { + delay = false + } else { + delay = true + } + userInfo = ["peerId": messageId.peerId.toInt64()] + threadIdentifier = "peer_\(messageId.peerId.toInt64())" + if messageId.peerId.namespace == Namespaces.Peer.CloudUser || messageId.peerId.namespace == Namespaces.Peer.CloudGroup { + category = "withReply" + } else { + category = "withMute" + } + case let .globallyUniqueId(_, peerId): + delay = false + category = "secret" + if let peerId = peerId { + userInfo = ["peerId": peerId.toInt64()] + threadIdentifier = "peer_\(peerId.toInt64())" + } + } + + if #available(iOS 10.0, *) { + let content = UNMutableNotificationContent() + if isLocked { + content.body = strings.LOCKED_MESSAGE("").0 + } else { + if title.isEmpty { + content.body = text + } else { + content.body = "\(title): \(text)" + } + } + if let apnsSound = apnsSound { + if apnsSound == "0" { + content.sound = nil + } else { + content.sound = UNNotificationSound(named: processedSoundName(apnsSound)) + } + } else { + content.sound = UNNotificationSound(named: "0.m4a") + } + if let threadIdentifier = threadIdentifier { + content.threadIdentifier = threadIdentifier + } + + content.categoryIdentifier = category + if let userInfo = userInfo { + content.userInfo = userInfo + } + + let request = UNNotificationRequest(identifier: notificationKey(requestId), content: content, trigger: delay ? UNTimeIntervalNotificationTrigger(timeInterval: 25.0, repeats: false) : nil) + + let center = UNUserNotificationCenter.current() + Logger.shared.log("NotificationManager", "adding \(requestId), delay: \(delay)") + center.add(request, withCompletionHandler: { error in + if let error = error { + Logger.shared.log("NotificationManager", "error adding \(requestId), delay: \(delay), error: \(String(describing: error))") + } + }) + + if delay { + notificationRequests[requestId] = CFAbsoluteTimeGetCurrent() + 25.0 + } + } else { + let notification = UILocalNotification() + if isLocked { + notification.alertBody = strings.LOCKED_MESSAGE("").0 + } else { + if #available(iOS 8.2, *) { + notification.alertTitle = title + notification.alertBody = text + } else { + if !title.isEmpty { + notification.alertBody = title + ": " + text + } else { + notification.alertBody = text + } + } + } + notification.category = category + var updatedUserInfo = userInfo ?? [:] + updatedUserInfo["id"] = notificationKey(requestId) + notification.userInfo = updatedUserInfo + if delay { + notification.fireDate = Date(timeIntervalSinceNow: 25.0) + } + if let apnsSound = apnsSound { + if apnsSound == "0" { + notification.soundName = nil + } else { + notification.soundName = processedSoundName(apnsSound) + } + } else { + notification.soundName = "0.m4a" + } + UIApplication.shared.scheduleLocalNotification(notification) + + if delay { + notificationRequests[requestId] = CFAbsoluteTimeGetCurrent() + 25.0 + } + } + } + } + + func commitRemoteNotification(originalRequestId: NotificationManagedNotificationRequestId?, messageIds: [MessageId]) -> Signal { + if let account = self.account { + return account.postbox.transaction { transaction -> ([(MessageId, [Message], Bool, PeerMessageSound, Bool)], Bool) in + var isLocked = false + if isAccessLocked(data: transaction.getAccessChallengeData(), at: Int32(CFAbsoluteTimeGetCurrent())) { + isLocked = true + } + + var results: [(MessageId, [Message], Bool, PeerMessageSound, Bool)] = [] + var updatedMessageIds = messageIds + if let originalRequestId = originalRequestId { + switch originalRequestId { + case let .messageId(id): + if !updatedMessageIds.contains(id) { + updatedMessageIds.append(id) + } + case .globallyUniqueId: + break + } + } + for id in updatedMessageIds { + let (messages, notify, sound, displayContents) = messagesForNotification(transaction: transaction, id: id, alwaysReturnMessage: true) + + if results.contains(where: { result in + return result.1.contains(where: { message in + return messages.contains(where: { + message.id == $0.id + }) + }) + }) { + continue + } + results.append((id, messages, notify, sound, displayContents)) + } + return (results, isLocked) + } + |> deliverOnMainQueue + |> beforeNext { + [weak self] results, isLocked in + if let strongSelf = self { + let delayUntilTimestamp: Int32 = strongSelf.account?.stateManager.getDelayNotificatonsUntil() ?? 0 + + for (id, messages, notify, sound, displayContents) in results { + let requestId: NotificationManagedNotificationRequestId = .messageId(id) + if let message = messages.first, message.id.peerId.namespace != Namespaces.Peer.SecretChat, !strongSelf.processedRequestIds.contains(requestId) { + let notificationRequestTimeout = strongSelf.notificationRequests[requestId] + if notificationRequestTimeout == nil || CFAbsoluteTimeGetCurrent() < notificationRequestTimeout! { + if #available(iOS 10.0, *) { + let center = UNUserNotificationCenter.current() + center.removePendingNotificationRequests(withIdentifiers: [notificationKey(requestId)]) + } else { + let key = notificationKey(requestId) + if let notifications = UIApplication.shared.scheduledLocalNotifications { + for notification in notifications { + if let userInfo = notification.userInfo, let id = userInfo["id"] as? String { + if id == key { + UIApplication.shared.cancelLocalNotification(notification) + break + } + } + } + } + } + + if !strongSelf.processedRequestIds.contains(requestId) { + strongSelf.processedRequestIds.insert(requestId) + + if notify { + var delayMessage = false + if message.timestamp <= delayUntilTimestamp && message.id.peerId.namespace != Namespaces.Peer.SecretChat { + delayMessage = true + } + strongSelf.processNotificationMessages([(messages, sound, displayContents, delayMessage)], isLocked: isLocked) + } + } + } + } + } + } + } |> map { _ in + return Void() + } + } else { + return .complete() + } + } + + private func processNotificationMessages(_ messageList: [([Message], PeerMessageSound, Bool, Bool)], isLocked: Bool) { + guard let account = self.account else { + Logger.shared.log("NotificationManager", "account missing") + return + } + let strings = (account.telegramApplicationContext.currentPresentationData.with { $0 }).strings + for (messages, sound, initialDisplayContents, delayMessage) in messageList { + for message in messages { + self.processedMessages.insert(message.id) + } + guard let firstMessage = messages.first else { + continue + } + let displayContents = initialDisplayContents && !isLocked + + let requestId: NotificationManagedNotificationRequestId + if let globallyUniqueId = firstMessage.globallyUniqueId, firstMessage.id.peerId.namespace == Namespaces.Peer.SecretChat { + requestId = .globallyUniqueId(globallyUniqueId, firstMessage.id.peerId) + } else { + requestId = .messageId(firstMessage.id) + } + + let notificationRequestTimeout = notificationRequests[requestId] + + if notificationRequestTimeout == nil || CFAbsoluteTimeGetCurrent() < notificationRequestTimeout! { + if self.isApplicationInForeground { + processedRequestIds.insert(requestId) + if notificationRequestTimeout != nil { + if #available(iOS 10.0, *) { + UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: [notificationKey(requestId)]) + } else { + let key = notificationKey(requestId) + if let notifications = UIApplication.shared.scheduledLocalNotifications { + for notification in notifications { + if let userInfo = notification.userInfo, let id = userInfo["id"] as? String { + if id == key { + UIApplication.shared.cancelLocalNotification(notification) + break + } + } + } + } + } + } + } else { + var title: String? + var body: String + var mediaRepresentations: [TelegramMediaImageRepresentation]? + var mediaInfo: [String: Any]? = nil + + body = firstMessage.text + if let peer = messageMainPeer(firstMessage) { + var displayAuthor = true + if let channel = peer as? TelegramChannel { + switch channel.info { + case .group: + displayAuthor = true + case .broadcast: + displayAuthor = false + } + } else if let _ = peer as? TelegramUser { + displayAuthor = false + } + + if let author = firstMessage.author, displayAuthor { + title = author.compactDisplayTitle + "@" + peer.displayTitle + } else { + title = peer.displayTitle + } + + if messages.count > 1 { + if messages[0].forwardInfo != nil { + if let author = firstMessage.author, displayAuthor { + title = nil + body = strings.CHAT_MESSAGE_FWDS(author.compactDisplayTitle, peer.displayTitle, "\(messages.count)").0 + } else { + title = nil + body = strings.MESSAGE_FWDS(peer.displayTitle, "\(messages.count)").0 + } + } else if messages[0].groupingKey != nil { + var kind = messageContentKind(messages[0], strings: strings, accountPeerId: account.peerId).key + for i in 1 ..< messages.count { + let nextKind = messageContentKind(messages[i], strings: strings, accountPeerId: account.peerId) + if kind != nextKind.key { + kind = .text + break + } + } + var isChannel = false + var isGroup = false + if let peer = firstMessage.peers[firstMessage.id.peerId] as? TelegramChannel { + if case .broadcast = peer.info { + isChannel = true + } else { + isGroup = true + } + } else if firstMessage.id.peerId.namespace == Namespaces.Peer.CloudGroup { + isGroup = true + } + title = nil + if isChannel { + switch kind { + case .image: + body = strings.CHANNEL_MESSAGE_PHOTOS(peer.compactDisplayTitle, "\(messages.count)").0 + default: + body = strings.CHANNEL_MESSAGES(peer.compactDisplayTitle, "\(messages.count)").0 + } + } else if isGroup, let author = firstMessage.author { + switch kind { + case .image: + body = strings.CHAT_MESSAGE_PHOTOS(author.compactDisplayTitle, peer.displayTitle, "\(messages.count)").0 + default: + body = strings.CHAT_MESSAGES(author.compactDisplayTitle, peer.displayTitle, "\(messages.count)").0 + } + } else { + switch kind { + case .image: + body = strings.MESSAGE_PHOTOS(peer.displayTitle, "\(messages.count)").0 + default: + body = strings.MESSAGES(peer.displayTitle, "\(messages.count)").0 + } + } + } + } + + if messages.count == 1 { + let additionalPeers = firstMessage.peers + var isPin = false + for media in messages[0].media { + if let action = media as? TelegramMediaAction { + if case .pinnedMessageUpdated = action.action { + isPin = true + } + } + } + + if let channel = peer as? TelegramChannel, case .broadcast = channel.info, isPin { + title = nil + } + let chatPeer = RenderedPeer(peerId: firstMessage.id.peerId, peers: additionalPeers) + let (_, _, messageText) = chatListItemStrings(strings: strings, message: firstMessage, chatPeer: chatPeer, accountPeerId: account.peerId) + body = messageText + + loop: for media in firstMessage.media { + if let image = media as? TelegramMediaImage { + mediaRepresentations = image.representations + if !firstMessage.containsSecretMedia, let account = self.account, let smallest = smallestImageRepresentation(image.representations), let largest = largestImageRepresentation(image.representations) { + var imageInfo: [String: Any] = [:] + + var thumbnailInfo: [String: Any] = [:] + thumbnailInfo["path"] = account.postbox.mediaBox.resourcePath(smallest.resource) + imageInfo["thumbnail"] = thumbnailInfo + + var fullSizeInfo: [String: Any] = [:] + fullSizeInfo["path"] = account.postbox.mediaBox.resourcePath(largest.resource) + imageInfo["fullSize"] = fullSizeInfo + + imageInfo["width"] = Int(largest.dimensions.width) + imageInfo["height"] = Int(largest.dimensions.height) + + mediaInfo = ["image": imageInfo] + } + break loop + } else if let file = media as? TelegramMediaFile { + if !firstMessage.containsSecretMedia { + //mediaRepresentations = file.previewRepresentations + } + break loop + } else if let location = media as? TelegramMediaMap { + if location.liveBroadcastingTimeout != nil { + if let chatMainPeer = chatPeer.chatMainPeer { + if let user = chatMainPeer as? TelegramUser { + body = strings.MESSAGE_GEOLIVE(user.displayTitle).0 + } else if let _ = chatMainPeer as? TelegramGroup, let author = firstMessage.author { + body = strings.MESSAGE_GEOLIVE(author.displayTitle).0 + } else if let channel = chatMainPeer as? TelegramChannel { + switch channel.info { + case .group: + if let author = firstMessage.author { + body = strings.MESSAGE_GEOLIVE(author.displayTitle).0 + } + case .broadcast: + body = strings.CHANNEL_MESSAGE_GEOLIVE(chatMainPeer.displayTitle).0 + } + } + } + break loop + } + } + } + } + } else { + body = strings.ENCRYPTED_MESSAGE("").0 + } + + if isLocked { + title = nil + } + if !displayContents { + body = strings.ENCRYPTED_MESSAGE("").0 + } + + var userInfo: [AnyHashable: Any] = ["peerId": firstMessage.id.peerId.toInt64()] + userInfo["messageId.namespace"] = firstMessage.id.namespace + userInfo["messageId.id"] = firstMessage.id.id + let category: String + if displayContents, let peer = firstMessage.peers[firstMessage.id.peerId] { + switch peer { + case _ as TelegramUser: + if let mediaInfo = mediaInfo { + category = "withReplyMedia" + userInfo["mediaInfo"] = mediaInfo + } else { + category = "withReply" + } + case _ as TelegramGroup: + if let mediaInfo = mediaInfo { + category = "withReplyMedia" + userInfo["mediaInfo"] = mediaInfo + } else { + category = "withReply" + } + case let channel as TelegramChannel: + if case .group = channel.info { + if let mediaInfo = mediaInfo { + category = "withReplyMedia" + userInfo["mediaInfo"] = mediaInfo + } else { + category = "withReply" + } + } else { + if let mediaInfo = mediaInfo { + category = "withMuteMedia" + userInfo["mediaInfo"] = mediaInfo + } else { + category = "withMute" + } + } + default: + category = "withMute" + } + } else { + category = "withMute" + } + + if #available(iOS 10.0, *) { + UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: [notificationKey(requestId)]) + + let content = UNMutableNotificationContent() + if let title = title { + content.title = title + } + content.body = body + switch sound { + case .none: + content.sound = nil + case .default: + content.sound = UNNotificationSound(named: "0.m4a") + default: + content.sound = UNNotificationSound(named: fileNameForNotificationSound(sound, defaultSound: nil) + ".m4a") + } + content.categoryIdentifier = category + content.userInfo = userInfo + + content.threadIdentifier = "peer_\(firstMessage.id.peerId.toInt64())" + + if mediaInfo != nil, let mediaRepresentations = mediaRepresentations { + if let account = self.account, let smallest = smallestImageRepresentation(mediaRepresentations) { + /*if let path = account.postbox.mediaBox.completedResourcePath(smallest.resource) { + var randomId: Int64 = 0 + arc4random_buf(&randomId, 8) + let tempPath = NSTemporaryDirectory() + "/\(randomId).jpg" + if let _ = try? FileManager.default.copyItem(atPath: path, toPath: tempPath) { + if let attachment = try? UNNotificationAttachment(identifier: "image", url: URL(fileURLWithPath: tempPath)) { + content.attachments = [attachment] + } + } + }*/ + } + } + + var trigger: UNTimeIntervalNotificationTrigger? + if delayMessage { + trigger = UNTimeIntervalNotificationTrigger(timeInterval: 25.0, repeats: false) + } + let request = UNNotificationRequest(identifier: notificationKey(requestId) + "_", content: content, trigger: trigger) + + let center = UNUserNotificationCenter.current() + Logger.shared.log("NotificationManager", "adding \(requestId), delay: \(delayMessage)") + center.add(request, withCompletionHandler: { error in + if let error = error { + Logger.shared.log("NotificationManager", "error adding \(requestId), delay: \(delayMessage), error: \(String(describing: error))") + } + }) + processedRequestIds.insert(requestId) + } else { + let key = notificationKey(requestId) + if let notifications = UIApplication.shared.scheduledLocalNotifications { + for notification in notifications { + if let userInfo = notification.userInfo, let id = userInfo["id"] as? String { + if id == key { + UIApplication.shared.cancelLocalNotification(notification) + break + } + } + } + } + + let notification = UILocalNotification() + if #available(iOS 10.0, *) { + notification.alertTitle = title + notification.alertBody = body + } else { + if let title = title { + notification.alertBody = title + ": " + body + } else { + notification.alertBody = body + } + } + notification.category = category + var updatedUserInfo = userInfo + updatedUserInfo["id"] = notificationKey(requestId) + "_" + notification.userInfo = userInfo + switch sound { + case .none: + notification.soundName = nil + case .default: + notification.soundName = "0.m4a" + default: + notification.soundName = fileNameForNotificationSound(sound, defaultSound: nil) + ".m4a" + } + + UIApplication.shared.presentLocalNotificationNow(notification) + } + } + } else { + Logger.shared.log("NotificationManager", "not showing message because of timeout") + } + self.notificationRequests.removeValue(forKey: requestId) + } + } + + private var currentNotificationCall: (peer: Peer?, internalId: CallSessionInternalId)? + + private func updateNotificationCall(call: (peer: Peer?, internalId: CallSessionInternalId)?) { + if let previousCall = currentNotificationCall { + if #available(iOS 10.0, *) { + let center = UNUserNotificationCenter.current() + center.removeDeliveredNotifications(withIdentifiers: ["call_\(previousCall.internalId)"]) + } else { + if let notifications = UIApplication.shared.scheduledLocalNotifications { + for notification in notifications { + if let userInfo = notification.userInfo, let callId = userInfo["callId"] as? String, callId == String(describing: previousCall.internalId) { + UIApplication.shared.cancelLocalNotification(notification) + } + } + } + } + } + self.currentNotificationCall = call + + guard let account = self.account else { + return + } + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + if let notificationCall = call { + if #available(iOS 10.0, *) { + let content = UNMutableNotificationContent() + content.body = presentationData.strings.PHONE_CALL_REQUEST(notificationCall.peer?.displayTitle ?? "").0 + content.sound = UNNotificationSound(named: "0.m4a") + content.categoryIdentifier = "incomingCall" + content.userInfo = [:] + + let request = UNNotificationRequest(identifier: "call_\(notificationCall.internalId)", content: content, trigger: nil) + + let center = UNUserNotificationCenter.current() + Logger.shared.log("NotificationManager", "adding call \(notificationCall.internalId)") + center.add(request, withCompletionHandler: { error in + if let error = error { + Logger.shared.log("NotificationManager", "error adding call \(notificationCall.internalId), error: \(String(describing: error))") + } + }) + + } else { + let notification = UILocalNotification() + notification.alertBody = presentationData.strings.PHONE_CALL_REQUEST(notificationCall.peer?.displayTitle ?? "").0 + notification.category = "incomingCall" + notification.userInfo = ["callId": String(describing: notificationCall.internalId)] + notification.soundName = "0.m4a" + UIApplication.shared.presentLocalNotificationNow(notification) + } + } + } + + func presentWatchContinuityNotification(messageId: MessageId) { + if #available(iOS 10.0, *) { + let center = UNUserNotificationCenter.current() + center.removeDeliveredNotifications(withIdentifiers: ["watch"]) + } else { + if let notifications = UIApplication.shared.scheduledLocalNotifications { + for notification in notifications { + if let category = notification.category, category == "watch" { + UIApplication.shared.cancelLocalNotification(notification) + } + } + } + } + guard let account = self.account else { + return + } + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + + var userInfo: [AnyHashable : Any] = [:] + userInfo["peerId"] = messageId.peerId.toInt64() + userInfo["messageId.namespace"] = messageId.namespace + userInfo["messageId.id"] = messageId.id + + if #available(iOS 10.0, *) { + let content = UNMutableNotificationContent() + content.body = presentationData.strings.WatchRemote_NotificationText + content.sound = UNNotificationSound(named: "0.m4a") + content.categoryIdentifier = "watch" + content.userInfo = userInfo + + let request = UNNotificationRequest(identifier: "watch", content: content, trigger: nil) + + let center = UNUserNotificationCenter.current() + Logger.shared.log("NotificationManager", "adding watch continuity") + center.add(request, withCompletionHandler: { error in + if let error = error { + Logger.shared.log("NotificationManager", "error adding watch continuity, error: \(String(describing: error))") + } + }) + + } else { + let notification = UILocalNotification() + notification.alertBody = presentationData.strings.WatchRemote_NotificationText + notification.category = "watch" + notification.userInfo = userInfo + notification.soundName = "0.m4a" + UIApplication.shared.presentLocalNotificationNow(notification) + } + } +} diff --git a/Telegram-iOS/PreFetchedLegacyResource.swift b/Telegram-iOS/PreFetchedLegacyResource.swift new file mode 100644 index 0000000000..de13c480da --- /dev/null +++ b/Telegram-iOS/PreFetchedLegacyResource.swift @@ -0,0 +1,20 @@ +import Foundation +import Postbox +import TelegramCore + +import LegacyComponents + +func preFetchedLegacyResourcePath(basePath: String, resource: MediaResource, cache: TGCache) -> String? { + + if let resource = resource as? CloudDocumentMediaResource { + let videoPath = "\(basePath)/Documents/video/remote\(String(resource.fileId, radix: 16)).mov" + if FileManager.default.fileExists(atPath: videoPath) { + return videoPath + } + let fileName = resource.fileName?.replacingOccurrences(of: "/", with: "_") ?? "file" + return pathFromLegacyFile(basePath: basePath, fileId: resource.fileId, isLocal: false, fileName: fileName) + } else if let resource = resource as? CloudFileMediaResource { + return cache.path(forCachedData: "\(resource.datacenterId)_\(resource.volumeId)_\(resource.localId)_\(resource.secret)") + } + return nil +} diff --git a/Telegram-iOS/Resources/PhoneCountries.txt b/Telegram-iOS/Resources/PhoneCountries.txt new file mode 100644 index 0000000000..0f0932ed27 --- /dev/null +++ b/Telegram-iOS/Resources/PhoneCountries.txt @@ -0,0 +1,232 @@ +1876;JM;Jamaica +1869;KN;Saint Kitts & Nevis +1868;TT;Trinidad & Tobago +1784;VC;Saint Vincent & the Grenadines +1767;DM;Dominica +1758;LC;Saint Lucia +1721;SX;Sint Maarten +1684;AS;American Samoa +1671;GU;Guam +1670;MP;Northern Mariana Islands +1664;MS;Montserrat +1649;TC;Turks & Caicos Islands +1473;GD;Grenada +1441;BM;Bermuda +1345;KY;Cayman Islands +1340;VI;US Virgin Islands +1284;VG;British Virgin Islands +1268;AG;Antigua & Barbuda +1264;AI;Anguilla +1246;BB;Barbados +1242;BS;Bahamas +998;UZ;Uzbekistan +996;KG;Kyrgyzstan +995;GE;Georgia +994;AZ;Azerbaijan +993;TM;Turkmenistan +992;TJ;Tajikistan +977;NP;Nepal +976;MN;Mongolia +975;BT;Bhutan +974;QA;Qatar +973;BH;Bahrain +972;IL;Israel +971;AE;United Arab Emirates +970;PS;Palestine +968;OM;Oman +967;YE;Yemen +966;SA;Saudi Arabia +965;KW;Kuwait +964;IQ;Iraq +963;SY;Syrian Arab Republic +962;JO;Jordan +961;LB;Lebanon +960;MV;Maldives +886;TW;Taiwan +880;BD;Bangladesh +856;LA;Laos +855;KH;Cambodia +853;MO;Macau +852;HK;Hong Kong +850;KP;North Korea +692;MH;Marshall Islands +691;FM;Micronesia +690;TK;Tokelau +689;PF;French Polynesia +688;TV;Tuvalu +687;NC;New Caledonia +686;KI;Kiribati +685;WS;Samoa +683;NU;Niue +682;CK;Cook Islands +681;WF;Wallis & Futuna +680;PW;Palau +679;FJ;Fiji +678;VU;Vanuatu +677;SB;Solomon Islands +676;TO;Tonga +675;PG;Papua New Guinea +674;NR;Nauru +673;BN;Brunei Darussalam +672;NF;Norfolk Island +670;TL;Timor-Leste +599;BQ;Bonaire, Sint Eustatius & Saba +599;CW;Curaçao +598;UY;Uruguay +597;SR;Suriname +596;MQ;Martinique +595;PY;Paraguay +594;GF;French Guiana +593;EC;Ecuador +592;GY;Guyana +591;BO;Bolivia +590;GP;Guadeloupe +509;HT;Haiti +508;PM;Saint Pierre & Miquelon +507;PA;Panama +506;CR;Costa Rica +505;NI;Nicaragua +504;HN;Honduras +503;SV;El Salvador +502;GT;Guatemala +501;BZ;Belize +500;FK;Falkland Islands +423;LI;Liechtenstein +421;SK;Slovakia +420;CZ;Czech Republic +383;XK;Kosovo +389;MK;Macedonia +387;BA;Bosnia & Herzegovina +386;SI;Slovenia +385;HR;Croatia +382;ME;Montenegro +381;RS;Serbia +380;UA;Ukraine +378;SM;San Marino +377;MC;Monaco +376;AD;Andorra +375;BY;Belarus +374;AM;Armenia +373;MD;Moldova +372;EE;Estonia +371;LV;Latvia +370;LT;Lithuania +359;BG;Bulgaria +358;FI;Finland +357;CY;Cyprus +356;MT;Malta +355;AL;Albania +354;IS;Iceland +353;IE;Ireland +352;LU;Luxembourg +351;PT;Portugal +350;GI;Gibraltar +299;GL;Greenland +298;FO;Faroe Islands +297;AW;Aruba +291;ER;Eritrea +290;SH;Saint Helena +269;KM;Comoros +268;SZ;Swaziland +267;BW;Botswana +266;LS;Lesotho +265;MW;Malawi +264;NA;Namibia +263;ZW;Zimbabwe +262;RE;Réunion +261;MG;Madagascar +260;ZM;Zambia +258;MZ;Mozambique +257;BI;Burundi +256;UG;Uganda +255;TZ;Tanzania +254;KE;Kenya +253;DJ;Djibouti +252;SO;Somalia +251;ET;Ethiopia +250;RW;Rwanda +249;SD;Sudan +248;SC;Seychelles +247;SH;Saint Helena +246;IO;Diego Garcia +245;GW;Guinea-Bissau +244;AO;Angola +243;CD;Congo (Dem. Rep.) +242;CG;Congo (Rep.) +241;GA;Gabon +240;GQ;Equatorial Guinea +239;ST;São Tomé & Príncipe +238;CV;Cape Verde +237;CM;Cameroon +236;CF;Central African Rep. +235;TD;Chad +234;NG;Nigeria +233;GH;Ghana +232;SL;Sierra Leone +231;LR;Liberia +230;MU;Mauritius +229;BJ;Benin +228;TG;Togo +227;NE;Niger +226;BF;Burkina Faso +225;CI;Côte d`Ivoire +224;GN;Guinea +223;ML;Mali +222;MR;Mauritania +221;SN;Senegal +220;GM;Gambia +218;LY;Libya +216;TN;Tunisia +213;DZ;Algeria +212;MA;Morocco +211;SS;South Sudan +98;IR;Iran +95;MM;Myanmar +94;LK;Sri Lanka +93;AF;Afghanistan +92;PK;Pakistan +91;IN;India +90;TR;Turkey +86;CN;China +84;VN;Vietnam +82;KR;South Korea +81;JP;Japan +66;TH;Thailand +65;SG;Singapore +64;NZ;New Zealand +63;PH;Philippines +62;ID;Indonesia +61;AU;Australia +60;MY;Malaysia +58;VE;Venezuela +57;CO;Colombia +56;CL;Chile +55;BR;Brazil +54;AR;Argentina +53;CU;Cuba +52;MX;Mexico +51;PE;Peru +49;DE;Germany +48;PL;Poland +47;NO;Norway +46;SE;Sweden +45;DK;Denmark +44;GB;United Kingdom +43;AT;Austria +41;CH;Switzerland +40;RO;Romania +39;IT;Italy +36;HU;Hungary +34;ES;Spain +33;FR;France +32;BE;Belgium +31;NL;Netherlands +30;GR;Greece +27;ZA;South Africa +20;EG;Egypt +7;RU;Russian Federation +7;KZ;Kazakhstan +1;US;USA +1;PR;Puerto Rico +1;DO;Dominican Rep. +1;CA;Canada diff --git a/Telegram-iOS/Resources/PhotoEditor/PhotoEditorCaption@2x.png b/Telegram-iOS/Resources/PhotoEditor/PhotoEditorCaption@2x.png new file mode 100644 index 0000000000..6ed8cb0d45 Binary files /dev/null and b/Telegram-iOS/Resources/PhotoEditor/PhotoEditorCaption@2x.png differ diff --git a/Telegram-iOS/Resources/PhotoEditor/PhotoEditorCaption@3x.png b/Telegram-iOS/Resources/PhotoEditor/PhotoEditorCaption@3x.png new file mode 100644 index 0000000000..3c785a48c1 Binary files /dev/null and b/Telegram-iOS/Resources/PhotoEditor/PhotoEditorCaption@3x.png differ diff --git a/Telegram-iOS/Resources/PhotoEditor/PhotoEditorMute@2x.png b/Telegram-iOS/Resources/PhotoEditor/PhotoEditorMute@2x.png new file mode 100644 index 0000000000..3012a49a60 Binary files /dev/null and b/Telegram-iOS/Resources/PhotoEditor/PhotoEditorMute@2x.png differ diff --git a/Telegram-iOS/Resources/PhotoEditor/PhotoEditorMuteActive@2x.png b/Telegram-iOS/Resources/PhotoEditor/PhotoEditorMuteActive@2x.png new file mode 100644 index 0000000000..61175da4aa Binary files /dev/null and b/Telegram-iOS/Resources/PhotoEditor/PhotoEditorMuteActive@2x.png differ diff --git a/Telegram-iOS/Resources/SFCompactRounded-Semibold.otf b/Telegram-iOS/Resources/SFCompactRounded-Semibold.otf new file mode 100644 index 0000000000..100e58bff7 Binary files /dev/null and b/Telegram-iOS/Resources/SFCompactRounded-Semibold.otf differ diff --git a/Telegram-iOS/Resources/begin_record.caf b/Telegram-iOS/Resources/begin_record.caf new file mode 100644 index 0000000000..3d2e9c4813 Binary files /dev/null and b/Telegram-iOS/Resources/begin_record.caf differ diff --git a/Telegram-iOS/Resources/intro/fast_arrow@2x.png b/Telegram-iOS/Resources/intro/fast_arrow@2x.png new file mode 100644 index 0000000000..2df70aac90 Binary files /dev/null and b/Telegram-iOS/Resources/intro/fast_arrow@2x.png differ diff --git a/Telegram-iOS/Resources/intro/fast_arrow_shadow@2x.png b/Telegram-iOS/Resources/intro/fast_arrow_shadow@2x.png new file mode 100644 index 0000000000..7bcffe069e Binary files /dev/null and b/Telegram-iOS/Resources/intro/fast_arrow_shadow@2x.png differ diff --git a/Telegram-iOS/Resources/intro/fast_body@2x.png b/Telegram-iOS/Resources/intro/fast_body@2x.png new file mode 100644 index 0000000000..6c1bda76f7 Binary files /dev/null and b/Telegram-iOS/Resources/intro/fast_body@2x.png differ diff --git a/Telegram-iOS/Resources/intro/fast_spiral@2x.png b/Telegram-iOS/Resources/intro/fast_spiral@2x.png new file mode 100644 index 0000000000..19f81efced Binary files /dev/null and b/Telegram-iOS/Resources/intro/fast_spiral@2x.png differ diff --git a/Telegram-iOS/Resources/intro/ic_bubble@2x.png b/Telegram-iOS/Resources/intro/ic_bubble@2x.png new file mode 100644 index 0000000000..e2c8ae334a Binary files /dev/null and b/Telegram-iOS/Resources/intro/ic_bubble@2x.png differ diff --git a/Telegram-iOS/Resources/intro/ic_bubble_dot@2x.png b/Telegram-iOS/Resources/intro/ic_bubble_dot@2x.png new file mode 100644 index 0000000000..3db545fc66 Binary files /dev/null and b/Telegram-iOS/Resources/intro/ic_bubble_dot@2x.png differ diff --git a/Telegram-iOS/Resources/intro/ic_cam@2x.png b/Telegram-iOS/Resources/intro/ic_cam@2x.png new file mode 100644 index 0000000000..4be47e3744 Binary files /dev/null and b/Telegram-iOS/Resources/intro/ic_cam@2x.png differ diff --git a/Telegram-iOS/Resources/intro/ic_cam_lens@2x.png b/Telegram-iOS/Resources/intro/ic_cam_lens@2x.png new file mode 100644 index 0000000000..5215262527 Binary files /dev/null and b/Telegram-iOS/Resources/intro/ic_cam_lens@2x.png differ diff --git a/Telegram-iOS/Resources/intro/ic_pencil@2x.png b/Telegram-iOS/Resources/intro/ic_pencil@2x.png new file mode 100644 index 0000000000..9fb7f4036e Binary files /dev/null and b/Telegram-iOS/Resources/intro/ic_pencil@2x.png differ diff --git a/Telegram-iOS/Resources/intro/ic_pin@2x.png b/Telegram-iOS/Resources/intro/ic_pin@2x.png new file mode 100644 index 0000000000..93f2aa5bee Binary files /dev/null and b/Telegram-iOS/Resources/intro/ic_pin@2x.png differ diff --git a/Telegram-iOS/Resources/intro/ic_smile@2x.png b/Telegram-iOS/Resources/intro/ic_smile@2x.png new file mode 100644 index 0000000000..7f81d87deb Binary files /dev/null and b/Telegram-iOS/Resources/intro/ic_smile@2x.png differ diff --git a/Telegram-iOS/Resources/intro/ic_smile_eye@2x.png b/Telegram-iOS/Resources/intro/ic_smile_eye@2x.png new file mode 100644 index 0000000000..2f2956ccee Binary files /dev/null and b/Telegram-iOS/Resources/intro/ic_smile_eye@2x.png differ diff --git a/Telegram-iOS/Resources/intro/ic_videocam@2x.png b/Telegram-iOS/Resources/intro/ic_videocam@2x.png new file mode 100644 index 0000000000..43ce262c17 Binary files /dev/null and b/Telegram-iOS/Resources/intro/ic_videocam@2x.png differ diff --git a/Telegram-iOS/Resources/intro/knot_down@2x.png b/Telegram-iOS/Resources/intro/knot_down@2x.png new file mode 100644 index 0000000000..a3d135b45d Binary files /dev/null and b/Telegram-iOS/Resources/intro/knot_down@2x.png differ diff --git a/Telegram-iOS/Resources/intro/knot_up1@2x.png b/Telegram-iOS/Resources/intro/knot_up1@2x.png new file mode 100644 index 0000000000..7117e5ab07 Binary files /dev/null and b/Telegram-iOS/Resources/intro/knot_up1@2x.png differ diff --git a/Telegram-iOS/Resources/intro/powerful_infinity@2x.png b/Telegram-iOS/Resources/intro/powerful_infinity@2x.png new file mode 100644 index 0000000000..ba99778a77 Binary files /dev/null and b/Telegram-iOS/Resources/intro/powerful_infinity@2x.png differ diff --git a/Telegram-iOS/Resources/intro/powerful_infinity_white@2x.png b/Telegram-iOS/Resources/intro/powerful_infinity_white@2x.png new file mode 100644 index 0000000000..fde63cd30e Binary files /dev/null and b/Telegram-iOS/Resources/intro/powerful_infinity_white@2x.png differ diff --git a/Telegram-iOS/Resources/intro/powerful_mask@2x.png b/Telegram-iOS/Resources/intro/powerful_mask@2x.png new file mode 100644 index 0000000000..ea7f3a5a43 Binary files /dev/null and b/Telegram-iOS/Resources/intro/powerful_mask@2x.png differ diff --git a/Telegram-iOS/Resources/intro/powerful_star@2x.png b/Telegram-iOS/Resources/intro/powerful_star@2x.png new file mode 100644 index 0000000000..61589f3153 Binary files /dev/null and b/Telegram-iOS/Resources/intro/powerful_star@2x.png differ diff --git a/Telegram-iOS/Resources/intro/private_door@2x.png b/Telegram-iOS/Resources/intro/private_door@2x.png new file mode 100644 index 0000000000..4197879ee9 Binary files /dev/null and b/Telegram-iOS/Resources/intro/private_door@2x.png differ diff --git a/Telegram-iOS/Resources/intro/private_screw@2x.png b/Telegram-iOS/Resources/intro/private_screw@2x.png new file mode 100644 index 0000000000..59ce382f71 Binary files /dev/null and b/Telegram-iOS/Resources/intro/private_screw@2x.png differ diff --git a/Telegram-iOS/Resources/intro/start_arrow@2x.png b/Telegram-iOS/Resources/intro/start_arrow@2x.png new file mode 100644 index 0000000000..fe59b3e2b5 Binary files /dev/null and b/Telegram-iOS/Resources/intro/start_arrow@2x.png differ diff --git a/Telegram-iOS/Resources/intro/start_arrow_ipad.png b/Telegram-iOS/Resources/intro/start_arrow_ipad.png new file mode 100644 index 0000000000..eaa2cd6dd1 Binary files /dev/null and b/Telegram-iOS/Resources/intro/start_arrow_ipad.png differ diff --git a/Telegram-iOS/Resources/intro/start_arrow_ipad@2x.png b/Telegram-iOS/Resources/intro/start_arrow_ipad@2x.png new file mode 100644 index 0000000000..08a47e44ac Binary files /dev/null and b/Telegram-iOS/Resources/intro/start_arrow_ipad@2x.png differ diff --git a/Telegram-iOS/Resources/intro/telegram_plane1@2x.png b/Telegram-iOS/Resources/intro/telegram_plane1@2x.png new file mode 100644 index 0000000000..f56072cb17 Binary files /dev/null and b/Telegram-iOS/Resources/intro/telegram_plane1@2x.png differ diff --git a/Telegram-iOS/Resources/intro/telegram_sphere@2x.png b/Telegram-iOS/Resources/intro/telegram_sphere@2x.png new file mode 100644 index 0000000000..15cc5f5277 Binary files /dev/null and b/Telegram-iOS/Resources/intro/telegram_sphere@2x.png differ diff --git a/Telegram-iOS/Resources/notifications/0.m4a b/Telegram-iOS/Resources/notifications/0.m4a new file mode 100644 index 0000000000..e3f9bdcb2a Binary files /dev/null and b/Telegram-iOS/Resources/notifications/0.m4a differ diff --git a/Telegram-iOS/Resources/notifications/1.m4a b/Telegram-iOS/Resources/notifications/1.m4a new file mode 100644 index 0000000000..e3f9bdcb2a Binary files /dev/null and b/Telegram-iOS/Resources/notifications/1.m4a differ diff --git a/Telegram-iOS/Resources/notifications/100.m4a b/Telegram-iOS/Resources/notifications/100.m4a new file mode 100644 index 0000000000..e3f9bdcb2a Binary files /dev/null and b/Telegram-iOS/Resources/notifications/100.m4a differ diff --git a/Telegram-iOS/Resources/notifications/101.m4a b/Telegram-iOS/Resources/notifications/101.m4a new file mode 100644 index 0000000000..a9752552fd Binary files /dev/null and b/Telegram-iOS/Resources/notifications/101.m4a differ diff --git a/Telegram-iOS/Resources/notifications/102.m4a b/Telegram-iOS/Resources/notifications/102.m4a new file mode 100644 index 0000000000..a12676f877 Binary files /dev/null and b/Telegram-iOS/Resources/notifications/102.m4a differ diff --git a/Telegram-iOS/Resources/notifications/103.m4a b/Telegram-iOS/Resources/notifications/103.m4a new file mode 100644 index 0000000000..0382f7ae90 Binary files /dev/null and b/Telegram-iOS/Resources/notifications/103.m4a differ diff --git a/Telegram-iOS/Resources/notifications/104.m4a b/Telegram-iOS/Resources/notifications/104.m4a new file mode 100644 index 0000000000..202132e3f3 Binary files /dev/null and b/Telegram-iOS/Resources/notifications/104.m4a differ diff --git a/Telegram-iOS/Resources/notifications/105.m4a b/Telegram-iOS/Resources/notifications/105.m4a new file mode 100644 index 0000000000..6337b98bfa Binary files /dev/null and b/Telegram-iOS/Resources/notifications/105.m4a differ diff --git a/Telegram-iOS/Resources/notifications/106.m4a b/Telegram-iOS/Resources/notifications/106.m4a new file mode 100644 index 0000000000..f4f340a00c Binary files /dev/null and b/Telegram-iOS/Resources/notifications/106.m4a differ diff --git a/Telegram-iOS/Resources/notifications/107.m4a b/Telegram-iOS/Resources/notifications/107.m4a new file mode 100644 index 0000000000..258ad748b8 Binary files /dev/null and b/Telegram-iOS/Resources/notifications/107.m4a differ diff --git a/Telegram-iOS/Resources/notifications/108.m4a b/Telegram-iOS/Resources/notifications/108.m4a new file mode 100644 index 0000000000..9ea727d8d8 Binary files /dev/null and b/Telegram-iOS/Resources/notifications/108.m4a differ diff --git a/Telegram-iOS/Resources/notifications/109.m4a b/Telegram-iOS/Resources/notifications/109.m4a new file mode 100644 index 0000000000..1c7cd9cb50 Binary files /dev/null and b/Telegram-iOS/Resources/notifications/109.m4a differ diff --git a/Telegram-iOS/Resources/notifications/110.m4a b/Telegram-iOS/Resources/notifications/110.m4a new file mode 100644 index 0000000000..c97210d816 Binary files /dev/null and b/Telegram-iOS/Resources/notifications/110.m4a differ diff --git a/Telegram-iOS/Resources/notifications/111.m4a b/Telegram-iOS/Resources/notifications/111.m4a new file mode 100644 index 0000000000..3d336ab95f Binary files /dev/null and b/Telegram-iOS/Resources/notifications/111.m4a differ diff --git a/Telegram-iOS/Resources/notifications/2.m4a b/Telegram-iOS/Resources/notifications/2.m4a new file mode 100644 index 0000000000..cdb4a44213 Binary files /dev/null and b/Telegram-iOS/Resources/notifications/2.m4a differ diff --git a/Telegram-iOS/Resources/notifications/3.m4a b/Telegram-iOS/Resources/notifications/3.m4a new file mode 100644 index 0000000000..ae3f82ca92 Binary files /dev/null and b/Telegram-iOS/Resources/notifications/3.m4a differ diff --git a/Telegram-iOS/Resources/notifications/4.m4a b/Telegram-iOS/Resources/notifications/4.m4a new file mode 100644 index 0000000000..ca9b0e5805 Binary files /dev/null and b/Telegram-iOS/Resources/notifications/4.m4a differ diff --git a/Telegram-iOS/Resources/notifications/5.m4a b/Telegram-iOS/Resources/notifications/5.m4a new file mode 100644 index 0000000000..59f79b964f Binary files /dev/null and b/Telegram-iOS/Resources/notifications/5.m4a differ diff --git a/Telegram-iOS/Resources/notifications/6.m4a b/Telegram-iOS/Resources/notifications/6.m4a new file mode 100644 index 0000000000..84f27d78fa Binary files /dev/null and b/Telegram-iOS/Resources/notifications/6.m4a differ diff --git a/Telegram-iOS/Resources/notifications/7.m4a b/Telegram-iOS/Resources/notifications/7.m4a new file mode 100644 index 0000000000..7c645cbf11 Binary files /dev/null and b/Telegram-iOS/Resources/notifications/7.m4a differ diff --git a/Telegram-iOS/Resources/notifications/8.m4a b/Telegram-iOS/Resources/notifications/8.m4a new file mode 100644 index 0000000000..c8f2c11d06 Binary files /dev/null and b/Telegram-iOS/Resources/notifications/8.m4a differ diff --git a/Telegram-iOS/Resources/notifications/9.m4a b/Telegram-iOS/Resources/notifications/9.m4a new file mode 100644 index 0000000000..9c0e8d9a89 Binary files /dev/null and b/Telegram-iOS/Resources/notifications/9.m4a differ diff --git a/Telegram-iOS/Resources/voip_busy.caf b/Telegram-iOS/Resources/voip_busy.caf new file mode 100644 index 0000000000..7a6d839506 Binary files /dev/null and b/Telegram-iOS/Resources/voip_busy.caf differ diff --git a/Telegram-iOS/Resources/voip_connecting.mp3 b/Telegram-iOS/Resources/voip_connecting.mp3 new file mode 100644 index 0000000000..fc425bab97 Binary files /dev/null and b/Telegram-iOS/Resources/voip_connecting.mp3 differ diff --git a/Telegram-iOS/Resources/voip_end.caf b/Telegram-iOS/Resources/voip_end.caf new file mode 100644 index 0000000000..c0a22b1389 Binary files /dev/null and b/Telegram-iOS/Resources/voip_end.caf differ diff --git a/Telegram-iOS/Resources/voip_fail.caf b/Telegram-iOS/Resources/voip_fail.caf new file mode 100644 index 0000000000..17e0da3de4 Binary files /dev/null and b/Telegram-iOS/Resources/voip_fail.caf differ diff --git a/Telegram-iOS/Resources/voip_ringback.caf b/Telegram-iOS/Resources/voip_ringback.caf new file mode 100644 index 0000000000..af7253776c Binary files /dev/null and b/Telegram-iOS/Resources/voip_ringback.caf differ diff --git a/Telegram-iOS/SnapshotAppearanceSettings.swift b/Telegram-iOS/SnapshotAppearanceSettings.swift new file mode 100644 index 0000000000..521e167c50 --- /dev/null +++ b/Telegram-iOS/SnapshotAppearanceSettings.swift @@ -0,0 +1,34 @@ +#if DEBUG + +import Foundation +import TelegramCore +import Postbox +import SwiftSignalKit +import Display +import TelegramUI + +func snapshotAppearanceSettings(application: UIApplication, mainWindow: UIWindow, window: Window1, statusBarHost: StatusBarHost) { + let (account, _) = snapshotEnvironment(application: application, mainWindow: mainWindow, statusBarHost: statusBarHost, theme: .day) + account.network.mockConnectionStatus = .online(proxyAddress: nil) + + let _ = (account.postbox.transaction { transaction -> Void in + if let hole = account.postbox.seedConfiguration.initializeChatListWithHole.topLevel { + transaction.replaceChatListHole(groupId: nil, index: hole.index, hole: nil) + } + + let accountPeer = TelegramUser(id: account.peerId, accessHash: nil, firstName: "Alena", lastName: "Shy", username: "alenashy", phone: "44321456789", photo: snapshotAvatar(account.postbox, 1), botInfo: nil, restrictionInfo: nil, flags: []) + transaction.updatePeersInternal([accountPeer], update: { _, updated in + return updated + }) + }).start() + + let rootController = TelegramRootController(account: account) + rootController.addRootControllers(showCallsTab: true) + window.viewController = rootController + rootController.rootTabController!.selectedIndex = 3 + rootController.pushViewController(themeSettingsController(account: account)) +} + +#endif + + diff --git a/Telegram-iOS/SnapshotChatList.swift b/Telegram-iOS/SnapshotChatList.swift new file mode 100644 index 0000000000..0025c86d9b --- /dev/null +++ b/Telegram-iOS/SnapshotChatList.swift @@ -0,0 +1,155 @@ +#if DEBUG + +import Foundation +import TelegramCore +import Postbox +import SwiftSignalKit +import Display +import TelegramUI + +private enum SnapshotPeerAvatar { + case none + case id(Int32) +} + +private func avatarImages(_ postbox: Postbox, _ value: SnapshotPeerAvatar) -> [TelegramMediaImageRepresentation] { + switch value { + case .none: + return [] + case let .id(id): + return snapshotAvatar(postbox, id) + } +} + +private enum SnapshotPeer { + case user(Int32, SnapshotPeerAvatar, String?, String?) + case secretChat(Int32, Int32, SnapshotPeerAvatar, String?, String?) + case channel(Int32, SnapshotPeerAvatar, String) + + func additionalPeer(_ postbox: Postbox) -> Peer? { + switch self { + case .user: + return nil + case let .secretChat(_, userId, avatar, first, last): + return TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: userId), accessHash: nil, firstName: first, lastName: last, username: nil, phone: nil, photo: avatarImages(postbox, avatar), botInfo: nil, restrictionInfo: nil, flags: []) + case .channel: + return nil + } + } + + var peerId: PeerId { + switch self { + case let .user(id, _, _, _): + return PeerId(namespace: Namespaces.Peer.CloudUser, id: id) + case let .secretChat(id, _, _, _, _): + return PeerId(namespace: Namespaces.Peer.SecretChat, id: id) + case let .channel(id, _, _): + return PeerId(namespace: Namespaces.Peer.CloudChannel, id: id) + } + } + + func peer(_ postbox: Postbox) -> Peer { + switch self { + case let .user(id, avatar, first, last): + return TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: id), accessHash: nil, firstName: first, lastName: last, username: nil, phone: nil, photo: avatarImages(postbox, avatar), botInfo: nil, restrictionInfo: nil, flags: []) + case let .secretChat(id, userId, _, _, _): + return TelegramSecretChat(id: PeerId(namespace: Namespaces.Peer.SecretChat, id: id), creationDate: 123, regularPeerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: userId), accessHash: 123, role: .creator, embeddedState: .active, messageAutoremoveTimeout: nil) + case let .channel(id, avatar, title): + return TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: id), accessHash: 123, title: title, username: nil, photo: avatarImages(postbox, avatar), creationDate: 123, version: 0, participationStatus: .member, info: .broadcast(TelegramChannelBroadcastInfo(flags: [])), flags: [], restrictionInfo: nil, adminRights: nil, bannedRights: nil, peerGroupId: nil) + } + } +} + +private struct SnapshotMessage { + let date: Int32 + let peer: SnapshotPeer + let text: String + let outgoing: Bool + + init(_ date: Int32, _ peer: SnapshotPeer, _ text: String, _ outgoing: Bool) { + self.date = date + self.peer = peer + self.text = text + self.outgoing = outgoing + } + + func storeMessage(_ accountPeerId: PeerId, _ baseDate: Int32) -> StoreMessage { + var flags: StoreMessageFlags = [] + if !self.outgoing { + flags.insert(.Incoming) + } + return StoreMessage(id: MessageId(peerId: self.peer.peerId, namespace: Namespaces.Message.Cloud, id: self.date), globallyUniqueId: nil, groupingKey: nil, timestamp: baseDate + self.date, flags: flags, tags: [], globalTags: [], localTags: [], forwardInfo: nil, authorId: outgoing ? accountPeerId : self.peer.peerId, text: self.text, attributes: [], media: []) + } +} + +private struct SnapshotChat { + let message: SnapshotMessage + let unreadCount: Int32 + let isPinned: Bool + let isMuted: Bool + + init(_ message: SnapshotMessage, unreadCount: Int32 = 0, isPinned: Bool = false, isMuted: Bool = false) { + self.message = message + self.unreadCount = unreadCount + self.isPinned = isPinned + self.isMuted = isMuted + } +} + +private let chatList: [SnapshotChat] = [ + .init(.init(100, .user(1, .id(7), "Jane", ""), "Well I do help animals. Maybe I'll have a few cats in my new luxury apartment. 😊", false), isPinned: true), + .init(.init(90, .user(3, .none, "Tyrion", "Lannister"), "Sometimes posession is an abstract concept. They took my purse, but the gold is still mine.", false), unreadCount: 1), + .init(.init(80, .user(2, .id(1), "Alena", "Shy"), "😍 Sticker", true)), + .init(.init(70, .secretChat(4, 4, .id(8), "Heisenberg", ""), "Thanks, Telegram helps me a lot. You have my financial support if you need more servers.", false)), + .init(.init(60, .user(5, .id(9), "Bender", ""), "I looove new iPhones! In fact, they invited me to a focus group.", false)), + .init(.init(50, .channel(6, .id(10), "World News Today"), "LaserBlastSafetyGuide.pdf", false), unreadCount: 1, isMuted: true), + .init(.init(40, .user(7, .id(11), "EVE", ""), "LaserBlastSafetyGuide.pdf", true)), + .init(.init(30, .user(8, .id(12), "Nick", ""), "It's impossible", false)) +] + +func snapshotChatList(application: UIApplication, mainWindow: UIWindow, window: Window1, statusBarHost: StatusBarHost) { + let (account, _) = snapshotEnvironment(application: application, mainWindow: mainWindow, statusBarHost: statusBarHost, theme: .night) + account.network.mockConnectionStatus = .online(proxyAddress: nil) + + let _ = (account.postbox.transaction { transaction -> Void in + if let hole = account.postbox.seedConfiguration.initializeChatListWithHole.topLevel { + transaction.replaceChatListHole(groupId: nil, index: hole.index, hole: nil) + } + + let accountPeer = TelegramUser(id: account.peerId, accessHash: nil, firstName: "Alena", lastName: "Shy", username: "alenashy", phone: "44321456789", photo: [], botInfo: nil, restrictionInfo: nil, flags: []) + transaction.updatePeersInternal([accountPeer], update: { _, updated in + return updated + }) + + let baseDate: Int32 = Int32(Date().timeIntervalSince1970) - 10000 + for item in chatList { + let peer = item.message.peer.peer(account.postbox) + + transaction.updatePeersInternal([peer], update: { _, updated in + return updated + }) + if let additionalPeer = item.message.peer.additionalPeer(account.postbox) { + transaction.updatePeersInternal([additionalPeer], update: { _, updated in + return updated + }) + } + transaction.updatePeerChatListInclusion(peer.id, inclusion: .ifHasMessages) + let _ = transaction.addMessages([item.message.storeMessage(account.peerId, baseDate)], location: .UpperHistoryBlock) + transaction.resetIncomingReadStates([peer.id: [Namespaces.Message.Cloud: .idBased(maxIncomingReadId: Int32.max - 1, maxOutgoingReadId: Int32.max - 1, maxKnownId: Int32.max - 1, count: item.unreadCount, markedUnread: false)]]) + if item.isMuted { + transaction.updateCurrentPeerNotificationSettings([peer.id: TelegramPeerNotificationSettings.defaultSettings.withUpdatedMuteState(.muted(until: Int32.max - 1))]) + } else { + transaction.updateCurrentPeerNotificationSettings([peer.id: TelegramPeerNotificationSettings.defaultSettings]) + } + } + transaction.setPinnedItemIds(chatList.filter{ $0.isPinned }.map{ .peer($0.message.peer.peerId) }) + }).start() + + let rootController = TelegramRootController(account: account) + rootController.addRootControllers(showCallsTab: true) + window.viewController = rootController + rootController.rootTabController!.selectedIndex = 0 + rootController.rootTabController!.selectedIndex = 2 +} + +#endif diff --git a/Telegram-iOS/SnapshotEnvironment.swift b/Telegram-iOS/SnapshotEnvironment.swift new file mode 100644 index 0000000000..b67165e134 --- /dev/null +++ b/Telegram-iOS/SnapshotEnvironment.swift @@ -0,0 +1,111 @@ +#if DEBUG + +import Foundation +import TelegramCore +import Postbox +import SwiftSignalKit +import TelegramUI +import Display + +enum SnapshotEnvironmentTheme { + case night + case day +} + +func snapshotEnvironment(application: UIApplication, mainWindow: UIWindow, statusBarHost: StatusBarHost, theme: SnapshotEnvironmentTheme) -> (Account, AccountManager) { + var randomId: Int64 = 0 + arc4random_buf(&randomId, 8) + let path = NSTemporaryDirectory() + "\(randomId)" + + Logger.setSharedLogger(Logger(basePath: path + "/logs")) + Logger.shared.logToFile = false + + let semaphore = DispatchSemaphore(value: 0) + var accountManagerValue: AccountManager? + initializeAccountManagement() + let _ = accountManager(basePath: path).start(next: { value in + accountManagerValue = value + semaphore.signal() + }) + semaphore.wait() + precondition(accountManagerValue != nil) + + var result: Account? + while true { + let account = currentAccount(allocateIfNotExists: true, networkArguments: NetworkInitializationArguments(apiId: 0, languagesCategory: "ios", appVersion: "unknown"), supplementary: false, manager: accountManagerValue!, rootPath: path, beginWithTestingEnvironment: true, auxiliaryMethods: AccountAuxiliaryMethods(updatePeerChatInputState: { _, _ in return nil }, fetchResource: { _, _, _, _ in + return .never() + }, fetchResourceMediaReferenceHash: { _ in + return .never() + })) |> take(1) + let semaphore = DispatchSemaphore(value: 0) + let _ = account.start(next: { value in + switch value! { + case .upgrading: + preconditionFailure() + case let .unauthorized(account): + let _ = account.postbox.transaction({ transaction -> Void in + let encoder = PostboxEncoder() + encoder.encodeInt32(1, forKey: "masterDatacenterId") + encoder.encodeInt64(PeerId(namespace: Namespaces.Peer.CloudUser, id: 1234567).toInt64(), forKey: "peerId") + + transaction.setState(AuthorizedAccountState(decoder: PostboxDecoder(buffer: encoder.readBufferNoCopy()))) + }).start() + case let .authorized(account): + result = account + } + semaphore.signal() + }) + semaphore.wait() + if result != nil { + break + } + } + + let applicationBindings = TelegramApplicationBindings(isMainApp: true, openUrl: { _ in + }, openUniversalUrl: { _, completion in + completion.completion(false) + }, canOpenUrl: { _ in + return false + }, getTopWindow: { + for window in application.windows.reversed() { + if window === mainWindow || window === statusBarHost.keyboardWindow { + return window + } + } + return application.windows.last + }, displayNotification: { _ in + }, applicationInForeground: .single(true), applicationIsActive: .single(true), clearMessageNotifications: { _ in + }, pushIdleTimerExtension: { + return EmptyDisposable + }, openSettings: { + }, openAppStorePage: { + }, getWindowHost: { + return nil + }, presentNativeController: { _ in + }, dismissNativeController: { + }) + + let _ = updatePresentationThemeSettingsInteractively(postbox: result!.postbox, { _ in + switch theme { + case .day: + return PresentationThemeSettings(chatWallpaper: .color(0xffffff), theme: .builtin(.day), themeAccentColor: nil, fontSize: .regular, automaticThemeSwitchSetting: AutomaticThemeSwitchSetting(trigger: .none, theme: .nightAccent), disableAnimations: false) + case .night: + return PresentationThemeSettings(chatWallpaper: .color(0x000000), theme: .builtin(.nightAccent), themeAccentColor: nil, fontSize: .regular, automaticThemeSwitchSetting: AutomaticThemeSwitchSetting(trigger: .none, theme: .nightAccent), disableAnimations: false) + } + }).start() + + let semaphore1 = DispatchSemaphore(value: 0) + var dataAndSettings: InitialPresentationDataAndSettings? + let _ = currentPresentationDataAndSettings(postbox: result!.postbox).start(next: { value in + dataAndSettings = value + semaphore1.signal() + }) + semaphore1.wait() + precondition(dataAndSettings != nil) + + result!.applicationContext = TelegramApplicationContext(applicationBindings: applicationBindings, accountManager: accountManagerValue!, account: result, initialPresentationDataAndSettings: dataAndSettings!, postbox: result!.postbox) + + return (result!, accountManagerValue!) +} + +#endif diff --git a/Telegram-iOS/SnapshotResources.swift b/Telegram-iOS/SnapshotResources.swift new file mode 100644 index 0000000000..298cb8ee7c --- /dev/null +++ b/Telegram-iOS/SnapshotResources.swift @@ -0,0 +1,32 @@ +#if DEBUG + +import Foundation +import Postbox +import SwiftSignalKit +import TelegramCore + +private var dataPath: String? + +func setupSnapshotData(_ path: String) { + dataPath = path +} + +func snapshotAvatar(_ postbox: Postbox, _ id: Int32) -> [TelegramMediaImageRepresentation] { + guard let path = dataPath else { + return [] + } + + guard let data = try? Data(contentsOf: URL(fileURLWithPath: path + "/Bitmap\(id).png")) else { + return [] + } + if let image = UIImage(data: data) { + let resource = LocalFileMediaResource(fileId: arc4random64(), size: data.count) + + postbox.mediaBox.storeResourceData(resource.id, data: data) + return [TelegramMediaImageRepresentation(dimensions: image.size, resource: resource)] + } else { + return [] + } +} + +#endif diff --git a/Telegram-iOS/SnapshotSecretChat.swift b/Telegram-iOS/SnapshotSecretChat.swift new file mode 100644 index 0000000000..4e197dc0cb --- /dev/null +++ b/Telegram-iOS/SnapshotSecretChat.swift @@ -0,0 +1,75 @@ +#if DEBUG + +import Foundation +import TelegramCore +import Postbox +import SwiftSignalKit +import Display +import TelegramUI + +private enum SnapshotMessage { + case text(String, Bool) + case timer(Int32, Bool) + + func storeMessage(_ postbox: Postbox, peerId: PeerId, userPeerId: PeerId, accountPeerId: PeerId, _ date: Int32) -> StoreMessage { + switch self { + case let .text(text, outgoing): + var flags: StoreMessageFlags = [] + if !outgoing { + flags.insert(.Incoming) + } + return StoreMessage(id: MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: date), globallyUniqueId: nil, groupingKey: nil, timestamp: date, flags: flags, tags: [], globalTags: [], localTags: [], forwardInfo: nil, authorId: outgoing ? accountPeerId : userPeerId, text: text, attributes: [], media: []) + case let .timer(timeout, outgoing): + var flags: StoreMessageFlags = [] + if !outgoing { + flags.insert(.Incoming) + } + return StoreMessage(id: MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: date), globallyUniqueId: nil, groupingKey: nil, timestamp: date, flags: flags, tags: [], globalTags: [], localTags: [], forwardInfo: nil, authorId: outgoing ? accountPeerId : userPeerId, text: "", attributes: [], media: [TelegramMediaAction(action: .messageAutoremoveTimeoutUpdated(timeout))]) + } + } +} + +private let messages: [SnapshotMessage] = [ + .text("Hey Eileen", true), + .text("So, why is Telegram cool?", true), + .text("Well, look. Telegram is superfast and you can use it on all your devices at the same time — phones, tablets, even desktops.", false), + .text("😴", true), + .text("And it has secret chats, like this one, with end-to-end encryption!", false), + .text("End encryption to what end??", true), + .text("Arrgh. Forget it. You can set a timer and send photos that will disappear when the time runs out. Yay!", false), + .timer(15, false) +] + +func snapshotSecretChat(application: UIApplication, mainWindow: UIWindow, window: Window1, statusBarHost: StatusBarHost) { + let (account, _) = snapshotEnvironment(application: application, mainWindow: mainWindow, statusBarHost: statusBarHost, theme: .night) + account.network.mockConnectionStatus = .online(proxyAddress: nil) + + let accountPeer = TelegramUser(id: account.peerId, accessHash: nil, firstName: "Alena", lastName: "Shy", username: "alenashy", phone: "44321456789", photo: [], botInfo: nil, restrictionInfo: nil, flags: []) + let userPeer = TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: 456), accessHash: nil, firstName: "Eileen", lastName: "Lockhard", username: nil, phone: "44321456789", photo: snapshotAvatar(account.postbox, 6), botInfo: nil, restrictionInfo: nil, flags: []) + let secretPeer = TelegramSecretChat(id: PeerId(namespace: Namespaces.Peer.SecretChat, id: 456), creationDate: 123, regularPeerId: userPeer.id, accessHash: 123, role: .creator, embeddedState: .active, messageAutoremoveTimeout: nil) + + let _ = (account.postbox.transaction { transaction -> Void in + if let hole = account.postbox.seedConfiguration.initializeChatListWithHole.topLevel { + transaction.replaceChatListHole(groupId: nil, index: hole.index, hole: nil) + } + + transaction.updatePeersInternal([accountPeer, userPeer, secretPeer], update: { _, updated in + return updated + }) + + transaction.updatePeerPresencesInternal([userPeer.id: TelegramUserPresence(status: .present(until: Int32.max - 1))]) + + var date: Int32 = Int32(Date().timeIntervalSince1970) - 1000 + for message in messages { + let _ = transaction.addMessages([message.storeMessage(account.postbox, peerId: secretPeer.id, userPeerId: userPeer.id, accountPeerId: account.peerId, date)], location: .UpperHistoryBlock) + date += 10 + } + }).start() + + let rootController = TelegramRootController(account: account) + rootController.addRootControllers(showCallsTab: true) + window.viewController = rootController + navigateToChatController(navigationController: rootController, account: account, chatLocation: .peer(secretPeer.id), animated: false) +} + +#endif diff --git a/Telegram-iOS/SnapshotSettings.swift b/Telegram-iOS/SnapshotSettings.swift new file mode 100644 index 0000000000..a878b706d5 --- /dev/null +++ b/Telegram-iOS/SnapshotSettings.swift @@ -0,0 +1,33 @@ +#if DEBUG + +import Foundation +import TelegramCore +import Postbox +import SwiftSignalKit +import Display +import TelegramUI + +func snapshotSettings(application: UIApplication, mainWindow: UIWindow, window: Window1, statusBarHost: StatusBarHost) { + let (account, accountManager) = snapshotEnvironment(application: application, mainWindow: mainWindow, statusBarHost: statusBarHost, theme: .night) + account.network.mockConnectionStatus = .online(proxyAddress: nil) + + let _ = (account.postbox.transaction { transaction -> Void in + if let hole = account.postbox.seedConfiguration.initializeChatListWithHole.topLevel { + transaction.replaceChatListHole(groupId: nil, index: hole.index, hole: nil) + } + + let accountPeer = TelegramUser(id: account.peerId, accessHash: nil, firstName: "Alena", lastName: "Shy", username: "alenashy", phone: "44321456789", photo: snapshotAvatar(account.postbox, 1), botInfo: nil, restrictionInfo: nil, flags: []) + transaction.updatePeersInternal([accountPeer], update: { _, updated in + return updated + }) + }).start() + + let rootController = TelegramRootController(account: account) + rootController.addRootControllers(showCallsTab: true) + window.viewController = rootController + rootController.rootTabController!.selectedIndex = 3 + rootController.pushViewController(settingsController(account: account, accountManager: accountManager)) +} + +#endif + diff --git a/Telegram-iOS/TGAutoDownloadPreferences.h b/Telegram-iOS/TGAutoDownloadPreferences.h new file mode 100644 index 0000000000..975ffbd435 --- /dev/null +++ b/Telegram-iOS/TGAutoDownloadPreferences.h @@ -0,0 +1,76 @@ +#import + +typedef enum { + TGNetworkTypeUnknown, + TGNetworkTypeNone, + TGNetworkTypeGPRS, + TGNetworkTypeEdge, + TGNetworkType3G, + TGNetworkTypeLTE, + TGNetworkTypeWiFi, +} TGNetworkType; + +typedef enum { + TGAutoDownloadModeNone = 0, + + TGAutoDownloadModeCellularContacts = 1 << 0, + TGAutoDownloadModeWifiContacts = 1 << 1, + + TGAutoDownloadModeCellularPrivateChats = 1 << 2, + TGAutoDownloadModeWifiPrivateChats = 1 << 3, + + TGAutoDownloadModeCellularGroups = 1 << 4, + TGAutoDownloadModeWifiGroups = 1 << 5, + + TGAutoDownloadModeCellularChannels = 1 << 6, + TGAutoDownloadModeWifiChannels = 1 << 7, + + TGAutoDownloadModeAutosavePhotosAll = TGAutoDownloadModeCellularContacts | TGAutoDownloadModeCellularPrivateChats | TGAutoDownloadModeCellularGroups | TGAutoDownloadModeCellularChannels, + + TGAutoDownloadModeAllPrivateChats = TGAutoDownloadModeCellularContacts | TGAutoDownloadModeWifiContacts | TGAutoDownloadModeCellularPrivateChats | TGAutoDownloadModeWifiPrivateChats, + TGAutoDownloadModeAllGroups = TGAutoDownloadModeCellularGroups | TGAutoDownloadModeWifiGroups | TGAutoDownloadModeCellularChannels | TGAutoDownloadModeWifiChannels, + TGAutoDownloadModeAll = TGAutoDownloadModeCellularContacts | TGAutoDownloadModeWifiContacts | TGAutoDownloadModeCellularPrivateChats | TGAutoDownloadModeWifiPrivateChats | TGAutoDownloadModeCellularGroups | TGAutoDownloadModeWifiGroups | TGAutoDownloadModeCellularChannels | TGAutoDownloadModeWifiChannels +} TGAutoDownloadMode; + +typedef enum { + TGAutoDownloadChatContact, + TGAutoDownloadChatOtherPrivateChat, + TGAutoDownloadChatGroup, + TGAutoDownloadChatChannel +} TGAutoDownloadChat; + +@interface TGAutoDownloadPreferences : NSObject + +@property (nonatomic, readonly) bool disabled; + +@property (nonatomic, readonly) TGAutoDownloadMode photos; +@property (nonatomic, readonly) TGAutoDownloadMode videos; +@property (nonatomic, readonly) int32_t maximumVideoSize; +@property (nonatomic, readonly) TGAutoDownloadMode documents; +@property (nonatomic, readonly) int32_t maximumDocumentSize; +@property (nonatomic, readonly) TGAutoDownloadMode gifs; +@property (nonatomic, readonly) TGAutoDownloadMode voiceMessages; +@property (nonatomic, readonly) TGAutoDownloadMode videoMessages; + +- (instancetype)updateDisabled:(bool)disabled; +- (instancetype)updatePhotosMode:(TGAutoDownloadMode)mode; +- (instancetype)updateVideosMode:(TGAutoDownloadMode)mode maximumSize:(int32_t)maximumSize; +- (instancetype)updateDocumentsMode:(TGAutoDownloadMode)mode maximumSize:(int32_t)maximumSize; +- (instancetype)updateGifsMode:(TGAutoDownloadMode)mode; +- (instancetype)updateVoiceMessagesMode:(TGAutoDownloadMode)mode; +- (instancetype)updateVideoMessagesMode:(TGAutoDownloadMode)mode; + ++ (bool)shouldDownload:(TGAutoDownloadMode)mode inChat:(TGAutoDownloadChat)chat networkType:(TGNetworkType)networkType; +- (bool)shouldDownloadPhotoInChat:(TGAutoDownloadChat)chat networkType:(TGNetworkType)networkType; +- (bool)shouldDownloadVideoInChat:(TGAutoDownloadChat)chat networkType:(TGNetworkType)networkType; +- (bool)shouldDownloadDocumentInChat:(TGAutoDownloadChat)chat networkType:(TGNetworkType)networkType; +- (bool)shouldDownloadGifInChat:(TGAutoDownloadChat)chat networkType:(TGNetworkType)networkType; +- (bool)shouldDownloadVoiceMessageInChat:(TGAutoDownloadChat)chat networkType:(TGNetworkType)networkType; +- (bool)shouldDownloadVideoMessageInChat:(TGAutoDownloadChat)chat networkType:(TGNetworkType)networkType; + +- (bool)isDefaultPreferences; + ++ (instancetype)defaultPreferences; ++ (instancetype)preferencesWithLegacyDownloadPrivatePhotos:(bool)privatePhotos groupPhotos:(bool)groupPhotos privateVoiceMessages:(bool)privateVoiceMessages groupVoiceMessages:(bool)groupVoiceMessages privateVideoMessages:(bool)privateVideoMessages groupVideoMessages:(bool)groupVideoMessages; + +@end diff --git a/Telegram-iOS/TGAutoDownloadPreferences.m b/Telegram-iOS/TGAutoDownloadPreferences.m new file mode 100644 index 0000000000..e84cc90ab2 --- /dev/null +++ b/Telegram-iOS/TGAutoDownloadPreferences.m @@ -0,0 +1,250 @@ +#import "TGAutoDownloadPreferences.h" + +@implementation TGAutoDownloadPreferences + +- (instancetype)initWithCoder:(NSCoder *)aDecoder +{ + self = [super init]; + if (self != nil) + { + _disabled = [aDecoder decodeBoolForKey:@"disabled"]; + _photos = [aDecoder decodeInt32ForKey:@"photos"]; + _videos = [aDecoder decodeInt32ForKey:@"videos"]; + _maximumVideoSize = [aDecoder decodeInt32ForKey:@"maxVideoSize"]; + _documents = [aDecoder decodeInt32ForKey:@"documents"]; + _maximumDocumentSize = [aDecoder decodeInt32ForKey:@"maxDocumentSize"]; + _voiceMessages = [aDecoder decodeInt32ForKey:@"voiceMessages"]; + _videoMessages = [aDecoder decodeInt32ForKey:@"videoMessages"]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder +{ + [aCoder encodeBool:_disabled forKey:@"disabled"]; + [aCoder encodeInt32:_photos forKey:@"photos"]; + [aCoder encodeInt32:_videos forKey:@"videos"]; + [aCoder encodeInt32:_maximumVideoSize forKey:@"maxVideoSize"]; + [aCoder encodeInt32:_documents forKey:@"documents"]; + [aCoder encodeInt32:_maximumDocumentSize forKey:@"maxDocumentSize"]; + [aCoder encodeInt32:_voiceMessages forKey:@"voiceMessages"]; + [aCoder encodeInt32:_videoMessages forKey:@"videoMessages"]; +} + ++ (instancetype)defaultPreferences +{ + TGAutoDownloadPreferences *preferences = [[TGAutoDownloadPreferences alloc] init]; + preferences->_photos = TGAutoDownloadModeAll; + preferences->_videos = TGAutoDownloadModeNone; + preferences->_maximumVideoSize = 10; + preferences->_documents = TGAutoDownloadModeNone; + preferences->_maximumDocumentSize = 10; + preferences->_voiceMessages = TGAutoDownloadModeAll; + preferences->_videoMessages = TGAutoDownloadModeAll; + return preferences; +} + ++ (instancetype)preferencesWithLegacyDownloadPrivatePhotos:(bool)privatePhotos groupPhotos:(bool)groupPhotos privateVoiceMessages:(bool)privateVoiceMessages groupVoiceMessages:(bool)groupVoiceMessages privateVideoMessages:(bool)privateVideoMessages groupVideoMessages:(bool)groupVideoMessages +{ + TGAutoDownloadPreferences *preferences = [[TGAutoDownloadPreferences alloc] init]; + + if (privatePhotos) + preferences->_photos |= TGAutoDownloadModeAllPrivateChats; + if (groupPhotos) + preferences->_photos |= TGAutoDownloadModeAllGroups; + + if (privateVoiceMessages) + preferences->_voiceMessages |= TGAutoDownloadModeAllPrivateChats; + if (groupVoiceMessages) + preferences->_voiceMessages |= TGAutoDownloadModeAllGroups; + + if (privateVideoMessages) + preferences->_videoMessages |= TGAutoDownloadModeAllPrivateChats; + if (groupVideoMessages) + preferences->_videoMessages |= TGAutoDownloadModeAllGroups; + + preferences->_maximumVideoSize = 10; + preferences->_maximumDocumentSize = 10; + + return preferences; +} + +- (instancetype)updateDisabled:(bool)disabled +{ + TGAutoDownloadPreferences *preferences = [[TGAutoDownloadPreferences alloc] init]; + preferences->_disabled = disabled; + preferences->_photos = _photos; + preferences->_videos = _videos; + preferences->_maximumVideoSize = _maximumVideoSize; + preferences->_documents = _documents; + preferences->_maximumDocumentSize = _maximumDocumentSize; + preferences->_voiceMessages = _voiceMessages; + preferences->_videoMessages = _videoMessages; + return preferences; +} + +- (instancetype)updatePhotosMode:(TGAutoDownloadMode)mode +{ + TGAutoDownloadPreferences *preferences = [[TGAutoDownloadPreferences alloc] init]; + preferences->_photos = mode; + preferences->_videos = _videos; + preferences->_maximumVideoSize = _maximumVideoSize; + preferences->_documents = _documents; + preferences->_maximumDocumentSize = _maximumDocumentSize; + preferences->_voiceMessages = _voiceMessages; + preferences->_videoMessages = _videoMessages; + return preferences; +} + +- (instancetype)updateVideosMode:(TGAutoDownloadMode)mode maximumSize:(int32_t)maximumSize +{ + TGAutoDownloadPreferences *preferences = [[TGAutoDownloadPreferences alloc] init]; + preferences->_photos = _photos; + preferences->_videos = mode; + preferences->_maximumVideoSize = maximumSize; + preferences->_documents = _documents; + preferences->_maximumDocumentSize = _maximumDocumentSize; + preferences->_voiceMessages = _voiceMessages; + preferences->_videoMessages = _videoMessages; + return preferences; +} + +- (instancetype)updateDocumentsMode:(TGAutoDownloadMode)mode maximumSize:(int32_t)maximumSize +{ + TGAutoDownloadPreferences *preferences = [[TGAutoDownloadPreferences alloc] init]; + preferences->_photos = _photos; + preferences->_videos = _videos; + preferences->_maximumVideoSize = _maximumVideoSize; + preferences->_documents = mode; + preferences->_maximumDocumentSize = maximumSize; + preferences->_voiceMessages = _voiceMessages; + preferences->_videoMessages = _videoMessages; + return preferences; +} + +- (instancetype)updateVoiceMessagesMode:(TGAutoDownloadMode)mode +{ + TGAutoDownloadPreferences *preferences = [[TGAutoDownloadPreferences alloc] init]; + preferences->_photos = _photos; + preferences->_videos = _videos; + preferences->_maximumVideoSize = _maximumVideoSize; + preferences->_documents = _documents; + preferences->_maximumDocumentSize = _maximumDocumentSize; + preferences->_voiceMessages = mode; + preferences->_videoMessages = _videoMessages; + return preferences; +} + +- (instancetype)updateVideoMessagesMode:(TGAutoDownloadMode)mode +{ + TGAutoDownloadPreferences *preferences = [[TGAutoDownloadPreferences alloc] init]; + preferences->_photos = _photos; + preferences->_videos = _videos; + preferences->_maximumVideoSize = _maximumVideoSize; + preferences->_documents = _documents; + preferences->_maximumDocumentSize = _maximumDocumentSize; + preferences->_voiceMessages = _voiceMessages; + preferences->_videoMessages = mode; + return preferences; +} + ++ (bool)shouldDownload:(TGAutoDownloadMode)mode inChat:(TGAutoDownloadChat)chat networkType:(TGNetworkType)networkType +{ + bool isWiFi = networkType == TGNetworkTypeWiFi; + bool isCellular = !isWiFi && networkType != TGNetworkTypeNone; + + bool shouldDownload = false; + switch (chat) + { + case TGAutoDownloadChatContact: + if (isCellular) + shouldDownload = (mode & TGAutoDownloadModeCellularContacts) != 0; + else if (isWiFi) + shouldDownload = (mode & TGAutoDownloadModeWifiContacts) != 0; + break; + + case TGAutoDownloadChatOtherPrivateChat: + if (isCellular) + shouldDownload = (mode & TGAutoDownloadModeCellularPrivateChats) != 0; + else if (isWiFi) + shouldDownload = (mode & TGAutoDownloadModeWifiPrivateChats) != 0; + break; + + case TGAutoDownloadChatGroup: + if (isCellular) + shouldDownload = (mode & TGAutoDownloadModeCellularGroups) != 0; + else if (isWiFi) + shouldDownload = (mode & TGAutoDownloadModeWifiGroups) != 0; + break; + + case TGAutoDownloadChatChannel: + if (isCellular) + shouldDownload = (mode & TGAutoDownloadModeCellularChannels) != 0; + else if (isWiFi) + shouldDownload = (mode & TGAutoDownloadModeWifiChannels) != 0; + break; + + default: + break; + } + return shouldDownload; +} + +- (bool)shouldDownloadPhotoInChat:(TGAutoDownloadChat)chat networkType:(TGNetworkType)networkType +{ + if (self.disabled) + return false; + + return [TGAutoDownloadPreferences shouldDownload:_photos inChat:chat networkType:networkType]; +} + +- (bool)shouldDownloadVideoInChat:(TGAutoDownloadChat)chat networkType:(TGNetworkType)networkType +{ + if (self.disabled) + return false; + + return [TGAutoDownloadPreferences shouldDownload:_videos inChat:chat networkType:networkType]; +} + +- (bool)shouldDownloadDocumentInChat:(TGAutoDownloadChat)chat networkType:(TGNetworkType)networkType +{ + if (self.disabled) + return false; + + return [TGAutoDownloadPreferences shouldDownload:_documents inChat:chat networkType:networkType]; +} + +- (bool)shouldDownloadVoiceMessageInChat:(TGAutoDownloadChat)chat networkType:(TGNetworkType)networkType +{ + if (self.disabled) + return false; + + return [TGAutoDownloadPreferences shouldDownload:_voiceMessages inChat:chat networkType:networkType]; +} + +- (bool)shouldDownloadVideoMessageInChat:(TGAutoDownloadChat)chat networkType:(TGNetworkType)networkType +{ + if (self.disabled) + return false; + + return [TGAutoDownloadPreferences shouldDownload:_videoMessages inChat:chat networkType:networkType]; +} + +- (bool)isDefaultPreferences +{ + return [self isEqual:[TGAutoDownloadPreferences defaultPreferences]]; +} + +- (BOOL)isEqual:(id)object +{ + if (object == self) + return YES; + + if (!object || ![object isKindOfClass:[self class]]) + return NO; + + TGAutoDownloadPreferences *preferences = (TGAutoDownloadPreferences *)object; + return preferences.photos == _photos && preferences.videos == _videos && preferences.documents == _documents && preferences.voiceMessages == _voiceMessages && preferences.videoMessages == _videoMessages && preferences.maximumVideoSize == _maximumVideoSize && preferences.maximumDocumentSize == _maximumDocumentSize && preferences.disabled == _disabled; +} + +@end diff --git a/Telegram-iOS/TGBridgeServer.h b/Telegram-iOS/TGBridgeServer.h new file mode 100644 index 0000000000..dc353f301e --- /dev/null +++ b/Telegram-iOS/TGBridgeServer.h @@ -0,0 +1,29 @@ +#import +#import + +@class TGBridgeSubscription; + +@interface TGBridgeServer : NSObject + +@property (nonatomic, readonly) NSURL * _Nullable temporaryFilesURL; + +@property (nonatomic, readonly) bool isRunning; + +- (instancetype)initWithHandler:(SSignal *(^)(TGBridgeSubscription *))handler fileHandler:(void (^)(NSString *, NSDictionary *))fileHandler dispatchOnQueue:(void (^)(void (^)(void)))dispatchOnQueue logFunction:(void (^)(NSString *))logFunction; +- (void)startRunning; + +- (SSignal *)watchAppInstalledSignal; +- (SSignal *)runningRequestsSignal; + +- (void)setAuthorized:(bool)authorized userId:(int32_t)userId; +- (void)setMicAccessAllowed:(bool)allowed; +- (void)setStartupData:(NSDictionary *)data; +- (void)pushContext; + +- (void)sendFileWithURL:(NSURL *)url metadata:(NSDictionary *)metadata asMessageData:(bool)asMessageData; +- (void)sendFileWithData:(NSData *)data metadata:(NSDictionary *)metadata errorHandler:(void (^)(void))errorHandler; + +- (NSInteger)wakeupNetwork; +- (void)suspendNetworkIfReady:(NSInteger)token; + +@end diff --git a/Telegram-iOS/TGBridgeServer.m b/Telegram-iOS/TGBridgeServer.m new file mode 100644 index 0000000000..1b0ce07fd3 --- /dev/null +++ b/Telegram-iOS/TGBridgeServer.m @@ -0,0 +1,765 @@ +#import "TGBridgeServer.h" +#import "TGBridgeCommon.h" + +#import +#import +#import + +#import "TGBridgeContext.h" + +@interface TGBridgeSignalManager : NSObject + +- (bool)startSignalForKey:(NSString *)key producer:(SSignal *(^)())producer; +- (void)haltSignalForKey:(NSString *)key; +- (void)haltAllSignals; + +@end + +@interface TGBridgeServer () +{ + SSignal *(^_handler)(TGBridgeSubscription *); + void (^_fileHandler)(NSString *, NSDictionary *); + void (^_logFunction)(NSString *); + void (^_dispatch)(void (^)(void)); + + bool _pendingStart; + + bool _processingNotification; + + int32_t _sessionId; + volatile int32_t _tasksVersion; + + TGBridgeContext *_activeContext; + + TGBridgeSignalManager *_signalManager; + + OSSpinLock _incomingQueueLock; + NSMutableArray *_incomingMessageQueue; + + bool _requestSubscriptionList; + NSArray *_initialSubscriptionList; + + OSSpinLock _outgoingQueueLock; + NSMutableArray *_outgoingMessageQueue; + + OSSpinLock _replyHandlerMapLock; + NSMutableDictionary *_replyHandlerMap; + + SPipe *_appInstalled; + + NSMutableDictionary *_runningTasks; + SVariable *_hasRunningTasks; +} + +@property (nonatomic, readonly) WCSession *session; + +@end + +@implementation TGBridgeServer + +- (instancetype)initWithHandler:(SSignal *(^)(TGBridgeSubscription *))handler fileHandler:(void (^)(NSString *, NSDictionary *))fileHandler dispatchOnQueue:(void (^)(void (^)(void)))dispatchOnQueue logFunction:(void (^)(NSString *))logFunction +{ + self = [super init]; + if (self != nil) + { + _handler = [handler copy]; + _fileHandler = [fileHandler copy]; + _dispatch = [dispatchOnQueue copy]; + _logFunction = [logFunction copy]; + + _runningTasks = [[NSMutableDictionary alloc] init]; + _hasRunningTasks = [[SVariable alloc] init]; + [_hasRunningTasks set:[SSignal single:@false]]; + + _signalManager = [[TGBridgeSignalManager alloc] init]; + _incomingMessageQueue = [[NSMutableArray alloc] init]; + + self.session.delegate = self; + [self.session activateSession]; + + _replyHandlerMap = [[NSMutableDictionary alloc] init]; + + _appInstalled = [[SPipe alloc] init]; + + _activeContext = [[TGBridgeContext alloc] initWithDictionary:[self.session applicationContext]]; + } + return self; +} + +- (void)log:(NSString *)message +{ + _logFunction(message); +} + +- (void)dispatch:(void (^)(void))action +{ + _dispatch(action); +} + +- (void)startRunning +{ + if (self.isRunning) + return; + + OSSpinLockLock(&_incomingQueueLock); + _isRunning = true; + + for (id message in _incomingMessageQueue) + [self handleMessage:message replyHandler:nil finishTask:nil completion:nil]; + + [_incomingMessageQueue removeAllObjects]; + OSSpinLockUnlock(&_incomingQueueLock); + + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + [self dispatch:^{ + _appInstalled.sink(@(self.session.isWatchAppInstalled)); + }]; + }); +} + +- (NSURL *)temporaryFilesURL +{ + return self.session.watchDirectoryURL; +} + +- (SSignal *)watchAppInstalledSignal +{ + return [[SSignal single:@(self.session.watchAppInstalled)] then:_appInstalled.signalProducer()]; +} + +- (SSignal *)runningRequestsSignal +{ + return _hasRunningTasks.signal; +} + +#pragma mark - + +- (void)setAuthorized:(bool)authorized userId:(int32_t)userId +{ + _activeContext = [_activeContext updatedWithAuthorized:authorized peerId:userId]; +} + +- (void)setMicAccessAllowed:(bool)allowed +{ + _activeContext = [_activeContext updatedWithMicAccessAllowed:allowed]; +} + +- (void)setStartupData:(NSDictionary *)data +{ + _activeContext = [_activeContext updatedWithPreheatData:data]; +} + +- (void)pushContext +{ + NSError *error; + [self.session updateApplicationContext:[_activeContext dictionary] error:&error]; + + //if (error != nil) + //TGLog(@"[BridgeServer][ERROR] Failed to push active application context: %@", error.localizedDescription); +} + +#pragma mark - + +- (void)handleMessageData:(NSData *)messageData task:(id)task replyHandler:(void (^)(NSData *))replyHandler completion:(void (^)(void))completion +{ + __block id runningTask = task; + void (^finishTask)(NSTimeInterval) = ^(NSTimeInterval delay) + { + if (runningTask == nil) + return; + + void (^block)(void) = ^ + { + [self dispatch:^{ + [runningTask dispose]; + //TGLog(@"[BridgeServer]: ended taskid: %d", runningTask); + runningTask = nil; + }]; + }; + + if (delay > DBL_EPSILON) + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)((delay) * NSEC_PER_SEC)), dispatch_get_main_queue(), block); + else + block(); + }; + + id message = [NSKeyedUnarchiver unarchiveObjectWithData:messageData]; + OSSpinLockLock(&_incomingQueueLock); + if (!self.isRunning) + { + [_incomingMessageQueue addObject:message]; + + if (replyHandler != nil) + replyHandler([NSData data]); + + finishTask(4.0); + + OSSpinLockUnlock(&_incomingQueueLock); + return; + } + OSSpinLockUnlock(&_incomingQueueLock); + + [self handleMessage:message replyHandler:replyHandler finishTask:finishTask completion:completion]; +} + +- (void)handleMessage:(id)message replyHandler:(void (^)(NSData *))replyHandler finishTask:(void (^)(NSTimeInterval))finishTask completion:(void (^)(void))completion +{ + if ([message isKindOfClass:[TGBridgeSubscription class]]) + { + TGBridgeSubscription *subcription = (TGBridgeSubscription *)message; + [self _createSubscription:subcription replyHandler:replyHandler finishTask:finishTask completion:completion]; + + //TGLog(@"[BridgeServer] Create subscription: %@", subcription); + } + else if ([message isKindOfClass:[TGBridgeDisposal class]]) + { + TGBridgeDisposal *disposal = (TGBridgeDisposal *)message; + [_signalManager haltSignalForKey:[NSString stringWithFormat:@"%lld", disposal.identifier]]; + + if (replyHandler != nil) + replyHandler([NSData data]); + + if (completion != nil) + completion(); + + //TGLog(@"[BridgeServer] Dispose subscription %lld", disposal.identifier); + + if (finishTask != nil) + finishTask(0); + } + else if ([message isKindOfClass:[TGBridgeSubscriptionList class]]) + { + TGBridgeSubscriptionList *list = (TGBridgeSubscriptionList *)message; + for (TGBridgeSubscription *subscription in list.subscriptions) + [self _createSubscription:subscription replyHandler:nil finishTask:nil completion:nil]; + + //TGLog(@"[BridgeServer] Received subscription list, applying"); + + if (replyHandler != nil) + replyHandler([NSData data]); + + if (finishTask != nil) + finishTask(4.0); + + if (completion != nil) + completion(); + } + else if ([message isKindOfClass:[TGBridgePing class]]) + { + TGBridgePing *ping = (TGBridgePing *)message; + if (_sessionId != ping.sessionId) + { + //TGLog(@"[BridgeServer] Session id mismatch"); + + if (_sessionId != 0) + { + //TGLog(@"[BridgeServer] Halt all active subscriptions"); + [_signalManager haltAllSignals]; + + OSSpinLockLock(&_outgoingQueueLock); + [_outgoingMessageQueue removeAllObjects]; + OSSpinLockUnlock(&_outgoingQueueLock); + } + + _sessionId = ping.sessionId; + + if (self.session.isReachable) + [self _requestSubscriptionList]; + else + _requestSubscriptionList = true; + } + else + { + if (_requestSubscriptionList) + { + _requestSubscriptionList = false; + [self _requestSubscriptionList]; + } + + [self _sendQueuedResponses]; + + if (replyHandler != nil) + replyHandler([NSData data]); + } + + if (completion != nil) + completion(); + + if (finishTask != nil) + finishTask(4.0); + } + else + { + if (completion != nil) + completion(); + if (finishTask != nil) + finishTask(1.0); + } +} + +- (void)_createSubscription:(TGBridgeSubscription *)subscription replyHandler:(void (^)(NSData *))replyHandler finishTask:(void (^)(NSTimeInterval))finishTask completion:(void (^)(void))completion +{ + SSignal *subscriptionHandler = _handler(subscription); + if (replyHandler != nil) + { + OSSpinLockLock(&_replyHandlerMapLock); + _replyHandlerMap[@(subscription.identifier)] = replyHandler; + OSSpinLockUnlock(&_replyHandlerMapLock); + } + + if (subscriptionHandler != nil) + { + [_signalManager startSignalForKey:[NSString stringWithFormat:@"%lld", subscription.identifier] producer:^SSignal * + { + STimer *timer = [[STimer alloc] initWithTimeout:2.0 repeat:false completion:^ + { + OSSpinLockLock(&_replyHandlerMapLock); + void (^reply)(NSData *) = _replyHandlerMap[@(subscription.identifier)]; + if (reply == nil) + { + OSSpinLockUnlock(&_replyHandlerMapLock); + + if (finishTask != nil) + finishTask(2.0); + return; + } + + reply([NSData data]); + [_replyHandlerMap removeObjectForKey:@(subscription.identifier)]; + OSSpinLockUnlock(&_replyHandlerMapLock); + + if (finishTask != nil) + finishTask(4.0); + + //TGLog(@"[BridgeServer]: subscription 0x%x hit 2.0s timeout, releasing reply handler", subscription.identifier); + } queue:[SQueue mainQueue]]; + [timer start]; + + return [[SSignal alloc] initWithGenerator:^id(__unused SSubscriber *subscriber) + { + return [subscriptionHandler startWithNext:^(id next) + { + [timer invalidate]; + [self _responseToSubscription:subscription message:next type:TGBridgeResponseTypeNext completion:completion]; + + if (finishTask != nil) + finishTask(4.0); + } error:^(id error) + { + [timer invalidate]; + [self _responseToSubscription:subscription message:error type:TGBridgeResponseTypeFailed completion:completion]; + + if (finishTask != nil) + finishTask(4.0); + } completed:^ + { + [timer invalidate]; + [self _responseToSubscription:subscription message:nil type:TGBridgeResponseTypeCompleted completion:completion]; + + if (finishTask != nil) + finishTask(4.0); + }]; + }]; + }]; + } + else + { + OSSpinLockLock(&_replyHandlerMapLock); + void (^reply)(NSData *) = _replyHandlerMap[@(subscription.identifier)]; + if (reply == nil) + { + OSSpinLockUnlock(&_replyHandlerMapLock); + + if (finishTask != nil) + finishTask(2.0); + return; + } + + reply([NSData data]); + [_replyHandlerMap removeObjectForKey:@(subscription.identifier)]; + OSSpinLockUnlock(&_replyHandlerMapLock); + + if (finishTask != nil) + finishTask(2.0); + } +} + +- (void)_responseToSubscription:(TGBridgeSubscription *)subscription message:(id)message type:(TGBridgeResponseType)type completion:(void (^)(void))completion +{ + TGBridgeResponse *response = nil; + switch (type) + { + case TGBridgeResponseTypeNext: + response = [TGBridgeResponse single:message forSubscription:subscription]; + break; + + case TGBridgeResponseTypeFailed: + response = [TGBridgeResponse fail:message forSubscription:subscription]; + break; + + case TGBridgeResponseTypeCompleted: + response = [TGBridgeResponse completeForSubscription:subscription]; + break; + + default: + break; + } + + OSSpinLockLock(&_replyHandlerMapLock); + void (^reply)(NSData *) = _replyHandlerMap[@(subscription.identifier)]; + if (reply != nil) + [_replyHandlerMap removeObjectForKey:@(subscription.identifier)]; + OSSpinLockUnlock(&_replyHandlerMapLock); + + if (_processingNotification) + { + [self _enqueueResponse:response forSubscription:subscription]; + + if (completion != nil) + completion(); + + return; + } + + NSData *messageData = [NSKeyedArchiver archivedDataWithRootObject:response]; + if (reply != nil && messageData.length < 64000) + { + reply(messageData); + + if (completion != nil) + completion(); + } + else + { + if (reply != nil) + reply([NSData data]); + + if (self.session.isReachable) + { + [self.session sendMessageData:messageData replyHandler:nil errorHandler:^(NSError *error) + { + //if (error != nil) + // TGLog(@"[BridgeServer]: send response for subscription %lld failed with error %@", subscription.identifier, error); + }]; + } + else + { + //TGLog(@"[BridgeServer]: client out of reach, queueing response for subscription %lld", subscription.identifier); + [self _enqueueResponse:response forSubscription:subscription]; + } + + if (completion != nil) + completion(); + } +} + +- (void)_enqueueResponse:(TGBridgeResponse *)response forSubscription:(TGBridgeSubscription *)subscription +{ + OSSpinLockLock(&_outgoingQueueLock); + NSMutableArray *updatedResponses = (_outgoingMessageQueue != nil) ? [_outgoingMessageQueue mutableCopy] : [[NSMutableArray alloc] init]; + + if (subscription.dropPreviouslyQueued) + { + NSMutableIndexSet *indexSet = [[NSMutableIndexSet alloc] init]; + + [updatedResponses enumerateObjectsUsingBlock:^(TGBridgeResponse *queuedResponse, NSUInteger index, __unused BOOL *stop) + { + if (queuedResponse.subscriptionIdentifier == subscription.identifier) + [indexSet addIndex:index]; + }]; + + [updatedResponses removeObjectsAtIndexes:indexSet]; + } + + [updatedResponses addObject:response]; + + _outgoingMessageQueue = updatedResponses; + OSSpinLockUnlock(&_outgoingQueueLock); +} + +- (void)_sendQueuedResponses +{ + if (_processingNotification) + return; + + OSSpinLockLock(&_outgoingQueueLock); + + if (_outgoingMessageQueue.count > 0) + { + //TGLog(@"[BridgeServer] Sending queued responses"); + + for (TGBridgeResponse *response in _outgoingMessageQueue) + { + NSData *messageData = [NSKeyedArchiver archivedDataWithRootObject:response]; + [self.session sendMessageData:messageData replyHandler:nil errorHandler:nil]; + } + + [_outgoingMessageQueue removeAllObjects]; + } + OSSpinLockUnlock(&_outgoingQueueLock); +} + +- (void)_requestSubscriptionList +{ + TGBridgeSubscriptionListRequest *request = [[TGBridgeSubscriptionListRequest alloc] initWithSessionId:_sessionId]; + NSData *messageData = [NSKeyedArchiver archivedDataWithRootObject:request]; + [self.session sendMessageData:messageData replyHandler:nil errorHandler:nil]; +} + +- (void)sendFileWithURL:(NSURL *)url metadata:(NSDictionary *)metadata asMessageData:(bool)asMessageData +{ + //TGLog(@"[BridgeServer] Sent file with metadata %@", metadata); + if (asMessageData && self.session.isReachable) { + NSData *data = [NSData dataWithContentsOfURL:url]; + [self sendFileWithData:data metadata:metadata errorHandler:^{ + [self.session transferFile:url metadata:metadata]; + }]; + } else { + [self.session transferFile:url metadata:metadata]; + } +} + +- (void)sendFileWithData:(NSData *)data metadata:(NSDictionary *)metadata errorHandler:(void (^)(void))errorHandler +{ + TGBridgeFile *file = [[TGBridgeFile alloc] initWithData:data metadata:metadata]; + NSData *messageData = [NSKeyedArchiver archivedDataWithRootObject:file]; + [self.session sendMessageData:messageData replyHandler:nil errorHandler:^(NSError *error) { + if (errorHandler != nil) + errorHandler(); + }]; +} + +#pragma mark - Tasks + +- (id)beginTask +{ + int64_t randomId = 0; + arc4random_buf(&randomId, 8); + NSNumber *taskId = @(randomId); + + _runningTasks[taskId] = @true; + [_hasRunningTasks set:[SSignal single:@{@"version": @(_tasksVersion++), @"running": @true}]]; + + SBlockDisposable *taskDisposable = [[SBlockDisposable alloc] initWithBlock:^{ + [_runningTasks removeObjectForKey:taskId]; + [_hasRunningTasks set:[SSignal single:@{@"version": @(_tasksVersion++), @"running": @(_runningTasks.count > 0)}]]; + }]; + + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)((4.0) * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + [self dispatch:^{ + [taskDisposable dispose]; + }]; + }); + + return taskDisposable; +} + +#pragma mark - Session Delegate + +- (void)handleReceivedData:(NSData *)messageData replyHandler:(void (^)(NSData *))replyHandler +{ + if (messageData.length == 0) + { + if (replyHandler != nil) + replyHandler([NSData data]); + return; + } + +// __block UIBackgroundTaskIdentifier backgroundTask; +// backgroundTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^ +// { +// if (replyHandler != nil) +// replyHandler([NSData data]); +// [[UIApplication sharedApplication] endBackgroundTask:backgroundTask]; +// }]; +// + + [self handleMessageData:messageData task:[self beginTask] replyHandler:replyHandler completion:^{}]; +} + +- (void)session:(WCSession *)__unused session didReceiveMessageData:(NSData *)messageData +{ + [self dispatch:^{ + [self handleReceivedData:messageData replyHandler:nil]; + }]; +} + +- (void)session:(WCSession *)__unused session didReceiveMessageData:(NSData *)messageData replyHandler:(void (^)(NSData *))replyHandler +{ + [self dispatch:^{ + [self handleReceivedData:messageData replyHandler:replyHandler]; + }]; +} + +- (void)session:(WCSession *)__unused session didReceiveFile:(WCSessionFile *)file +{ + NSDictionary *metadata = file.metadata; + if (metadata == nil || ![metadata[TGBridgeIncomingFileTypeKey] isEqualToString:TGBridgeIncomingFileTypeAudio]) + return; + + NSError *error; + NSURL *tempURL = [NSURL URLWithString:file.fileURL.lastPathComponent relativeToURL:self.temporaryFilesURL]; + [[NSFileManager defaultManager] createDirectoryAtPath:self.temporaryFilesURL.path withIntermediateDirectories:true attributes:nil error:&error]; + [[NSFileManager defaultManager] moveItemAtURL:file.fileURL toURL:tempURL error:&error]; + + [self dispatch:^{ + _fileHandler(tempURL.path, file.metadata); + }]; +} + +- (void)session:(WCSession *)__unused session didFinishFileTransfer:(WCSessionFileTransfer *)__unused fileTransfer error:(NSError *)__unused error +{ + +} + +- (void)session:(nonnull WCSession *)session activationDidCompleteWithState:(WCSessionActivationState)activationState error:(nullable NSError *)error { + +} + + +- (void)sessionDidBecomeInactive:(nonnull WCSession *)session { + +} + + +- (void)sessionDidDeactivate:(nonnull WCSession *)session { + +} + +- (void)sessionWatchStateDidChange:(WCSession *)session +{ + [self dispatch:^{ + if (session.isWatchAppInstalled) + [self pushContext]; + + _appInstalled.sink(@(session.isWatchAppInstalled)); + }]; +} + +- (void)sessionReachabilityDidChange:(WCSession *)session +{ + NSLog(@"[TGBridgeServer] Reachability changed: %d", session.isReachable); +} + +#pragma mark - + +- (NSInteger)wakeupNetwork +{ + return 0; +} + +- (void)suspendNetworkIfReady:(NSInteger)token +{ +} + +#pragma mark - + +- (WCSession *)session +{ + return [WCSession defaultSession]; +} + +@end + + +@interface TGBridgeSignalManager() +{ + OSSpinLock _lock; + NSMutableDictionary *_disposables; +} +@end + +@implementation TGBridgeSignalManager + +- (instancetype)init +{ + self = [super init]; + if (self != nil) + { + _disposables = [[NSMutableDictionary alloc] init]; + } + return self; +} + +- (void)dealloc +{ + NSArray *disposables = nil; + OSSpinLockLock(&_lock); + disposables = [_disposables allValues]; + OSSpinLockUnlock(&_lock); + + for (id disposable in disposables) + { + [disposable dispose]; + } +} + +- (bool)startSignalForKey:(NSString *)key producer:(SSignal *(^)())producer +{ + if (key == nil) + return false; + + bool produce = false; + OSSpinLockLock(&_lock); + if (_disposables[key] == nil) + { + _disposables[key] = [[SMetaDisposable alloc] init]; + produce = true; + } + OSSpinLockUnlock(&_lock); + + if (produce) + { + __weak TGBridgeSignalManager *weakSelf = self; + id disposable = [producer() startWithNext:nil error:^(__unused id error) + { + __strong TGBridgeSignalManager *strongSelf = weakSelf; + if (strongSelf != nil) + { + OSSpinLockLock(&strongSelf->_lock); + [strongSelf->_disposables removeObjectForKey:key]; + OSSpinLockUnlock(&strongSelf->_lock); + } + } completed:^ + { + __strong TGBridgeSignalManager *strongSelf = weakSelf; + if (strongSelf != nil) + { + OSSpinLockLock(&strongSelf->_lock); + [strongSelf->_disposables removeObjectForKey:key]; + OSSpinLockUnlock(&strongSelf->_lock); + } + }]; + + OSSpinLockLock(&_lock); + [(SMetaDisposable *)_disposables[key] setDisposable:disposable]; + OSSpinLockUnlock(&_lock); + } + + return produce; +} + +- (void)haltSignalForKey:(NSString *)key +{ + if (key == nil) + return; + + OSSpinLockLock(&_lock); + if (_disposables[key] != nil) + { + [_disposables[key] dispose]; + [_disposables removeObjectForKey:key]; + } + OSSpinLockUnlock(&_lock); +} + +- (void)haltAllSignals +{ + OSSpinLockLock(&_lock); + for (NSObject *disposable in _disposables.allValues) + [disposable dispose]; + [_disposables removeAllObjects]; + OSSpinLockUnlock(&_lock); +} + +@end diff --git a/Telegram-iOS/TGPresentationAutoNightPreferences.h b/Telegram-iOS/TGPresentationAutoNightPreferences.h new file mode 100644 index 0000000000..1a403c5215 --- /dev/null +++ b/Telegram-iOS/TGPresentationAutoNightPreferences.h @@ -0,0 +1,31 @@ +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +typedef enum +{ + TGPresentationAutoNightModeDisabled, + TGPresentationAutoNightModeBrightness, + TGPresentationAutoNightModeScheduled, + TGPresentationAutoNightModeSunsetSunrise +} TGPresentationAutoNightMode; + +@interface TGPresentationAutoNightPreferences : NSObject + +@property (nonatomic, readonly) TGPresentationAutoNightMode mode; + +@property (nonatomic, readonly) CGFloat brightnessThreshold; + +@property (nonatomic, readonly) int32_t scheduleStart; +@property (nonatomic, readonly) int32_t scheduleEnd; + +@property (nonatomic, readonly) CGFloat latitude; +@property (nonatomic, readonly) CGFloat longitude; +@property (nonatomic, readonly) NSString *cachedLocationName; + +@property (nonatomic, readonly) int32_t preferredPalette; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Telegram-iOS/TGPresentationAutoNightPreferences.m b/Telegram-iOS/TGPresentationAutoNightPreferences.m new file mode 100644 index 0000000000..e125ea54f0 --- /dev/null +++ b/Telegram-iOS/TGPresentationAutoNightPreferences.m @@ -0,0 +1,23 @@ +#import "TGPresentationAutoNightPreferences.h" + +@implementation TGPresentationAutoNightPreferences + +- (instancetype)initWithCoder:(NSCoder *)aDecoder { + self = [super init]; + if (self != nil) { + _mode = [aDecoder decodeInt32ForKey:@"m"]; + _brightnessThreshold = [aDecoder decodeDoubleForKey:@"b"]; + _scheduleStart = [aDecoder decodeInt32ForKey:@"ss"]; + _scheduleEnd = [aDecoder decodeInt32ForKey:@"se"]; + _latitude = [aDecoder decodeDoubleForKey:@"lat"]; + _longitude = [aDecoder decodeDoubleForKey:@"lon"]; + _cachedLocationName = [aDecoder decodeObjectForKey:@"loc"]; + _preferredPalette = [aDecoder decodeInt32ForKey:@"p"]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder { +} + +@end diff --git a/Telegram-iOS/TGProxyItem.h b/Telegram-iOS/TGProxyItem.h new file mode 100644 index 0000000000..72c1b76fd3 --- /dev/null +++ b/Telegram-iOS/TGProxyItem.h @@ -0,0 +1,15 @@ +#import + +@interface TGProxyItem : NSObject + +@property (nonatomic, readonly) NSString *server; +@property (nonatomic, readonly) int16_t port; +@property (nonatomic, readonly) NSString *username; +@property (nonatomic, readonly) NSString *password; +@property (nonatomic, readonly) NSString *secret; + +@property (nonatomic, readonly) bool isMTProxy; + +- (instancetype)initWithServer:(NSString *)server port:(int16_t)port username:(NSString *)username password:(NSString *)password secret:(NSString *)secret; + +@end diff --git a/Telegram-iOS/TGProxyItem.m b/Telegram-iOS/TGProxyItem.m new file mode 100644 index 0000000000..edc270d3ae --- /dev/null +++ b/Telegram-iOS/TGProxyItem.m @@ -0,0 +1,73 @@ +#import "TGProxyItem.h" + +static bool TGObjectCompare(id obj1, id obj2) { + if (obj1 == nil && obj2 == nil) + return true; + + return [obj1 isEqual:obj2]; +} + +@implementation TGProxyItem + +- (instancetype)initWithServer:(NSString *)server port:(int16_t)port username:(NSString *)username password:(NSString *)password secret:(NSString *)secret +{ + self = [super init]; + if (self != nil) + { + _server = server; + _port = port; + _username = username; + _password = password; + _secret = secret; + } + return self; +} + +- (instancetype)initWithCoder:(NSCoder *)aDecoder +{ + return [self initWithServer:[aDecoder decodeObjectForKey:@"server"] port:(int16_t)[aDecoder decodeInt32ForKey:@"port"] username:[aDecoder decodeObjectForKey:@"user"] password:[aDecoder decodeObjectForKey:@"pass"] secret:[aDecoder decodeObjectForKey:@"secret"]]; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder +{ + [aCoder encodeObject:_server forKey:@"server"]; + [aCoder encodeInt32:_port forKey:@"port"]; + [aCoder encodeObject:_username forKey:@"user"]; + [aCoder encodeObject:_password forKey:@"pass"]; + [aCoder encodeObject:_secret forKey:@"secret"]; +} + +- (bool)isMTProxy +{ + return _secret.length > 0; +} + +- (BOOL)isEqual:(id)object +{ + if (object == self) + return true; + + if (!object || ![object isKindOfClass:[self class]]) + return false; + + TGProxyItem *proxy = (TGProxyItem *)object; + + if (![_server isEqualToString:proxy.server]) + return false; + + if (_port != proxy.port) + return false; + + if (!TGObjectCompare(_username ?: @"", proxy.username ?: @"")) + return false; + + if (!TGObjectCompare(_password ?: @"", proxy.password ?: @"")) + return false; + + if (!TGObjectCompare(_secret ?: @"", proxy.secret ?: @"")) + return false; + + return true; +} + +@end diff --git a/Telegram-iOS/Telegram-Bridging-Header.h b/Telegram-iOS/Telegram-Bridging-Header.h new file mode 100644 index 0000000000..6d119f44a8 --- /dev/null +++ b/Telegram-iOS/Telegram-Bridging-Header.h @@ -0,0 +1,17 @@ +#ifndef Telegram_iOS_Telegram_Bridging_Header_h +#define Telegram_iOS_Telegram_Bridging_Header_h + +#import "BuildConfig.h" +#import "TGAutoDownloadPreferences.h" +#import "TGPresentationAutoNightPreferences.h" +#import "TGProxyItem.h" +#import "UIImage+ImageEffects.h" + +#import "TGBridgeServer.h" +#import "TGBridgeCommon.h" +#import "TGBridgeSubscriptions.h" +#import "TGBridgeChat.h" +#import "TGBridgeUser.h" +#import "TGBridgeLocationVenue.h" + +#endif diff --git a/Telegram-iOS/Telegram-iOS-AppStore.entitlements b/Telegram-iOS/Telegram-iOS-AppStore.entitlements new file mode 100644 index 0000000000..b593d27900 --- /dev/null +++ b/Telegram-iOS/Telegram-iOS-AppStore.entitlements @@ -0,0 +1,19 @@ + + + + + aps-environment + production + com.apple.developer.associated-domains + + applinks:telegram.me + applinks:t.me + + com.apple.developer.siri + + com.apple.security.application-groups + + group.org.telegram.TelegramHD + + + diff --git a/Telegram-iOS/Telegram-iOS-AppStoreLLC.entitlements b/Telegram-iOS/Telegram-iOS-AppStoreLLC.entitlements new file mode 100644 index 0000000000..9936cfa965 --- /dev/null +++ b/Telegram-iOS/Telegram-iOS-AppStoreLLC.entitlements @@ -0,0 +1,37 @@ + + + + + com.apple.developer.icloud-services + + CloudDocuments + CloudKit + + com.apple.developer.icloud-container-identifiers + + iCloud.$(CFBundleIdentifier) + + aps-environment + production + com.apple.developer.associated-domains + + applinks:telegram.me + applinks:t.me + + com.apple.developer.siri + + com.apple.security.application-groups + + group.ph.telegra.Telegraph + + com.apple.developer.in-app-payments + + merchant.ph.telegra.Telegraph + merchant.yandex.ph.telegra.Telegraph + merchant.sberbank.ph.telegra.Telegraph + merchant.sberbank.test.ph.telegra.Telegraph + merchant.privatbank.test.telergramios + merchant.privatbank.prod.telergram + + + diff --git a/Telegram-iOS/Telegram-iOS-Hockeyapp.entitlements b/Telegram-iOS/Telegram-iOS-Hockeyapp.entitlements new file mode 100644 index 0000000000..448f80b602 --- /dev/null +++ b/Telegram-iOS/Telegram-iOS-Hockeyapp.entitlements @@ -0,0 +1,32 @@ + + + + + aps-environment + development + com.apple.developer.associated-domains + + applinks:telegram.me + applinks:t.me + + com.apple.developer.icloud-container-identifiers + + iCloud.org.telegram.Telegram-iOS + + com.apple.developer.icloud-services + + CloudKit + CloudDocuments + + com.apple.developer.siri + + com.apple.developer.ubiquity-container-identifiers + + iCloud.org.telegram.Telegram-iOS + + com.apple.security.application-groups + + group.org.telegram.Telegram-iOS + + + diff --git a/Telegram-iOS/UIImage+ImageEffects.h b/Telegram-iOS/UIImage+ImageEffects.h new file mode 100644 index 0000000000..d13a415abf --- /dev/null +++ b/Telegram-iOS/UIImage+ImageEffects.h @@ -0,0 +1,9 @@ + + +#import + +@interface UIImage (ImageEffects) + +- (UIImage *)applyScreenshotEffect; + +@end diff --git a/Telegram-iOS/UIImage+ImageEffects.m b/Telegram-iOS/UIImage+ImageEffects.m new file mode 100644 index 0000000000..cdb6322cf2 --- /dev/null +++ b/Telegram-iOS/UIImage+ImageEffects.m @@ -0,0 +1,147 @@ + + +#import "UIImage+ImageEffects.h" + +#import +#import + +@implementation UIImage (ImageEffects) + +- (UIImage *)applyScreenshotEffect +{ + UIColor *tintColor = [UIColor colorWithWhite:1.0f alpha:0.3f]; + return [self applyBlurWithRadius:10 tintColor:tintColor saturationDeltaFactor:1.8f maskImage:nil]; +} + +- (UIImage *)applyBlurWithRadius:(CGFloat)blurRadius tintColor:(UIColor *)tintColor saturationDeltaFactor:(CGFloat)saturationDeltaFactor maskImage:(UIImage *)maskImage +{ + // Check pre-conditions. + if (self.size.width < 1 || self.size.height < 1) { + NSLog (@"*** error: invalid size: (%.2f x %.2f). Both dimensions must be >= 1: %@", self.size.width, self.size.height, self); + return nil; + } + if (!self.CGImage) { + NSLog (@"*** error: image must be backed by a CGImage: %@", self); + return nil; + } + if (maskImage && !maskImage.CGImage) { + NSLog (@"*** error: maskImage must be backed by a CGImage: %@", maskImage); + return nil; + } + + CGRect imageRect = { CGPointZero, self.size }; + UIImage *effectImage = self; + + BOOL hasBlur = blurRadius > __FLT_EPSILON__; + BOOL hasSaturationChange = fabs(saturationDeltaFactor - 1.) > __FLT_EPSILON__; + if (hasBlur || hasSaturationChange) { + UIGraphicsBeginImageContextWithOptions(self.size, NO, [[UIScreen mainScreen] scale]); + CGContextRef effectInContext = UIGraphicsGetCurrentContext(); + CGContextScaleCTM(effectInContext, 1.0, -1.0); + CGContextTranslateCTM(effectInContext, 0, -self.size.height); + CGContextDrawImage(effectInContext, imageRect, self.CGImage); + + vImage_Buffer effectInBuffer; + effectInBuffer.data = CGBitmapContextGetData(effectInContext); + effectInBuffer.width = CGBitmapContextGetWidth(effectInContext); + effectInBuffer.height = CGBitmapContextGetHeight(effectInContext); + effectInBuffer.rowBytes = CGBitmapContextGetBytesPerRow(effectInContext); + + UIGraphicsBeginImageContextWithOptions(self.size, NO, [[UIScreen mainScreen] scale]); + CGContextRef effectOutContext = UIGraphicsGetCurrentContext(); + vImage_Buffer effectOutBuffer; + effectOutBuffer.data = CGBitmapContextGetData(effectOutContext); + effectOutBuffer.width = CGBitmapContextGetWidth(effectOutContext); + effectOutBuffer.height = CGBitmapContextGetHeight(effectOutContext); + effectOutBuffer.rowBytes = CGBitmapContextGetBytesPerRow(effectOutContext); + + if (hasBlur) { + // A description of how to compute the box kernel width from the Gaussian + // radius (aka standard deviation) appears in the SVG spec: + // http://www.w3.org/TR/SVG/filters.html#feGaussianBlurElement + // + // For larger values of 's' (s >= 2.0), an approximation can be used: Three + // successive box-blurs build a piece-wise quadratic convolution kernel, which + // approximates the Gaussian kernel to within roughly 3%. + // + // let d = floor(s * 3*sqrt(2*pi)/4 + 0.5) + // + // ... if d is odd, use three box-blurs of size 'd', centered on the output pixel. + // + CGFloat inputRadius = blurRadius * [[UIScreen mainScreen] scale]; + NSUInteger radius = (NSUInteger)(floor(inputRadius * 3.0f * ((CGFloat)sqrt(2 * M_PI)) / 4 + 0.5f)); + if (radius % 2 != 1) { + radius += 1; // force radius to be odd so that the three box-blur methodology works. + } + vImageBoxConvolve_ARGB8888(&effectInBuffer, &effectOutBuffer, NULL, 0, 0, (uint32_t)radius, (uint32_t)radius, 0, kvImageEdgeExtend); + vImageBoxConvolve_ARGB8888(&effectOutBuffer, &effectInBuffer, NULL, 0, 0, (uint32_t)radius, (uint32_t)radius, 0, kvImageEdgeExtend); + vImageBoxConvolve_ARGB8888(&effectInBuffer, &effectOutBuffer, NULL, 0, 0, (uint32_t)radius, (uint32_t)radius, 0, kvImageEdgeExtend); + } + BOOL effectImageBuffersAreSwapped = NO; + if (hasSaturationChange) { + CGFloat s = saturationDeltaFactor; + CGFloat floatingPointSaturationMatrix[] = { + 0.0722f + 0.9278f * s, 0.0722f - 0.0722f * s, 0.0722f - 0.0722f * s, 0, + 0.7152f - 0.7152f * s, 0.7152f + 0.2848f * s, 0.7152f - 0.7152f * s, 0, + 0.2126f - 0.2126f * s, 0.2126f - 0.2126f * s, 0.2126f + 0.7873f * s, 0, + 0, 0, 0, 1, + }; + const int32_t divisor = 256; + NSUInteger matrixSize = sizeof(floatingPointSaturationMatrix)/sizeof(floatingPointSaturationMatrix[0]); + int16_t saturationMatrix[matrixSize]; + for (NSUInteger i = 0; i < matrixSize; ++i) { + saturationMatrix[i] = (int16_t)floor(floatingPointSaturationMatrix[i] * divisor); + } + if (hasBlur) { + vImageMatrixMultiply_ARGB8888(&effectOutBuffer, &effectInBuffer, saturationMatrix, divisor, NULL, NULL, kvImageNoFlags); + effectImageBuffersAreSwapped = YES; + } + else { + vImageMatrixMultiply_ARGB8888(&effectInBuffer, &effectOutBuffer, saturationMatrix, divisor, NULL, NULL, kvImageNoFlags); + } + } + if (!effectImageBuffersAreSwapped) + effectImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + + if (effectImageBuffersAreSwapped) + effectImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + } + + // Set up output context. + UIGraphicsBeginImageContextWithOptions(self.size, NO, [[UIScreen mainScreen] scale]); + CGContextRef outputContext = UIGraphicsGetCurrentContext(); + CGContextScaleCTM(outputContext, 1.0, -1.0); + CGContextTranslateCTM(outputContext, 0, -self.size.height); + + // Draw base image. + CGContextDrawImage(outputContext, imageRect, self.CGImage); + + // Draw effect image. + if (hasBlur) { + CGContextSaveGState(outputContext); + if (maskImage) { + CGContextClipToMask(outputContext, imageRect, maskImage.CGImage); + } + CGContextDrawImage(outputContext, imageRect, effectImage.CGImage); + CGContextRestoreGState(outputContext); + } + + // Add in color tint. + if (tintColor) { + CGContextSaveGState(outputContext); + CGContextSetFillColorWithColor(outputContext, tintColor.CGColor); + CGContextFillRect(outputContext, imageRect); + CGContextRestoreGState(outputContext); + } + + // Output image is ready. + UIImage *outputImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + + return outputImage; +} + + +@end diff --git a/Telegram-iOS/WakeupManager.swift b/Telegram-iOS/WakeupManager.swift new file mode 100644 index 0000000000..f31105442b --- /dev/null +++ b/Telegram-iOS/WakeupManager.swift @@ -0,0 +1,305 @@ +import Foundation +import TelegramCore +import SwiftSignalKit +import UIKit +import Postbox +import UserNotifications +import TelegramUI + +private final class WakeupManagerTask { + let nativeId: UIBackgroundTaskIdentifier + let id: Int32 + let timer: SwiftSignalKit.Timer + + init(nativeId: UIBackgroundTaskIdentifier, id: Int32, timer: SwiftSignalKit.Timer) { + self.nativeId = nativeId + self.id = id + self.timer = timer + } + + deinit { + assert(Queue.mainQueue().isCurrent()) + self.timer.invalidate() + } +} + +private final class WakeupManagerState { + var nextTaskId: Int32 = 0 + var currentTask: WakeupManagerTask? + var currentServiceTask: WakeupManagerTask? +} + +private struct CombinedRunningImportantTasks: Equatable { + let serviceTasks: AccountRunningImportantTasks + let backgroundLocation: Bool + let watchTasks: WatchRunningTasks? + + var isEmpty: Bool { + var hasWatchTask = false + if let watchTasks = self.watchTasks { + hasWatchTask = watchTasks.running + } + return self.serviceTasks.isEmpty && !self.backgroundLocation && !hasWatchTask + } + + static func ==(lhs: CombinedRunningImportantTasks, rhs: CombinedRunningImportantTasks) -> Bool { + return lhs.serviceTasks == rhs.serviceTasks && lhs.backgroundLocation == rhs.backgroundLocation && lhs.watchTasks == rhs.watchTasks + } +} + +final class WakeupManager { + private var state = WakeupManagerState() + + var account: Account? { + didSet { + assert(Queue.mainQueue().isCurrent()) + } + } + + private let isProcessingNotificationsValue = ValuePromise(false, ignoreRepeated: true) + private let isProcessingServiceTasksValue = ValuePromise(false, ignoreRepeated: true) + var isWokenUp: Signal { + return combineLatest([self.isProcessingNotificationsValue.get(), isProcessingServiceTasksValue.get()]) + |> map { values -> Bool in + for value in values { + if value { + return true + } + } + return false + } + } + + private var inForegroundDisposable: Disposable? + private var runningServiceTasksDisposable: Disposable? + private var runningServiceTasksValue: CombinedRunningImportantTasks = CombinedRunningImportantTasks(serviceTasks: [], backgroundLocation: false, watchTasks: nil) + private let wakeupDisposable = MetaDisposable() + + private var wakeupResultSubscribers: [(Int32, ([MessageId]) -> Signal)] = [] + + init(inForeground: Signal, runningServiceTasks: Signal, runningBackgroundLocationTasks: Signal, runningWatchTasks: Signal) { + self.inForegroundDisposable = (inForeground |> distinctUntilChanged |> deliverOnMainQueue).start(next: { [weak self] value in + if let strongSelf = self { + if value { + if let currentTask = strongSelf.state.currentTask { + strongSelf.state.currentTask = nil + Logger.shared.log("WakeupManager", "ending task #\(currentTask.id) (entered foreground)") + currentTask.timer.invalidate() + strongSelf.wakeupDisposable.set(nil) + strongSelf.isProcessingNotificationsValue.set(false) + UIApplication.shared.endBackgroundTask(currentTask.nativeId) + } + } + } + }) + self.runningServiceTasksDisposable = (combineLatest(inForeground, runningServiceTasks, runningBackgroundLocationTasks, runningWatchTasks) + |> map { inForeground, runningServiceTasks, runningBackgroundLocationTasks, runningWatchTasks -> CombinedRunningImportantTasks in + let combinedTasks = CombinedRunningImportantTasks(serviceTasks: runningServiceTasks, backgroundLocation: runningBackgroundLocationTasks, watchTasks: runningWatchTasks) + if !inForeground && !combinedTasks.isEmpty { + return combinedTasks + } else { + return CombinedRunningImportantTasks(serviceTasks: [], backgroundLocation: false, watchTasks: nil) + } + } + |> distinctUntilChanged + |> deliverOnMainQueue).start(next: { [weak self] value in + if let strongSelf = self { + strongSelf.runningServiceTasksValue = value + if !value.isEmpty { + //assert(strongSelf.state.currentServiceTask == nil) + strongSelf.wakeupForServiceTasks(timeout: value.serviceTasks.contains(.pendingMessages) ? 85.0 : 25.0) + } else if let currentServiceTask = strongSelf.state.currentServiceTask { + strongSelf.state.currentServiceTask = nil + Logger.shared.log("WakeupManager", "ending service task #\(currentServiceTask.id)") + currentServiceTask.timer.invalidate() + strongSelf.isProcessingServiceTasksValue.set(false) + + Queue.mainQueue().after(2.0, { + UIApplication.shared.endBackgroundTask(currentServiceTask.nativeId) + }) + } + } + }) + } + + deinit { + self.inForegroundDisposable?.dispose() + self.wakeupDisposable.dispose() + } + + private func reportCompletionToSubscribersAndGetUnreadCount(maxId: Int32, messageIds: [MessageId]) -> Signal { + var collectedSignals: [Signal] = [] + while !self.wakeupResultSubscribers.isEmpty { + let first = self.wakeupResultSubscribers[0] + if first.0 <= maxId { + self.wakeupResultSubscribers.remove(at: 0) + collectedSignals.append(first.1(messageIds)) + } + } + return combineLatest(collectedSignals) + |> map { _ -> Void in + return Void() + } |> mapToSignal { [weak self] _ -> Signal in + if let strongSelf = self, let account = strongSelf.account, !messageIds.isEmpty { + return account.postbox.transaction { transaction -> Int32? in + let (unreadCount, _) = renderedTotalUnreadCount(transaction: transaction) + return unreadCount + } + } else { + return .single(nil) + } + } + } + + private func wakeupForServiceTasks(timeout: Double = 25.0) { + assert(Queue.mainQueue().isCurrent()) + + var endTask: WakeupManagerTask? + let updatedId: Int32 = self.state.nextTaskId + self.state.nextTaskId += 1 + + let handleExpiration: (Bool) -> Void = { [weak self] byTimer in + Queue.mainQueue().async { + if let strongSelf = self { + if let currentServiceTask = strongSelf.state.currentServiceTask { + if currentServiceTask.id == updatedId { + if byTimer && strongSelf.runningServiceTasksValue.serviceTasks.contains(.pendingMessages) { + /*if #available(iOS 10.0, *) { + let content = UNMutableNotificationContent() + content.body = "Please open the app to continue sending messages" + content.sound = UNNotificationSound.default() + content.categoryIdentifier = "error" + + let request = UNNotificationRequest(identifier: "reply-error", content: content, trigger: nil) + + let center = UNUserNotificationCenter.current() + center.add(request) + }*/ + } + + Logger.shared.log("WakeupManager", "handleExpiration(by timer: \(byTimer)) invoked, ending service task #\(currentServiceTask.id)") + strongSelf.state.currentServiceTask = nil + currentServiceTask.timer.invalidate() + strongSelf.isProcessingServiceTasksValue.set(false) + UIApplication.shared.endBackgroundTask(currentServiceTask.nativeId) + } else { + Logger.shared.log("WakeupManager", "handleExpiration(by timer: \(byTimer)) invoked, current service task doesn't match") + } + } else { + Logger.shared.log("WakeupManager", "handleExpiration(by timer: \(byTimer)) invoked, no current service task") + } + } + } + } + + let updatedNativeId = UIApplication.shared.beginBackgroundTask(withName: "service", expirationHandler: { + handleExpiration(false) + }) + Logger.shared.log("WakeupManager", "started service task #\(updatedId)") + let updatedTimer = SwiftSignalKit.Timer(timeout: timeout, repeat: false, completion: { + handleExpiration(true) + }, queue: Queue.mainQueue()) + let updatedTask = WakeupManagerTask(nativeId: updatedNativeId, id: updatedId, timer: updatedTimer) + + if let currentServiceTask = self.state.currentServiceTask { + endTask = currentServiceTask + } + self.state.currentServiceTask = updatedTask + self.isProcessingServiceTasksValue.set(true) + + updatedTimer.start() + + if let endTask = endTask { + Logger.shared.log("WakeupManager", "ending service task #\(endTask.id) (replaced by #\(updatedTask.id))") + endTask.timer.invalidate() + UIApplication.shared.endBackgroundTask(endTask.nativeId) + } + } + + func wakeupForIncomingMessages(timeout: Double = 25.0, completion: (([MessageId]) -> Signal)? = nil) { + assert(Queue.mainQueue().isCurrent()) + guard let account = self.account else { + return + } + + var endTask: WakeupManagerTask? + let updatedId: Int32 = self.state.nextTaskId + self.state.nextTaskId += 1 + + if let completion = completion { + self.wakeupResultSubscribers.append((updatedId, completion)) + } + + let handleExpiration: (Bool) -> Void = { [weak self] byTimer in + if let strongSelf = self { + if let currentTask = strongSelf.state.currentTask { + if currentTask.id == updatedId { + Logger.shared.log("WakeupManager", "handleExpiration(by timer: \(byTimer)) invoked, ending task #\(currentTask.id)") + strongSelf.state.currentTask = nil + currentTask.timer.invalidate() + strongSelf.isProcessingNotificationsValue.set(false) + let _ = strongSelf.reportCompletionToSubscribersAndGetUnreadCount(maxId: updatedId, messageIds: []).start() + UIApplication.shared.endBackgroundTask(currentTask.nativeId) + } else { + Logger.shared.log("WakeupManager", "handleExpiration(by timer: \(byTimer)) invoked, current task doesn't match") + } + } else { + Logger.shared.log("WakeupManager", "handleExpiration(by timer: \(byTimer)) invoked, no current task") + } + } + } + + let updatedNativeId = UIApplication.shared.beginBackgroundTask(withName: "wakeup", expirationHandler: { + handleExpiration(false) + }) + Logger.shared.log("WakeupManager", "started task #\(updatedId)") + let updatedTimer = SwiftSignalKit.Timer(timeout: timeout, repeat: false, completion: { + handleExpiration(true) + }, queue: Queue.mainQueue()) + let updatedTask = WakeupManagerTask(nativeId: updatedNativeId, id: updatedId, timer: updatedTimer) + + if let currentTask = self.state.currentTask { + endTask = currentTask + } + self.state.currentTask = updatedTask + self.isProcessingNotificationsValue.set(true) + + updatedTimer.start() + + if let endTask = endTask { + Logger.shared.log("WakeupManager", "ending task #\(endTask.id) (replaced by #\(updatedTask.id))") + endTask.timer.invalidate() + UIApplication.shared.endBackgroundTask(endTask.nativeId) + } + + self.wakeupDisposable.set((account.stateManager.pollStateUpdateCompletion() |> deliverOnMainQueue |> mapToSignal { [weak self] messageIds -> Signal in + if let strongSelf = self { + Logger.shared.log("WakeupManager", "pollStateUpdateCompletion messageIds: \(messageIds)") + return strongSelf.reportCompletionToSubscribersAndGetUnreadCount(maxId: updatedId, messageIds: messageIds) + } else { + return .complete() + } + } |> deliverOnMainQueue).start(next: { [weak self] maybeUnreadCount in + if let strongSelf = self { + if let maybeUnreadCount = maybeUnreadCount { + if UIApplication.shared.applicationIconBadgeNumber != Int(maybeUnreadCount) { + UIApplication.shared.applicationIconBadgeNumber = Int(maybeUnreadCount) + } + } + if let currentTask = strongSelf.state.currentTask { + if currentTask.id == updatedId { + Logger.shared.log("WakeupManager", "account state wakeup completed, ending task #\(currentTask.id)") + strongSelf.isProcessingNotificationsValue.set(false) + strongSelf.state.currentTask = nil + currentTask.timer.invalidate() + UIApplication.shared.endBackgroundTask(currentTask.nativeId) + } else { + Logger.shared.log("WakeupManager", "account state wakeup completed, current task doesn't match") + } + } else { + Logger.shared.log("WakeupManager", "account state wakeup completed, no current task") + } + } + })) + } +} diff --git a/Telegram-iOS/WatchBridge.swift b/Telegram-iOS/WatchBridge.swift new file mode 100644 index 0000000000..486784844c --- /dev/null +++ b/Telegram-iOS/WatchBridge.swift @@ -0,0 +1,521 @@ +import Postbox +import TelegramCore +import TelegramUI + +func makePeerIdFromBridgeIdentifier(_ identifier: Int64) -> PeerId? { + if identifier < 0 && identifier > Int32.min { + return PeerId(namespace: Namespaces.Peer.CloudGroup, id: Int32(clamping: -identifier)) + } else if identifier < Int64(Int32.min) * 2 && identifier > Int64(Int32.min) * 3 { + return PeerId(namespace: Namespaces.Peer.CloudChannel, id: Int32(clamping: Int64(Int32.min) &* 2 &- identifier)) + } else if identifier > 0 && identifier < Int32.max { + return PeerId(namespace: Namespaces.Peer.CloudUser, id: Int32(clamping: identifier)) + } else { + return nil + } +} + +func makeBridgeIdentifier(_ peerId: PeerId) -> Int64 { + switch peerId.namespace { + case Namespaces.Peer.CloudGroup: + return -Int64(peerId.id) + case Namespaces.Peer.CloudChannel: + return Int64(Int32.min) * 2 - Int64(peerId.id) + default: + return Int64(peerId.id) + } +} + +func makeBridgeDeliveryState(_ message: Message?) -> TGBridgeMessageDeliveryState { + if let message = message { + if message.flags.contains(.Failed) { + return .failed + } + else if message.flags.contains(.Sending) { + return .pending + } + } + return .delivered +} + +private func makeBridgeImage(_ image: TelegramMediaImage?) -> TGBridgeImageMediaAttachment? { + if let image = image, let representation = largestImageRepresentation(image.representations) { + let bridgeImage = TGBridgeImageMediaAttachment() + bridgeImage.imageId = image.imageId.id + bridgeImage.dimensions = representation.dimensions + return bridgeImage + } else { + return nil + } +} + +func makeBridgeDocument(_ file: TelegramMediaFile?) -> TGBridgeDocumentMediaAttachment? { + if let file = file { + let bridgeDocument = TGBridgeDocumentMediaAttachment() + bridgeDocument.documentId = file.fileId.id + bridgeDocument.fileSize = Int32(file.size ?? 0) + for attribute in file.attributes { + switch attribute { + case let .FileName(fileName): + bridgeDocument.fileName = fileName + case .Animated: + bridgeDocument.isAnimated = true + case let .ImageSize(size): + bridgeDocument.imageSize = NSValue(cgSize: size) + case let .Sticker(displayText, packReference, _): + bridgeDocument.isSticker = true + bridgeDocument.stickerAlt = displayText + if let packReference = packReference, case let .id(id, accessHash) = packReference { + bridgeDocument.stickerPackId = id + bridgeDocument.stickerPackAccessHash = accessHash + } + case let .Audio(_, duration, title, performer, _): + bridgeDocument.duration = Int32(clamping: duration) + bridgeDocument.title = title + bridgeDocument.performer = performer + default: + break + } + } + return bridgeDocument + } + return nil +} + +func makeBridgeMedia(message: Message, strings: PresentationStrings, chatPeer: Peer? = nil, filterUnsupportedActions: Bool = true) -> [TGBridgeMediaAttachment] { + var bridgeMedia: [TGBridgeMediaAttachment] = [] + + if let forward = message.forwardInfo { + let bridgeForward = TGBridgeForwardedMessageMediaAttachment() + bridgeForward.peerId = makeBridgeIdentifier(forward.author.id) + if let sourceMessageId = forward.sourceMessageId { + bridgeForward.mid = sourceMessageId.id + } + bridgeForward.date = forward.date + bridgeMedia.append(bridgeForward) + } + + for attribute in message.attributes { + if let reply = attribute as? ReplyMessageAttribute, let replyMessage = message.associatedMessages[reply.messageId] { + let bridgeReply = TGBridgeReplyMessageMediaAttachment() + bridgeReply.mid = reply.messageId.id + bridgeReply.message = makeBridgeMessage(replyMessage, strings: strings) + bridgeMedia.append(bridgeReply) + } else if let entities = attribute as? TextEntitiesMessageAttribute { + var bridgeEntities: [Any] = [] + for entity in entities.entities { + var bridgeEntity: TGBridgeMessageEntity? = nil + switch entity.type { + case .Url: + bridgeEntity = TGBridgeMessageEntityUrl() + bridgeEntity?.range = NSRange(entity.range) + case .TextUrl: + bridgeEntity = TGBridgeMessageEntityTextUrl() + bridgeEntity?.range = NSRange(entity.range) + case .Email: + bridgeEntity = TGBridgeMessageEntityEmail() + bridgeEntity?.range = NSRange(entity.range) + case .Mention: + bridgeEntity = TGBridgeMessageEntityMention() + bridgeEntity?.range = NSRange(entity.range) + case .Hashtag: + bridgeEntity = TGBridgeMessageEntityHashtag() + bridgeEntity?.range = NSRange(entity.range) + case .BotCommand: + bridgeEntity = TGBridgeMessageEntityBotCommand() + bridgeEntity?.range = NSRange(entity.range) + case .Bold: + bridgeEntity = TGBridgeMessageEntityBold() + bridgeEntity?.range = NSRange(entity.range) + case .Italic: + bridgeEntity = TGBridgeMessageEntityItalic() + bridgeEntity?.range = NSRange(entity.range) + case .Code: + bridgeEntity = TGBridgeMessageEntityCode() + bridgeEntity?.range = NSRange(entity.range) + case .Pre: + bridgeEntity = TGBridgeMessageEntityPre() + bridgeEntity?.range = NSRange(entity.range) + default: + break + } + if let bridgeEntity = bridgeEntity { + bridgeEntities.append(bridgeEntity) + } + } + if !bridgeEntities.isEmpty { + let attachment = TGBridgeMessageEntitiesAttachment() + attachment.entities = bridgeEntities + bridgeMedia.append(attachment) + } + } + } + + for m in message.media { + if let image = m as? TelegramMediaImage, let bridgeImage = makeBridgeImage(image) { + bridgeMedia.append(bridgeImage) + } + else if let file = m as? TelegramMediaFile { + if file.isVideo { + let bridgeVideo = TGBridgeVideoMediaAttachment() + bridgeVideo.videoId = file.fileId.id + + for attribute in file.attributes { + switch attribute { + case let .Video(duration, size, flags): + bridgeVideo.duration = Int32(clamping: duration) + bridgeVideo.dimensions = size + bridgeVideo.round = flags.contains(.instantRoundVideo) + default: + break + } + } + + bridgeMedia.append(bridgeVideo) + } else if file.isVoice { + let bridgeAudio = TGBridgeAudioMediaAttachment() + bridgeAudio.audioId = file.fileId.id + bridgeAudio.fileSize = Int32(clamping: file.size ?? 0) + + for attribute in file.attributes { + switch attribute { + case let .Audio(_, duration, _, _, _): + bridgeAudio.duration = Int32(clamping: duration) + default: + break + } + } + + bridgeMedia.append(bridgeAudio) + } else if let bridgeDocument = makeBridgeDocument(file) { + bridgeMedia.append(bridgeDocument) + } + } else if let action = m as? TelegramMediaAction { + var bridgeAction: TGBridgeActionMediaAttachment? = nil + var consumed = false + switch action.action { + case let .groupCreated(title): + bridgeAction = TGBridgeActionMediaAttachment() + if chatPeer is TelegramGroup { + bridgeAction?.actionType = .createChat + bridgeAction?.actionData = ["title": title] + } else if let channel = chatPeer as? TelegramChannel { + if case .group = channel.info { + bridgeAction?.actionType = .createChat + bridgeAction?.actionData = ["title": title] + } else { + bridgeAction?.actionType = .channelCreated + } + } + case let .phoneCall(_, discardReason, _): + let bridgeAttachment = TGBridgeUnsupportedMediaAttachment() + let incoming = message.flags.contains(.Incoming) + var compactTitle: String = "" + var subTitle: String = "" + if let discardReason = discardReason { + switch discardReason { + case .busy, .disconnect: + compactTitle = strings.Notification_CallCanceled + subTitle = strings.Notification_CallCanceledShort + case .missed: + compactTitle = incoming ? strings.Notification_CallMissed : strings.Notification_CallCanceled + subTitle = incoming ? strings.Notification_CallMissedShort : strings.Notification_CallCanceledShort + case .hangup: + break + } + } + if compactTitle.isEmpty { + compactTitle = incoming ? strings.Notification_CallIncoming : strings.Notification_CallOutgoing + subTitle = incoming ? strings.Notification_CallIncomingShort : strings.Notification_CallOutgoingShort + } + bridgeAttachment.compactTitle = compactTitle + bridgeAttachment.title = strings.Watch_Message_Call + bridgeAttachment.subtitle = subTitle + bridgeMedia.append(bridgeAttachment) + consumed = true + default: + break + } + if let bridgeAction = bridgeAction { + bridgeMedia.append(bridgeAction) + } else if !consumed && !filterUnsupportedActions { + let bridgeAttachment = TGBridgeUnsupportedMediaAttachment() + bridgeAttachment.compactTitle = "" + bridgeAttachment.title = "" + bridgeMedia.append(bridgeAttachment) + } + } else if let contact = m as? TelegramMediaContact { + let bridgeContact = TGBridgeContactMediaAttachment() + if let peerId = contact.peerId { + bridgeContact.uid = Int32(clamping: makeBridgeIdentifier(peerId)) + } + bridgeContact.firstName = contact.firstName + bridgeContact.lastName = contact.lastName + bridgeContact.phoneNumber = contact.phoneNumber + bridgeContact.prettyPhoneNumber = formatPhoneNumber(contact.phoneNumber) + bridgeMedia.append(bridgeContact) + } else if let map = m as? TelegramMediaMap { + let bridgeLocation = TGBridgeLocationMediaAttachment() + bridgeLocation.latitude = map.latitude + bridgeLocation.longitude = map.longitude + if let venue = map.venue { + let bridgeVenue = TGBridgeVenueAttachment() + bridgeVenue.title = venue.title + bridgeVenue.address = venue.address + bridgeVenue.provider = venue.provider + bridgeVenue.venueId = venue.id + bridgeLocation.venue = bridgeVenue + } + bridgeMedia.append(bridgeLocation) + } else if let webpage = m as? TelegramMediaWebpage { + if case let .Loaded(content) = webpage.content { + let bridgeWebpage = TGBridgeWebPageMediaAttachment() + bridgeWebpage.webPageId = webpage.id?.id ?? 0 + bridgeWebpage.url = content.url + bridgeWebpage.displayUrl = content.displayUrl + bridgeWebpage.pageType = content.type + bridgeWebpage.siteName = content.websiteName + bridgeWebpage.title = content.title + bridgeWebpage.pageDescription = content.text + bridgeWebpage.photo = makeBridgeImage(content.image) + bridgeWebpage.embedUrl = content.embedUrl + bridgeWebpage.embedType = content.embedType + bridgeWebpage.embedSize = content.embedSize ?? CGSize() + bridgeWebpage.duration = NSNumber(integerLiteral: content.duration ?? 0) + bridgeWebpage.author = content.author + bridgeMedia.append(bridgeWebpage) + } + } else if let game = m as? TelegramMediaGame { + let bridgeAttachment = TGBridgeUnsupportedMediaAttachment() + bridgeAttachment.compactTitle = game.title + bridgeAttachment.title = strings.Watch_Message_Game + bridgeAttachment.subtitle = game.title + bridgeMedia.append(bridgeAttachment) + } else if let invoice = m as? TelegramMediaInvoice { + let bridgeAttachment = TGBridgeUnsupportedMediaAttachment() + bridgeAttachment.compactTitle = invoice.title + bridgeAttachment.title = strings.Watch_Message_Invoice + bridgeAttachment.subtitle = invoice.title + bridgeMedia.append(bridgeAttachment) + } + } + return bridgeMedia +} + +func makeBridgeChat(_ entry: ChatListEntry, strings: PresentationStrings) -> (TGBridgeChat, [Int64 : TGBridgeUser])? { + if case let .MessageEntry(index, message, readState, _, _, renderedPeer, _) = entry { + guard index.messageIndex.id.peerId.namespace != Namespaces.Peer.SecretChat else { + return nil + } + let (bridgeChat, participants) = makeBridgeChat(renderedPeer.peer) + bridgeChat.date = TimeInterval(index.messageIndex.timestamp) + if let message = message { + if let author = message.author { + bridgeChat.fromUid = Int32(clamping: makeBridgeIdentifier(author.id)) + } + bridgeChat.text = message.text + bridgeChat.outgoing = !message.flags.contains(.Incoming) + bridgeChat.deliveryState = makeBridgeDeliveryState(message) + bridgeChat.deliveryError = message.flags.contains(.Failed) + bridgeChat.media = makeBridgeMedia(message: message, strings: strings, filterUnsupportedActions: false) + } + bridgeChat.unread = readState?.isUnread ?? false + bridgeChat.unreadCount = readState?.count ?? 0 + + var bridgeUsers: [Int64 : TGBridgeUser] = participants + if let bridgeUser = makeBridgeUser(message?.author, presence: nil) { + bridgeUsers[bridgeUser.identifier] = bridgeUser + } + if let user = renderedPeer.peer as? TelegramUser, user.id != message?.author?.id, let bridgeUser = makeBridgeUser(user, presence: nil) { + bridgeUsers[bridgeUser.identifier] = bridgeUser + } + + return (bridgeChat, bridgeUsers) + } + return nil +} + +func makeBridgeChat(_ peer: Peer?, view: PeerView? = nil) -> (TGBridgeChat, [Int64 : TGBridgeUser]) { + let bridgeChat = TGBridgeChat() + var bridgeUsers: [Int64 : TGBridgeUser] = [:] + if let peer = peer { + bridgeChat.identifier = makeBridgeIdentifier(peer.id) + bridgeChat.userName = peer.addressName + } + if let group = peer as? TelegramGroup { + bridgeChat.isGroup = true + bridgeChat.groupTitle = group.title + bridgeChat.participantsCount = Int32(clamping: group.participantCount) + + if let representation = smallestImageRepresentation(group.photo) { + bridgeChat.groupPhotoSmall = legacyImageLocationUri(resource: representation.resource) + } + if let representation = largestImageRepresentation(group.photo) { + bridgeChat.groupPhotoBig = legacyImageLocationUri(resource: representation.resource) + } + if let view = view, let cachedData = view.cachedData as? CachedGroupData, let participants = cachedData.participants { + bridgeChat.participantsCount = Int32(clamping: participants.participants.count) + var bridgeParticipants: [Int64] = [] + for participant in participants.participants { + if let user = view.peers[participant.peerId], let bridgeUser = makeBridgeUser(user, presence: view.peerPresences[user.id]) { + bridgeParticipants.append(bridgeUser.identifier) + bridgeUsers[bridgeUser.identifier] = bridgeUser + } + } + bridgeChat.participants = bridgeParticipants + } + } else if let channel = peer as? TelegramChannel { + bridgeChat.isChannel = true + bridgeChat.groupTitle = channel.title + if case .group = channel.info { + bridgeChat.isChannelGroup = true + } + bridgeChat.verified = channel.flags.contains(.isVerified) + + if let representation = smallestImageRepresentation(channel.photo) { + bridgeChat.groupPhotoSmall = legacyImageLocationUri(resource: representation.resource) + } + if let representation = largestImageRepresentation(channel.photo) { + bridgeChat.groupPhotoBig = legacyImageLocationUri(resource: representation.resource) + } + if let view = view, let cachedData = view.cachedData as? CachedChannelData { + bridgeChat.about = cachedData.about + } + } + + // _hasLeftGroup = [aDecoder decodeBoolForKey:TGBridgeChatHasLeftGroupKey]; + // _isKickedFromGroup = [aDecoder decodeBoolForKey:TGBridgeChatIsKickedFromGroupKey]; + return (bridgeChat, bridgeUsers) +} + +func makeBridgeUser(_ peer: Peer?, presence: PeerPresence? = nil, cachedData: CachedPeerData? = nil) -> TGBridgeUser? { + if let user = peer as? TelegramUser { + let bridgeUser = TGBridgeUser() + bridgeUser.identifier = makeBridgeIdentifier(user.id) + bridgeUser.firstName = user.firstName + bridgeUser.lastName = user.lastName + bridgeUser.userName = user.addressName + bridgeUser.phoneNumber = user.phone + if let phone = user.phone { + bridgeUser.prettyPhoneNumber = formatPhoneNumber(phone) + } + if let presence = presence as? TelegramUserPresence { + let timestamp = 0 + switch presence.status { + case .recently: + bridgeUser.lastSeen = -2 + case .lastWeek: + bridgeUser.lastSeen = -3 + case .lastMonth: + bridgeUser.lastSeen = -4 + case .none: + bridgeUser.lastSeen = -5 + case let .present(statusTimestamp): + if statusTimestamp > timestamp { + bridgeUser.online = true + } + bridgeUser.lastSeen = TimeInterval(statusTimestamp) + } + } + if let cachedData = cachedData as? CachedUserData { + bridgeUser.about = cachedData.about + } + if let representation = smallestImageRepresentation(user.photo) { + bridgeUser.photoSmall = legacyImageLocationUri(resource: representation.resource) + } + if let representation = largestImageRepresentation(user.photo) { + bridgeUser.photoBig = legacyImageLocationUri(resource: representation.resource) + } + if user.botInfo != nil { + bridgeUser.kind = .bot + bridgeUser.botKind = .generic + } + bridgeUser.verified = user.flags.contains(.isVerified) + return bridgeUser + } else { + return nil + } +} + +func makeBridgePeers(_ message: Message) -> [Int64 : Any] { + var bridgeUsers: [Int64 : Any] = [:] + for (_, peer) in message.peers { + if peer is TelegramUser, let bridgeUser = makeBridgeUser(peer, presence: nil) { + bridgeUsers[bridgeUser.identifier] = bridgeUser + } else if peer is TelegramGroup || peer is TelegramChannel { + let bridgeChat = makeBridgeChat(peer) + bridgeUsers[bridgeChat.0.identifier] = bridgeChat.0 + } + } + if let author = message.author, let bridgeUser = makeBridgeUser(author) { + bridgeUsers[bridgeUser.identifier] = bridgeUser + } + return bridgeUsers +} + +func makeBridgeMessage(_ entry: MessageHistoryEntry, strings: PresentationStrings) -> (TGBridgeMessage, [Int64 : TGBridgeUser])? { + if case let .MessageEntry(message, read, _, _) = entry, let bridgeMessage = makeBridgeMessage(message, strings: strings) { + if message.id.namespace == Namespaces.Message.Local && !message.flags.contains(.Failed) { + return nil + } + + bridgeMessage.unread = !read + + var bridgeUsers: [Int64 : TGBridgeUser] = [:] + if let bridgeUser = makeBridgeUser(message.author, presence: nil) { + bridgeUsers[bridgeUser.identifier] = bridgeUser + } + for (_, peer) in message.peers { + if let bridgeUser = makeBridgeUser(peer, presence: nil) { + bridgeUsers[bridgeUser.identifier] = bridgeUser + } + } + + return (bridgeMessage, bridgeUsers) + } + return nil +} + +func makeBridgeMessage(_ message: Message, strings: PresentationStrings, chatPeer: Peer? = nil) -> TGBridgeMessage? { + var chatPeer = chatPeer + if chatPeer == nil { + chatPeer = message.peers[message.id.peerId] + } + + let bridgeMessage = TGBridgeMessage() + bridgeMessage.identifier = message.id.id + bridgeMessage.date = TimeInterval(message.timestamp) + bridgeMessage.randomId = message.globallyUniqueId ?? 0 +// bridgeMessage.unread = false + bridgeMessage.outgoing = !message.flags.contains(.Incoming) + if let author = message.author { + bridgeMessage.fromUid = makeBridgeIdentifier(author.id) + } + bridgeMessage.toUid = makeBridgeIdentifier(message.id.peerId) + bridgeMessage.cid = makeBridgeIdentifier(message.id.peerId) + bridgeMessage.text = message.text + bridgeMessage.deliveryState = makeBridgeDeliveryState(message) + bridgeMessage.media = makeBridgeMedia(message: message, strings: strings, chatPeer: chatPeer) + return bridgeMessage +} + +func makeVenue(from bridgeVenue: TGBridgeVenueAttachment?) -> MapVenue? { + if let bridgeVenue = bridgeVenue { + return MapVenue(title: bridgeVenue.title, address: bridgeVenue.address, provider: bridgeVenue.provider, id: bridgeVenue.venueId, type: "") + } + return nil +} + +func makeBridgeLocationVenue(_ contextResult: ChatContextResultMessage) -> TGBridgeLocationVenue? { + if case let .mapLocation(mapMedia, _) = contextResult { + let bridgeVenue = TGBridgeLocationVenue() + bridgeVenue.coordinate = CLLocationCoordinate2D(latitude: mapMedia.latitude, longitude: mapMedia.longitude) + if let venue = mapMedia.venue { + bridgeVenue.name = venue.title + bridgeVenue.address = venue.address + bridgeVenue.provider = venue.provider + bridgeVenue.identifier = venue.id + } + return bridgeVenue + } + return nil +} diff --git a/Telegram-iOS/WatchCommunicationManager.swift b/Telegram-iOS/WatchCommunicationManager.swift new file mode 100644 index 0000000000..a79a31d5fa --- /dev/null +++ b/Telegram-iOS/WatchCommunicationManager.swift @@ -0,0 +1,190 @@ +import Foundation +import SwiftSignalKit +import Postbox +import TelegramCore +import TelegramUI + +final class WatchCommunicationManager { + private let queue: Queue + private var server: TGBridgeServer! + + private let contextDisposable = MetaDisposable() + private let presetsDisposable = MetaDisposable() + + let account = Promise(nil) + private let presets = Promise(nil) + private let navigateToMessagePipe = ValuePipe() + + init(queue: Queue, context: Promise) { + self.queue = queue + + let handlers = allWatchRequestHandlers.reduce([String : AnyClass]()) { (map, handler) -> [String : AnyClass] in + var map = map + if let handler = handler as? WatchRequestHandler.Type { + for case let subscription as TGBridgeSubscription.Type in handler.handledSubscriptions { + if let name = subscription.subscriptionName() { + map[name] = handler + } + } + } + return map + } + + self.server = TGBridgeServer(handler: { [weak self] subscription -> SSignal? in + guard let strongSelf = self, let subscription = subscription, let handler = handlers[subscription.name] as? WatchRequestHandler.Type else { + return nil + } + return handler.handle(subscription: subscription, manager: strongSelf) + }, fileHandler: { [weak self] path, metadata in + guard let strongSelf = self, let path = path, let metadata = metadata as? [String : Any] else { + return + } + if metadata[TGBridgeIncomingFileTypeKey] as? String == TGBridgeIncomingFileTypeAudio { + let _ = WatchAudioHandler.handleFile(path: path, metadata: metadata, manager: strongSelf).start() + } + }, dispatchOnQueue: { [weak self] block in + if let strongSelf = self, let block = block { + strongSelf.queue.justDispatch(block) + } + }, logFunction: { value in + if let value = value { + Logger.shared.log("WatchBridge", value) + } + }) + self.server.startRunning() + + self.contextDisposable.set((combineLatest(self.watchAppInstalled, context.get() |> deliverOn(self.queue))).start(next: { [weak self] appInstalled, appContext in + guard let strongSelf = self, appInstalled else { + return + } + if let appContext = appContext, case let .authorized(context) = appContext { + strongSelf.account.set(.single(context.account)) + strongSelf.server.setAuthorized(true, userId: context.account.peerId.id) + strongSelf.server.setMicAccessAllowed(false) + strongSelf.server.pushContext() + strongSelf.server.setMicAccessAllowed(true) + strongSelf.server.pushContext() + + let watchPresetSettingsKey = ApplicationSpecificPreferencesKeys.watchPresetSettings + strongSelf.presets.set(context.account.postbox.preferencesView(keys: [watchPresetSettingsKey]) + |> map({ preferences -> WatchPresetSettings in + return (preferences.values[watchPresetSettingsKey] as? WatchPresetSettings) ?? WatchPresetSettings.defaultSettings + })) + } else { + strongSelf.account.set(.single(nil)) + strongSelf.server.setAuthorized(false, userId: 0) + strongSelf.server.pushContext() + + strongSelf.presets.set(.single(nil)) + } + })) + + self.presetsDisposable.set((combineLatest(self.watchAppInstalled, self.presets.get() |> distinctUntilChanged |> deliverOn(self.queue), context.get() |> deliverOn(self.queue))).start(next: { [weak self] appInstalled, presets, appContext in + guard let strongSelf = self, let presets = presets, let appContext = appContext, case let .authorized(context) = appContext, appInstalled, let tempPath = strongSelf.watchTemporaryStorePath else { + return + } + let presentationData = context.account.telegramApplicationContext.currentPresentationData.with { $0 } + let defaultSuggestions: [String : String] = [ + "OK": presentationData.strings.Watch_Suggestion_OK, + "Thanks": presentationData.strings.Watch_Suggestion_Thanks, + "WhatsUp": presentationData.strings.Watch_Suggestion_WhatsUp, + "TalkLater": presentationData.strings.Watch_Suggestion_TalkLater, + "CantTalk": presentationData.strings.Watch_Suggestion_CantTalk, + "HoldOn": presentationData.strings.Watch_Suggestion_HoldOn, + "BRB": presentationData.strings.Watch_Suggestion_BRB, + "OnMyWay": presentationData.strings.Watch_Suggestion_OnMyWay + ] + + var suggestions: [String : String] = [:] + for (key, defaultValue) in defaultSuggestions { + suggestions[key] = presets.customPresets[key] ?? defaultValue + } + + let fileManager = FileManager.default + let presetsFileUrl = URL(fileURLWithPath: tempPath + "/presets.dat") + + if fileManager.fileExists(atPath: presetsFileUrl.path) { + try? fileManager.removeItem(atPath: presetsFileUrl.path) + } + let data = NSKeyedArchiver.archivedData(withRootObject: suggestions) + try? data.write(to: presetsFileUrl) + + let _ = strongSelf.sendFile(url: presetsFileUrl, metadata: [TGBridgeIncomingFileIdentifierKey: "presets"]).start() + })) + } + + deinit { + self.contextDisposable.dispose() + self.presetsDisposable.dispose() + } + + var arguments: WatchManagerArguments { + return WatchManagerArguments(appInstalled: self.watchAppInstalled, navigateToMessageRequested: self.navigateToMessagePipe.signal(), runningTasks: self.runningTasks) + } + + func requestNavigateToMessage(messageId: MessageId) { + self.navigateToMessagePipe.putNext(messageId) + } + + private var watchAppInstalled: Signal { + return Signal { subscriber in + let disposable = self.server.watchAppInstalledSignal()?.start(next: { value in + if let value = value as? NSNumber { + subscriber.putNext(value.boolValue) + } + }) + return ActionDisposable { + disposable?.dispose() + } + } |> deliverOn(self.queue) + } + + private var runningTasks: Signal { + return Signal { subscriber in + let disposable = self.server.runningRequestsSignal()?.start(next: { value in + if let value = value as? Dictionary { + if let running = value["running"] as? Bool, let version = value["version"] as? Int32 { + subscriber.putNext(WatchRunningTasks(running: running, version: version)) + } + } + }) + return ActionDisposable { + disposable?.dispose() + } + } |> deliverOn(self.queue) + } + + var watchTemporaryStorePath: String? { + return self.server.temporaryFilesURL?.path + } + + func sendFile(url: URL, metadata: Dictionary, asMessageData: Bool = false) -> Signal { + return Signal { subscriber in + self.server.sendFile(with: url, metadata: metadata, asMessageData: asMessageData) + subscriber.putCompletion() + return EmptyDisposable + } |> runOn(self.queue) + } + func sendFile(data: Data, metadata: Dictionary) -> Signal { + return Signal { subscriber in + self.server.sendFile(with: data, metadata: metadata, errorHandler: {}) + subscriber.putCompletion() + return EmptyDisposable + } |> runOn(self.queue) + } +} + +func watchCommunicationManager(context: Promise) -> Signal { + return Signal { subscriber in + let queue = Queue() + queue.async { + if #available(iOSApplicationExtension 9.0, *) { + subscriber.putNext(WatchCommunicationManager(queue: queue, context: context)) + } else { + subscriber.putNext(nil) + } + subscriber.putCompletion() + } + return EmptyDisposable + } +} diff --git a/Telegram-iOS/WatchRequestHandlers.swift b/Telegram-iOS/WatchRequestHandlers.swift new file mode 100644 index 0000000000..39eefaecba --- /dev/null +++ b/Telegram-iOS/WatchRequestHandlers.swift @@ -0,0 +1,894 @@ +import Foundation +import SwiftSignalKit +import Postbox +import Display +import TelegramCore +import TelegramUI +import LegacyComponents + +let allWatchRequestHandlers: [AnyClass] = [ + WatchChatListHandler.self, + WatchChatMessagesHandler.self, + WatchSendMessageHandler.self, + WatchPeerInfoHandler.self, + WatchMediaHandler.self, + WatchStickersHandler.self, + WatchAudioHandler.self, + WatchLocationHandler.self, + WatchPeerSettingsHandler.self, + WatchContinuationHandler.self, +] + +protocol WatchRequestHandler: AnyObject { + static var handledSubscriptions: [Any] { get } + static func handle(subscription: TGBridgeSubscription, manager: WatchCommunicationManager) -> SSignal +} + +final class WatchChatListHandler: WatchRequestHandler { + static var handledSubscriptions: [Any] { + return [TGBridgeChatListSubscription.self] + } + + static func handle(subscription: TGBridgeSubscription, manager: WatchCommunicationManager) -> SSignal { + if let args = subscription as? TGBridgeChatListSubscription { + let limit = Int(args.limit) + return SSignal { subscriber in + let signal = manager.account.get() + |> take(1) + |> mapToSignal({ account -> Signal<(ChatListView, PresentationData), NoError> in + if let account = account { + return account.viewTracker.tailChatListView(groupId: nil, count: limit) + |> map { chatListView, _ -> (ChatListView, PresentationData) in + return (chatListView, account.telegramApplicationContext.currentPresentationData.with { $0 }) + } + } else { + return .complete() + } + }) + let disposable = signal.start(next: { chatListView, presentationData in + var chats: [TGBridgeChat] = [] + var users: [Int64 : TGBridgeUser] = [:] + for entry in chatListView.entries.reversed() { + if let (chat, chatUsers) = makeBridgeChat(entry, strings: presentationData.strings) { + chats.append(chat) + users = users.merging(chatUsers, uniquingKeysWith: { (_, last) in last }) + } + } + subscriber?.putNext([ TGBridgeChatsArrayKey: chats, TGBridgeUsersDictionaryKey: users ]) + }) + + return SBlockDisposable { + disposable.dispose() + } + } + } else { + return SSignal.fail(nil) + } + } +} + + +final class WatchChatMessagesHandler: WatchRequestHandler { + static var handledSubscriptions: [Any] { + return [ + TGBridgeChatMessageListSubscription.self, + TGBridgeChatMessageSubscription.self, + TGBridgeReadChatMessageListSubscription.self + ] + } + + static func handle(subscription: TGBridgeSubscription, manager: WatchCommunicationManager) -> SSignal { + if let args = subscription as? TGBridgeChatMessageListSubscription, let peerId = makePeerIdFromBridgeIdentifier(args.peerId) { + return SSignal { subscriber in + let limit = Int(args.rangeMessageCount) + let signal = manager.account.get() + |> take(1) + |> mapToSignal({ account -> Signal<(MessageHistoryView, Bool, PresentationData), NoError> in + if let account = account { + return account.viewTracker.aroundMessageHistoryViewForLocation(.peer(peerId), index: .upperBound, anchorIndex: .upperBound, count: limit, fixedCombinedReadStates: nil) + |> map { messageHistoryView, _, _ -> (MessageHistoryView, Bool, PresentationData) in + return (messageHistoryView, peerId == account.peerId, account.telegramApplicationContext.currentPresentationData.with { $0 }) + } + } else { + return .complete() + } + }) + let disposable = signal.start(next: { messageHistoryView, savedMessages, presentationData in + var messages: [TGBridgeMessage] = [] + var users: [Int64 : TGBridgeUser] = [:] + for entry in messageHistoryView.entries.reversed() { + if let (message, messageUsers) = makeBridgeMessage(entry, strings: presentationData.strings) { + messages.append(message) + users = users.merging(messageUsers, uniquingKeysWith: { (_, last) in last }) + } + } + subscriber?.putNext([ TGBridgeMessagesArrayKey: messages, TGBridgeUsersDictionaryKey: users ]) + }) + + return SBlockDisposable { + disposable.dispose() + } + } + } else if let args = subscription as? TGBridgeReadChatMessageListSubscription, let peerId = makePeerIdFromBridgeIdentifier(args.peerId) { + return SSignal { subscriber in + let signal = manager.account.get() + |> take(1) + |> mapToSignal({ account -> Signal in + if let account = account { + let messageId = MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: args.messageId) + return applyMaxReadIndexInteractively(postbox: account.postbox, stateManager: account.stateManager, index: MessageIndex(id: messageId, timestamp: 0)) + } else { + return .complete() + } + }) + let disposable = signal.start(next: { _ in + subscriber?.putNext(true) + }, completed: { + subscriber?.putCompletion() + }) + + return SBlockDisposable { + disposable.dispose() + } + } + } else if let args = subscription as? TGBridgeChatMessageSubscription, let peerId = makePeerIdFromBridgeIdentifier(args.peerId) { + return SSignal { subscriber in + let signal = manager.account.get() + |> take(1) + |> mapToSignal({ account -> Signal<(Message, PresentationData)?, NoError> in + if let account = account { + let messageSignal = downloadMessage(postbox: account.postbox, network: account.network, messageId: MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: args.messageId)) + |> map { message -> (Message, PresentationData)? in + if let message = message { + return (message, account.telegramApplicationContext.currentPresentationData.with { $0 }) + } else { + return nil + } + } + return messageSignal |> timeout(3.5, queue: Queue.concurrentDefaultQueue(), alternate: .single(nil)) + } else { + return .single(nil) + } + }) + let disposable = signal.start(next: { messageAndPresentationData in + if let (message, presentationData) = messageAndPresentationData, let bridgeMessage = makeBridgeMessage(message, strings: presentationData.strings) { + let peers = makeBridgePeers(message) + var response: [String : Any] = [TGBridgeMessageKey: bridgeMessage, TGBridgeUsersDictionaryKey: peers] + if peerId.namespace != Namespaces.Peer.CloudUser { + response[TGBridgeChatKey] = peers[makeBridgeIdentifier(peerId)] + } + subscriber?.putNext(response) + } + subscriber?.putCompletion() + }) + return SBlockDisposable { + disposable.dispose() + } + } + } + return SSignal.fail(nil) + } +} + +final class WatchSendMessageHandler: WatchRequestHandler { + static var handledSubscriptions: [Any] { + return [ + TGBridgeSendTextMessageSubscription.self, + TGBridgeSendLocationMessageSubscription.self, + TGBridgeSendStickerMessageSubscription.self, + TGBridgeSendForwardedMessageSubscription.self + ] + } + + static func handle(subscription: TGBridgeSubscription, manager: WatchCommunicationManager) -> SSignal { + return SSignal { subscriber in + let signal = manager.account.get() + |> take(1) + |> mapToSignal({ account -> Signal in + if let account = account { + var messageSignal: Signal<(EnqueueMessage?, PeerId?), NoError>? + if let args = subscription as? TGBridgeSendTextMessageSubscription { + let peerId = makePeerIdFromBridgeIdentifier(args.peerId) + var replyMessageId: MessageId? + if args.replyToMid != 0, let peerId = peerId { + replyMessageId = MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: args.replyToMid) + } + messageSignal = .single((.message(text: args.text, attributes: [], mediaReference: nil, replyToMessageId: replyMessageId, localGroupingKey: nil), peerId)) + } else if let args = subscription as? TGBridgeSendLocationMessageSubscription, let location = args.location { + let peerId = makePeerIdFromBridgeIdentifier(args.peerId) + let map = TelegramMediaMap(latitude: location.latitude, longitude: location.longitude, geoPlace: nil, venue: makeVenue(from: location.venue), liveBroadcastingTimeout: nil) + messageSignal = .single((.message(text: "", attributes: [], mediaReference: .standalone(media: map), replyToMessageId: nil, localGroupingKey: nil), peerId)) + } else if let args = subscription as? TGBridgeSendStickerMessageSubscription { + let peerId = makePeerIdFromBridgeIdentifier(args.peerId) + messageSignal = mediaForSticker(documentId: args.document.documentId, account: account) + |> map({ media -> (EnqueueMessage?, PeerId?) in + if let media = media { + return (.message(text: "", attributes: [], mediaReference: .standalone(media: media), replyToMessageId: nil, localGroupingKey: nil), peerId) + } else { + return (nil, nil) + } + }) + } else if let args = subscription as? TGBridgeSendForwardedMessageSubscription { + let peerId = makePeerIdFromBridgeIdentifier(args.targetPeerId) + if let forwardPeerId = makePeerIdFromBridgeIdentifier(args.peerId) { + messageSignal = .single((.forward(source: MessageId(peerId: forwardPeerId, namespace: Namespaces.Message.Cloud, id: args.messageId), grouping: .none), peerId)) + } + } + + if let messageSignal = messageSignal { + return messageSignal |> mapToSignal({ message, peerId -> Signal in + if let message = message, let peerId = peerId { + return enqueueMessages(account: account, peerId: peerId, messages: [message]) |> mapToSignal({ _ in + return .single(true) + }) + } else { + return .complete() + } + }) + } + } + return .complete() + }) + + let disposable = signal.start(next: { _ in + subscriber?.putNext(true) + }, completed: { + subscriber?.putCompletion() + }) + + return SBlockDisposable { + disposable.dispose() + } + } + } +} + +final class WatchPeerInfoHandler: WatchRequestHandler { + static var handledSubscriptions: [Any] { + return [ + TGBridgeUserInfoSubscription.self, + TGBridgeUserBotInfoSubscription.self, + TGBridgeConversationSubscription.self + ] + } + + static func handle(subscription: TGBridgeSubscription, manager: WatchCommunicationManager) -> SSignal { + if let args = subscription as? TGBridgeUserInfoSubscription { + return SSignal { subscriber in + let signal = manager.account.get() + |> take(1) + |> mapToSignal({ account -> Signal in + if let account = account, let userId = args.userIds.first as? Int64, let peerId = makePeerIdFromBridgeIdentifier(userId) { + return account.viewTracker.peerView(peerId) + } else { + return .complete() + } + }) + let disposable = signal.start(next: { view in + if let user = makeBridgeUser(peerViewMainPeer(view), presence: view.peerPresences[view.peerId], cachedData: view.cachedData) { + subscriber?.putNext([user.identifier: user]) + } else { + subscriber?.putCompletion() + } + }) + + return SBlockDisposable { + disposable.dispose() + } + } + } else if let _ = subscription as? TGBridgeUserBotInfoSubscription { + return SSignal.complete() +// return SSignal { subscriber in +// let signal = manager.account.get() +// |> take(1) +// |> mapToSignal({ account -> Signal in +// if let account = account, let userId = args.userIds.first as? Int64, let peerId = makePeerIdFromBridgeIdentifier(userId) { +// return peerCommands(account: account, id: peerId) +// } else { +// return .complete() +// } +// }) +// let disposable = signal.start(next: { view in +// if let user = makeBridgeUser(peerViewMainPeer(view), presence: view.peerPresences[view.peerId], cachedData: view.cachedData) { +// subscriber?.putNext([user.identifier: user]) +// } else { +// subscriber?.putCompletion() +// } +// }) +// +// return SBlockDisposable { +// disposable.dispose() +// } +// } + } else if let args = subscription as? TGBridgeConversationSubscription { + return SSignal { subscriber in + let signal = manager.account.get() |> take(1) |> mapToSignal({ account -> Signal in + if let account = account, let peerId = makePeerIdFromBridgeIdentifier(args.peerId) { + return account.viewTracker.peerView(peerId) + } else { + return .complete() + } + }) + let disposable = signal.start(next: { view in + let (chat, users) = makeBridgeChat(peerViewMainPeer(view), view: view) + subscriber?.putNext([ TGBridgeChatKey: chat, TGBridgeUsersDictionaryKey: users ]) + }) + + return SBlockDisposable { + disposable.dispose() + } + } + } + return SSignal.fail(nil) + } +} + +private func mediaForSticker(documentId: Int64, account: Account) -> Signal { + return account.postbox.itemCollectionsView(orderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers], namespaces: [Namespaces.ItemCollection.CloudStickerPacks], aroundIndex: nil, count: 50) + |> take(1) + |> map { view -> TelegramMediaFile? in + for view in view.orderedItemListsViews { + for entry in view.items { + if let file = (entry.contents as? SavedStickerItem)?.file { + if file.id?.id == documentId { + return file + } + } else if let file = (entry.contents as? RecentMediaItem)?.media as? TelegramMediaFile { + if file.id?.id == documentId { + return file + } + } + } + } + return nil + } +} + +private let roundCorners = { () -> UIImage in + let diameter: CGFloat = 44.0 + UIGraphicsBeginImageContextWithOptions(CGSize(width: diameter, height: diameter), false, 0.0) + let context = UIGraphicsGetCurrentContext()! + context.setBlendMode(.copy) + context.setFillColor(UIColor.black.cgColor) + context.fill(CGRect(origin: CGPoint(), size: CGSize(width: diameter, height: diameter))) + context.setBlendMode(.clear) + context.setFillColor(UIColor.clear.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: diameter, height: diameter))) + let image = UIGraphicsGetImageFromCurrentImageContext()!.stretchableImage(withLeftCapWidth: Int(diameter / 2.0), topCapHeight: Int(diameter / 2.0)) + UIGraphicsEndImageContext() + return image +}() + +private func sendData(manager: WatchCommunicationManager, data: Data, key: String, ext: String, type: String, forceAsData: Bool = false) { + if let tempPath = manager.watchTemporaryStorePath, !forceAsData { + let tempFileUrl = URL(fileURLWithPath: tempPath + "/\(key)\(ext)") + let _ = try? data.write(to: tempFileUrl) + let _ = manager.sendFile(url: tempFileUrl, metadata: [TGBridgeIncomingFileTypeKey: type, TGBridgeIncomingFileIdentifierKey: key]).start() + } else { + let _ = manager.sendFile(data: data, metadata: [TGBridgeIncomingFileTypeKey: type, TGBridgeIncomingFileIdentifierKey: key]).start() + } +} + +final class WatchMediaHandler: WatchRequestHandler { + static var handledSubscriptions: [Any] { + return [ + TGBridgeMediaThumbnailSubscription.self, + TGBridgeMediaAvatarSubscription.self, + TGBridgeMediaStickerSubscription.self + ] + } + + static private let disposable = DisposableSet() + + static func handle(subscription: TGBridgeSubscription, manager: WatchCommunicationManager) -> SSignal { + if let args = subscription as? TGBridgeMediaAvatarSubscription, let peerId = makePeerIdFromBridgeIdentifier(args.peerId) { + let key = "\(args.url!)_\(args.type.rawValue)" + let targetSize: CGSize + var compressionRate: CGFloat = 0.5 + var round = false + switch args.type { + case .small: + targetSize = CGSize(width: 19, height: 19); + compressionRate = 0.5 + case .profile: + targetSize = CGSize(width: 44, height: 44); + round = true + case .large: + targetSize = CGSize(width: 150, height: 150); + } + + return SSignal { subscriber in + let signal = manager.account.get() + |> take(1) + |> mapToSignal({ account -> Signal in + if let account = account { + return account.postbox.transaction { transaction -> Peer? in + guard let peer = transaction.getPeer(peerId) else { + return nil + } + if let peer = peer as? TelegramSecretChat { + return transaction.getPeer(peer.regularPeerId) + } else { + return peer + } + } |> mapToSignal({ peer -> Signal in + if let peer = peer, let representation = peer.smallProfileImage { + let imageData = peerAvatarImageData(account: account, peer: peer, authorOfMessage: nil, representation: representation) + if let imageData = imageData { + return imageData |> deliverOn(account.graphicsThreadPool) + |> map { data -> UIImage? in + if let data = data, let image = generateImage(targetSize, contextGenerator: { size, context -> Void in + if let imageSource = CGImageSourceCreateWithData(data as CFData, nil), let dataImage = CGImageSourceCreateImageAtIndex(imageSource, 0, nil) { + context.setBlendMode(.copy) + context.draw(dataImage, in: CGRect(origin: CGPoint(), size: targetSize)) + if round { + context.setBlendMode(.normal) + context.draw(roundCorners.cgImage!, in: CGRect(origin: CGPoint(), size: targetSize)) + } + } + }, scale: 2.0) { + return image + } + return nil + } + } + } + return .single(nil) + }) + } else { + return .complete() + } + }) + + let disposable = signal.start(next: { image in + if let image = image, let imageData = UIImageJPEGRepresentation(image, compressionRate) { + sendData(manager: manager, data: imageData, key: key, ext: ".jpg", type: TGBridgeIncomingFileTypeImage) + } + subscriber?.putNext(key) + }, completed: { + subscriber?.putCompletion() + }) + + return SBlockDisposable { + disposable.dispose() + } + } + } else if let args = subscription as? TGBridgeMediaStickerSubscription { + let key = "sticker_\(args.documentId)_\(Int(args.size.width))x\(Int(args.size.height))_\(args.notification ? 1 : 0)" + return SSignal { subscriber in + let signal = manager.account.get() + |> take(1) + |> mapToSignal({ account -> Signal in + if let account = account { + var mediaSignal: Signal<(TelegramMediaFile, FileMediaReference)?, NoError>? = nil + if args.stickerPackId != 0 { + mediaSignal = mediaForSticker(documentId: args.documentId, account: account) + |> map { media -> (TelegramMediaFile, FileMediaReference)? in + if let media = media { + return (media, .standalone(media: media)) + } else { + return nil + } + } + } else if args.stickerPeerId != 0, let peerId = makePeerIdFromBridgeIdentifier(args.stickerPeerId) { + mediaSignal = account.postbox.transaction { transaction -> Message? in + return transaction.getMessage(MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: args.stickerMessageId)) + } + |> map { message -> (TelegramMediaFile, FileMediaReference)? in + if let message = message { + for media in message.media { + if let media = media as? TelegramMediaFile { + return (media, .message(message: MessageReference(message), media: media)) + } + } + } + return nil + } + } + var size: CGSize = args.size + if let mediaSignal = mediaSignal { + return mediaSignal + |> mapToSignal { mediaAndFileReference -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> in + if let (media, fileReference) = mediaAndFileReference { + if let dimensions = media.dimensions { + size = dimensions + } + self.disposable.add(freeMediaFileInteractiveFetched(account: account, fileReference: fileReference).start()) + return chatMessageSticker(account: account, file: media, small: false, fetched: true, onlyFullSize: true) + } + return .complete() + } + |> map{ f -> UIImage? in + let context = f(TransformImageArguments(corners: ImageCorners(), imageSize: size.fitted(args.size), boundingSize: args.size, intrinsicInsets: UIEdgeInsets(), emptyColor: args.notification ? UIColor(rgb: 0xe5e5ea) : .black, scale: 2.0)) + return context?.generateImage() + } + } + } + return .complete() + }) + + let disposable = signal.start(next: { image in + if let image = image, let imageData = UIImageJPEGRepresentation(image, 0.2) { + sendData(manager: manager, data: imageData, key: key, ext: ".jpg", type: TGBridgeIncomingFileTypeImage, forceAsData: args.notification) + } + subscriber?.putNext(key) + }, completed: { + subscriber?.putCompletion() + }) + + return SBlockDisposable { + disposable.dispose() + } + } + } else if let args = subscription as? TGBridgeMediaThumbnailSubscription { + let key = "\(args.peerId)_\(args.messageId)" + return SSignal { subscriber in + let signal = manager.account.get() + |> take(1) + |> mapToSignal({ account -> Signal in + if let account = account, let peerId = makePeerIdFromBridgeIdentifier(args.peerId) { + var roundVideo = false + return account.postbox.transaction { transaction -> Message? in + return transaction.getMessage(MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: args.messageId)) + } + |> mapToSignal { message -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> in + if let message = message, !message.containsSecretMedia { + var imageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? + var updatedMediaReference: AnyMediaReference? + var candidateMediaReference: AnyMediaReference? + var imageDimensions: CGSize? + for media in message.media { + if let image = media as? TelegramMediaImage, let resource = largestImageRepresentation(image.representations)?.resource { + self.disposable.add(messageMediaImageInteractiveFetched(account: account, message: message, image: image, resource: resource, storeToDownloadsPeerType: nil).start()) + candidateMediaReference = .message(message: MessageReference(message), media: media) + break + } else if let _ = media as? TelegramMediaFile { + candidateMediaReference = .message(message: MessageReference(message), media: media) + break + } else if let webPage = media as? TelegramMediaWebpage, case let .Loaded(content) = webPage.content, let image = content.image, let resource = largestImageRepresentation(image.representations)?.resource { + self.disposable.add(messageMediaImageInteractiveFetched(account: account, message: message, image: image, resource: resource, storeToDownloadsPeerType: nil).start()) + candidateMediaReference = .webPage(webPage: WebpageReference(webPage), media: image) + break + } + } + if let imageReference = candidateMediaReference?.concrete(TelegramMediaImage.self) { + updatedMediaReference = imageReference.abstract + if let representation = largestRepresentationForPhoto(imageReference.media) { + imageDimensions = representation.dimensions + } + } else if let fileReference = candidateMediaReference?.concrete(TelegramMediaFile.self) { + updatedMediaReference = fileReference.abstract + if let representation = largestImageRepresentation(fileReference.media.previewRepresentations), !fileReference.media.isSticker { + imageDimensions = representation.dimensions + } + } + if let updatedMediaReference = updatedMediaReference, imageDimensions != nil { + if let imageReference = updatedMediaReference.concrete(TelegramMediaImage.self) { + imageSignal = chatMessagePhotoThumbnail(account: account, photoReference: imageReference, onlyFullSize: true) + } else if let fileReference = updatedMediaReference.concrete(TelegramMediaFile.self) { + if fileReference.media.isVideo { + imageSignal = chatMessageVideoThumbnail(account: account, fileReference: fileReference) + roundVideo = fileReference.media.isInstantVideo + } else if let iconImageRepresentation = smallestImageRepresentation(fileReference.media.previewRepresentations) { + imageSignal = chatWebpageSnippetFile(account: account, fileReference: fileReference, representation: iconImageRepresentation) + } + } + } + if let signal = imageSignal { + return signal + } + } + return .complete() + } |> map{ f -> UIImage? in + var insets = UIEdgeInsets() + if roundVideo { + insets = UIEdgeInsetsMake(-2, -2, -2, -2) + } + let context = f(TransformImageArguments(corners: ImageCorners(), imageSize: args.size, boundingSize: args.size, intrinsicInsets: insets, scale: 2.0)) + return context?.generateImage() + } + } else { + return .complete() + } + }) + + let disposable = signal.start(next: { image in + if let image = image, let imageData = UIImageJPEGRepresentation(image, 0.5) { + sendData(manager: manager, data: imageData, key: key, ext: ".jpg", type: TGBridgeIncomingFileTypeImage, forceAsData: args.notification) + } + subscriber?.putNext(key) + }, completed: { + subscriber?.putCompletion() + }) + + return SBlockDisposable { + disposable.dispose() + } + } + } + return SSignal.fail(nil) + } +} + +final class WatchStickersHandler: WatchRequestHandler { + static var handledSubscriptions: [Any] { + return [TGBridgeRecentStickersSubscription.self] + } + + static func handle(subscription: TGBridgeSubscription, manager: WatchCommunicationManager) -> SSignal { + if let args = subscription as? TGBridgeRecentStickersSubscription { + return SSignal { subscriber in + let signal = manager.account.get() + |> take(1) + |> mapToSignal({ account -> Signal in + if let account = account { + return account.postbox.itemCollectionsView(orderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers], namespaces: [Namespaces.ItemCollection.CloudStickerPacks], aroundIndex: nil, count: 50) |> take(1) + } else { + return .complete() + } + }) + let disposable = signal.start(next: { view in + var stickers: [TGBridgeDocumentMediaAttachment] = [] + var added: Set = [] + outer: for view in view.orderedItemListsViews { + for entry in view.items { + if let file = (entry.contents as? SavedStickerItem)?.file { + if let sticker = makeBridgeDocument(file), !added.contains(sticker.documentId) { + stickers.append(sticker) + added.insert(sticker.documentId) + } + } else if let file = (entry.contents as? RecentMediaItem)?.media as? TelegramMediaFile { + if let sticker = makeBridgeDocument(file), !added.contains(sticker.documentId) { + stickers.append(sticker) + added.insert(sticker.documentId) + } + } + if stickers.count == args.limit { + break outer + } + } + } + subscriber?.putNext(stickers) + }) + + return SBlockDisposable { + disposable.dispose() + } + } + } + return SSignal.fail(nil) + } +} + +final class WatchAudioHandler: WatchRequestHandler { + static var handledSubscriptions: [Any] { + return [ + TGBridgeAudioSubscription.self, + TGBridgeAudioSentSubscription.self + ] + } + + static private let disposable = DisposableSet() + + static func handle(subscription: TGBridgeSubscription, manager: WatchCommunicationManager) -> SSignal { + if let args = subscription as? TGBridgeAudioSubscription { + let key = "audio_\(args.peerId)_\(args.messageId)" + return SSignal { subscriber in + let signal = manager.account.get() + |> take(1) + |> mapToSignal({ account -> Signal in + if let account = account, let peerId = makePeerIdFromBridgeIdentifier(args.peerId) { + return account.postbox.transaction { transaction -> Message? in + return transaction.getMessage(MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: args.messageId)) + } + |> mapToSignal { message -> Signal in + if let message = message { + for media in message.media { + if let file = media as? TelegramMediaFile { + self.disposable.add(messageMediaFileInteractiveFetched(account: account, message: message, file: file, userInitiated: true).start()) + return account.postbox.mediaBox.resourceData(file.resource) + |> mapToSignal({ data -> Signal in + if let tempPath = manager.watchTemporaryStorePath, data.complete { + let outputPath = tempPath + "/\(key).m4a" + return legacyDecodeOpusAudio(path: data.path, outputPath: outputPath) + } else { + return .complete() + } + }) + } + } + } + return .complete() + } + } else { + return .complete() + } + }) + + let disposable = signal.start(next: { path in + let _ = manager.sendFile(url: URL(fileURLWithPath: path), metadata: [TGBridgeIncomingFileTypeKey: TGBridgeIncomingFileTypeAudio, TGBridgeIncomingFileIdentifierKey: key]).start() + subscriber?.putNext(key) + }, completed: { + subscriber?.putCompletion() + }) + + return SBlockDisposable { + disposable.dispose() + } + } + //let outputPath = manager.watchTemporaryStorePath + "/\(key).opus" + } else if let args = subscription as? TGBridgeAudioSentSubscription { + + } + return SSignal.fail(nil) + } + + static func handleFile(path: String, metadata: Dictionary, manager: WatchCommunicationManager) -> Signal { + let randomId = metadata[TGBridgeIncomingFileRandomIdKey] as? Int64 + let peerId = metadata[TGBridgeIncomingFilePeerIdKey] as? Int64 + let replyToMid = metadata[TGBridgeIncomingFileReplyToMidKey] as? Int32 + + if let randomId = randomId, let id = peerId, let peerId = makePeerIdFromBridgeIdentifier(id) { + return combineLatest(manager.account.get() |> take(1), legacyEncodeOpusAudio(path: path)) + |> map({ account, pathAndDuration -> Void in + let (path, duration) = pathAndDuration + if let account = account, let path = path, let data = try? Data(contentsOf: URL(fileURLWithPath: path)) { + let resource = LocalFileMediaResource(fileId: randomId) + account.postbox.mediaBox.storeResourceData(resource.id, data: data) + + + var replyMessageId: MessageId? = nil + if let replyToMid = replyToMid, replyToMid != 0 { + replyMessageId = MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: replyToMid) + } + + let _ = enqueueMessages(account: account, peerId: peerId, messages: [.message(text: "", attributes: [], mediaReference: .standalone(media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: [], mimeType: "audio/ogg", size: data.count, attributes: [.Audio(isVoice: true, duration: Int(duration), title: nil, performer: nil, waveform: nil)])), replyToMessageId: replyMessageId, localGroupingKey: nil)]).start() + } + }) + } else { + return .complete() + } + } +} + +final class WatchLocationHandler: WatchRequestHandler { + static var handledSubscriptions: [Any] { + return [TGBridgeNearbyVenuesSubscription.self] + } + + static func handle(subscription: TGBridgeSubscription, manager: WatchCommunicationManager) -> SSignal { + if let args = subscription as? TGBridgeNearbyVenuesSubscription { + return SSignal { subscriber in + let signal = manager.account.get() + |> take(1) + |> mapToSignal({ account -> Signal<[ChatContextResultMessage], NoError> in + if let account = account { + return resolvePeerByName(account: account, name: "foursquare") + |> take(1) + |> mapToSignal { peerId -> Signal in + guard let peerId = peerId else { + return .single(nil) + } + return requestChatContextResults(account: account, botId: peerId, peerId: account.peerId, query: "", location: .single((args.coordinate.latitude, args.coordinate.longitude)), offset: "") + } + |> mapToSignal { contextResult -> Signal<[ChatContextResultMessage], NoError> in + guard let contextResult = contextResult else { + return .single([]) + } + return .single(contextResult.results.map { $0.message }) + } + } else { + return .complete() + } + }) + + let disposable = signal.start(next: { results in + var venues: [TGBridgeLocationVenue] = [] + for result in results { + if let venue = makeBridgeLocationVenue(result) { + venues.append(venue) + } + } + subscriber?.putNext(venues) + }) + + return SBlockDisposable { + disposable.dispose() + } + } + } + return SSignal.fail(nil) + } +} + +final class WatchPeerSettingsHandler: WatchRequestHandler { + static var handledSubscriptions: [Any] { + return [ + TGBridgePeerSettingsSubscription.self, + TGBridgePeerUpdateNotificationSettingsSubscription.self, + TGBridgePeerUpdateBlockStatusSubscription.self + ] + } + + static func handle(subscription: TGBridgeSubscription, manager: WatchCommunicationManager) -> SSignal { + if let args = subscription as? TGBridgePeerSettingsSubscription { + return SSignal { subscriber in + let signal = manager.account.get() + |> take(1) + |> mapToSignal({ account -> Signal in + if let account = account, let peerId = makePeerIdFromBridgeIdentifier(args.peerId) { + return account.viewTracker.peerView(peerId) + } else { + return .complete() + } + }) + let disposable = signal.start(next: { view in + var muted = false + var blocked = false + + if let notificationSettings = view.notificationSettings as? TelegramPeerNotificationSettings, case .muted = notificationSettings.muteState { + muted = true + } + if let cachedData = view.cachedData as? CachedUserData { + blocked = cachedData.isBlocked + } + + subscriber?.putNext([ "muted": muted, "blocked": blocked ]) + }) + + return SBlockDisposable { + disposable.dispose() + } + } + } else { + return SSignal { subscriber in + let signal = manager.account.get() + |> take(1) + |> mapToSignal({ account -> Signal in + if let account = account { + var signal: Signal? + + if let args = subscription as? TGBridgePeerUpdateNotificationSettingsSubscription, let peerId = makePeerIdFromBridgeIdentifier(args.peerId) { + signal = togglePeerMuted(account: account, peerId: peerId) + } else if let args = subscription as? TGBridgePeerUpdateBlockStatusSubscription, let peerId = makePeerIdFromBridgeIdentifier(args.peerId) { + signal = requestUpdatePeerIsBlocked(account: account, peerId: peerId, isBlocked: args.blocked) + } + + if let signal = signal { + return signal |> mapToSignal({ _ in + return .single(true) + }) + } else { + return .complete() + } + } else { + return .complete() + } + }) + + let disposable = signal.start(next: { _ in + subscriber?.putNext(true) + }, completed: { + subscriber?.putCompletion() + }) + + return SBlockDisposable { + disposable.dispose() + } + } + } + } +} + +final class WatchContinuationHandler: WatchRequestHandler { + static var handledSubscriptions: [Any] { + return [TGBridgeRemoteSubscription.self] + } + + static func handle(subscription: TGBridgeSubscription, manager: WatchCommunicationManager) -> SSignal { + if let args = subscription as? TGBridgeRemoteSubscription, let peerId = makePeerIdFromBridgeIdentifier(args.peerId) { + manager.requestNavigateToMessage(messageId: MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: args.messageId)) + } + return SSignal.fail(nil) + } +} diff --git a/Telegram-iOS/ar.lproj/AppIntentVocabulary.plist b/Telegram-iOS/ar.lproj/AppIntentVocabulary.plist new file mode 100644 index 0000000000..0a71b7adba --- /dev/null +++ b/Telegram-iOS/ar.lproj/AppIntentVocabulary.plist @@ -0,0 +1,17 @@ + + + + + IntentPhrases + + + IntentName + INSendMessageIntent + IntentExamples + + أرسل رسالة لخالد عبر تيليجرام (Telegram) وأخبره أن هديته وصلت إلى المنزل + + + + + diff --git a/Telegram-iOS/ar.lproj/InfoPlist.strings b/Telegram-iOS/ar.lproj/InfoPlist.strings new file mode 100644 index 0000000000..979491d3b3 --- /dev/null +++ b/Telegram-iOS/ar.lproj/InfoPlist.strings @@ -0,0 +1,16 @@ +/* Localized versions of Info.plist keys */ + +"CFBundleDisplayName" = "تيليجرام"; + +"NSContactsUsageDescription" = "سيقوم تيليجرام برفع جهات الاتصال الخاصة بك باستمرار إلى خوادم التخزين السحابية ذات التشفير العالي لتتمكن من التواصل مع أصدقائك من خلال جميع أجهزتك."; +"NSLocationWhenInUseUsageDescription" = "عندما ترغب في مشاركة مكانك مع أصدقائك، تيليجرام يحتاج لصلاحيات لعرض الخريطة لهم."; +"NSLocationAlwaysUsageDescription" = "عندما تقوم بمشاركة موقعك مع أصدقائك، تيليجرام يحتاج إلى الصلاحية ليعرض لهم الخريطة. كما تحتاج لإعطاء تيليجرام الصلاحية لتتمكن من إرسال موقعك من ساعة آبل."; +"NSCameraUsageDescription" = "نحتاج ذلك لتتمكن من التقاط وإرسال الصور والفيديوهات."; +"NSPhotoLibraryUsageDescription" = "نحتاج ذلك لتتمكن من إرسال الصور والفيديوهات من ألبوم الصور."; +"NSMicrophoneUsageDescription" = "نحتاج ذلك لتتمكن من تسجيل رسائل صوتية وفيديوهات بالصوت لترسلها.."; +"NSSiriUsageDescription" = "يمكنك استخدام سيري لإرسال رسائلك."; + +"NSLocationAlwaysAndWhenInUseUsageDescription" = "عندما تختار أن تشارك مكانك بشكل حي مع أصدقائك في المحادثة، يحتاج تيليجرام إلى الوصول لموقعك في الخلفية حتى بعد إغلاق تيليجرام خلال فترة المشاركة."; +"NSLocationAlwaysUsageDescription" = "عندما تقوم بمشاركة موقعك مع أصدقائك، تيليجرام يحتاج إلى الصلاحية ليعرض لهم الخريطة. كما تحتاج لإعطاء تيليجرام الصلاحية لتتمكن من إرسال موقعك من ساعة آبل."; +"NSLocationWhenInUseUsageDescription" = "عندما ترغب في مشاركة مكانك مع أصدقائك، تيليجرام يحتاج لصلاحيات لعرض الخريطة لهم."; + diff --git a/Telegram-iOS/ar.lproj/Localizable.strings b/Telegram-iOS/ar.lproj/Localizable.strings new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Telegram-iOS/de.lproj/AppIntentVocabulary.plist b/Telegram-iOS/de.lproj/AppIntentVocabulary.plist new file mode 100644 index 0000000000..3956121060 --- /dev/null +++ b/Telegram-iOS/de.lproj/AppIntentVocabulary.plist @@ -0,0 +1,17 @@ + + + + + IntentPhrases + + + IntentName + INSendMessageIntent + IntentExamples + + Sende Lisa eine Telegram-Nachricht, dass ich in 15 Minuten da bin. + + + + + diff --git a/Telegram-iOS/de.lproj/InfoPlist.strings b/Telegram-iOS/de.lproj/InfoPlist.strings new file mode 100644 index 0000000000..ff1210247f --- /dev/null +++ b/Telegram-iOS/de.lproj/InfoPlist.strings @@ -0,0 +1,14 @@ +/* Localized versions of Info.plist keys */ + +"NSContactsUsageDescription" = "Telegram lädt deine Kontakte durchgehend auf die stark verschlüsselten Cloud Server, damit du dich mit deinen Freunden auf all deinen Geräten verbinden kannst."; +"NSLocationWhenInUseUsageDescription" = "Wenn du Freunden deinen Standort mitteilen willst, musst du Telegram den Zugriff darauf erlauben."; +"NSLocationAlwaysUsageDescription" = "Wenn du Freunden deinen Standort mitteilen willst, musst du Telegram den Zugriff darauf erlauben. Diese Bereichtigung wird auch für die Apple Watch benötigt."; +"NSCameraUsageDescription" = "Brauchen wir, damit du Bilder und Videos aufnehmen und teilen kannst."; +"NSPhotoLibraryUsageDescription" = "Brauchen wir, damit du Bilder und Videos aus deiner Fotomediathek teilen kannst."; +"NSMicrophoneUsageDescription" = "Brauchen wir, damit du Sprachnachrichten aufnehmen und Videos mit Ton teilen kannst."; +"NSSiriUsageDescription" = "Mit Siri kannst du Nachrichten senden."; + +"NSLocationAlwaysAndWhenInUseUsageDescription" = "Wenn du deinen Live-Standort mit Freunden im Chat teilen möchtest, benötigt Telegram so lange im Hintergrund Zugriff auf deinen Standort, bis du ihn nicht mehr teilen willst."; +"NSLocationAlwaysUsageDescription" = "Wenn du Freunden deinen Standort mitteilen willst, musst du Telegram den Zugriff darauf erlauben. Diese Bereichtigung wird auch für die Apple Watch benötigt."; +"NSLocationWhenInUseUsageDescription" = "Wenn du Freunden deinen Standort mitteilen willst, musst du Telegram den Zugriff darauf erlauben."; + diff --git a/Telegram-iOS/de.lproj/Localizable.strings b/Telegram-iOS/de.lproj/Localizable.strings new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Telegram-iOS/en.lproj/AppIntentVocabulary.plist b/Telegram-iOS/en.lproj/AppIntentVocabulary.plist new file mode 100644 index 0000000000..504ece4483 --- /dev/null +++ b/Telegram-iOS/en.lproj/AppIntentVocabulary.plist @@ -0,0 +1,17 @@ + + + + + IntentPhrases + + + IntentName + INSendMessageIntent + IntentExamples + + Send a Telegram message to Alex saying I'll be there in 10 minutes + + + + + diff --git a/Telegram-iOS/en.lproj/InfoPlist.strings b/Telegram-iOS/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..119c5d2f9a --- /dev/null +++ b/Telegram-iOS/en.lproj/InfoPlist.strings @@ -0,0 +1,13 @@ +/* Localized versions of Info.plist keys */ + +"NSContactsUsageDescription" = "Telegram will continuously upload your contacts to its heavily encrypted cloud servers to let you connect with your friends across all your devices."; +"NSLocationWhenInUseUsageDescription" = "When you send your location to your friends, Telegram needs access to show them a map."; +"NSCameraUsageDescription" = "We need this so that you can take and share photos and videos."; +"NSPhotoLibraryUsageDescription" = "We need this so that you can share photos and videos from your photo library."; +"NSPhotoLibraryAddUsageDescription" = "We need this so that you can save photos and videos to your photo library."; +"NSMicrophoneUsageDescription" = "We need this so that you can record and share voice messages and videos with sound."; +"NSSiriUsageDescription" = "You can use Siri to send messages."; + +"NSLocationAlwaysAndWhenInUseUsageDescription" = "When you choose to share your Live Location with friends in a chat, Telegram needs background access to your location to keep them updated for the duration of the live sharing."; +"NSLocationAlwaysUsageDescription" = "When you choose to share your live location with friends in a chat, Telegram needs background access to your location to keep them updated for the duration of the live sharing. You also need this to send locations from an Apple Watch."; +"NSLocationWhenInUseUsageDescription" = "When you send your location to your friends, Telegram needs access to show them a map."; diff --git a/Telegram-iOS/en.lproj/Localizable.strings b/Telegram-iOS/en.lproj/Localizable.strings new file mode 100644 index 0000000000..b7f416ed8c --- /dev/null +++ b/Telegram-iOS/en.lproj/Localizable.strings @@ -0,0 +1,3502 @@ +// Notifications +"MESSAGE_TEXT" = "%1$@: %2$@"; +"MESSAGE_NOTEXT" = "%1$@ sent you a message"; +"MESSAGE_PHOTO" = "%1$@ sent you a photo"; +"MESSAGE_PHOTO_SECRET" = "%1$@ sent you a self-destructing photo"; +"MESSAGE_VIDEO" = "%1$@ sent you a video"; +"MESSAGE_VIDEO_SECRET" = "%1$@ sent you a self-destructing video"; +"MESSAGE_ROUND" = "%1$@ sent you a video message"; +"MESSAGE_CONTACT" = "%1$@ shared a contact with you"; +"MESSAGE_GEO" = "%1$@ sent you a map"; +"MESSAGE_GEOLIVE" = "%1$@ started sharing their live location"; +"MESSAGE_DOC" = "%1$@ sent you a file"; +"MESSAGE_AUDIO" = "%1$@ sent you a voice message"; +"MESSAGE_GIF" = "%1$@ sent you a GIF"; +"ENCRYPTED_MESSAGE" = "You have a new message%1$@"; +"LOCKED_MESSAGE" = "You have a new message%1$@"; +"MESSAGE_SCREENSHOT" = "%1$@ took a screenshot!"; +"ENCRYPTION_REQUEST" = "New encryption request%1$@"; +"ENCRYPTION_ACCEPT" = "Your encryption request was accepted%1$@"; +"MESSAGE_PHOTOS" = "%1$@ sent you %2$@ photos"; +"MESSAGES" = "%1$@ sent you %2$@ messages"; + +"CHAT_MESSAGE_TEXT" = "%1$@@%2$@: %3$@"; +"CHAT_MESSAGE_NOTEXT" = "%1$@ sent a message to the group %2$@"; +"CHAT_MESSAGE_PHOTO" = "%1$@ sent a photo to the group %2$@"; +"CHAT_MESSAGE_VIDEO" = "%1$@ sent a video to the group %2$@"; +"CHAT_MESSAGE_ROUND" = "%1$@ sent a video message to the group %2$@"; +"CHAT_MESSAGE_CONTACT" = "%1$@ shared a contact in the group %2$@"; +"CHAT_MESSAGE_GEO" = "%1$@ sent a map to the group %2$@"; +"CHAT_MESSAGE_GEOLIVE" = "%1$@ started sharing their live location with %2$@"; +"CHAT_MESSAGE_DOC" = "%1$@ sent a file to the group %2$@"; +"CHAT_MESSAGE_AUDIO" = "%1$@ sent a voice message to the group %2$@"; +"CHAT_MESSAGE_GIF" = "%1$@ sent a GIF to the group %2$@"; +"CHAT_CREATED" = "%1$@ invited you to the group %2$@"; +"CHAT_TITLE_EDITED" = "%1$@ edited the group's %2$@ name"; +"CHAT_PHOTO_EDITED" = "%1$@ edited the group's %2$@ photo"; +"CHAT_ADD_MEMBER" = "%1$@ invited %3$@ to the group %2$@"; +"CHAT_ADD_YOU" = "%1$@ invited you to the group %2$@"; +"CHAT_DELETE_YOU" = "%1$@ removed you from the group %2$@"; +"CHAT_DELETE_MEMBER" = "%1$@ removed %3$@ from the group %2$@"; +"CHAT_LEFT" = "%1$@ left the group %2$@"; +"CHAT_RETURNED" = "%1$@ returned to the group %2$@"; +"CHAT_MESSAGE_PHOTOS" = "%1$@ sent %3$@ photos to the group %2$@"; +"CHAT_MESSAGES" = "%1$@ sent %3$@ messages to the group %2$@"; + +"MESSAGE_STICKER" = "%1$@ sent you a %2$@sticker"; +"CHAT_MESSAGE_STICKER" = "%1$@ sent a %3$@sticker to the group %2$@"; + +"CONTACT_JOINED" = "%1$@ joined Telegram!"; + +"AUTH_REGION" = "Login from a new device %1$@, location: %2$@"; + +"MESSAGE_FWDS" = "%1$@ forwarded you %2$@ messages"; +"CHAT_MESSAGE_FWDS" = "%1$@ forwarded %3$@ messages to the group %2$@"; + +"CHANNEL_MESSAGE_TEXT" = "%1$@: %2$@"; +"CHANNEL_MESSAGE_NOTEXT" = "%1$@ posted a message"; +"CHANNEL_MESSAGE_PHOTO" = "%1$@ posted a photo"; +"CHANNEL_MESSAGE_VIDEO" = "%1$@ posted a video"; +"CHANNEL_MESSAGE_ROUND" = "%1$@ posted a video message"; +"CHANNEL_MESSAGE_DOC" = "%1$@ posted a document"; +"CHANNEL_MESSAGE_STICKER" = "%1$@ posted a %2$@sticker"; +"CHANNEL_MESSAGE_AUDIO" = "%1$@ posted a voice message"; +"CHANNEL_MESSAGE_CONTACT" = "%1$@ posted a contact"; +"CHANNEL_MESSAGE_GEO" = "%1$@ posted a map"; +"CHANNEL_MESSAGE_GEOLIVE" = "%1$@ posted a live location"; +"CHANNEL_MESSAGE_GIF" = "%1$@ posted a GIF"; +"CHANNEL_MESSAGE_PHOTOS" = "%1$@ posted %2$@ photos"; +"CHANNEL_MESSAGES" = "%1$@ posted %2$@ messages"; + +"PINNED_TEXT" = "%1$@ pinned \"%2$@\""; +"PINNED_NOTEXT" = "%1$@ pinned a message"; +"PINNED_PHOTO" = "%1$@ pinned a photo"; +"PINNED_VIDEO" = "%1$@ pinned a video"; +"PINNED_ROUND" = "%1$@ pinned a video message"; +"PINNED_DOC" = "%1$@ pinned a file"; +"PINNED_STICKER" = "%1$@ pinned a %2$@sticker"; +"PINNED_AUDIO" = "%1$@ pinned a voice message"; +"PINNED_CONTACT" = "%1$@ pinned a contact"; +"PINNED_GEO" = "%1$@ pinned a map"; +"PINNED_GEOLIVE" = "%1$@ pinned a live location"; +"PINNED_GIF" = "%1$@ pinned a GIF"; + +"MESSAGE_GAME" = "%1$@ invited you to play %2$@"; +"CHANNEL_MESSAGE_GAME" = "%1$@ invited you to play %2$@"; +"CHAT_MESSAGE_GAME" = "%1$@ invited the group %2$@ to play %3$@"; +"PINNED_GAME" = "%1$@ pinned a game"; + +"PHONE_CALL_REQUEST" = "%1$@ is calling you"; +"PHONE_CALL_MISSED" = "You missed a call from %1$@"; + +// Common +"Common.OK" = "OK"; +"Common.Cancel" = "Cancel"; +"Common.Edit" = "Edit"; +"Common.edit" = "edit"; +"Common.Done" = "Done"; +"Common.Next" = "Next"; +"Common.Delete" = "Delete"; +"Common.Create" = "Create"; +"Common.Back" = "Back"; +"Common.Close" = "Close"; +"Common.Yes" = "Yes"; +"Common.No" = "No"; +"Common.TakePhotoOrVideo" = "Take Photo or Video"; +"Common.TakePhoto" = "Take Photo"; +"Common.ChoosePhoto" = "Choose Photo"; +"Common.of" = "of"; +"Common.Search" = "Search"; +"Common.More" = "More"; +"Common.Select" = "Select"; + +// State +"State.Connecting" = "Connecting..."; +"State.connecting" = "connecting..."; +"State.ConnectingToProxy" = "Connecting to Proxy..."; +"State.ConnectingToProxyInfo" = "tap here for settings"; +"State.Updating" = "Updating..."; +"State.WaitingForNetwork" = "Waiting for network"; + +// Presence +"Presence.online" = "online"; + +// Date +"Month.GenJanuary" = "January"; +"Month.GenFebruary" = "February"; +"Month.GenMarch" = "March"; +"Month.GenApril" = "April"; +"Month.GenMay" = "May"; +"Month.GenJune" = "June"; +"Month.GenJuly" = "July"; +"Month.GenAugust" = "August"; +"Month.GenSeptember" = "September"; +"Month.GenOctober" = "October"; +"Month.GenNovember" = "November"; +"Month.GenDecember" = "December"; +"Month.ShortJanuary" = "Jan"; +"Month.ShortFebruary" = "Feb"; +"Month.ShortMarch" = "Mar"; +"Month.ShortApril" = "Apr"; +"Month.ShortMay" = "May"; +"Month.ShortJune" = "Jun"; +"Month.ShortJuly" = "Jul"; +"Month.ShortAugust" = "Aug"; +"Month.ShortSeptember" = "Sep"; +"Month.ShortOctober" = "Oct"; +"Month.ShortNovember" = "Nov"; +"Month.ShortDecember" = "Dec"; +"Weekday.ShortMonday" = "Mon"; +"Weekday.ShortTuesday" = "Tue"; +"Weekday.ShortWednesday" = "Wed"; +"Weekday.ShortThursday" = "Thu"; +"Weekday.ShortFriday" = "Fri"; +"Weekday.ShortSaturday" = "Sat"; +"Weekday.ShortSunday" = "Sun"; +"Weekday.Today" = "Today"; +"Weekday.Yesterday" = "Yesterday"; + +"Time.TodayAt" = "today at %@"; +"Time.YesterdayAt" = "yesterday at %@"; + +"LastSeen.JustNow" = "last seen just now"; +"LastSeen.MinutesAgo_0" = "last seen %@ minutes ago"; //three to ten +"LastSeen.MinutesAgo_1" = "last seen 1 minute ago"; //one +"LastSeen.MinutesAgo_2" = "last seen 2 minutes ago"; //two +"LastSeen.MinutesAgo_3_10" = "last seen %@ minutes ago"; //three to ten +"LastSeen.MinutesAgo_many" = "last seen %@ minutes ago"; // more than ten +"LastSeen.MinutesAgo_any" = "last seen %@ minutes ago"; // more than ten +"LastSeen.HoursAgo_0" = "last seen %@ hours ago"; +"LastSeen.HoursAgo_1" = "last seen 1 hour ago"; +"LastSeen.HoursAgo_2" = "last seen 2 hours ago"; +"LastSeen.HoursAgo_3_10" = "last seen %@ hours ago"; +"LastSeen.HoursAgo_any" = "last seen %@ hours ago"; +"LastSeen.HoursAgo_many" = "last seen %@ hours ago"; +"LastSeen.HoursAgo_0" = "last seen %@ hours ago"; +"LastSeen.YesterdayAt" = "last seen yesterday at %@"; +"LastSeen.AtDate" = "last seen %@"; +"LastSeen.Lately" = "last seen recently"; +"LastSeen.WithinAWeek" = "last seen within a week"; +"LastSeen.WithinAMonth" = "last seen within a month"; +"LastSeen.ALongTimeAgo" = "last seen a long time ago"; +"LastSeen.Offline" = "offline"; + +"Date.DialogDateFormat" = "{month} {day}"; +"Date.ChatDateHeader" = "%1$@ %2$@"; +"Date.ChatDateHeaderYear" = "%1$@ %2$@, %3$@"; + +// Tour +"Tour.Title1" = "Telegram"; +"Tour.Text1" = "The world's **fastest** messaging app.\nIt is **free** and **secure**."; + +"Tour.Title2" = "Fast"; +"Tour.Text2" = "**Telegram** delivers messages\nfaster than any other application."; + +"Tour.Title3" = "Powerful"; +"Tour.Text3" = "**Telegram** has no limits on\nthe size of your chats and media."; + +"Tour.Title4" = "Secure"; +"Tour.Text4" = "**Telegram** keeps your messages\nsafe from hacker attacks."; + +"Tour.Title5" = "Cloud-Based"; +"Tour.Text5" = "**Telegram** lets you access your\nmessages from multiple devices."; + +"Tour.Title6" = "Free"; +"Tour.Text6" = "**Telegram** is free forever. No ads.\nNo subscription fees."; + +"Tour.StartButton" = "Start Messaging"; + +// Login +"Login.PhoneAndCountryHelp" = "Please confirm your country code and enter your phone number."; +"Login.CodeSentInternal" = "We've sent the code to the **Telegram** app on your other device"; +"Login.HaveNotReceivedCodeInternal" = "Haven't received the code?"; +"Login.CodeSentSms" = "We have sent you an SMS with the code"; +"Login.Code" = "Code"; +"Login.WillCallYou" = "Telegram will call you in %@"; +"Login.CallRequestState2" = "Requesting a call from Telegram..."; +"Login.CallRequestState3" = "Telegram dialed your number\n[Didn't get the code?]"; +"Login.EmailNotConfiguredError" = "Please set up an email account."; +"Login.EmailCodeSubject" = "%@, no code"; +"Login.EmailCodeBody" = "My phone number is:\n%@\nI can't get an activation code for Telegram."; +"Login.UnknownError" = "An error occurred. Please try again later"; +"Login.InvalidCodeError" = "You have entered an invalid code. Please try again."; +"Login.NetworkError" = "Please check your internet connection and try again."; +"Login.CodeExpiredError" = "Code expired. Please try again."; +"Login.CodeFloodError" = "Limit exceeded. Please try again later."; +"Login.InvalidPhoneError" = "Invalid phone number. Please try again."; +"Login.InvalidFirstNameError" = "Invalid first name. Please try again."; +"Login.InvalidLastNameError" = "Invalid last name. Please try again."; + +"Login.PhoneTitle" = "Your Phone"; +"Login.PhonePlaceholder" = "Your phone number"; +"Login.CountryCode" = "Country Code"; +"Login.InvalidCountryCode" = "Invalid Country Code"; + +"Login.InfoTitle" = "Your Info"; +"Login.InfoAvatarAdd" = "add"; +"Login.InfoAvatarPhoto" = "photo"; +"Login.InfoFirstNamePlaceholder" = "First Name"; +"Login.InfoLastNamePlaceholder" = "Last Name"; +"Login.InfoDeletePhoto" = "Delete Photo"; +"Login.InfoHelp" = "Enter your name and add a profile picture."; + +// Login.SelectCountry +"Login.SelectCountry.Title" = "Country"; + +// Dialog List +"DialogList.TabTitle" = "Chats"; +"DialogList.Title" = "Chats"; +"DialogList.SearchLabel" = "Search for messages or users"; +"DialogList.NoMessagesTitle" = "You have no conversations yet"; +"DialogList.NoMessagesText" = "Start messaging by pressing the pencil button in the top right corner or go to the Contacts section."; +"DialogList.SingleTypingSuffix" = "%@ is typing"; +"DialogList.SingleRecordingAudioSuffix" = "%@ is recording audio"; +"DialogList.SingleUploadingPhotoSuffix" = "%@ is sending photo"; +"DialogList.SingleUploadingVideoSuffix" = "%@ is sending video"; +"DialogList.SingleRecordingVideoMessageSuffix" = "%@ is recording video"; +"DialogList.SingleUploadingFileSuffix" = "%@ is sending file"; +"DialogList.MultipleTypingSuffix" = "%d are typing"; +"DialogList.Typing" = "typing"; +"DialogList.ClearHistoryConfirmation" = "Clear History"; +"DialogList.DeleteConversationConfirmation" = "Delete and Exit"; +"DialogList.AwaitingEncryption" = "Waiting for %@ to get online..."; +"DialogList.EncryptionRejected" = "Secret chat cancelled"; +"DialogList.EncryptionProcessing" = "Exchanging encryption keys..."; +"DialogList.EncryptedChatStartedOutgoing" = "%@ joined your secret chat."; +"DialogList.EncryptedChatStartedIncoming" = "%@ created a secret chat."; + +// Compose +"Compose.TokenListPlaceholder" = "Whom would you like to message?"; +"Compose.NewMessage" = "New Message"; +"Compose.NewGroup" = "New Group"; +"Compose.NewEncryptedChat" = "New Secret Chat"; +"Compose.Create" = "Create"; + +// Contacts +"Contacts.TabTitle" = "Contacts"; +"Contacts.Title" = "Contacts"; +"Contacts.FailedToSendInvitesMessage" = "An error occurred."; +"Contacts.AccessDeniedError" = "Telegram does not have access to your contacts"; +"Contacts.AccessDeniedHelpLandscape" = "Please go to your %@ Settings — Privacy — Contacts.\nThen select ON for Telegram."; +"Contacts.AccessDeniedHelpPortrait" = "Please go to your %@ Settings — Privacy — Contacts. Then select ON for Telegram."; +"Contacts.AccessDeniedHelpON" = "ON"; +"Contacts.InviteToTelegram" = "Invite to Telegram"; +"Contacts.InviteFriends" = "Invite Friends"; +"Contacts.SelectAll" = "Select All"; + +// Conversation +"Conversation.InputTextPlaceholder" = "Message"; +"Conversation.typing" = "typing"; +"Conversation.MessageDeliveryFailed" = "Your message was not sent. Tap \"Resend\" to send this message."; +"Conversation.MessageDialogEdit" = "Edit"; +"Conversation.MessageDialogRetry" = "Resend"; +"Conversation.MessageDialogRetryAll" = "Resend %1$d Messages"; +"Conversation.MessageDialogDelete" = "Delete"; +"Conversation.LinkDialogOpen" = "Open"; +"Conversation.LinkDialogCopy" = "Copy"; +"Conversation.ForwardTitle" = "Forward"; +"Conversation.ForwardChats" = "Chats"; +"Conversation.ForwardContacts" = "Contacts"; +"Conversation.StatusKickedFromGroup" = "you were removed from the group"; +"Conversation.StatusLeftGroup" = "you have left the group"; +"Conversation.StatusTyping" = "typing"; +"Conversation.Call" = "Call"; +"Conversation.Mute" = "Mute"; +"Conversation.Unmute" = "Unmute"; +"Conversation.Edit" = "Edit"; +"Conversation.Info" = "Info"; +"Conversation.Search" = "Search"; +"Conversation.Unblock" = "Unblock"; +"Conversation.ClearAll" = "Delete All"; +"Conversation.Location" = "Location"; +"Conversation.Contact" = "Contact"; +"Conversation.BlockUser" = "Block User"; +"Conversation.UnblockUser" = "Unblock User"; +"Conversation.UnsupportedMedia" = "This message is not supported on your version of Telegram. Update the app to view:\nhttps://telegram.org/update"; +"Conversation.EncryptionWaiting" = "Waiting for %@ to get online..."; +"Conversation.EncryptionProcessing" = "Exchanging encryption keys..."; +"Conversation.EmptyPlaceholder" = "No messages here yet..."; +"Conversation.EncryptedPlaceholderTitleIncoming" = "%@ invited you to join a secret chat."; +"Conversation.EncryptedPlaceholderTitleOutgoing" = "You have invited %@ to join a secret chat."; +"Conversation.EncryptedDescriptionTitle" = "Secret chats:"; +"Conversation.EncryptedDescription1" = "Use end-to-end encryption"; +"Conversation.EncryptedDescription2" = "Leave no trace on our servers"; +"Conversation.EncryptedDescription3" = "Have a self-destruct timer"; +"Conversation.EncryptedDescription4" = "Do not allow forwarding"; +"Conversation.ContextMenuCopy" = "Copy"; +"Conversation.ContextMenuDelete" = "Delete"; +"Conversation.ContextMenuForward" = "Forward"; +"Conversation.ContextMenuMore" = "More..."; + +"Conversation.StatusMembers_0" = "%@ members"; +"Conversation.StatusMembers_1" = "1 member"; +"Conversation.StatusMembers_2" = "2 members"; +"Conversation.StatusMembers_3_10" = "%@ members"; +"Conversation.StatusMembers_many" = "%@ members"; +"Conversation.StatusMembers_any" = "%@ members"; + +"Conversation.StatusOnline_1" = "1 online"; +"Conversation.StatusOnline_2" = "2 online"; +"Conversation.StatusOnline_3_10" = "%@ online"; +"Conversation.StatusOnline_any" = "%@ online"; +"Conversation.StatusOnline_many" = "%@ online"; +"Conversation.StatusOnline_0" = "%@ online"; + +"Conversation.UnreadMessages" = "Unread Messages"; + +// Notification +"Notification.RenamedChat" = "%@ renamed group"; +"Notification.RenamedChannel" = "Channel renamed"; +"Notification.ChangedGroupPhoto" = "%@ changed group photo"; +"Notification.RemovedGroupPhoto" = "%@ removed group photo"; +"Notification.JoinedChat" = "%@ joined the group"; +"Notification.JoinedChannel" = "%@ joined the channel"; +"Notification.Invited" = "%@ invited %@"; +"Notification.LeftChat" = "%@ left the group"; +"Notification.LeftChannel" = "%@ left the channel"; +"Notification.Kicked" = "%@ removed %@"; +"Notification.CreatedChat" = "%@ created a group"; +"Notification.CreatedChannel" = "Channel created"; +"Notification.CreatedChatWithTitle" = "%@ created the group \"%@\""; +"Notification.Joined" = "%@ joined Telegram"; +"Notification.ChangedGroupName" = "%@ changed group name to \"%@\""; +"Notification.NewAuthDetected" = "%1$@,\nWe detected a login into your account from a new device on %2$@, %3$@ at %4$@\n\nDevice: %5$@\nLocation: %6$@\n\nIf this wasn't you, you can go to Settings — Privacy and Security — Sessions and terminate that session.\n\nIf you think that somebody logged in to your account against your will, you can enable two-step verification in Privacy and Security settings.\n\nSincerely,\nThe Telegram Team"; +"Notification.MessageLifetimeChanged" = "%1$@ set the self-destruct timer to %2$@"; +"Notification.MessageLifetimeChangedOutgoing" = "You set the self-destruct timer to %1$@"; +"Notification.MessageLifetimeRemoved" = "%1$@ disabled the self-destruct timer"; +"Notification.MessageLifetimeRemovedOutgoing" = "You disabled the self-destruct timer"; +"Notification.MessageLifetime2s" = "2 seconds"; +"Notification.MessageLifetime5s" = "5 seconds"; +"Notification.MessageLifetime1m" = "1 minute"; +"Notification.MessageLifetime1h" = "1 hour"; +"Notification.MessageLifetime1d" = "1 day"; +"Notification.MessageLifetime1w" = "1 week"; + +// Message +"Message.Photo" = "Photo"; +"Message.Video" = "Video"; +"Message.Location" = "Location"; +"Message.Contact" = "Contact"; +"Message.File" = "File"; +"Message.Sticker" = "Sticker"; +"Message.Audio" = "Voice Message"; +"Message.ForwardedMessage" = "Forwarded Message\nFrom: %@"; +"Message.Animation" = "GIF"; + +// Conversation Profile +"ConversationProfile.ErrorCreatingConversation" = "An error occurred"; +"ConversationProfile.UnknownAddMemberError" = "An unexpected error has occurred. Our wizards have been notified and will fix the problem soon. Sorry."; +"ConversationProfile.UsersTooMuchError" = "Sorry, this group is full. You cannot add any more members here."; + +"ConversationProfile.LeaveDeleteAndExit" = "Delete and Exit"; + +"Conversation.Megabytes" = "%.1f MB"; +"Conversation.Kilobytes" = "%d KB"; +"Conversation.Bytes" = "%d B"; +"Conversation.ShareMyContactInfo" = "Share My Contact Info"; +"Conversation.AddContact" = "Add Contact"; +"Conversation.SendMessage" = "Send Message"; +"Conversation.EncryptionCanceled" = "Secret chat cancelled"; +"Conversation.DeleteManyMessages" = "Delete Messages"; +"Conversation.SlideToCancel" = "Slide to cancel"; +"Conversation.ApplyLocalization" = "Apply Localization"; +"Conversation.OpenFile" = "Open File"; + +// Media Picker +"MediaPicker.Send" = "Send"; +"SearchImages.Title" = "Albums"; +"MediaPicker.CameraRoll" = "Camera Roll"; +"SearchImages.NoImagesFound" = "No images found"; + +// User Profile +"Profile.CreateEncryptedChatError" = "An error occurred."; +"Profile.CreateEncryptedChatOutdatedError" = "Cannot create a secret chat with %@.\n%@ is using an older version of Telegram and needs to update first."; +"Profile.CreateNewContact" = "Create New Contact"; +"Profile.AddToExisting" = "Add to Existing Contact"; +"Profile.EncryptionKey" = "Encryption Key"; +"Profile.MessageLifetimeForever" = "Off"; +"Profile.MessageLifetime2s" = "2s"; +"Profile.MessageLifetime5s" = "5s"; +"Profile.MessageLifetime1m" = "1m"; +"Profile.MessageLifetime1h" = "1h"; +"Profile.MessageLifetime1d" = "1d"; +"Profile.MessageLifetime1w" = "1w"; +"Profile.ShareContactButton" = "Share Contact"; + +// User Info +"UserInfo.Title" = "Info"; +"UserInfo.FirstNamePlaceholder" = "First Name"; +"UserInfo.LastNamePlaceholder" = "Last Name"; +"UserInfo.GenericPhoneLabel" = "mobile"; +"UserInfo.SendMessage" = "Send Message"; +"UserInfo.AddContact" = "Add Contact"; +"UserInfo.ShareContact" = "Share Contact"; +"UserInfo.StartSecretChat" = "Start Secret Chat"; +"UserInfo.DeleteContact" = "Delete Contact"; +"UserInfo.CreateNewContact" = "Create New Contact"; +"UserInfo.AddToExisting" = "Add to Existing"; +"UserInfo.AddPhone" = "add phone"; +"UserInfo.NotificationsEnabled" = "Enabled"; +"UserInfo.NotificationsDisabled" = "Disabled"; +"UserInfo.NotificationsEnable" = "Enable"; +"UserInfo.NotificationsDisable" = "Disable"; +"UserInfo.Invite" = "Invite to Telegram"; + +// New Contact +"NewContact.Title" = "New Contact"; + +// Phone Label +"PhoneLabel.Title" = "Label"; + +// Secret Chat +"SecretChat.Title" = "Secret Chat"; + +// Group Info +"GroupInfo.Title" = "Group Info"; +"GroupInfo.GroupNamePlaceholder" = "Group Name"; +"GroupInfo.BroadcastListNamePlaceholder" = "List Name"; +"GroupInfo.SetGroupPhoto" = "Set Group Photo"; +"GroupInfo.SetGroupPhotoStop" = "Stop"; +"GroupInfo.SetGroupPhotoDelete" = "Delete Photo"; +"GroupInfo.Notifications" = "Notifications"; +"GroupInfo.Sound" = "Sound"; +"GroupInfo.SharedMedia" = "Shared Media"; +"GroupInfo.SharedMediaNone" = "None"; +"GroupInfo.DeleteAndExit" = "Delete and Exit"; +"GroupInfo.DeleteAndExitConfirmation" = "You will not be able to join this group again."; +"GroupInfo.ParticipantCount_1" = "1 MEMBER"; +"GroupInfo.ParticipantCount_2" = "2 MEMBERS"; +"GroupInfo.ParticipantCount_3_10" = "%@ MEMBERS"; +"GroupInfo.ParticipantCount_any" = "%@ MEMBERS"; +"GroupInfo.ParticipantCount_many" = "%@ MEMBERS"; +"GroupInfo.ParticipantCount_0" = "%@ MEMBERS"; +"GroupInfo.AddParticipant" = "Add Member"; +"GroupInfo.AddParticipantTitle" = "Contacts"; +"GroupInfo.AddParticipantConfirmation" = "Add %@ to the group?"; +"GroupInfo.LeftStatus" = "You have left the group"; + +// Encryption Key +"EncryptionKey.Title" = "Encryption Key"; +"EncryptionKey.Description" = "This image and text were derived from the encryption key for this secret chat with %1$@.\n\n If they look the same on %2$@'s device, end-to-end encryption is guaranteed.\n\nLearn more at telegram.org"; + +// Conversation media +"ConversationMedia.Title" = "Media"; + +// Preview +"Preview.DeletePhoto" = "Delete Photo"; +"Preview.SaveToCameraRoll" = "Save to Camera Roll"; + +// Map +"Map.ChooseLocationTitle" = "Location"; +"Map.Map" = "Map"; +"Map.Satellite" = "Satellite"; +"Map.Hybrid" = "Hybrid"; +"Map.GetDirections" = "Get Directions"; +"Map.OpenInGoogleMaps" = "Open in Google Maps"; + +// Web +"Web.Error" = "Couldn't load page"; +"Web.OpenExternal" = "Open in Safari"; + +// Document +"Document.TargetConfirmationFormat" = "Send file ({size}) to {target}?"; + +// Dialog List +"DialogList.You" = "You"; + +// Settings +"Settings.TabTitle" = "Settings"; +"Settings.SetProfilePhoto" = "Set Profile Photo"; +"Settings.Logout" = "Log Out"; +"Settings.Title" = "Settings"; +"Settings.NotificationsAndSounds" = "Notifications and Sounds"; +"Settings.ChatSettings" = "Data and Storage"; +"Settings.BlockedUsers" = "Blocked Users"; +"Settings.ChatBackground" = "Chat Background"; +"Settings.Support" = "Ask a Question"; +"Settings.FAQ" = "Telegram FAQ"; +"Settings.FAQ_URL" = "https://telegram.org/faq#general"; +"Settings.FAQ_Intro" = "Please note that Telegram Support is done by volunteers. We try to respond as quickly as possible, but it may take a while.\n\nPlease take a look at the Telegram FAQ: it has important troubleshooting tips and answers to most questions."; +"Settings.FAQ_Button" = "FAQ"; +"Settings.SaveIncomingPhotos" = "Save Incoming Photos"; + +// Notifications and Sounds +"Notifications.Title" = "Notifications"; +"Notifications.MessageNotifications" = "MESSAGE NOTIFICATIONS"; +"Notifications.MessageNotificationsAlert" = "Alert"; +"Notifications.MessageNotificationsPreview" = "Message Preview"; +"Notifications.MessageNotificationsSound" = "Sound"; +"Notifications.MessageNotificationsHelp" = "You can set custom notifications for specific users on their Info page."; + +"Notifications.GroupNotifications" = "GROUP NOTIFICATIONS"; +"Notifications.GroupNotificationsAlert" = "Alert"; +"Notifications.GroupNotificationsPreview" = "Message Preview"; +"Notifications.GroupNotificationsSound" = "Sound"; +"Notifications.GroupNotificationsHelp" = "You can set custom notifications for specific groups on the Group Info page."; + +"Notifications.ChannelNotifications" = "CHANNEL NOTIFICATIONS"; +"Notifications.ChannelNotificationsAlert" = "Alert"; +"Notifications.ChannelNotificationsPreview" = "Message Preview"; +"Notifications.ChannelNotificationsSound" = "Sound"; +"Notifications.ChannelNotificationsHelp" = "You can set custom notifications for specific channels on the Channel Info page."; + +"Notifications.TextTone" = "Text Tone"; +"Notifications.AlertTones" = "ALERT TONES"; +"Notifications.ClassicTones" = "CLASSIC"; + +"Notifications.InAppNotifications" = "IN-APP NOTIFICATIONS"; +"Notifications.InAppNotificationsSounds" = "In-App Sounds"; +"Notifications.InAppNotificationsVibrate" = "In-App Vibrate"; +"Notifications.InAppNotificationsPreview" = "In-App Preview"; + +"Notifications.Reset" = "Reset"; +"Notifications.ResetAllNotifications" = "Reset All Notifications"; +"Notifications.ResetAllNotificationsHelp" = "Undo all custom notification settings for all your contacts and groups."; + +// Chat Settings +"ChatSettings.Title" = "Data and Storage"; +"ChatSettings.Appearance" = "APPEARANCE"; +"ChatSettings.TextSize" = "Text Size"; +"ChatSettings.TextSizeUnits" = "pt"; +"ChatSettings.AutomaticPhotoDownload" = "AUTOMATIC PHOTO DOWNLOAD"; +"ChatSettings.AutomaticAudioDownload" = "AUTOMATIC AUDIO DOWNLOAD"; +"ChatSettings.PrivateChats" = "Private Chats"; +"ChatSettings.Groups" = "Groups"; + +// Usage +"Cache.Title" = "Storage Usage"; +"Cache.ClearCache" = "Clear Cache"; +"Cache.KeepMedia" = "Keep Media"; +"Cache.Help" = "Photos, videos and other files from cloud chats that you have **not accessed** during this period will be removed from this device to save disk space.\n\nAll media will stay in the Telegram cloud and can be re-downloaded if you need it again."; + +// Blocked Users +"BlockedUsers.Title" = "Blocked"; +"BlockedUsers.SelectUserTitle" = "Block User"; +"BlockedUsers.BlockUser" = "Block User"; +"BlockedUsers.BlockTitle" = "Block"; +"BlockedUsers.LeavePrefix" = "Leave "; +"BlockedUsers.Info" = "Blocked users can't send you messages or add you to groups. They will not see your profile pictures, online and last seen status."; +"BlockedUsers.AddNew" = "Add New..."; +"BlockedUsers.Unblock" = "Unblock"; + +// Wallpaper +"Wallpaper.Title" = "Chat Background"; +"Wallpaper.PhotoLibrary" = "Photo Library"; +"Wallpaper.Set" = "Set"; +"Wallpaper.Wallpaper" = "Wallpaper"; + +"Notification.SecretChatMessageScreenshot" = "%@ took a screenshot!"; +"Notification.SecretChatScreenshot" = "Screenshot taken!"; + +"BroadcastListInfo.AddRecipient" = "Add Recipient"; + +"Settings.LogoutConfirmationTitle" = "Log out?"; +"Settings.LogoutConfirmationText" = "\nNote that you can seamlessly use Telegram on all your devices at once.\n\nRemember, logging out kills all your Secret Chats."; + +"Login.PadPhoneHelp" = "\nYou can use your main mobile number to log in to Telegram on all devices.\nDon't use your iPad's SIM number here — we'll need to send you an SMS.\n\nIs this number correct?\n{number}"; +"Login.PadPhoneHelpTitle" = "Your Number"; + +"MessageTimer.Custom" = "Custom"; + +"MessageTimer.Forever" = "Forever"; + +"MessageTimer.Seconds_1" = "%@ second"; +"MessageTimer.Seconds_2" = "%@ seconds"; +"MessageTimer.Seconds_3_10" = "%@ seconds"; +"MessageTimer.Seconds_any" = "%@ seconds"; +"MessageTimer.Seconds_many" = "%@ seconds"; +"MessageTimer.Seconds_0" = "%@ seconds"; +"MessageTimer.Minutes_1" = "%@ minute"; +"MessageTimer.Minutes_2" = "%@ minutes"; +"MessageTimer.Minutes_3_10" = "%@ minutes"; +"MessageTimer.Minutes_any" = "%@ minutes"; +"MessageTimer.Minutes_many" = "%@ minutes"; +"MessageTimer.Minutes_0" = "%@ minutes"; +"MessageTimer.Hours_1" = "%@ hour"; +"MessageTimer.Hours_2" = "%@ hours"; +"MessageTimer.Hours_3_10" = "%@ hours"; +"MessageTimer.Hours_any" = "%@ hours"; +"MessageTimer.Hours_many" = "%@ hours"; +"MessageTimer.Hours_0" = "%@ hours"; +"MessageTimer.Days_1" = "%@ day"; +"MessageTimer.Days_2" = "%@ days"; +"MessageTimer.Days_3_10" = "%@ days"; +"MessageTimer.Days_any" = "%@ days"; +"MessageTimer.Days_many" = "%@ days"; +"MessageTimer.Days_0" = "%@ days"; +"MessageTimer.Weeks_1" = "%@ week"; +"MessageTimer.Weeks_2" = "%@ weeks"; +"MessageTimer.Weeks_3_10" = "%@ weeks"; +"MessageTimer.Weeks_any" = "%@ weeks"; +"MessageTimer.Weeks_many" = "%@ weeks"; +"MessageTimer.Weeks_0" = "%@ weeks"; +"MessageTimer.Months_1" = "%@ month"; +"MessageTimer.Months_2" = "%@ months"; +"MessageTimer.Months_3_10" = "%@ months"; +"MessageTimer.Months_any" = "%@ months"; +"MessageTimer.Months_many" = "%@ months"; +"MessageTimer.Months_0" = "%@ months"; +"MessageTimer.Years_1" = "%@ year"; +"MessageTimer.Years_2" = "%@ years"; +"MessageTimer.Years_3_10" = "%@ years"; +"MessageTimer.Years_any" = "%@ years"; +"MessageTimer.Months_many" = "%@ years"; + +"MessageTimer.ShortSeconds_1" = "%@s"; +"MessageTimer.ShortSeconds_2" = "%@s"; +"MessageTimer.ShortSeconds_3_10" = "%@s"; +"MessageTimer.ShortSeconds_any" = "%@s"; +"MessageTimer.ShortSeconds_many" = "%@s"; +"MessageTimer.ShortSeconds_0" = "%@s"; +"MessageTimer.ShortMinutes_1" = "%@m"; +"MessageTimer.ShortMinutes_2" = "%@m"; +"MessageTimer.ShortMinutes_3_10" = "%@m"; +"MessageTimer.ShortMinutes_any" = "%@m"; +"MessageTimer.ShortMinutes_many" = "%@m"; +"MessageTimer.ShortMinutes_0" = "%@m"; +"MessageTimer.ShortHours_1" = "%@h"; +"MessageTimer.ShortHours_2" = "%@h"; +"MessageTimer.ShortHours_3_10" = "%@h"; +"MessageTimer.ShortHours_any" = "%@h"; +"MessageTimer.ShortHours_many" = "%@h"; +"MessageTimer.ShortHours_0" = "%@h"; +"MessageTimer.ShortDays_1" = "%@d"; +"MessageTimer.ShortDays_2" = "%@d"; +"MessageTimer.ShortDays_3_10" = "%@d"; +"MessageTimer.ShortDays_any" = "%@d"; +"MessageTimer.ShortDays_many" = "%@d"; +"MessageTimer.ShortDays_0" = "%@d"; +"MessageTimer.ShortWeeks_1" = "%@w"; +"MessageTimer.ShortWeeks_2" = "%@w"; +"MessageTimer.ShortWeeks_3_10" = "%@w"; +"MessageTimer.ShortWeeks_any" = "%@w"; +"MessageTimer.ShortWeeks_many" = "%@w"; +"MessageTimer.ShortWeeks_0" = "%@w"; + +"Activity.UploadingPhoto" = "sending photo"; +"Activity.UploadingVideo" = "sending video"; +"Activity.UploadingDocument" = "sending file"; +"Activity.RecordingAudio" = "recording audio"; +"Activity.RecordingVideoMessage" = "recording video"; + +"Compatibility.SecretMediaVersionTooLow" = "%@ is using an older version of Telegram, so secret photos will be shown in compatibility mode.\n\nOnce %@ updates Telegram, photos with timers for 1 minute or less will start working in 'Tap and hold to view' mode, and you will be notified whenever the other party takes a screenshot."; + +"Contacts.GlobalSearch" = "Global Search"; +"Profile.Username" = "username"; +"Settings.Username" = "Username"; +"Settings.UsernameEmpty" = "Add"; + +"Username.Title" = "Username"; +"Username.Placeholder" = "Your Username"; +"Username.Help" = "You can choose a username on **Telegram**. If you do, other people will be able to find you by this username and contact you without knowing your phone number.\n\nYou can use **a-z**, **0-9** and underscores. Minimum length is **5** characters."; +"Username.InvalidTooShort" = "A username must have at least 5 characters."; +"Username.InvalidStartsWithNumber" = "Sorry, a username can't start with a number."; +"Username.InvalidCharacters" = "Sorry, this username is invalid."; +"Username.InvalidTaken" = "Sorry, this username is already taken."; + +"Username.CheckingUsername" = "Checking username..."; +"Username.UsernameIsAvailable" = "%@ is available."; + +"WebSearch.Images" = "Images"; +"WebSearch.GIFs" = "GIFs"; +"WebSearch.RecentSectionTitle" = "Recent"; +"WebSearch.RecentSectionClear" = "Clear"; + +"Settings.PrivacySettings" = "Privacy and Security"; + +"UserCount_1" = "1 user"; +"UserCount_2" = "2 users"; +"UserCount_3_10" = "%@ users"; +"UserCount_any" = "%@ users"; +"UserCount_many" = "%@ users"; +"UserCount_0" = "%@ users"; + +"PrivacySettings.Title" = "Privacy and Security"; + +"PrivacySettings.PrivacyTitle" = "PRIVACY"; +"PrivacySettings.LastSeen" = "Last Seen"; +"PrivacySettings.LastSeenEverybody" = "Everybody"; +"PrivacySettings.LastSeenContacts" = "My Contacts"; +"PrivacySettings.LastSeenNobody" = "Nobody"; + +"PrivacySettings.LastSeenEverybodyMinus" = "Everybody (-%@)"; +"PrivacySettings.LastSeenContactsPlus" = "My Contacts (+%@)"; +"PrivacySettings.LastSeenContactsMinus" = "My Contacts (-%@)"; +"PrivacySettings.LastSeenContactsMinusPlus" = "My Contacts (-%@, +%@)"; +"PrivacySettings.LastSeenNobodyPlus" = "Nobody (+%@)"; + +"PrivacySettings.SecurityTitle" = "SECURITY"; + +"PrivacySettings.DeleteAccountTitle" = "DELETE MY ACCOUNT"; +"PrivacySettings.DeleteAccountIfAwayFor" = "If Away For"; +"PrivacySettings.DeleteAccountHelp" = "If you do not log in at least once within this period, your account will be deleted along with all groups, messages and contacts."; + +"PrivacyLastSeenSettings.Title" = "Last Seen"; +"PrivacyLastSeenSettings.CustomHelp" = "Important: you won't be able to see Last Seen times for people with whom you don't share your Last Seen time. Approximate last seen will be shown instead (recently, within a week, within a month)."; +"PrivacyLastSeenSettings.AlwaysShareWith" = "Always Share With"; +"PrivacyLastSeenSettings.NeverShareWith" = "Never Share With"; +"PrivacyLastSeenSettings.CustomShareSettingsHelp" = "These settings will override the values above."; + +"PrivacyLastSeenSettings.CustomShareSettings.Delete" = "Delete"; +"PrivacyLastSeenSettings.AlwaysShareWith.Title" = "Always Share"; +"PrivacyLastSeenSettings.AlwaysShareWith.Placeholder" = "Always share with users..."; +"PrivacyLastSeenSettings.NeverShareWith.Title" = "Never Share"; +"PrivacyLastSeenSettings.NeverShareWith.Placeholder" = "Never share with users..."; +"PrivacyLastSeenSettings.EmpryUsersPlaceholder" = "Add Users"; +"PrivacyLastSeenSettings.AddUsers_1" = "Add 1 user to this list?"; +"PrivacyLastSeenSettings.AddUsers_2" = "Add 2 users to this list?"; +"PrivacyLastSeenSettings.AddUsers_3_10" = "Add %@ users to this list?"; +"PrivacyLastSeenSettings.AddUsers_any" = "Add %@ users to this list?"; +"PrivacyLastSeenSettings.AddUsers_many" = "Add %@ users to this list?"; +"PrivacyLastSeenSettings.AddUsers_0" = "Add %@ users to this list?"; + +// Photo Editor +"PhotoEditor.DiscardChanges" = "Discard Changes"; + +"PhotoEditor.Original" = "Original"; + +"PhotoEditor.CropReset" = "RESET"; +"PhotoEditor.CropAuto" = "AUTO"; +"PhotoEditor.CropAspectRatioOriginal" = "Original"; +"PhotoEditor.CropAspectRatioSquare" = "Square"; + +"PhotoEditor.EnhanceTool" = "Enhance"; +"PhotoEditor.ExposureTool" = "Brightness"; +"PhotoEditor.ContrastTool" = "Contrast"; +"PhotoEditor.WarmthTool" = "Warmth"; +"PhotoEditor.SaturationTool" = "Saturation"; +"PhotoEditor.HighlightsTool" = "Highlights"; +"PhotoEditor.ShadowsTool" = "Shadows"; +"PhotoEditor.VignetteTool" = "Vignette"; +"PhotoEditor.GrainTool" = "Grain"; +"PhotoEditor.SharpenTool" = "Sharpen"; + +"PhotoEditor.BlurToolOff" = "Off"; +"PhotoEditor.BlurToolRadial" = "Radial"; +"PhotoEditor.BlurToolLinear" = "Linear"; + +"PhotoEditor.Set" = "Set"; +"PhotoEditor.Skip" = "Skip"; + +// Camera +"Camera.PhotoMode" = "PHOTO"; +"Camera.VideoMode" = "VIDEO"; +"Camera.SquareMode" = "SQUARE"; +"Camera.FlashOff" = "Off"; +"Camera.FlashOn" = "On"; +"Camera.FlashAuto" = "Auto"; +"Camera.Retake" = "Retake"; + +"Settings.PhoneNumber" = "Change Number"; + +"PhoneNumberHelp.Help" = "You can change your Telegram number here. Your account and all your cloud data — messages, media, contacts, etc. will be moved to the new number.\n\n**Important:** all your Telegram contacts will get your **new number** added to their address book, provided they had your old number and you haven't blocked them in Telegram."; +"PhoneNumberHelp.Alert" = "All your Telegram contacts will get your new number added to their address book, provided they had your old number and you haven't blocked them in Telegram."; +"PhoneNumberHelp.ChangeNumber" = "Change Number"; + +"ChangePhoneNumberNumber.Title" = "Change Number"; +"ChangePhoneNumberNumber.NewNumber" = "NEW NUMBER"; +"ChangePhoneNumberNumber.Help" = "We will send an SMS with a confirmation code to your new number."; +"ChangePhoneNumberNumber.NumberPlaceholder" = "Enter your new number"; + +"ChangePhoneNumberCode.Code" = "YOUR CODE"; +"ChangePhoneNumberCode.CodePlaceholder" = "Code"; +"ChangePhoneNumberCode.Help" = "We have sent you an SMS with the code"; +"ChangePhoneNumberCode.CallTimer" = "Telegram will call you in %@"; +"ChangePhoneNumberCode.RequestingACall" = "Requesting a call from Telegram..."; +"ChangePhoneNumberCode.Called" = "Telegram dialed your number"; + +"LoginPassword.Title" = "Your Password"; +"LoginPassword.PasswordPlaceholder" = "Password"; +"LoginPassword.InvalidPasswordError" = "Invalid password. Please try again."; +"LoginPassword.FloodError" = "Limit exceeded. Please try again later."; +"LoginPassword.ForgotPassword" = "Forgot password?"; +"LoginPassword.PasswordHelp" = "Two-Step verification enabled. Your account is protected with an additional password."; +"LoginPassword.ResetAccount" = "Reset Account"; + +"QuickSend.Photos_1" = "Send 1 Photo"; +"QuickSend.Photos_2" = "Send 2 Photos"; +"QuickSend.Photos_3_10" = "Send %@ Photos"; +"QuickSend.Photos_any" = "Send %@ Photos"; +"QuickSend.Photos_many" = "Send %@ Photos"; +"QuickSend.Photos_0" = "Send %@ Photos"; + +"Share.Title" = "Share"; +"Forward.ConfirmMultipleFiles_1" = "Send 1 file to {target}?"; +"Forward.ConfirmMultipleFiles_2" = "Send 2 files to {target}?"; +"Forward.ConfirmMultipleFiles_3_10" = "Send %@ files to {target}?"; +"Forward.ConfirmMultipleFiles_any" = "Send %@ files to {target}?"; +"Forward.ConfirmMultipleFiles_many" = "Send %@ files to {target}?"; +"Forward.ConfirmMultipleFiles_0" = "Send %@ files to {target}?"; + +"Notification.Reply" = "Reply"; +"Notification.Mute1h" = "Mute for 1 hour"; +"Notification.Mute1hMin" = "Mute for 1h"; +"Conversation.ContextMenuShare" = "Share"; + +"SharedMedia.TitleAll" = "Shared Media"; + +"SharedMedia.Photo_1" = "1 photo"; +"SharedMedia.Photo_2" = "2 photos"; +"SharedMedia.Photo_3_10" = "%@ photos"; +"SharedMedia.Photo_any" = "%@ photos"; +"SharedMedia.Photo_many" = "%@ photos"; +"SharedMedia.Photo_0" = "%@ photos"; + +"SharedMedia.Video_1" = "1 video"; +"SharedMedia.Video_2" = "2 videos"; +"SharedMedia.Video_3_10" = "%@ videos"; +"SharedMedia.Video_any" = "%@ videos"; +"SharedMedia.Video_many" = "%@ videos"; +"SharedMedia.Video_0" = "%@ videos"; + +"SharedMedia.File_1" = "1 file"; +"SharedMedia.File_2" = "2 files"; +"SharedMedia.File_3_10" = "%@ files"; +"SharedMedia.File_any" = "%@ files"; +"SharedMedia.File_many" = "%@ files"; +"SharedMedia.File_0" = "%@ files"; + +"SharedMedia.Generic_1" = "1 media file"; +"SharedMedia.Generic_2" = "2 media files"; +"SharedMedia.Generic_3_10" = "%@ media files"; +"SharedMedia.Generic_any" = "%@ media files"; +"SharedMedia.Generic_many" = "%@ media files"; +"SharedMedia.Generic_0" = "%@ media files"; + +"FileSize.B" = "%@ B"; +"FileSize.KB" = "%@ KB"; +"FileSize.MB" = "%@ MB"; +"FileSize.GB" = "%@ GB"; + +"DownloadingStatus" = "Downloading %@ of %@"; + +"Time.MonthOfYear_m1" = "January %@"; +"Time.MonthOfYear_m2" = "February %@"; +"Time.MonthOfYear_m3" = "March %@"; +"Time.MonthOfYear_m4" = "April %@"; +"Time.MonthOfYear_m5" = "May %@"; +"Time.MonthOfYear_m6" = "June %@"; +"Time.MonthOfYear_m7" = "July %@"; +"Time.MonthOfYear_m8" = "August %@"; +"Time.MonthOfYear_m9" = "September %@"; +"Time.MonthOfYear_m10" = "October %@"; +"Time.MonthOfYear_m11" = "November %@"; +"Time.MonthOfYear_m12" = "December %@"; + +"Time.PreciseDate_m1" = "Jan %1$@, %2$@ at %3$@"; +"Time.PreciseDate_m2" = "Feb %1$@, %2$@ at %3$@"; +"Time.PreciseDate_m3" = "Mar %1$@, %2$@ at %3$@"; +"Time.PreciseDate_m4" = "Apr %1$@, %2$@ at %3$@"; +"Time.PreciseDate_m5" = "May %1$@, %2$@ at %3$@"; +"Time.PreciseDate_m6" = "Jun %1$@, %2$@ at %3$@"; +"Time.PreciseDate_m7" = "Jul %1$@, %2$@ at %3$@"; +"Time.PreciseDate_m8" = "Aug %1$@, %2$@ at %3$@"; +"Time.PreciseDate_m9" = "Sep %1$@, %2$@ at %3$@"; +"Time.PreciseDate_m10" = "Oct %1$@, %2$@ at %3$@"; +"Time.PreciseDate_m11" = "Nov %1$@, %2$@ at %3$@"; +"Time.PreciseDate_m12" = "Dec %1$@, %2$@ at %3$@"; + +"MuteFor.Hours_1" = "Mute for 1 hour"; +"MuteFor.Hours_2" = "Mute for 2 hours"; +"MuteFor.Hours_3_10" = "Mute for %@ hours"; +"MuteFor.Hours_any" = "Mute for %@ hours"; +"MuteFor.Hours_many" = "Mute for %@ hours"; +"MuteFor.Hours_0" = "Mute for %@ hours"; + +"MuteFor.Days_1" = "Mute for 1 day"; +"MuteFor.Days_2" = "Mute for 2 days"; +"MuteFor.Days_3_10" = "Mute for %@ days"; +"MuteFor.Days_any" = "Mute for %@ days"; +"MuteFor.Days_many" = "Mute for %@ days"; +"MuteFor.Days_0" = "Mute for %@ days"; + +"MuteExpires.Minutes_1" = "in 1 minute"; +"MuteExpires.Minutes_2" = "in 2 minutes"; +"MuteExpires.Minutes_3_10" = "in %@ minutes"; +"MuteExpires.Minutes_any" = "in %@ minutes"; +"MuteExpires.Minutes_many" = "in %@ minutes"; +"MuteExpires.Minutes_0" = "in %@ minutes"; + +"MuteExpires.Hours_1" = "in 1 hour"; +"MuteExpires.Hours_2" = "in 2 hours"; +"MuteExpires.Hours_3_10" = "in %@ hours"; +"MuteExpires.Hours_any" = "in %@ hours"; +"MuteExpires.Hours_many" = "in %@ hours"; +"MuteExpires.Hours_0" = "in %@ hours"; + +"MuteExpires.Days_1" = "in 1 day"; +"MuteExpires.Days_2" = "in 2 days"; +"MuteExpires.Days_3_10" = "in %@ days"; +"MuteExpires.Days_any" = "in %@ days"; +"MuteExpires.Days_many" = "in %@ days"; +"MuteExpires.Days_0" = "in %@ days"; + +"SharedMedia.EmptyTitle" = "No media files yet"; +"SharedMedia.EmptyText" = "Share photos and videos in this chat\n — or this paperclip stays unhappy."; +"SharedMedia.EmptyFilesText" = "You can send and receive\nfiles of any type up to 1.5 GB each\nand access them anywhere."; + +"ShareFileTip.Title" = "Sharing Files"; +"ShareFileTip.Text" = "You can share **uncompressed** media files from your Camera Roll here.\n\nTo share files of any other type, open them on your %@ (e.g. in your browser), tap **Open in...** or the action button and choose Telegram."; +"ShareFileTip.CloseTip" = "Close Tip"; + +"DialogList.SearchSectionDialogs" = "Chats and Contacts"; +"DialogList.SearchSectionGlobal" = "Global Search"; +"DialogList.SearchSectionMessages" = "Messages"; + +"Username.LinkHint" = "This link opens a chat with you in Telegram:[\nhttps://t.me/%@]"; +"Username.LinkCopied" = "Copied link to clipboard"; + +"SharedMedia.DeleteItemsConfirmation_1" = "Delete media file?"; +"SharedMedia.DeleteItemsConfirmation_2" = "Delete 2 media files?"; +"SharedMedia.DeleteItemsConfirmation_3_10" = "Delete %@ media files?"; +"SharedMedia.DeleteItemsConfirmation_any" = "Delete %@ media files?"; +"SharedMedia.DeleteItemsConfirmation_many" = "Delete %@ media files?"; +"SharedMedia.DeleteItemsConfirmation_0" = "Delete %@ media files?"; + +"PrivacySettings.Passcode" = "Passcode Lock"; +"PasscodeSettings.Title" = "Passcode Lock"; +"PasscodeSettings.TurnPasscodeOn" = "Turn Passcode On"; +"PasscodeSettings.TurnPasscodeOff" = "Turn Passcode Off"; +"PasscodeSettings.ChangePasscode" = "Change Passcode"; +"PasscodeSettings.Help" = "When you set up an additional passcode, a lock icon will appear on the chats page. Tap it to lock and unlock the app.\n\nNote: if you forget the passcode, you'll need to delete and reinstall the app. All secret chats will be lost."; +"PasscodeSettings.UnlockWithTouchId" = "Unlock with Touch ID"; +"PasscodeSettings.SimplePasscode" = "Simple Passcode"; +"PasscodeSettings.SimplePasscodeHelp" = "A simple passcode is a 4 digit number."; +"PasscodeSettings.EncryptData" = "Encrypt Local Database"; +"PasscodeSettings.EncryptDataHelp" = "Experimental feature, use with caution. Encrypt your local Telegram data, using a derivative of your passcode as the key."; + +"EnterPasscode.EnterTitle" = "Enter your Telegram Passcode"; +"EnterPasscode.ChangeTitle" = "Change Passcode"; +"EnterPasscode.EnterPasscode" = "Enter your Telegram Passcode"; +"EnterPasscode.EnterNewPasscodeNew" = "Enter a passcode"; +"EnterPasscode.EnterNewPasscodeChange" = "Enter your new passcode"; +"EnterPasscode.RepeatNewPasscode" = "Re-enter your new passcode"; +"EnterPasscode.EnterCurrentPasscode" = "Enter your current passcode"; +"EnterPasscode.TouchId" = "Enter your passcode"; + +"DialogList.PasscodeLockHelp" = "Tap to lock Telegram"; + +"PasscodeSettings.AutoLock" = "Auto-Lock"; +"PasscodeSettings.AutoLock.Disabled" = "Disabled"; +"PasscodeSettings.AutoLock.IfAwayFor_1minute" = "If away for 1 min"; +"PasscodeSettings.AutoLock.IfAwayFor_5minutes" = "If away for 5 min"; +"PasscodeSettings.AutoLock.IfAwayFor_1hour" = "If away for 1 hour"; +"PasscodeSettings.AutoLock.IfAwayFor_5hours" = "If away for 5 hours"; + +"PasscodeSettings.FailedAttempts_1" = "1 Failed Passcode Attempt"; +"PasscodeSettings.FailedAttempts_2" = "2 Failed Passcode Attempts"; +"PasscodeSettings.FailedAttempts_3_10" = "%@ Failed Passcode Attempts"; +"PasscodeSettings.FailedAttempts_any" = "%@ Failed Passcode Attempt"; +"PasscodeSettings.FailedAttempts_many" = "%@ Failed Passcode Attempts"; +"PasscodeSettings.FailedAttempts_0" = "%@ Failed Passcode Attempts"; +"PasscodeSettings.TryAgainIn1Minute" = "Try again in 1 minute"; + +"AccessDenied.Title" = "Please Allow Access"; + +"AccessDenied.Contacts" = "Telegram messaging is based on your existing contact list.\n\nPlease go to Settings > Privacy > Contacts and set Telegram to ON."; + +"AccessDenied.VoiceMicrophone" = "Telegram needs access to your microphone to send voice messages.\n\nPlease go to Settings > Privacy > Microphone and set Telegram to ON."; + +"AccessDenied.VideoMicrophone" = "Telegram needs access to your microphone to record sound in videos recording.\n\nPlease go to Settings > Privacy > Microphone and set Telegram to ON."; + +"AccessDenied.MicrophoneRestricted" = "Microphone access is restricted for Telegram.\n\nPlease go to Settings > General > Restrictions > Microphone and set Telegram to ON."; + + +"AccessDenied.Camera" = "Telegram needs access to your camera to take photos and videos.\n\nPlease go to Settings > Privacy > Camera and set Telegram to ON."; + +"AccessDenied.CameraRestricted" = "Camera access is restricted for Telegram.\n\nPlease go to Settings > General > Restrictions > Camera and set Telegram to ON."; + +"AccessDenied.CameraDisabled" = "Camera access is globally restricted on your phone.\n\nPlease go to Settings > General > Restrictions and set Camera to ON"; + +"AccessDenied.PhotosAndVideos" = "Telegram needs access to your photo library to send photos and videos.\n\nPlease go to Settings > Privacy > Photos and set Telegram to ON."; + +"AccessDenied.SaveMedia" = "Telegram needs access to your photo library to save photos and videos.\n\nPlease go to Settings > Privacy > Photos and set Telegram to ON."; + +"AccessDenied.PhotosRestricted" = "Photo access is restricted for Telegram.\n\nPlease go to Settings > General > Restrictions > Photos and set Telegram to ON."; + +"AccessDenied.LocationDenied" = "Telegram needs access to your location so that you can share it with your contacts.\n\nPlease go to Settings > Privacy > Location Services and set Telegram to ON."; + +"AccessDenied.LocationDisabled" = "Telegram needs access to your location so that you can share it with your contacts.\n\nPlease go to Settings > Privacy > Location Services and set it to ON."; + +"AccessDenied.LocationTracking" = "Telegram needs access to your location to show you on the map.\n\nPlease go to Settings > Privacy > Location Services and set it to ON."; + +"AccessDenied.Settings" = "Settings"; + +"WebSearch.RecentClearConfirmation" = "Are you sure you want to clear recent images?"; + +"FeatureDisabled.Oops" = "Oops"; + +"Conversation.ContextMenuReply" = "Reply"; + +"ForwardedMessages_1" = "Forwarded message"; +"ForwardedMessages_2" = "2 forwarded messages"; +"ForwardedMessages_3_10" = "%@ forwarded messages"; +"ForwardedMessages_any" = "%@ forwarded messages"; +"ForwardedMessages_many" = "%@ forwarded messages"; +"ForwardedMessages_0" = "%@ forwarded messages"; + +"ForwardedFiles_1" = "Forwarded file"; +"ForwardedFiles_2" = "2 forwarded files"; +"ForwardedFiles_3_10" = "%@ forwarded files"; +"ForwardedFiles_any" = "%@ forwarded files"; +"ForwardedFiles_many" = "%@ forwarded files"; +"ForwardedFiles_0" = "%@ forwarded files"; + +"ForwardedStickers_1" = "Forwarded sticker"; +"ForwardedStickers_2" = "2 forwarded stickers"; +"ForwardedStickers_3_10" = "%@ forwarded stickers"; +"ForwardedStickers_any" = "%@ forwarded stickers"; +"ForwardedStickers_many" = "%@ forwarded stickers"; +"ForwardedStickers_0" = "%@ forwarded stickers"; + +"ForwardedPhotos_1" = "Forwarded photo"; +"ForwardedPhotos_2" = "2 forwarded photos"; +"ForwardedPhotos_3_10" = "%@ forwarded photos"; +"ForwardedPhotos_any" = "%@ forwarded photos"; +"ForwardedPhotos_many" = "%@ forwarded photos"; +"ForwardedPhotos_0" = "%@ forwarded photos"; + +"ForwardedVideos_1" = "Forwarded video"; +"ForwardedVideos_2" = "2 forwarded videos"; +"ForwardedVideos_3_10" = "%@ forwarded videos"; +"ForwardedVideos_any" = "%@ forwarded videos"; +"ForwardedVideos_many" = "%@ forwarded videos"; +"ForwardedVideos_0" = "%@ forwarded videos"; + +"ForwardedAudios_1" = "Forwarded audio"; +"ForwardedAudios_2" = "2 forwarded audios"; +"ForwardedAudios_3_10" = "%@ forwarded audios"; +"ForwardedAudios_any" = "%@ forwarded audios"; +"ForwardedAudios_many" = "%@ forwarded audios"; +"ForwardedAudios_0" = "%@ forwarded audios"; + +"ForwardedLocations_1" = "Forwarded location"; +"ForwardedLocations_2" = "2 forwarded locations"; +"ForwardedLocations_3_10" = "%@ forwarded locations"; +"ForwardedLocations_any" = "%@ forwarded locations"; +"ForwardedLocations_many" = "%@ forwarded locations"; +"ForwardedLocations_0" = "%@ forwarded locations"; + +"ForwardedGifs_1" = "Forwarded GIF"; +"ForwardedGifs_2" = "2 forwarded GIFs"; +"ForwardedGifs_3_10" = "%@ forwarded GIFs"; +"ForwardedGifs_any" = "%@ forwarded GIFs"; +"ForwardedGifs_many" = "%@ forwarded GIFs"; +"ForwardedGifs_0" = "%@ forwarded GIFs"; + +"ForwardedContacts_1" = "Forwarded contact"; +"ForwardedContacts_2" = "2 forwarded contacts"; +"ForwardedContacts_3_10" = "%@ forwarded contacts"; +"ForwardedContacts_any" = "%@ forwarded contacts"; +"ForwardedContacts_many" = "%@ forwarded contacts"; +"ForwardedContacts_0" = "%@ forwarded contacts"; + +"ForwardedAuthors2" = "%@, %@"; +"ForwardedAuthorsOthers_1" = "%@ and 1 other"; +"ForwardedAuthorsOthers_2" = "%@ and 2 others"; +"ForwardedAuthorsOthers_3_10" = "%@ and %@ others"; +"ForwardedAuthorsOthers_any" = "%@ and %@ others"; +"ForwardedAuthorsOthers_many" = "%@ and %@ others"; +"ForwardedAuthorsOthers_0" = "%@ and %@ others"; + +"PrivacySettings.TwoStepAuth" = "Two-Step Verification"; +"TwoStepAuth.Title" = "Two-Step Verification"; +"TwoStepAuth.SetPassword" = "Set Additional Password"; +"TwoStepAuth.SetPasswordHelp" = "You can set a password that will be required when you log in on a new device in addition to the code you get in the SMS."; +"TwoStepAuth.SetupPasswordTitle" = "Your Password"; + +"TwoStepAuth.SetupHintTitle" = "Password Hint"; +"TwoStepAuth.SetupHint" = "Please create a hint for your password:"; + +"TwoStepAuth.ChangePassword" = "Change Password"; +"TwoStepAuth.RemovePassword" = "Turn Password Off"; +"TwoStepAuth.SetupEmail" = "Set Recovery E-Mail"; +"TwoStepAuth.ChangeEmail" = "Change Recovery E-Mail"; +"TwoStepAuth.PendingEmailHelp" = "Your recovery e-mail %@ is not yet active and pending confirmation."; +"TwoStepAuth.GenericHelp" = "You have enabled Two-Step verification.\nYou'll need the password you set up here to log in to your Telegram account."; + +"TwoStepAuth.ConfirmationTitle" = "Two-Step Verification"; +"TwoStepAuth.ConfirmationText" = "Please check your e-mail and click on the validation link to complete Two-Step Verification setup. Be sure to check the spam folder as well."; +"TwoStepAuth.ConfirmationAbort" = "Abort Two-Step Verification Setup"; + +"TwoStepAuth.SetupPasswordEnterPasswordNew" = "Enter a password:"; +"TwoStepAuth.SetupPasswordEnterPasswordChange" = "Please enter your new password:"; +"TwoStepAuth.SetupPasswordConfirmPassword" = "Please re-enter your password:"; +"TwoStepAuth.SetupPasswordConfirmFailed" = "Passwords don't match. Please try again."; + +"TwoStepAuth.EnterPasswordTitle" = "Password"; +"TwoStepAuth.EnterPasswordPassword" = "Password"; +"TwoStepAuth.EnterPasswordHint" = "Hint: %@"; +"TwoStepAuth.EnterPasswordHelp" = "You have enabled Two-Step Verification, so your account is protected with an additional password."; +"TwoStepAuth.EnterPasswordInvalid" = "Invalid password. Please try again."; +"TwoStepAuth.EnterPasswordForgot" = "Forgot password?"; + +"TwoStepAuth.EmailTitle" = "Recovery E-Mail"; +"TwoStepAuth.EmailSkip" = "Skip"; +"TwoStepAuth.EmailSkipAlert" = "No, seriously.\n\nIf you forget your password, you will lose access to your Telegram account. There will be no way to restore it."; +"TwoStepAuth.Email" = "E-Mail"; +"TwoStepAuth.EmailPlaceholder" = "Your E-Mail"; +"TwoStepAuth.EmailHelp" = "Please add your valid e-mail. It is the only way to recover a forgotten password."; +"TwoStepAuth.EmailInvalid" = "Invalid e-mail address. Please try again."; +"TwoStepAuth.EmailSent" = "We have sent you an e-mail to confirm your address."; +"TwoStepAuth.PasswordSet" = "Your password for Two-Step Verification is now active."; +"TwoStepAuth.PasswordRemoveConfirmation" = "Are you sure you want to disable your password?"; +"TwoStepAuth.EmailCodeExpired" = "This confirmation code has expired. Please try again."; + +"TwoStepAuth.RecoveryUnavailable" = "Since you haven't provided a recovery e-mail when setting up your password, your remaining options are either to remember your password or to reset your account."; +"TwoStepAuth.RecoveryFailed" = "Your remaining options are either to remember your password or to reset your account."; +"TwoStepAuth.ResetAccountHelp" = "You will lose all your chats and messages, along with any media and files you've shared, if you proceed with resetting your account."; +"TwoStepAuth.ResetAccountConfirmation" = "You will lose all your chats and messages, along with any media and files you've shared, if you proceed with resetting your account."; + +"TwoStepAuth.RecoveryTitle" = "E-Mail Code"; +"TwoStepAuth.RecoveryCode" = "Code"; +"TwoStepAuth.RecoveryCodeHelp" = "Please check your e-mail and enter the 6-digit code we've sent there to deactivate your cloud password."; +"TwoStepAuth.RecoveryCodeInvalid" = "Invalid code. Please try again."; +"TwoStepAuth.RecoveryCodeExpired" = "We have sent you a new 6-digit code."; +"TwoStepAuth.RecoveryEmailUnavailable" = "Having trouble accessing your e-mail %@?"; + +"TwoStepAuth.FloodError" = "Limit exceeded. Please try again later."; + +"Conversation.FilePhotoOrVideo" = "Photo or Video"; +"Conversation.FileICloudDrive" = "iCloud Drive"; +"Conversation.FileDropbox" = "Dropbox"; + +"Conversation.FileOpenIn" = "Open in..."; +"Conversation.FileHowToText" = "To share files of any type, open them on your %@ (e.g. in your browser), tap **Open in...** or the action button and choose Telegram."; + +"Map.LocationTitle" = "Location"; +"Map.OpenInMaps" = "Open in Maps"; +"Map.OpenInHereMaps" = "Open in HERE Maps"; +"Map.OpenInYandexMaps" = "Open in Yandex Maps"; +"Map.OpenInYandexNavigator" = "Open in Yandex Navigator"; +"Map.OpenIn" = "Open In"; + +"Map.SendThisLocation" = "Send This Location"; +"Map.SendMyCurrentLocation" = "Send My Current Location"; +"Map.Locating" = "Locating..."; +"Map.ChooseAPlace" = "Or choose a place"; +"Map.AccurateTo" = "Accurate to %@"; +"Map.Search" = "Search places nearby"; +"Map.ShowPlaces" = "Show places"; +"Map.LoadError" = "An error occurred. Please try again."; +"Map.LocatingError" = "Failed to locate"; +"Map.Unknown" = "Unknown location"; + +"Map.DistanceAway" = "%@ away"; +"Map.ETAMinutes_0" = "%@ min"; +"Map.ETAMinutes_1" = "%@ min"; +"Map.ETAMinutes_2" = "%@ min"; +"Map.ETAMinutes_3_10" = "%@ min"; +"Map.ETAMinutes_any" = "%@ min"; +"Map.ETAMinutes_many" = "%@ min"; +"Map.ETAMinutes_0" = "%@ min"; +"Map.ETAHours_1" = "%@ h"; +"Map.ETAHours_2" = "%@ h"; +"Map.ETAHours_3_10" = "%@ h"; +"Map.ETAHours_any" = "%@ h"; +"Map.ETAHours_many" = "%@ h"; + +"ChangePhone.ErrorOccupied" = "The number %@ is already connected to a Telegram account. Please delete that account before migrating to the new number."; + +"AccessDenied.LocationTracking" = "Telegram needs access to your location to show you on the map.\n\nPlease go to Settings > Privacy > Location Services and set it to ON."; + +"PrivacySettings.AuthSessions" = "Active Sessions"; +"AuthSessions.Title" = "Active Sessions"; +"AuthSessions.CurrentSession" = "CURRENT SESSION"; +"AuthSessions.TerminateOtherSessions" = "Terminate all other sessions"; +"AuthSessions.TerminateOtherSessionsHelp" = "Logs out all devices except for this one."; +"AuthSessions.TerminateSession" = "Terminate session"; +"AuthSessions.OtherSessions" = "ACTIVE SESSIONS"; +"AuthSessions.EmptyTitle" = "No other sessions"; +"AuthSessions.EmptyText" = "You can log in to Telegram from other mobile, tablet and desktop devices, using the same phone number. All your data will be instantly synchronized."; +"AuthSessions.AppUnofficial" = "(ID: %@)"; + +"WebPreview.GettingLinkInfo" = "Getting Link Info..."; + +"Preview.OpenInInstagram" = "Open in Instagram"; + +"MediaPicker.AddCaption" = "Add a caption..."; + +"GroupInfo.InviteByLink" = "Invite to Group via Link"; + +"GroupInfo.InviteLink.Title" = "Invite Link"; +"GroupInfo.InviteLink.LinkSection" = "LINK"; +"GroupInfo.InviteLink.Help" = "Anyone who has Telegram installed will be able to join your group by following this link."; +"GroupInfo.InviteLink.CopyLink" = "Copy Link"; +"GroupInfo.InviteLink.RevokeLink" = "Revoke Link"; +"GroupInfo.InviteLink.ShareLink" = "Share Link"; +"GroupInfo.InviteLink.RevokeAlert.Text" = "Are you sure you want to revoke this link? Once you do, no one will be able to join the group using it."; +"GroupInfo.InviteLink.RevokeAlert.Revoke" = "Revoke"; +"GroupInfo.InviteLink.RevokeAlert.Success" = "The previous invite link is now inactive. A new invite link has just been generated."; +"GroupInfo.InviteLink.CopyAlert.Success" = "Link copied to clipboard."; + +"UserInfo.ShareMyContactInfo" = "Share My Contact Info"; + +"GroupInfo.InvitationLinkAcceptChannel" = "Do you want to join the channel \"%@\"?"; +"GroupInfo.InvitationLinkDoesNotExist" = "Sorry, this group does not seem to exist."; +"GroupInfo.InvitationLinkGroupFull" = "Sorry, this group is already full."; + +"Core.ServiceUserStatus" = "Service Notifications"; + +"Notification.JoinedGroupByLink" = "%@ joined the group via invite link"; + +"ChatSettings.Other" = "OTHER"; +"ChatSettings.Stickers" = "Stickers"; + +"StickerPacksSettings.Title" = "Stickers"; +"StickerPacksSettings.ShowStickersButton" = "Show Stickers Tab"; +"StickerPacksSettings.ShowStickersButtonHelp" = "A sticker icon will appear in the input field."; + +"StickerPacksSettings.StickerPacksSection" = "STICKER SETS"; +"StickerPacksSettings.ManagingHelp" = "Artists are welcome to add their own sticker sets using our @stickers bot.\n\nTap on a sticker to view and add the whole set."; + +"StickerPack.BuiltinPackName" = "Great Minds"; +"StickerPack.StickerCount_1" = "1 sticker"; +"StickerPack.StickerCount_2" = "2 stickers"; +"StickerPack.StickerCount_3_10" = "%@ stickers"; +"StickerPack.StickerCount_any" = "%@ stickers"; +"StickerPack.StickerCount_many" = "%@ stickers"; +"StickerPack.StickerCount_0" = "%@ stickers"; + +"StickerPack.AddStickerCount_1" = "Add 1 Sticker"; +"StickerPack.AddStickerCount_2" = "Add 2 Stickers"; +"StickerPack.AddStickerCount_3_10" = "Add %@ Stickers"; +"StickerPack.AddStickerCount_any" = "Add %@ Stickers"; +"StickerPack.AddStickerCount_many" = "Add %@ Stickers"; +"StickerPack.AddStickerCount_0" = "Add %@ Stickers"; + +"Conversation.ContextMenuStickerPackAdd" = "Add Stickers"; +"Conversation.ContextMenuStickerPackInfo" = "Info"; + +"MediaPicker.Nof" = "%@ of"; + +"UserInfo.ShareBot" = "Share"; +"UserInfo.InviteBotToGroup" = "Add To Group"; +"Profile.BotInfo" = "about"; + +"Target.SelectGroup" = "Choose Group"; +"Target.InviteToGroupConfirmation" = "Add the bot to \"%@\"?"; +"Target.InviteToGroupErrorAlreadyInvited" = "The bot is already a member of the group."; +"Bot.GenericBotStatus" = "bot"; +"Bot.DescriptionTitle" = "What can this bot do?"; +"Bot.GroupStatusReadsHistory" = "has access to messages"; +"Bot.GroupStatusDoesNotReadHistory" = "has no access to messages"; +"Bot.Start" = "Start"; +"UserInfo.BotSettings" = "Settings"; +"UserInfo.BotHelp" = "Help"; + +"Contacts.SearchLabel" = "Search for contacts or usernames"; +"ChatSearch.SearchPlaceholder" = "Search"; + +"WatchRemote.NotificationText" = "Open this notification on your phone to view the message from your Apple Watch"; +"WatchRemote.AlertTitle" = "Message from your Apple Watch"; +"WatchRemote.AlertText" = "Open the message here?"; +"WatchRemote.AlertOpen" = "Open"; + +"Conversation.SearchPlaceholder" = "Search this chat"; +"Conversation.SearchNoResults" = "No Results"; + +"GroupInfo.AddUserLeftError" = "Sorry, if a person left a group, only a mutual contact can bring them back (they need to have your phone number, and you need theirs)."; + +"DialogList.SearchSectionRecent" = "Recent"; + +"DialogList.DeleteBotConfirmation" = "Delete"; +"DialogList.DeleteBotConversationConfirmation" = "Delete and Stop"; +"Bot.Stop" = "Stop Bot"; +"Bot.Unblock" = "Restart Bot"; + +"Login.PhoneNumberHelp" = "Help"; +"Login.EmailPhoneSubject" = "Invalid number %@"; +"Login.EmailPhoneBody" = "I'm trying to use my mobile phone number: %@\nBut Telegram says it's invalid. Please help.\nAdditional Info: %@, %@."; + +"SharedMedia.TitleLink" = "Shared Links"; +"SharedMedia.EmptyLinksText" = "All links shared in this chat will appear here."; + +"SharedMedia.Link_1" = "1 link"; +"SharedMedia.Link_2" = "2 links"; +"SharedMedia.Link_3_10" = "%@ links"; +"SharedMedia.Link_any" = "%@ links"; +"SharedMedia.Link_many" = "%@ links"; +"SharedMedia.Link_0" = "%@ links"; + +"Compose.NewChannel" = "New Channel"; +"GroupInfo.ChannelListNamePlaceholder" = "Channel Name"; + +"Channel.MessagePhotoUpdated" = "Channel photo updated"; +"Channel.MessagePhotoRemoved" = "Channel photo removed"; +"Channel.MessageTitleUpdated" = "Channel renamed to \"%@\""; +"Channel.TitleInfo" = "Channel Info"; + +"Channel.UpdatePhotoItem" = "Set Channel Photo"; + +"Channel.AboutItem" = "about"; +"Channel.LinkItem" = "share link"; +"Channel.Edit.AboutItem" = "Description"; +"Channel.Edit.LinkItem" = "Link"; + +"Channel.Username.Title" = "Link"; +"Channel.Username.Help" = "You can choose a channel name on **Telegram**. If you do, other people will be able to find your channel by this name.\n\nYou can use **a-z**, **0-9** and underscores. Minimum length is **5** characters."; +"Channel.Username.LinkHint" = "This link opens your channel in Telegram:[\nhttps://t.me/%@]"; +"Channel.Username.InvalidTooShort" = "Channel names must have at least 5 characters."; +"Channel.Username.InvalidStartsWithNumber" = "Channel names can't start with a number."; +"Channel.Username.InvalidCharacters" = "Sorry, this name is invalid."; +"Channel.Username.InvalidTaken" = "Sorry, this name is already taken."; +"Channel.Username.CheckingUsername" = "Checking name..."; +"Channel.Username.UsernameIsAvailable" = "%@ is available."; + +"Channel.LeaveChannel" = "Leave Channel"; + +"Channel.About.Title" = "Description"; + +"Channel.About.Placeholder" = "Description"; +"Channel.About.Help" = "You can provide an optional description for your channel."; +"Group.About.Help" = "You can provide an optional description for your group."; + +"Channel.Status" = "channel"; +"Group.Status" = "group"; + +"Compose.NewChannel.Members" = "MEMBERS"; + +"ChannelInfo.ConfirmLeave" = "Leave Channel"; +"Channel.JoinChannel" = "Join"; +"Forward.ChannelReadOnly" = "Sorry, you can't post to this channel."; + +"Channel.ErrorAccessDenied" = "Sorry, this channel is private."; +"Conversation.InputTextBroadcastPlaceholder" = "Broadcast"; + +"Channel.NotificationLoading" = "Loading..."; + +"Compose.ChannelTokenListPlaceholder" = "Search for contacts or usernames"; +"Compose.GroupTokenListPlaceholder" = "Search for contacts or usernames"; + +"Compose.ChannelMembers" = "Members"; + +"Channel.Setup.TypeHeader" = "CHANNEL TYPE"; +"Channel.Setup.TypePrivate" = "Private"; +"Channel.Setup.TypePublic" = "Public"; +"Channel.Setup.TypePublicHelp" = "Public channels can be found in search, anyone can join them."; +"Channel.Setup.TypePrivateHelp" = "Private channels can only be joined via an invite link."; + +"Channel.Setup.Title" = "Channel"; + +"Channel.Username.CreatePublicLinkHelp" = "People can share this link with others and find your channel using Telegram search."; +"Channel.Username.CreatePrivateLinkHelp" = "People can join your channel by following this link. You can revoke the link at any time."; + +"Channel.Setup.PublicNoLink" = "Please choose a link for your public channel, so that people can find it in search and share with others.\n\nIf you're not interested, we suggest creating a private channel instead."; + +"Channel.Edit.PrivatePublicLinkAlert" = "Please note that if you choose a public link for your channel, anyone will be able to find it in search and join.\n\nDo not create this link if you want your channel to stay private."; + +"Channel.Info.Description" = "description"; + +"Channel.Info.Management" = "Admins"; +"Channel.Info.Banned" = "Blacklist"; +"Channel.Info.Members" = "Members"; + +"Channel.Members.AddMembers" = "Add Members"; +"Channel.Members.AddMembersHelp" = "Only channel admins can see this list."; +"Channel.Members.Title" = "Members"; +"Channel.BlackList.Title" = "Blacklist"; +"Channel.Management.Title" = "Admins"; +"Channel.Management.LabelCreator" = "Creator"; +"Channel.Management.LabelEditor" = "Admin"; + +"Channel.Management.AddModerator" = "Add Admin"; +"Channel.Management.AddModeratorHelp" = "You can add admins to help you manage your channel."; + +"Channel.Members.InviteLink" = "Invite via Link"; + +"Channel.Management.ErrorNotMember" = "%@ hasn't joined the channel yet. Do you want to invite them?"; + +"Channel.Moderator.AccessLevelRevoke" = "Dismiss Admin"; + +"Channel.Moderator.Title" = "Admin"; + +"Notification.ChannelInviter" = "%@ invited you to this channel"; +"Notification.ChannelInviterSelf" = "You joined this channel"; + +"Notification.GroupInviter" = "%@ invited you to this group"; +"Notification.GroupInviterSelf" = "You joined this group"; + +"ChannelInfo.DeleteChannel" = "Delete Channel"; +"ChannelInfo.DeleteChannelConfirmation" = "Wait! Deleting this channel will remove all members and all messages will be lost. Delete the channel anyway?"; + +"ChannelInfo.ChannelForbidden" = "Sorry, the channel \"%@\" is no longer accessible."; +"ChannelInfo.AddParticipantConfirmation" = "Add %@ to the channel?"; + +"PhotoEditor.FadeTool" = "Fade"; +"PhotoEditor.TintTool" = "Tint"; +"PhotoEditor.ShadowsTint" = "Shadows"; +"PhotoEditor.HighlightsTint" = "Highlights"; +"PhotoEditor.CurvesTool" = "Curves"; +"PhotoEditor.CurvesAll" = "All"; +"PhotoEditor.CurvesRed" = "Red"; +"PhotoEditor.CurvesGreen" = "Green"; +"PhotoEditor.CurvesBlue" = "Blue"; + +"Channel.ErrorAddBlocked" = "Sorry, you can't add this user to channels."; +"Channel.ErrorAddTooMuch" = "Sorry, you can only add the first 200 members to a channel. Note that an unlimited number of people may join via the channel's link."; + +"ChannelIntro.Title" = "What is a Channel?"; +"ChannelIntro.Text" = "Channels are a new tool for\nbroadcasting your messages\nto large audiences."; +"ChannelIntro.CreateChannel" = "Create Channel"; + +"ShareMenu.Send" = "Send"; + +"Conversation.ReportSpam" = "Report Spam"; +"Conversation.ReportSpamConfirmation" = "Are you sure you want to report spam from this user?"; +"SharedMedia.EmptyMusicText" = "All music shared in this chat will appear here."; + +"ChatSettings.AutoPlayAnimations" = "Autoplay GIFs"; + +"GroupInfo.ChatAdmins" = "Add Admins"; + +"ChatAdmins.Title" = "Chat Admins"; +"ChatAdmins.AllMembersAreAdmins" = "All Members Are Admins"; +"ChatAdmins.AllMembersAreAdminsOnHelp" = "All members can add new members, edit name and photo of the group."; +"ChatAdmins.AllMembersAreAdminsOffHelp" = "Only admins can add and remove members, edit name and photo of the group."; +"ChatAdmins.AdminLabel" = "admin"; + +"Group.MessagePhotoUpdated" = "Group photo updated"; +"Group.MessagePhotoRemoved" = "Group photo removed"; + +"Group.UpgradeNoticeHeader" = "MEMBERS LIMIT REACHED"; + +"Group.UpgradeNoticeText1" = "To go over the limit and get additional features, upgrade to a supergroup:"; +"Group.UpgradeNoticeText2" = "• Supergroups can get up to {supergroup_member_limit} members\n• New members see the entire chat history\n• Admins delete messages for everyone\n• Notifications are muted by default"; +"GroupInfo.UpgradeButton" = "Upgrade to supergroup"; +"Group.UpgradeConfirmation" = "Warning: this action is irreversible. It is not possible to downgrade a supergroup to a regular group."; + +"Notification.GroupActivated" = "Group deactivated"; +"Notification.ChannelMigratedFrom" = "This group was upgraded to a supergroup"; + +"GroupInfo.DeactivatedStatus" = "Group Deactivated"; + +"Notification.RenamedGroup" = "Group renamed"; + +"Group.ErrorAddTooMuchBots" = "Sorry, you've reached the maximum number of bots for this group."; +"Group.ErrorAddTooMuchAdmins" = "Sorry, you've reached the maximum number of admins for this group."; +"Group.ErrorAddBlocked" = "Sorry, you can't add this user to groups."; +"Group.ErrorNotMutualContact" = "Sorry, you can only add mutual contacts to groups at the moment."; + +"Conversation.SendMessageErrorFlood" = "Sorry, you can only send messages to mutual contacts at the moment."; +"Generic.ErrorMoreInfo" = "More Info"; + +"ChannelInfo.DeleteGroup" = "Delete Group"; +"ChannelInfo.DeleteGroupConfirmation" = "Wait! Deleting this group will remove all members and all messages will be lost. Delete the group anyway?"; + +"ReportPeer.Report" = "Report"; + +"ReportPeer.ReasonSpam" = "Spam"; +"ReportPeer.ReasonViolence" = "Violence"; +"ReportPeer.ReasonPornography" = "Pornography"; +"ReportPeer.ReasonOther" = "Other"; + +"ReportPeer.AlertSuccess" = "Thank you!\nYour report will be reviewed by our team very soon."; + +"Login.TermsOfServiceLabel" = "By signing up,\nyou agree to the [Terms of Service]."; +"Login.TermsOfServiceHeader" = "Terms of Service"; + +"ReportPeer.ReasonOther.Placeholder" = "Description"; +"ReportPeer.ReasonOther.Title" = "Report"; +"ReportPeer.ReasonOther.Send" = "Send"; + +"Group.Management.AddModeratorHelp" = "You can add admins to help you manage your group."; + +"Watch.AppName" = "Telegram"; +"Watch.Compose.AddContact" = "Choose Contact"; +"Watch.Compose.CreateMessage" = "Create Message"; +"Watch.Compose.CurrentLocation" = "Current Location"; +"Watch.Compose.Send" = "Send"; +"Watch.Contacts.NoResults" = "No matching\ncontacts found"; +"Watch.ChatList.NoConversationsTitle" = "No Conversations"; +"Watch.ChatList.NoConversationsText" = "To start messaging,\npress firmly, then tap\nNew Message"; +"Watch.ChatList.Compose" = "New Message"; + +"Watch.Conversation.Reply" = "Reply"; +"Watch.Conversation.Unblock" = "Unblock"; +"Watch.Conversation.UserInfo" = "Info"; +"Watch.Conversation.GroupInfo" = "Group Info"; +"Watch.Bot.Restart" = "Restart"; + +"Watch.UserInfo.Title" = "Info"; +"Watch.UserInfo.Service" = "service notifications"; + +"Watch.UserInfo.Block" = "Block"; +"Watch.UserInfo.Unblock" = "Unblock"; +"Watch.UserInfo.Mute_1" = "Mute for 1 hour"; +"Watch.UserInfo.Mute_2" = "Mute for 2 hours"; +"Watch.UserInfo.Mute_3_10" = "Mute for %@ hours"; +"Watch.UserInfo.Mute_any" = "Mute for %@ hours"; +"Watch.UserInfo.Mute_many" = "Mute for %@ hours"; +"Watch.UserInfo.Mute_0" = "Mute for %@ hours"; +"Watch.UserInfo.MuteTitle" = "Mute"; +"Watch.UserInfo.Unmute" = "Unmute"; + +"Watch.GroupInfo.Title" = "Group Info"; +"Watch.ChannelInfo.Title" = "Channel Info"; + +"Watch.Message.ForwardedFrom" = "Forwarded from"; + +"Watch.Notification.Joined" = "Joined Telegram"; + +"Watch.MessageView.Title" = "Message"; +"Watch.MessageView.Forward" = "Forward"; +"Watch.MessageView.Reply" = "Reply"; +"Watch.MessageView.ViewOnPhone" = "View On Phone"; + +"Watch.PhotoView.Title" = "Photo"; + +"Watch.Stickers.Recents" = "Recents"; +"Watch.Stickers.RecentPlaceholder" = "Your most frequently used stickers will appear here"; +"Watch.Stickers.StickerPacks" = "Sticker Sets"; + +"Watch.Location.Current" = "Current Location"; +"Watch.Location.Access" = "Allow Telegram to access location on your phone"; + +"Watch.AuthRequired" = "Log in to Telegram on your phone to get started"; + +"Watch.NoConnection" = "No Connection"; +"Watch.ConnectionDescription" = "Your Watch needs to be connected for the app to work"; + +"Watch.Time.ShortTodayAt" = "Today %@"; +"Watch.Time.ShortYesterdayAt" = "Yesterday %@"; +"Watch.Time.ShortWeekdayAt" = "%1$@ %2$@"; +"Watch.Time.ShortFullAt" = "%1$@ %2$@"; + +"Watch.LastSeen.JustNow" = "just now"; +"Watch.LastSeen.MinutesAgo_1" = "1 minute ago"; +"Watch.LastSeen.MinutesAgo_2" = "2 minutes ago"; +"Watch.LastSeen.MinutesAgo_3_10" = "%@ minutes ago"; +"Watch.LastSeen.MinutesAgo_any" = "%@ minutes ago"; +"Watch.LastSeen.MinutesAgo_many" = "%@ minutes ago"; +"Watch.LastSeen.MinutesAgo_0" = "%@ minutes ago"; +"Watch.LastSeen.HoursAgo_1" = "1 hour ago"; +"Watch.LastSeen.HoursAgo_2" = "2 hours ago"; +"Watch.LastSeen.HoursAgo_3_10" = "%@ hours ago"; +"Watch.LastSeen.HoursAgo_any" = "%@ hours ago"; +"Watch.LastSeen.HoursAgo_many" = "%@ hours ago"; +"Watch.LastSeen.HoursAgo_0" = "%@ hours ago"; +"Watch.LastSeen.YesterdayAt" = "yesterday at %@"; +"Watch.LastSeen.AtDate" = "%@"; +"Watch.LastSeen.Lately" = "recently"; +"Watch.LastSeen.WithinAWeek" = "within a week"; +"Watch.LastSeen.WithinAMonth" = "within a month"; +"Watch.LastSeen.ALongTimeAgo" = "a long time ago"; + +"Watch.Suggestion.OK" = "OK"; +"Watch.Suggestion.Thanks" = "Thanks!"; +"Watch.Suggestion.WhatsUp" = "What's up?"; +"Watch.Suggestion.TalkLater" = "Talk later?"; +"Watch.Suggestion.CantTalk" = "Can't talk now..."; +"Watch.Suggestion.HoldOn" = "Hold on a sec..."; +"Watch.Suggestion.BRB" = "BRB"; +"Watch.Suggestion.OnMyWay" = "I'm on my way."; +"Cache.Photos" = "Photos"; +"Cache.Videos" = "Videos"; +"Cache.Music" = "Music"; +"Cache.Files" = "Files"; +"Cache.Clear" = "Clear (%@)"; +"Cache.ClearNone" = "Clear"; +"Cache.ClearProgress" = "Please Wait..."; +"Cache.ClearEmpty" = "Empty"; +"Cache.ByPeerHeader" = "CHATS"; +"Cache.Indexing" = "Telegram is calculating current cache size.\nThis can take a few minutes."; + +"ExplicitContent.AlertTitle" = "Sorry"; +"ExplicitContent.AlertChannel" = "You can't access this channel because it violates App Store rules."; + +"StickerSettings.ContextHide" = "Archive"; + +"Conversation.LinkDialogSave" = "Save"; +"Conversation.GifTooltip" = "Tap here to access saved GIFs"; + +"AttachmentMenu.PhotoOrVideo" = "Photo or Video"; +"AttachmentMenu.File" = "File"; + +"AttachmentMenu.SendPhoto_1" = "Send 1 Photo"; +"AttachmentMenu.SendPhoto_2" = "Send 2 Photos"; +"AttachmentMenu.SendPhoto_3_10" = "Send %@ Photos"; +"AttachmentMenu.SendPhoto_any" = "Send %@ Photos"; +"AttachmentMenu.SendPhoto_many" = "Send %@ Photos"; +"AttachmentMenu.SendPhoto_0" = "Send %@ Photos"; + +"AttachmentMenu.SendVideo_1" = "Send 1 Video"; +"AttachmentMenu.SendVideo_2" = "Send 2 Videos"; +"AttachmentMenu.SendVideo_3_10" = "Send %@ Videos"; +"AttachmentMenu.SendVideo_any" = "Send %@ Videos"; +"AttachmentMenu.SendVideo_many" = "Send %@ Videos"; +"AttachmentMenu.SendVideo_0" = "Send %@ Videos"; + +"AttachmentMenu.SendGif_1" = "Send 1 GIF"; +"AttachmentMenu.SendGif_2" = "Send 2 GIFs"; +"AttachmentMenu.SendGif_3_10" = "Send %@ GIFs"; +"AttachmentMenu.SendGif_any" = "Send %@ GIFs"; +"AttachmentMenu.SendGif_many" = "Send %@ GIFs"; +"AttachmentMenu.SendGif_0" = "Send %@ GIFs"; + +"AttachmentMenu.SendItem_1" = "Send 1 Item"; +"AttachmentMenu.SendItem_2" = "Send 2 Items"; +"AttachmentMenu.SendItem_3_10" = "Send %@ Items"; +"AttachmentMenu.SendItem_any" = "Send %@ Items"; +"AttachmentMenu.SendItem_many" = "Send %@ Items"; +"AttachmentMenu.SendItem_0" = "Send %@ Items"; + +"AttachmentMenu.SendAsFile" = "Send as File"; +"AttachmentMenu.SendAsFiles" = "Send as Files"; + +"Conversation.Processing" = "Processing..."; + +"Conversation.MessageViaUser" = "via %@"; + +"CreateGroup.SoftUserLimitAlert" = "You will be able to add more users after you finish creating the group and convert it to a supergroup."; + +"Privacy.GroupsAndChannels" = "Groups"; +"Privacy.GroupsAndChannels.WhoCanAddMe" = "WHO CAN ADD ME TO GROUP CHATS"; +"Privacy.GroupsAndChannels.CustomHelp" = "You can restrict who can add you to groups and channels with granular precision."; +"Privacy.GroupsAndChannels.AlwaysAllow" = "Always Allow"; +"Privacy.GroupsAndChannels.NeverAllow" = "Never Allow"; +"Privacy.GroupsAndChannels.CustomShareHelp" = "These users will or will not be able to add you to groups and channels regardless of the settings above."; + +"Privacy.GroupsAndChannels.AlwaysAllow.Title" = "Always Allow"; +"Privacy.GroupsAndChannels.AlwaysAllow.Placeholder" = "Always allow..."; +"Privacy.GroupsAndChannels.NeverAllow.Title" = "Never Allow"; +"Privacy.GroupsAndChannels.NeverAllow.Placeholder" = "Never allow..."; + +"Privacy.GroupsAndChannels.InviteToGroupError" = "Sorry, you cannot add %@ to groups because of %@'s privacy settings."; +"Privacy.GroupsAndChannels.InviteToChannelError" = "Sorry, you cannot add %@ to channels because of %@'s privacy settings."; +"Privacy.GroupsAndChannels.InviteToChannelMultipleError" = "Sorry, you can't create a group with these users due to their privacy settings."; + +"ChannelMembers.WhoCanAddMembers" = "Who can add members"; +"ChannelMembers.WhoCanAddMembers.AllMembers" = "All Members"; +"ChannelMembers.WhoCanAddMembers.Admins" = "Only Admins"; +"ChannelMembers.WhoCanAddMembersAllHelp" = "Everybody can add new members."; +"ChannelMembers.WhoCanAddMembersAdminsHelp" = "Only admins can add new members."; + +"ChannelMembers.GroupAdminsTitle" = "GROUP ADMINS"; +"ChannelMembers.ChannelAdminsTitle" = "CHANNEL ADMINS"; +"MusicPlayer.VoiceNote" = "Voice Message"; + +"PrivacyLastSeenSettings.WhoCanSeeMyTimestamp" = "WHO CAN SEE MY TIMESTAMP"; + +"PrivacyLastSeenSettings.GroupsAndChannelsHelp" = "Change who can add you to groups and channels."; +"MusicPlayer.VoiceNote" = "Voice Message"; + +"Watch.Microphone.Access" = "Allow Telegram to access the microphone on your phone"; + +"Settings.AppleWatch" = "Apple Watch"; +"AppleWatch.Title" = "Apple Watch"; +"AppleWatch.ReplyPresets" = "REPLY PRESETS"; +"AppleWatch.ReplyPresetsHelp" = "You can select one of these default replies when you compose or reply to a message, or you can change them to anything you like."; + +"KeyCommand.FocusOnInputField" = "Write Message"; +"KeyCommand.Find" = "Search"; +"KeyCommand.ScrollUp" = "Scroll Up"; +"KeyCommand.ScrollDown" = "Scroll Down"; +"KeyCommand.NewMessage" = "New Message"; +"KeyCommand.JumpToPreviousChat" = "Jump to Previous Chat"; +"KeyCommand.JumpToNextChat" = "Jump to Next Chat"; +"KeyCommand.JumpToPreviousUnreadChat" = "Jump to Previous Unread Chat"; +"KeyCommand.JumpToNextUnreadChat" = "Jump to Next Unread Chat"; +"KeyCommand.SendMessage" = "Send Message"; +"KeyCommand.ChatInfo" = "Chat Info"; + +"Conversation.SecretLinkPreviewAlert" = "Would you like to enable extended link previews in Secret Chats? Note that link previews are generated on Telegram servers."; +"Conversation.SecretChatContextBotAlert" = "Please note that inline bots are provided by third-party developers. For the bot to work, the symbols you type after the bot's username are sent to the respective developer."; + +"Map.OpenInWaze" = "Open in Waze"; + +"ShareMenu.CopyShareLink" = "Copy Link"; + +"Channel.SignMessages" = "Sign Messages"; +"Channel.SignMessages.Help" = "Add names of the admins to the messages they post."; + +"Channel.EditMessageErrorGeneric" = "Sorry, you can't edit this message."; + +"Conversation.InputTextSilentBroadcastPlaceholder" = "Silent Broadcast"; +"Conversation.SilentBroadcastTooltipOn" = "Members will be notified when you post"; +"Conversation.SilentBroadcastTooltipOff" = "Members will not be notified when you post"; + +"Settings.About" = "Bio"; +"GroupInfo.LabelAdmin" = "admin"; + +"Conversation.Pin" = "Pin"; +"Conversation.Unpin" = "Unpin"; +"Conversation.Report" = "Report Spam"; +"Conversation.PinnedMessage" = "Pinned Message"; + +"Conversation.Moderate.Delete" = "Delete Message"; +"Conversation.Moderate.Ban" = "Ban User"; +"Conversation.Moderate.Report" = "Report Spam"; +"Conversation.Moderate.DeleteAllMessages" = "Delete All From %@"; + +"Group.Username.InvalidTooShort" = "Group names must have at least 5 characters."; +"Group.Username.InvalidStartsWithNumber" = "Group names can't start with a number."; + +"Notification.PinnedTextMessage" = "%@ pinned \"%@\""; +"Notification.PinnedPhotoMessage" = "%@ pinned a photo"; +"Notification.PinnedVideoMessage" = "%@ pinned a video"; +"Notification.PinnedRoundMessage" = "%@ pinned a video message"; +"Notification.PinnedAudioMessage" = "%@ pinned a voice message"; +"Notification.PinnedDocumentMessage" = "%@ pinned a file"; +"Notification.PinnedAnimationMessage" = "%@ pinned a GIF"; +"Notification.PinnedStickerMessage" = "%@ pinned a sticker"; +"Notification.PinnedLocationMessage" = "%@ pinned a map"; +"Notification.PinnedContactMessage" = "%@ pinned a contact"; +"Notification.PinnedDeletedMessage" = "%@ pinned deleted message"; + +"Message.PinnedTextMessage" = "pinned \"%@\""; +"Message.PinnedPhotoMessage" = "pinned photo"; +"Message.PinnedVideoMessage" = "pinned video"; +"Message.PinnedAudioMessage" = "pinned voice message"; +"Message.PinnedDocumentMessage" = "pinned file"; +"Message.PinnedAnimationMessage" = "pinned GIF"; +"Message.PinnedStickerMessage" = "pinned sticker"; +"Message.PinnedLocationMessage" = "pinned location"; +"Message.PinnedContactMessage" = "pinned contact"; + +"Notification.PinnedMessage" = "pinned message"; + +"GroupInfo.ConvertToSupergroup" = "Convert to Supergroup"; + +"ConvertToSupergroup.Title" = "Supergroup"; +"ConvertToSupergroup.HelpTitle" = "**In supergroups:**"; +"ConvertToSupergroup.HelpText" = "• New members can see the full message history\n• Deleted messages will disappear for all members\n• Admins can pin important messages\n• Creator can set a public link for the group"; + +"ConvertToSupergroup.Note" = "**Note**: this action can't be undone."; + +"GroupInfo.GroupType" = "Group Type"; + +"Group.Setup.TypeHeader" = "GROUP TYPE"; +"Group.Setup.TypePublicHelp" = "Public groups can be found in search, chat history is available to everyone and anyone can join."; +"Group.Setup.TypePrivateHelp" = "Private groups can only be joined if you were invited or have an invite link."; + +"Group.Username.CreatePublicLinkHelp" = "People can share this link with others and find your group using Telegram search."; +"Group.Username.CreatePrivateLinkHelp" = "People can join your group by following this link. You can revoke the link at any time."; + +"Conversation.PinMessageAlertGroup" = "Pin this message and notify all members of the group?"; +"Conversation.PinMessageAlert.OnlyPin" = "Only Pin"; + +"Conversation.UnpinMessageAlert" = "Would you like to unpin this message?"; + +"Settings.About.Title" = "Bio"; +"Settings.About.Help" = "Any details such as age, occupation or city.\nExample: 23 y.o. designer from San Francisco."; + +"Profile.About" = "bio"; + +"Conversation.StatusKickedFromChannel" = "you were removed from the channel"; + +"Generic.OpenHiddenLinkAlert" = "Open %@?"; + +"Resolve.ErrorNotFound" = "Sorry, this user doesn't seem to exist."; + +"StickerPack.Share" = "Share"; +"StickerPack.Send" = "Send Sticker"; + +"StickerPack.RemoveStickerCount_1" = "Remove 1 Sticker"; +"StickerPack.RemoveStickerCount_2" = "Remove 2 Stickers"; +"StickerPack.RemoveStickerCount_3_10" = "Remove %@ Stickers"; +"StickerPack.RemoveStickerCount_any" = "Remove %@ Stickers"; +"StickerPack.RemoveStickerCount_many" = "Remove %@ Stickers"; +"StickerPack.RemoveStickerCount_0" = "Remove %@ Stickers"; + +"StickerPack.HideStickers" = "Hide Stickers"; +"StickerPack.ShowStickers" = "Show Stickers"; + +"ShareMenu.ShareTo" = "Share to"; +"ShareMenu.SelectChats" = "Select chats"; +"ShareMenu.Comment" = "Add a comment..."; + +"MediaPicker.Videos" = "Videos"; + +"Coub.TapForSound" = "Tap for sound"; + +"Preview.SaveGif" = "Save GIF"; +"Preview.DeleteGif" = "Delete GIF"; +"Preview.CopyAddress" = "Copy Address"; + +"Conversation.ShareBotLocationConfirmationTitle" = "Share Your Location?"; +"Conversation.ShareBotLocationConfirmation" = "This will send your current location to the bot."; + +"Conversation.ShareBotContactConfirmationTitle" = "Share Your Phone Number?"; +"Conversation.ShareBotContactConfirmation" = "The bot will know your phone number. This can be useful for integration with other services."; + +"Conversation.ShareInlineBotLocationConfirmation" = "This bot would like to know your location each time you send it a request. This can be used to provide location-specific results."; + +"StickerPack.ErrorNotFound" = "Sorry, this sticker set doesn't seem to exist."; + +"Camera.TapAndHoldForVideo" = "Tap and hold for video"; + +"DialogList.RecentTitlePeople" = "People"; + +"Conversation.MessageEditedLabel" = "edited"; +"Conversation.EditingMessagePanelTitle" = "Edit Message"; + +"DialogList.Draft" = "Draft"; +"Embed.PlayingInPIP" = "This video is playing in Picture in Picture"; + +"StickerPacksSettings.FeaturedPacks" = "Trending Stickers"; +"FeaturedStickerPacks.Title" = "Trending Stickers"; + +"Invitation.JoinGroup" = "Join Group"; +"Invitation.Members_1" = "1 member:"; +"Invitation.Members_2" = "2 members:"; +"Invitation.Members_3_10" = "%@ members:"; +"Invitation.Members_any" = "%@ members:"; +"Invitation.Members_many" = "%@ members:"; +"Invitation.Members_0" = "%@ members:"; + +"StickerPacksSettings.ArchivedPacks" = "Archived Stickers"; +"StickerPacksSettings.ArchivedPacks.Info" = "You can have up to 200 sticker sets installed.\nUnused stickers are archived when you add more."; + +"Conversation.CloudStorageInfo.Title" = "Your Cloud Storage"; +"Conversation.ClousStorageInfo.Description1" = "• Forward messages here to save them"; +"Conversation.ClousStorageInfo.Description2" = "• Send media and files to store them"; +"Conversation.ClousStorageInfo.Description3" = "• Access this chat from any device"; +"Conversation.ClousStorageInfo.Description4" = "• Use search to quickly find things"; + +"Conversation.CloudStorage.ChatStatus" = "chat with yourself"; + +"ArchivedPacksAlert.Title" = "Some of your older sticker sets have been archived. You can reactivate them in the Sticker Settings."; + +"StickerSettings.ContextInfo" = "If you archive a sticker set, you can quickly restore it later from the Archived Stickers section."; + +"Contacts.TopSection" = "CONTACTS"; + +"Login.ResetAccountProtected.Title" = "Reset Account"; +"Login.ResetAccountProtected.Text" = "Since the account %@ is active and protected by a password, we will delete it in 1 week for security purposes.\n\nYou can cancel this process at any time."; +"Login.ResetAccountProtected.TimerTitle" = "You'll be able to reset your account in:"; +"Login.ResetAccountProtected.Reset" = "Reset"; +"Login.ResetAccountProtected.LimitExceeded" = "Your recent attempts to reset this account have been cancelled by its active user. Please try again in 7 days."; + +"Login.CodeSentCall" = "We are calling your phone to dictate a code."; + +"Login.WillSendSms" = "Telegram will send you an SMS in %@"; +"Login.SmsRequestState2" = "Requesting an SMS from Telegram..."; +"Login.SmsRequestState3" = "Telegram sent you an SMS\n[Didn't get the code?]"; + +"CancelResetAccount.Title" = "Cancel Account Reset"; +"CancelResetAccount.TextSMS" = "Somebody with access to your phone number %@ has requested to delete your Telegram account and reset your 2-Step Verification password.\n\nIf it wasn't you, please enter the code we've just sent you via SMS to your number."; + +"CancelResetAccount.Success" = "The deletion process was cancelled for your account %@."; +"MediaPicker.MomentsDateRangeSameMonthYearFormat" = "{month} {day1} – {day2}, {year}"; + +"Paint.Clear" = "Clear All"; +"Paint.ClearConfirm" = "Clear Painting"; +"Paint.Delete" = "Delete"; +"Paint.Edit" = "Edit"; +"Paint.Duplicate" = "Duplicate"; +"Paint.Stickers" = "Stickers"; +"Paint.RecentStickers" = "Recent"; +"Paint.Masks" = "Masks"; + +"Paint.Outlined" = "Outlined"; +"Paint.Regular" = "Regular"; + +"MediaPicker.VideoMuteDescription" = "Sound is now muted, so the video will autoplay and loop like a GIF."; + + +"Group.Username.RemoveExistingUsernamesInfo" = "Sorry, you have reserved too many public usernames. You can revoke the link from one of your older groups or channels, or create a private entity instead."; + +"ServiceMessage.GameScoreExtended_1" = "{name} scored %@ in {game}"; +"ServiceMessage.GameScoreExtended_2" = "{name} scored %@ in {game}"; +"ServiceMessage.GameScoreExtended_3_10" = "{name} scored %@ in {game}"; +"ServiceMessage.GameScoreExtended_any" = "{name} scored %@ in {game}"; +"ServiceMessage.GameScoreExtended_many" = "{name} scored %@ in {game}"; +"ServiceMessage.GameScoreExtended_0" = "{name} scored %@ in {game}"; + +"ServiceMessage.GameScoreSelfExtended_1" = "You scored %@ in {game}"; +"ServiceMessage.GameScoreSelfExtended_2" = "You scored %@ in {game}"; +"ServiceMessage.GameScoreSelfExtended_3_10" = "You scored %@ in {game}"; +"ServiceMessage.GameScoreSelfExtended_any" = "You scored %@ in {game}"; +"ServiceMessage.GameScoreSelfExtended_many" = "You scored %@ in {game}"; +"ServiceMessage.GameScoreSelfExtended_0" = "You scored %@ in {game}"; + +"ServiceMessage.GameScoreSimple_1" = "{name} scored %@"; +"ServiceMessage.GameScoreSimple_2" = "{name} scored %@"; +"ServiceMessage.GameScoreSimple_3_10" = "{name} scored %@"; +"ServiceMessage.GameScoreSimple_any" = "{name} scored %@"; +"ServiceMessage.GameScoreSimple_many" = "{name} scored %@"; +"ServiceMessage.GameScoreSimple_0" = "{name} scored %@"; + +"ServiceMessage.GameScoreSelfSimple_1" = "You scored %@"; +"ServiceMessage.GameScoreSelfSimple_2" = "You scored %@"; +"ServiceMessage.GameScoreSelfSimple_3_10" = "You scored %@"; +"ServiceMessage.GameScoreSelfSimple_any" = "You scored %@"; +"ServiceMessage.GameScoreSelfSimple_many" = "You scored %@"; +"ServiceMessage.GameScoreSelfSimple_0" = "You scored %@"; + +"Notification.GameScoreExtended_1" = "scored %@ in {game}"; +"Notification.GameScoreExtended_2" = "scored %@ in {game}"; +"Notification.GameScoreExtended_3_10" = "scored %@ in {game}"; +"Notification.GameScoreExtended_any" = "scored %@ in {game}"; +"Notification.GameScoreExtended_many" = "scored %@ in {game}"; +"Notification.GameScoreExtended_0" = "scored %@ in {game}"; + +"Notification.GameScoreSelfExtended_1" = "scored %@ in {game}"; +"Notification.GameScoreSelfExtended_2" = "scored %@ in {game}"; +"Notification.GameScoreSelfExtended_3_10" = "scored %@ in {game}"; +"Notification.GameScoreSelfExtended_any" = "scored %@ in {game}"; +"Notification.GameScoreSelfExtended_many" = "scored %@ in {game}"; +"Notification.GameScoreSelfExtended_0" = "scored %@ in {game}"; + +"Notification.GameScoreSimple_1" = "scored %@"; +"Notification.GameScoreSimple_2" = "scored %@"; +"Notification.GameScoreSimple_3_10" = "scored %@"; +"Notification.GameScoreSimple_any" = "scored %@"; +"Notification.GameScoreSimple_many" = "scored %@"; +"Notification.GameScoreSimple_0" = "scored %@"; + +"Notification.GameScoreSelfSimple_1" = "scored %@"; +"Notification.GameScoreSelfSimple_2" = "scored %@"; +"Notification.GameScoreSelfSimple_3_10" = "scored %@"; +"Notification.GameScoreSelfSimple_any" = "scored %@"; +"Notification.GameScoreSelfSimple_many" = "scored %@"; +"Notification.GameScoreSelfSimple_0" = "scored %@"; + +"Stickers.Install" = "ADD"; + +"MaskStickerSettings.Title" = "Masks"; +"MaskStickerSettings.Info" = "You can add masks to photos and videos you send. To do this, open the photo editor before sending a photo or video."; + +"StickerPack.Add" = "Add"; +"StickerPack.AddMaskCount_1" = "Add 1 Mask"; +"StickerPack.AddMaskCount_2" = "Add 2 Masks"; +"StickerPack.AddMaskCount_3_10" = "Add %@ Masks"; +"StickerPack.AddMaskCount_any" = "Add %@ Masks"; +"StickerPack.AddMaskCount_many" = "Add %@ Masks"; +"StickerPack.AddMaskCount_0" = "Add %@ Masks"; + +"StickerPack.RemoveMaskCount_1" = "Remove 1 Mask"; +"StickerPack.RemoveMaskCount_2" = "Remove 2 Masks"; +"StickerPack.RemoveMaskCount_3_10" = "Remove %@ Masks"; +"StickerPack.RemoveMaskCount_any" = "Remove %@ Masks"; +"StickerPack.RemoveMaskCount_many" = "Remove %@ Masks"; +"StickerPack.RemoveMaskCount_0" = "Remove %@ Masks"; + +"Conversation.BotInteractiveUrlAlert" = "Allow %@ to pass your Telegram name and id (not your phone number) to pages you open with this bot?"; +"StickerPacksSettings.ArchivedMasks" = "Archived Masks"; +"StickerSettings.MaskContextInfo" = "If you archive a set of masks, you can quickly restore it later from the Archived Masks section."; +"StickerPacksSettings.ArchivedMasks.Info" = "You can have up to 200 sets of masks. +Unused sets are archived when you add more."; + +"CloudStorage.Title" = "Cloud Storage"; + +"Widget.AuthRequired" = "Log in to Telegram"; +"Widget.NoUsers" = "Start messaging to see your friends here"; + +"ShareMenu.CopyShareLinkGame" = "Copy link to game"; + +"Message.PinnedGame" = "pinned a game"; + +"Target.ShareGameConfirmationPrivate" = "Share the game with %@?"; +"Target.ShareGameConfirmationGroup" = "Share the game with \"%@\"?"; + +"Activity.PlayingGame" = "playing game"; +"Activity.UploadingVideoMessage" = "sending video"; + +"DialogList.SinglePlayingGameSuffix" = "%@ is playing a game"; + +"UserInfo.GroupsInCommon" = "Groups In Common"; +"Conversation.InstantPagePreview" = "INSTANT VIEW"; + +"StickerPack.ViewPack" = "View Sticker Set"; +"InstantPage.AuthorAndDateTitle" = "By %1$@ • %2$@"; +"InstantPage.FeedbackButton" = "Leave feedback about this preview"; +"Conversation.JumpToDate" = "Jump To Date"; +"Conversation.AddToReadingList" = "Add to Reading List"; + +"AccessDenied.CallMicrophone" = "Telegram needs access to your microphone for voice calls.\n\nPlease go to Settings > Privacy > Microphone and set Telegram to ON."; + +"Call.EncryptionKey.Title" = "Encryption Key"; + +"Application.Name" = "Telegram"; +"DialogList.Pin" = "Pin"; +"DialogList.Unpin" = "Unpin"; +"DialogList.PinLimitError" = "Sorry, you can pin no more than %@ chats to the top."; + +"Conversation.DeleteMessagesForMe" = "Delete for me"; +"Conversation.DeleteMessagesFor" = "Delete for me and %@"; +"Conversation.DeleteMessagesForEveryone" = "Delete for everyone"; + +"NetworkUsageSettings.Title" = "Network Usage"; +"NetworkUsageSettings.Cellular" = "Cellular"; +"NetworkUsageSettings.Wifi" = "Wi-Fi"; + +"NetworkUsageSettings.GeneralDataSection" = "MESSAGES"; +"NetworkUsageSettings.MediaImageDataSection" = "PHOTOS"; +"NetworkUsageSettings.MediaVideoDataSection" = "VIDEOS"; +"NetworkUsageSettings.MediaAudioDataSection" = "AUDIO"; +"NetworkUsageSettings.MediaDocumentDataSection" = "DOCUMENTS"; +"NetworkUsageSettings.TotalSection" = "TOTAL BYTES"; +"NetworkUsageSettings.BytesSent" = "Bytes Sent"; +"NetworkUsageSettings.BytesReceived" = "Bytes Received"; + +"NetworkUsageSettings.ResetStats" = "Reset Statistics"; +"NetworkUsageSettings.ResetStatsConfirmation" = "Do you want to reset your usage statistics?"; +"NetworkUsageSettings.CellularUsageSince" = "Cellular usage since %@"; +"NetworkUsageSettings.WifiUsageSince" = "Wi-Fi usage since %@"; + +"Settings.CallSettings" = "Voice Calls"; + +"Calls.TabTitle" = "Calls"; +"Calls.All" = "All"; +"Calls.Missed" = "Missed"; + +"CallSettings.Title" = "Voice Calls"; +"CallSettings.RecentCalls" = "Recent Calls"; +"CallSettings.TabIcon" = "Show Calls Tab"; +"CallSettings.TabIconDescription" = "A call icon will appear in the tab bar."; +"CallSettings.UseLessData" = "Use Less Data"; +"CallSettings.Never" = "Never"; +"CallSettings.OnMobile" = "On Mobile Network"; +"CallSettings.Always" = "Always"; +"CallSettings.UseLessDataLongDescription" = "Using less data may improve your experience on bad networks, but will slightly decrease audio quality."; + +"Calls.CallTabTitle" = "Calls Tab"; +"Calls.CallTabDescription" = "You can add a Calls Tab to the tab bar."; +"Calls.NotNow" = "Not Now"; +"Calls.AddTab" = "Add Tab"; +"Calls.NewCall" = "New Call"; + +"Calls.RatingTitle" = "Please rate the quality\nof your Telegram call"; +"Calls.SubmitRating" = "Submit"; + +"Call.Seconds_1" = "%@ second"; +"Call.Seconds_2" = "%@ seconds"; +"Call.Seconds_3_10" = "%@ seconds"; +"Call.Seconds_any" = "%@ seconds"; +"Call.Seconds_many" = "%@ seconds"; +"Call.Seconds_0" = "%@ seconds"; +"Call.Minutes_1" = "%@ minute"; +"Call.Minutes_2" = "%@ minutes"; +"Call.Minutes_3_10" = "%@ minutes"; +"Call.Minutes_any" = "%@ minutes"; +"Call.Minutes_many" = "%@ minutes"; +"Call.Minutes_0" = "%@ minutes"; + +"Call.ShortSeconds_1" = "%@ sec"; +"Call.ShortSeconds_2" = "%@ sec"; +"Call.ShortSeconds_3_10" = "%@ sec"; +"Call.ShortSeconds_any" = "%@ sec"; +"Call.ShortSeconds_many" = "%@ sec"; +"Call.ShortSeconds_0" = "%@ sec"; +"Call.ShortMinutes_1" = "%@ min"; +"Call.ShortMinutes_2" = "%@ min"; +"Call.ShortMinutes_3_10" = "%@ min"; +"Call.ShortMinutes_any" = "%@ min"; +"Call.ShortMinutes_many" = "%@ min"; +"Call.ShortMinutes_0" = "%@ min"; + +"Notification.CallTimeFormat" = "%1$@ (%2$@)"; // 1 - type, 2 - duration +"Notification.CallOutgoing" = "Outgoing Call"; +"Notification.CallIncoming" = "Incoming Call"; +"Notification.CallMissed" = "Missed Call"; +"Notification.CallCanceled" = "Cancelled Call"; +"Notification.CallOutgoingShort" = "Outgoing"; +"Notification.CallIncomingShort" = "Incoming"; +"Notification.CallMissedShort" = "Missed"; +"Notification.CallCanceledShort" = "Cancelled"; +"Notification.CallFormat" = "%1$@, %2$@"; // 1 - time, 2 - duration + + + +"Call.ConnectionErrorTitle" = "Unable to Call"; +"Call.ConnectionErrorMessage" = "Please check your internet connection and try again."; + +"Call.CallAgain" = "Call Again"; + +"Login.PhoneFloodError" = "Sorry, you have deleted and re-created your account too many times recently. Please wait for a few days before signing up again."; + +"Checkout.Title" = "Checkout"; +"Checkout.TotalAmount" = "Total"; +"Checkout.TotalPaidAmount" = "Total Paid"; +"Checkout.PaymentMethod" = "Payment Method"; +"Checkout.ShippingMethod" = "Shipping Method"; +"Checkout.ShippingAddress" = "Shipping Information"; +"Checkout.Name" = "Name"; +"Checkout.Email" = "E-Mail"; +"Checkout.Phone" = "Phone"; +"Checkout.PayPrice" = "Pay %@"; +"Checkout.PayNone" = "Pay"; + +"Checkout.PaymentMethod.Title" = "Payment Method"; +"Checkout.PaymentMethod.New" = "New Card..."; + +"Checkout.NewCard.Title" = "New Card"; +"Checkout.NewCard.PaymentCard" = "PAYMENT CARD"; +"Checkout.NewCard.SaveInfo" = "Save Payment Information"; +"Checkout.NewCard.SaveInfoEnableHelp" = "You can save your payment information for future use.\nPlease [turn on Two-Step Verification] to enable this."; +"Checkout.NewCard.SaveInfoHelp" = "You can save your payment information for future use."; +"Checkout.NewCard.CardholderNameTitle" = "CARDHOLDER"; +"Checkout.NewCard.CardholderNamePlaceholder" = "Cardholder Name"; +"Checkout.NewCard.PostcodeTitle" = "BILLING ADDRESS"; +"Checkout.NewCard.PostcodePlaceholder" = "Zip Code"; + +"Checkout.ShippingOption.Title" = "Shipping Method"; + +"Checkout.ErrorProviderAccountInvalid" = "This bot can't accept payments at the moment. Please try again later."; +"Checkout.ErrorProviderAccountTimeout" = "This bot can't process payments at the moment. Please try again later."; +"Checkout.ErrorInvoiceAlreadyPaid" = "You have already paid for this item."; + +"Checkout.ErrorGeneric" = "An error occurred while processing your payment. Your card has not been billed."; +"Checkout.ErrorPaymentFailed" = "Payment failed. Your card has not been billed."; +"Checkout.ErrorPrecheckoutFailed" = "The bot couldn't process your payment. Your card has not been billed."; + +"CheckoutInfo.Title" = "Shipping Information"; +"CheckoutInfo.ShippingInfoTitle" = "SHIPPING ADDRESS"; +"CheckoutInfo.ShippingInfoAddress1" = "Address 1"; +"CheckoutInfo.ShippingInfoAddress1Placeholder" = "Address"; +"CheckoutInfo.ShippingInfoAddress2" = "Address 2"; +"CheckoutInfo.ShippingInfoAddress2Placeholder" = "Address"; +"CheckoutInfo.ShippingInfoState" = "State"; +"CheckoutInfo.ShippingInfoStatePlaceholder" = "State"; +"CheckoutInfo.ShippingInfoCity" = "City"; +"CheckoutInfo.ShippingInfoCityPlaceholder" = "City"; +"CheckoutInfo.ShippingInfoCountry" = "Country"; +"CheckoutInfo.ShippingInfoCountryPlaceholder" = "Country"; +"CheckoutInfo.ShippingInfoPostcode" = "Postcode"; +"CheckoutInfo.ShippingInfoPostcodePlaceholder" = "Postcode"; +"CheckoutInfo.ReceiverInfoTitle" = "RECEIVER"; +"CheckoutInfo.ReceiverInfoName" = "Name"; +"CheckoutInfo.ReceiverInfoNamePlaceholder" = "Name Surname"; +"CheckoutInfo.ReceiverInfoEmail" = "Email"; +"CheckoutInfo.ReceiverInfoEmailPlaceholder" = "Email"; +"CheckoutInfo.ReceiverInfoPhone" = "Phone"; +"CheckoutInfo.SaveInfo" = "Save Info"; +"CheckoutInfo.SaveInfoHelp" = "You can save your shipping information for future use."; +"CheckoutInfo.Pay" = "Pay"; + +"Checkout.Receipt.Title" = "Receipt"; + +"Message.ReplyActionButtonShowReceipt" = "Show Receipt"; +"Message.InvoiceLabel" = "INVOICE"; + +"CheckoutInfo.ErrorShippingNotAvailable" = "Shipping to the selected country is not available."; +"CheckoutInfo.ErrorPostcodeInvalid" = "Please enter a valid postcode."; +"CheckoutInfo.ErrorStateInvalid" = "Please enter a valid state."; +"CheckoutInfo.ErrorCityInvalid" = "Please enter a valid city."; +"CheckoutInfo.ErrorNameInvalid" = "Please enter a valid name."; +"CheckoutInfo.ErrorEmailInvalid" = "Please enter a valid e-mail address."; +"CheckoutInfo.ErrorPhoneInvalid" = "Please enter a valid phone number."; + +"Checkout.WebConfirmation.Title" = "Complete Payment"; +"Checkout.PasswordEntry.Title" = "Payment Confirmation"; +"Checkout.PasswordEntry.Pay" = "Pay"; +"Checkout.PasswordEntry.Text" = "Your card %@ is on file. To pay with this card, please enter your 2-Step-Verification password."; + +"Checkout.SavePasswordTimeout" = "Would you like to save your password for %@?"; +"Checkout.SavePasswordTimeoutAndTouchId" = "Would you like to save your password for %@ and use Touch ID instead?"; +"Checkout.PayWithTouchId" = "Pay with Touch ID"; +"Checkout.EnterPassword" = "Enter Password"; + +"Your_card_has_expired" = "Your card has expired."; + +/* Error when the card was declined by the credit card networks */ +"Your_card_was_declined" = "Your card was declined."; + +/* Error when the card's expiration month is not valid */ +"Your_cards_expiration_month_is_invalid" ="You've entered an invalid expiration month."; + +/* Error when the card's expiration year is not valid */ +"Your_cards_expiration_year_is_invalid" ="You've entered an invalid expiration year."; + +/* Error when the card number is not valid */ +"Your_cards_number_is_invalid" = "You've entered an invalid card number."; + +/* Error when the card's CVC is not valid */ +"Your_cards_security_code_is_invalid" = "You've entered an invalid security code."; + +"MESSAGE_INVOICE" = "%1$@ sent you an invoice for %2$@"; +"CHAT_MESSAGE_INVOICE" = "%1$@ sent an invoice for %3$@ to the group %2$@"; +"PINNED_INVOICE" = "%1$@ pinned an invoice"; + +"Message.PinnedInvoice" = "pinned an invoice"; + +"User.DeletedAccount" = "Deleted Account"; + +"Settings.SaveEditedPhotos" = "Save Edited Photos"; + +"Message.PaymentSent" = "Payment: %@"; +"Notification.PaymentSent" = "You have just successfully transferred {amount} to {name} for {title}"; + +"Common.NotNow" = "Not Now"; + +"Calls.RatingFeedback" = "Write a comment..."; + +"Call.StatusIncoming" = "Telegram Audio..."; +"Call.StatusRequesting" = "Contacting..."; +"Call.StatusWaiting" = "Waiting..."; +"Call.StatusRinging" = "Ringing..."; +"Call.StatusConnecting" = "Connecting..."; +"Call.StatusOngoing" = "Telegram Audio %@"; +"Call.StatusEnded" = "Call Ended"; +"Call.StatusFailed" = "Call Failed"; +"Call.StatusBusy" = "Busy"; +"Call.Accept" = "Accept"; +"Call.Decline" = "Decline"; + +"Call.StatusBar" = "Touch to return to call %@"; + +"Call.ParticipantVersionOutdatedError" = "%@'s app does not support calls. They need to update their app before you can call them."; + +"Privacy.Calls" = "Voice Calls"; + +"Privacy.Calls.WhoCanCallMe" = "WHO CAN CALL ME"; +"Privacy.Calls.CustomHelp" = "You can restrict who can call you with granular precision."; +"Privacy.Calls.AlwaysAllow" = "Always Allow"; +"Privacy.Calls.NeverAllow" = "Never Allow"; +"Privacy.Calls.CustomShareHelp" = "These users will or will not be able to call you regardless of the settings above."; + +"Privacy.Calls.AlwaysAllow.Title" = "Always Allow"; +"Privacy.Calls.AlwaysAllow.Placeholder" = "Always allow..."; +"Privacy.Calls.NeverAllow.Title" = "Never Allow"; +"Privacy.Calls.NeverAllow.Placeholder" = "Never allow..."; + +"PhotoEditor.QualityTool" = "Quality"; +"PhotoEditor.QualityVeryLow" = "Very Low"; +"PhotoEditor.QualityLow" = "Low"; +"PhotoEditor.QualityMedium" = "Medium"; +"PhotoEditor.QualityHigh" = "High"; +"PhotoEditor.QualityVeryHigh" = "Very High"; + +"Settings.SaveEditedPhotos" = "Save Edited Photos"; + +"Calls.NoCallsPlaceholder" = "Your recent calls will appear here"; +"Calls.NoMissedCallsPlacehoder" = "You have no missed calls"; + +"Call.CallInProgressTitle" = "Call in Progress"; +"Call.CallInProgressMessage" = "Finish call with %1$@ and start a new one with %2$@?"; + +"Call.Message" = "Message"; + +"UserInfo.TapToCall" = "Tap to make an end-to-end encrypted call"; +"Call.GroupFormat" = "%1$@ (%2$@)"; + +"NetworkUsageSettings.CallDataSection" = "CALLS"; + +"Call.PrivacyErrorMessage" = "Sorry, %@ doesn't accept calls."; + +"Notification.CallBack" = "Call Back"; + +"Call.AudioRouteSpeaker" = "Speaker"; +"Call.AudioRouteHeadphones" = "Headphones"; +"Call.AudioRouteHide" = "Hide"; + +"Call.PhoneCallInProgressMessage" = "You can’t place a Telegram call if you’re already on a phone call."; +"Call.RecordingDisabledMessage" = "Please end your call before recording a voice message."; + +"Call.EmojiDescription" = "If these emoji are the same on %@'s screen, this call is 100%% secure."; + +"Message.VideoMessage" = "Video Message"; + +"Conversation.HoldForAudio" = "Hold to record audio. Tap to switch to video."; +"Conversation.HoldForVideo" = "Hold to record video. Tap to switch to audio."; + +"UserInfo.TelegramCall" = "Telegram Call"; +"UserInfo.PhoneCall" = "Phone Call"; + +"SharedMedia.CategoryMedia" = "Media"; +"SharedMedia.CategoryDocs" = "Docs"; +"SharedMedia.CategoryLinks" = "Links"; +"SharedMedia.CategoryOther" = "Audio"; + +"AccessDenied.VideoMessageCamera" = "Telegram needs access to your camera to send video messages.\n\nPlease go to Settings > Privacy > Camera and set Telegram to ON."; +"AccessDenied.VideoMessageMicrophone" = "Telegram needs access to your microphone to send video messages.\n\nPlease go to Settings > Privacy > Microphone and set Telegram to ON."; + +"ChatSettings.AutomaticVideoMessageDownload" = "AUTOMATIC VIDEO MESSAGE DOWNLOAD"; + +"ForwardedVideoMessages_1" = "Forwarded video message"; +"ForwardedVideoMessages_2" = "2 forwarded video messages"; +"ForwardedVideoMessages_3_10" = "%@ forwarded video messages"; +"ForwardedVideoMessages_any" = "%@ forwarded video messages"; +"ForwardedVideoMessages_many" = "%@ forwarded video messages"; +"ForwardedVideoMessages_0" = "%@ forwarded video messages"; + +"Conversation.DiscardVoiceMessageTitle" = "Discard Voice Message"; +"Conversation.DiscardVoiceMessageDescription" = "Are you sure you want to stop recording and discard\nyour voice message?"; +"Conversation.DiscardVoiceMessageAction" = "Discard"; + +"Message.ForwardedMessageShort" = "Forwarded From\n%@"; + +"Checkout.LiabilityAlertTitle" = "Warning"; +"Checkout.LiabilityAlert" = "Neither Telegram, nor %1$@ will have access to your credit card information. Credit card details will be handled only by the payment system, %2$@.\n\nPayments will go directly to the developer of %1$@. Telegram cannot provide any guarantees, so proceed at your own risk. In case of problems, please contact the developer of %1$@ or your bank."; + +"Settings.AppLanguage" = "Language"; +"Settings.AppLanguage.Unofficial" = "UNOFFICIAL"; + +"InstantPage.AutoNightTheme" = "Auto-Night Theme"; + +"Privacy.PaymentsTitle" = "PAYMENTS"; +"Privacy.PaymentsClearInfo" = "Clear payment & shipping info"; +"Privacy.PaymentsClearInfoHelp" = "You can delete your shipping info and instruct all payment providers to remove your saved credit cards. Note that Telegram never stores your credit card data."; +"Privacy.PaymentsClear.PaymentInfo" = "Payment Info"; +"Privacy.PaymentsClear.ShippingInfo" = "Shipping Info"; + +"Channel.EditAdmin.PermissionsHeader" = "WHAT CAN THIS ADMIN DO?"; +"Channel.EditAdmin.PermissionChangeInfo" = "Change Channel Info"; +"Group.EditAdmin.PermissionChangeInfo" = "Change Group Info"; +"Channel.EditAdmin.PermissionPostMessages" = "Post Messages"; +"Channel.EditAdmin.PermissionEditMessages" = "Edit Messages"; +"Channel.EditAdmin.PermissionDeleteMessages" = "Delete Messages"; +"Channel.EditAdmin.PermissionBanUsers" = "Ban Users"; +"Channel.EditAdmin.PermissionInviteUsers" = "Add Users"; +"Channel.EditAdmin.PermissionPinMessages" = "Pin Messages"; +"Channel.EditAdmin.PermissionAddAdmins" = "Add New Admins"; + +"Channel.EditAdmin.PermissinAddAdminOn" = "This Admin will be able to add new admins with the same (or more limited) permissions."; +"Channel.EditAdmin.PermissinAddAdminOff" = "This Admin will not be able to add new admins."; + +"Login.ContinueWithLocalization" = "Continue with English"; +"Localization.LanguageName" = "English"; +"Localization.ChooseLanguage" = "Choose Your Language"; +"Localization.EnglishLanguageName" = "English"; +"Localization.LanguageOther" = "Other"; +"Localization.LanguageCustom" = "Custom"; + +"Channel.BanUser.Title" = "Ban User"; +"Channel.BanUser.PermissionsHeader" = "User Restrictions"; +"Channel.BanUser.PermissionReadMessages" = "Can Read Messages"; +"Channel.BanUser.PermissionSendMessages" = "Can Send Messages"; +"Channel.BanUser.PermissionSendMedia" = "Can Send Media"; +"Channel.BanUser.PermissionSendStickersAndGifs" = "Can Send Stickers & GIFs"; +"Channel.BanUser.PermissionEmbedLinks" = "Can Embed Links"; +"Channel.BanUser.Unban" = "Unban"; + +"Channel.BanUser.BlockFor" = "Block For"; + +"Channel.BanList.BlockedTitle" = "BLOCKED"; +"Channel.BanList.RestrictedTitle" = "RESTRICTED"; + +"Group.Info.AdminLog" = "Recent Actions"; +"Channel.AdminLog.InfoPanelTitle" = "What Is This?"; +"Channel.AdminLog.InfoPanelAlertTitle" = "What is the event log?"; +"Channel.AdminLog.InfoPanelAlertText" = "This is a list of all service actions taken by the group's members and admins in the last 48 hours."; + +"Channel.AdminLog.BanReadMessages" = "Read Messages"; +"Channel.AdminLog.BanSendMessages" = "Send Messages"; +"Channel.AdminLog.BanSendMedia" = "Send Media"; +"Channel.AdminLog.BanSendStickers" = "Send Stickers"; +"Channel.AdminLog.BanEmbedLinks" = "Embed Links"; +"Channel.AdminLog.BanSendGifs" = "Send GIFs"; +"Channel.AdminLog.MessageRestricted" = "%@ changed restrictions for %@ (%@)"; +"Channel.AdminLog.MessageAdmin" = "%@ changed privileges for %@ (%@)"; + +"Channel.AdminLog.CanChangeInfo" = "Change Info"; +"Channel.AdminLog.CanSendMessages" = "Post Messages"; +"Channel.AdminLog.CanDeleteMessages" = "Delete Messages"; +"Channel.AdminLog.CanBanUsers" = "Ban Users"; +"Channel.AdminLog.CanInviteUsers" = "Add Users"; +"Channel.AdminLog.CanChangeInviteLink" = "Invite Users Via Link"; +"Channel.AdminLog.CanPinMessages" = "Pin Messages"; +"Channel.AdminLog.CanAddAdmins" = "Add New Admins"; +"Channel.AdminLog.CanEditMessages" = "Edit Messages"; + +"Channel.AdminLog.MessageToggleInvitesOn" = "%@ enabled group invites"; +"Channel.AdminLog.MessageToggleInvitesOff" = "%@ disabled group invites"; + +"Channel.AdminLog.MessageUnpinned" = "%@ unpinned message"; + +"Channel.AdminLog.MessageToggleSignaturesOn" = "%@ enabled signatures"; +"Channel.AdminLog.MessageToggleSignaturesOff" = "%@ disabled signatures"; + +"Channel.AdminLog.MessageChangedGroupUsername" = "%@ changed group link:"; +"Channel.AdminLog.MessageChangedChannelUsername" = "%@ changed channel link:"; +"Channel.AdminLog.MessageRemovedGroupUsername" = "%@ removed group link"; +"Channel.AdminLog.MessageRemovedChannelUsername" = "%@ removed channel link"; + +"Channel.AdminLog.MessageChangedGroupAbout" = "%@ edited group description"; +"Channel.AdminLog.MessageChangedChannelAbout" = "%@ edited channel description"; + +"Channel.AdminLog.MessageEdited" = "%@ edited message:"; +"Channel.AdminLog.CaptionEdited" = "%@ edited caption:"; +"Channel.AdminLog.MessageDeleted" = "%@ deleted message:"; +"Channel.AdminLog.MessagePinned" = "%@ pinned message:"; + +"Channel.AdminLog.MessageInvitedName" = "invited %1$@"; +"Channel.AdminLog.MessageInvitedNameUsername" = "invited %1$@ (%2$@)"; +"Channel.AdminLog.MessageKickedName" = "banned %1$@"; +"Channel.AdminLog.MessageKickedNameUsername" = "banned %1$@ (%2$@)"; +"Channel.AdminLog.MessageUnkickedName" = "unbanned %1$@"; +"Channel.AdminLog.MessageUnkickedNameUsername" = "unbanned %1$@ (%2$@)"; +"Channel.AdminLog.MessageRestrictedName" = "changed restrictions for %1$@"; +"Channel.AdminLog.MessageRestrictedNameUsername" = "changed restrictions for %1$@ (%2$@)"; +"Channel.AdminLog.MessagePromotedName" = "changed privileges for %1$@"; +"Channel.AdminLog.MessagePromotedNameUsername" = "changed privileges for %1$@ (%2$@)"; +"Channel.AdminLog.MessageRestrictedUntil" = "until %@"; +"Channel.AdminLog.MessageRestrictedForever" = "indefinitely"; +"Channel.AdminLog.MessageRestrictedNewSetting" = "now: %@"; + +"Channel.AdminLog.MessagePreviousMessage" = "Original message"; +"Channel.AdminLog.MessagePreviousCaption" = "Original caption"; +"Channel.AdminLog.MessagePreviousLink" = "Previous link"; +"Channel.AdminLog.MessagePreviousDescription" = "Previous description"; + +"Contacts.MemberSearchSectionTitleGroup" = "Group Members"; + +"Channel.AdminLog.TitleAllEvents" = "All Actions"; +"Channel.AdminLog.TitleSelectedEvents" = "Selected Actions"; +"Channel.AdminLogFilter.Title" = "Filter"; +"Channel.AdminLogFilter.EventsTitle" = "ACTIONS"; +"Channel.AdminLogFilter.EventsAll" = "All Actions"; +"Channel.AdminLogFilter.EventsRestrictions" = "New Restrictions"; +"Channel.AdminLogFilter.EventsAdmins" = "New Admins"; +"Channel.AdminLogFilter.EventsNewMembers" = "New Members"; +"Channel.AdminLogFilter.EventsInfo" = "Group Info"; +"Channel.AdminLogFilter.ChannelEventsInfo" = "Channel Info"; +"Channel.AdminLogFilter.EventsDeletedMessages" = "Deleted Messages"; +"Channel.AdminLogFilter.EventsEditedMessages" = "Edited Messages"; +"Channel.AdminLogFilter.EventsPinned" = "Pinned Messages"; +"Channel.AdminLogFilter.EventsLeaving" = "Members Removed"; +"Channel.AdminLogFilter.AdminsTitle" = "ADMINS"; +"Channel.AdminLogFilter.AdminsAll" = "All Admins"; + +"Group.ErrorSendRestrictedStickers" = "Sorry, the admins of this group have restricted you from sending stickers."; +"Group.ErrorSendRestrictedMedia" = "Sorry, the admins of this group have restricted you from sending media."; + +"SharedMedia.ViewInChat" = "View in Chat"; + +"Channel.Info.BlackList" = "Blacklist"; + +"Channel.Management.PromotedBy" = "Promoted by %@"; +"DialogList.LanguageTooltip" = "You can change the language later in Settings"; + +"Contacts.PhoneNumber" = "Phone Number"; +"Contacts.AddPhoneNumber" = "Add %@"; +"Contacts.ShareTelegram" = "Share Telegram"; + +"Conversation.ViewChannel" = "VIEW CHANNEL"; +"Conversation.ViewGroup" = "VIEW GROUP"; + +"GroupInfo.ActionPromote" = "Promote"; +"GroupInfo.ActionRestrict" = "Restrict"; + +"Conversation.RestrictedTextTimed" = "The admins of this group have restricted you from writing here until %@."; +"Conversation.RestrictedText" = "The admins of this group have restricted you from writing here."; + +"Conversation.RestrictedInlineTimed" = "The admins of this group have restricted you from posting inline content here until %@."; +"Conversation.RestrictedInline" = "The admins of this group have restricted you from posting inline content here."; + +"Conversation.RestrictedMediaTimed" = "The admins of this group have restricted you from posting media content here until %@."; +"Conversation.RestrictedMedia" = "The admins of this group have restricted you from posting media content here."; + +"Conversation.RestrictedStickersTimed" = "The admins of this group have restricted you from posting stickers here until %@."; +"Conversation.RestrictedStickers" = "The admins of this group have restricted you from posting stickers here."; + +"ChatSettings.ConnectionType.Title" = "CONNECTION TYPE"; +"ChatSettings.ConnectionType.UseProxy" = "Use Proxy"; +"ChatSettings.ConnectionType.UseSocks5" = "SOCKS5"; + +"SocksProxySetup.Title" = "Proxy"; + +"SocksProxySetup.TypeNone" = "Disabled"; +"SocksProxySetup.TypeSocks" = "SOCKS5"; + +"SocksProxySetup.Connection" = "CONNECTION"; +"SocksProxySetup.Hostname" = "Server"; +"SocksProxySetup.Port" = "Port"; + +"SocksProxySetup.Credentials" = "CREDENTIALS (OPTIONAL)"; +"SocksProxySetup.Username" = "Username"; +"SocksProxySetup.Password" = "Password"; + +"Channel.AdminLog.EmptyTitle" = "No actions here yet"; +"Channel.AdminLog.EmptyText" = "No service actions were taken by the channel members and admins in the last 48 hours."; +"Group.AdminLog.EmptyText" = "No service actions were taken by the group's members and admins in the last 48 hours."; +"Broadcast.AdminLog.EmptyText" = "No service actions were taken by the channel's admins in the last 48 hours."; + +"Channel.AdminLog.EmptyFilterTitle" = "No actions found"; +"Channel.AdminLog.EmptyFilterQueryText" = "No recent actions that contain '%@' have been found."; +"Channel.AdminLog.EmptyFilterText" = "No recent actions that match your query have been found."; + +"Channel.AdminLog.EmptyMessageText" = "Empty"; + +"Camera.Title" = "Take Photo or Video"; + +"Channel.Members.AddAdminErrorNotAMember" = "Sorry, you can't add this user as an admin because they are not a member of this group and you are not allowed to invite them."; + +"Channel.Members.AddAdminErrorBlacklisted" = "Sorry, you can't add this user as an admin because they are in the blacklist and you can't unban them."; + +"Channel.Members.AddBannedErrorAdmin" = "Sorry, you can't ban this user because they are an admin in this group and you are not allowed to demote them."; + +"Group.Members.AddMemberBotErrorNotAllowed" = "Sorry, you don't have the necessary permissions to add bots to this group."; + +"Privacy.Calls.P2P" = "Peer-to-Peer"; +"Privacy.Calls.P2PHelp" = "Disabling peer-to-peer will relay all calls through Telegram servers to avoid revealing your IP address, but will slightly decrease audio quality."; + +"Privacy.Calls.Integration" = "iOS Call Integration"; +"Privacy.Calls.IntegrationHelp" = "iOS Call Integration shows Telegram calls on the lock screen and in the system's call history. If iCloud sync is enabled, your call history is shared with Apple."; + +"Call.ReportPlaceholder" = "What went wrong?"; +"Call.ReportIncludeLog" = "Send technical information"; +"Call.ReportIncludeLogDescription" = "This won't reveal the contents of your conversation, but will help us fix the issue sooner."; +"Call.ReportSkip" = "Skip"; +"Call.ReportSend" = "Send"; + +"Channel.EditAdmin.CannotEdit" = "You cannot edit the rights of this admin."; +"Call.RateCall" = "Rate This Call"; + +"Settings.ApplyProxyAlert" = "Are you sure you want to enable this proxy?\nServer: %1$@\nPort: %2$@\n\nYou can change your proxy server later it in the Settings (Data and Storage)."; +"Settings.ApplyProxyAlertCredentials" = "Are you sure you want to enable this proxy?\nServer: %1$@\nPort: %2$@\nUsername: %3$@\nPassword: %4$@\n\nYou can change your proxy server later it in the Settings (Data and Storage)."; +"Settings.ApplyProxyAlertEnable" = "Enable"; + +"Channel.Management.RestrictedBy" = "Restricted by %@"; + +"Stickers.FrequentlyUsed" = "Recently Used"; + +"Contacts.ImportersCount_1" = "1 contact on Telegram"; +"Contacts.ImportersCount_2" = "2 contacts on Telegram"; +"Contacts.ImportersCount_3_10" = "%@ contacts on Telegram"; +"Contacts.ImportersCount_any" = "%@ contacts on Telegram"; +"Contacts.ImportersCount_many" = "%@ contacts on Telegram"; +"Contacts.ImportersCount_0" = "%@ contacts on Telegram"; + +"Conversation.ContextMenuBan" = "Ban"; + +"SocksProxySetup.UseForCalls" = "Use for calls"; +"SocksProxySetup.UseForCallsHelp" = "Proxy servers may degrade the quality of your calls."; + +"InviteText.URL" = "https://telegram.org/dl"; +"InviteText.SingleContact" = "Hey, I'm using Telegram to chat. Join me! Download it here: %@"; +"InviteText.ContactsCountText_1" = "Hey, I'm using Telegram to chat. Join me! Download it here: {url}"; +"InviteText.ContactsCountText_2" = "Hey, I'm using Telegram to chat – and so are 2 of our other contacts. Join us! Download it here: {url}"; +"InviteText.ContactsCountText_3_10" = "Hey, I'm using Telegram to chat – and so are %@ of our other contacts. Join us! Download it here: {url}"; +"InviteText.ContactsCountText_any" = "Hey, I'm using Telegram to chat – and so are %@ of our other contacts. Join us! Download it here: {url}"; +"InviteText.ContactsCountText_many" = "Hey, I'm using Telegram to chat – and so are %@ of our other contacts. Join us! Download it here: {url}"; +"InviteText.ContactsCountText_0" = "Hey, I'm using Telegram to chat. Join me! Download it here: {url}"; + +"Invite.LargeRecipientsCountWarning" = "Please note that it may take some time for your device to send all of these invitations"; + +"Contacts.InviteSearchLabel" = "Search for contacts"; + +"Message.ImageExpired" = "Photo has expired"; +"Message.VideoExpired" = "Video has expired"; + +"SecretImage.Title" = "Disappearing Photo"; +"SecretVideo.Title" = "Disappearing Video"; +"SecretTimer.ImageDescription" = "If you set a timer, the photo will self-destruct after it was viewed."; +"SecretTimer.VideoDescription" = "If you set a timer, the video will self-destruct after it was viewed."; + +"PhotoEditor.TiltShift" = "Tilt Shift"; + +"Notification.SecretChatMessageScreenshotSelf" = "You took a screenshot!"; + +"Settings.AboutEmpty" = "Add"; + +"SecretImage.NotViewedYet" = "%@ hasn't opened this photo yet"; +"SecretVideo.NotViewedYet" = "%@ hasn't played this video yet"; + +"UserInfo.About.Placeholder" = "Bio"; + +"Call.StatusNoAnswer" = "No Answer"; + +"Conversation.SearchByName.Prefix" = "from: "; +"Conversation.SearchByName.Placeholder" = "Search Members"; + +"Login.PhoneBannedError" = "Your phone was banned."; + +"Clipboard.SendPhoto" = "Send Photo"; + +"HashtagSearch.AllChats" = "All Chats"; + +"Stickers.AddToFavorites" = "Add to Favorites"; +"Stickers.RemoveFromFavorites" = "Remove from Favorites"; + +"Channel.Info.Stickers" = "Group Sticker Set"; +"Channel.Stickers.Placeholder" = "stickerset"; +"Channel.Stickers.YourStickers" = "CHOOSE FROM YOUR STICKERS"; + +"Stickers.FavoriteStickers" = "Favorite Stickers"; +"Stickers.GroupStickers" = "Group Stickers"; +"Stickers.GroupChooseStickerPack" = "CHOOSE STICKER SET"; +"Stickers.GroupStickersHelp" = "You can choose a set that will be available to all group members when they are chatting in this group."; + +"Channel.AdminLog.MessageChangedGroupStickerPack" = "%@ changed group sticker set"; +"Channel.AdminLog.MessageRemovedGroupStickerPack" = "%@ removed group sticker set"; + +"Conversation.ContextMenuCopyLink" = "Copy Link"; + +"Channel.Stickers.Searching" = "Searching..."; +"Channel.Stickers.NotFound" = "No such sticker set found"; +"Channel.Stickers.NotFoundHelp" = "Try again or choose from the list below"; +"Channel.Stickers.CreateYourOwn" = "You can create your own custom sticker set using @stickers bot."; + +"MediaPicker.TimerTooltip" = "You can now set a self-destruct timer"; + +"UserInfo.BlockConfirmation" = "Block %@?"; + +"FastTwoStepSetup.Title" = "Password & Email"; +"FastTwoStepSetup.PasswordSection" = "PASSWORD"; +"FastTwoStepSetup.PasswordPlaceholder" = "Enter a password"; +"FastTwoStepSetup.PasswordConfirmationPlaceholder" = "Re-enter your password"; +"FastTwoStepSetup.PasswordHelp" = "Please create a password to protect your payment info. You'll be asked to enter it when you log in."; +"FastTwoStepSetup.EmailSection" = "RECOVERY E-MAIL"; +"FastTwoStepSetup.EmailPlaceholder" = "Your E-Mail"; +"FastTwoStepSetup.EmailHelp" = "Please add your valid e-mail. It is the only way to recover a forgotten password."; + +"Conversation.ViewMessage" = "VIEW MESSAGE"; + +"GroupInfo.GroupHistory" = "Chat History For New Members"; +"GroupInfo.GroupHistoryVisible" = "Visible"; +"GroupInfo.GroupHistoryHidden" = "Hidden"; + +"Group.Setup.HistoryTitle" = "Chat History Settings"; +"Group.Setup.HistoryHeader" = "CHAT HISTORY FOR NEW MEMBERS"; +"Group.Setup.HistoryVisible" = "Visible"; +"Group.Setup.HistoryHidden" = "Hidden"; + +"Group.Setup.HistoryVisibleHelp" = "New members will see messages that were sent before they joined."; +"Group.Setup.HistoryHiddenHelp" = "New members won't see earlier messages."; + +"Channel.AdminLog.MessageGroupPreHistoryVisible" = "%@ made the group history visible for new members"; +"Channel.AdminLog.MessageGroupPreHistoryHidden" = "%@ made the group history hidden from new members"; + +"Map.PullUpForPlaces" = "PULL UP TO SEE PLACES NEARBY"; +"Map.ShareLiveLocation" = "Share My Live Location for..."; +"Map.ShareLiveLocationHelp" = "Updated in real time as you move"; +"Map.StopLiveLocation" = "Stop Sharing Location"; +"Map.Directions" = "Directions"; +"Map.DirectionsDriveEta" = "%@ drive"; +"Map.Location" = "Location"; +"Map.YouAreHere" = "you are here"; +"Map.LiveLocationShowAll" = "Show All"; + +"Map.LiveLocationTitle" = "Live Location"; +"Map.LiveLocationPrivateDescription" = "Choose for how long %@ will see your accurate location."; +"Map.LiveLocationGroupDescription" = "Choose for how long people in this chat will see your accurate location."; +"Map.LiveLocationFor15Minutes" = "for 15 minutes"; +"Map.LiveLocationFor1Hour" = "for 1 hour"; +"Map.LiveLocationFor8Hours" = "for 8 hours"; +"Map.LiveLocationShortHour" = "%@h"; + +"Message.LiveLocation" = "Live Location"; +"Conversation.LiveLocation" = "Live Location"; + +"Conversation.LiveLocationYou" = "You"; +"Conversation.LiveLocationYouAnd" = "*You* and %@"; +"Conversation.LiveLocationMembersCount_1" = "1 member"; +"Conversation.LiveLocationMembersCount_2" = "2 members"; +"Conversation.LiveLocationMembersCount_3_10" = "%@ members"; +"Conversation.LiveLocationMembersCount_any" = "%@ members"; +"Conversation.LiveLocationMembersCount_many" = "%@ members"; +"Conversation.LiveLocationMembersCount_0" = "%@ members"; + +"Conversation.Admin" = "admin"; + +"LiveLocationUpdated.JustNow" = "updated just now"; +"LiveLocationUpdated.MinutesAgo_0" = "updated %@ minutes ago"; //three to ten +"LiveLocationUpdated.MinutesAgo_1" = "updated 1 minute ago"; //one +"LiveLocationUpdated.MinutesAgo_2" = "updated 2 minutes ago"; //two +"LiveLocationUpdated.MinutesAgo_3_10" = "updated %@ minutes ago"; //three to ten +"LiveLocationUpdated.MinutesAgo_many" = "updated %@ minutes ago"; // more than ten +"LiveLocationUpdated.MinutesAgo_any" = "updated %@ minutes ago"; // more than ten +"LiveLocationUpdated.TodayAt" = "updated at %@"; +"LiveLocationUpdated.YesterdayAt" = "updated yesterday at %@"; + +"LiveLocation.MenuChatsCount_1" = "You are sharing Live Location with 1 chat."; +"LiveLocation.MenuChatsCount_2" = "You are sharing Live Location with 2 chats."; +"LiveLocation.MenuChatsCount_3_10" = "You are sharing Live Location with %@ chats."; +"LiveLocation.MenuChatsCount_any" = "You are sharing Live Location with %@ chats."; +"LiveLocation.MenuChatsCount_many" = "You are sharing Live Location with %@ chats."; +"LiveLocation.MenuChatsCount_0" = "You are sharing Live Location with %@ chats."; +"LiveLocation.MenuStopAll" = "Stop All"; + +"DialogList.LiveLocationSharingTo" = "sharing with %@"; +"DialogList.LiveLocationChatsCount_1" = "sharing with 1 chat"; +"DialogList.LiveLocationChatsCount_2" = "sharing with 2 chats"; +"DialogList.LiveLocationChatsCount_3_10" = "sharing with %@ chats"; +"DialogList.LiveLocationChatsCount_any" = "sharing with %@ chats"; +"DialogList.LiveLocationChatsCount_many" = "sharing with %@ chats"; +"DialogList.LiveLocationChatsCount_0" = "sharing with %@ chats"; + +"Notification.PinnedLiveLocationMessage" = "%@ pinned a live location"; +"Message.PinnedLiveLocationMessage" = "pinned live location"; + +"NotificationSettings.ContactJoined" = "New Contacts"; + +"AccessDenied.LocationAlwaysDenied" = "If you'd like to share your Live Location with friends, Telegram needs location access when the app is in the background.\n\nPlease go to Settings > Privacy > Location Services and set Telegram to Always."; + +"UserInfo.UnblockConfirmation" = "Unblock %@?"; + +"Login.BannedPhoneSubject" = "Banned phone number: %@"; +"Login.BannedPhoneBody" = "I'm trying to use my mobile phone number: %@\nBut Telegram says it's banned. Please help."; + +"Conversation.StopLiveLocation" = "Stop Sharing"; + +"Settings.SavedMessages" = "Saved Messages"; +"Conversation.SavedMessages" = "Saved Messages"; +"DialogList.SavedMessages" = "Saved Messages"; + +"MediaPicker.TapToUngroupDescription" = "Tap to send media separately"; +"MediaPicker.GroupDescription" = "Group media into one message"; +"MediaPicker.UngroupDescription" = "Show media as separate messages"; + +"EditProfile.Title" = "Edit Profile"; +"EditProfile.NameAndPhotoHelp" = "Enter your name and add an optional profile photo."; + +"Settings.SetUsername" = "Set Username"; + +"DialogList.SearchSubtitleFormat" = "%1$@, %2$@"; + +"Media.ShareThisPhoto" = "This Photo"; +"Media.SharePhoto_1" = "%@ Photo"; +"Media.SharePhoto_2" = "All %@ Photos"; +"Media.SharePhoto_3_10" = "All %@ Photos"; +"Media.SharePhoto_any" = "All %@ Photos"; +"Media.SharePhoto_many" = "All %@ Photos"; +"Media.SharePhoto_0" = "All %@ Photos"; + +"Media.ShareThisVideo" = "This Video"; +"Media.ShareVideo_1" = "%@ Video"; +"Media.ShareVideo_2" = "All %@ Videos"; +"Media.ShareVideo_3_10" = "All %@ Videos"; +"Media.ShareVideo_any" = "All %@ Videos"; +"Media.ShareVideo_many" = "All %@ Videos"; +"Media.ShareVideo_0" = "All %@ Videos"; + +"Media.ShareItem_1" = "%@ Item"; +"Media.ShareItem_2" = "All %@ Items"; +"Media.ShareItem_3_10" = "All %@ Items"; +"Media.ShareItem_any" = "All %@ Items"; +"Media.ShareItem_many" = "All %@ Items"; +"Media.ShareItem_0" = "All %@ Items"; + +"Settings.ViewPhoto" = "View Photo"; + +"DialogList.SavedMessagesTooltip" = "You can find your Saved Messages in Settings"; + +"PasscodeSettings.UnlockWithFaceId" = "Unlock with Face ID"; +"Checkout.SavePasswordTimeoutAndFaceId" = "Would you like to save your password for %@ and use Face ID instead?"; +"Checkout.PayWithFaceId" = "Pay with Face ID"; + +"Conversation.StatusSubscribers_0" = "%@ subscribers"; +"Conversation.StatusSubscribers_1" = "%@ subscriber"; +"Conversation.StatusSubscribers_2" = "%@ subscribers"; +"Conversation.StatusSubscribers_3_10" = "%@ subscribers"; +"Conversation.StatusSubscribers_many" = "%@ subscribers"; +"Conversation.StatusSubscribers_any" = "%@ subscribers"; + +"DialogList.SavedMessagesHelp" = "Forward messages here for quick access"; + +"PrivacySettings.PasscodeAndTouchId" = "Passcode & Touch ID"; +"PrivacySettings.PasscodeAndFaceId" = "Passcode & Face ID"; + +"TwoStepAuth.AdditionalPassword" = "Additional Password"; + +"PasscodeSettings.HelpTop" = "When you set up an additional passcode, a lock icon will appear on the chats page. Tap it to lock and unlock the app."; +"PasscodeSettings.HelpBottom" = "Note: if you forget the passcode, you'll need to delete and reinstall the app. All secret chats will be lost."; + +"Channel.Setup.TypePublicHelp" = "Public channels can be found in search, channel history is available to everyone and anyone can join."; +"Channel.Setup.TypePrivateHelp" = "Private channels can only be joined if you were invited or have an invite link."; +"Group.Username.InvalidTooShort" = "Group names must have at least 5 characters."; +"Group.Username.InvalidStartsWithNumber" = "Group names can't start with a number."; +"Group.Username.CreatePublicLinkHelp" = "People can share this link with others and find your group using Telegram search."; +"Channel.TypeSetup.Title" = "Channel Type"; + +"Group.Setup.TypePrivate" = "Private"; +"Group.Setup.TypePublic" = "Public"; + +"Channel.Info.Subscribers" = "Subscribers"; +"Channel.Subscribers.Title" = "Subscribers"; +"Conversation.InfoGroup" = "Group"; + +"Privacy.PaymentsClearInfoDoneHelp" = "Payment & shipping info cleared."; + +"InfoPlist.NSContactsUsageDescription" = "Telegram will continuously upload your contacts to its heavily encrypted cloud servers to let you connect with your friends across all your devices."; +"InfoPlist.NSLocationWhenInUseUsageDescription" = "When you send your location to your friends, Telegram needs access to show them a map."; +"InfoPlist.NSCameraUsageDescription" = "We need this so that you can take and share photos and videos."; +"InfoPlist.NSPhotoLibraryUsageDescription" = "We need this so that you can share photos and videos from your photo library."; +"InfoPlist.NSPhotoLibraryAddUsageDescription" = "We need this so that you can save photos and videos to your photo library."; +"InfoPlist.NSMicrophoneUsageDescription" = "We need this so that you can record and share voice messages and videos with sound."; +"InfoPlist.NSSiriUsageDescription" = "You can use Siri to send messages."; +"InfoPlist.NSLocationAlwaysAndWhenInUseUsageDescription" = "When you choose to share your Live Location with friends in a chat, Telegram needs background access to your location to keep them updated for the duration of the live sharing."; +"InfoPlist.NSLocationAlwaysUsageDescription" = "When you choose to share your live location with friends in a chat, Telegram needs background access to your location to keep them updated for the duration of the live sharing. You also need this to send locations from an Apple Watch."; +"InfoPlist.NSLocationWhenInUseUsageDescription" = "When you send your location to your friends, Telegram needs access to show them a map."; +"InfoPlist.NSFaceIDUsageDescription" = "You can use Face ID to unlock the app."; + +"Privacy.Calls.P2PNever" = "Never"; +"Privacy.Calls.P2PContacts" = "My Contacts"; +"Privacy.Calls.P2PAlways" = "Always"; + +"ChatSettings.AutoDownloadTitle" = "AUTOMATIC MEDIA DOWNLOAD"; +"ChatSettings.AutoDownloadEnabled" = "Auto-Download Media"; +"ChatSettings.AutoDownloadPhotos" = "Photos"; +"ChatSettings.AutoDownloadVideos" = "Videos"; +"ChatSettings.AutoDownloadDocuments" = "Documents"; +"ChatSettings.AutoDownloadVoiceMessages" = "Voice Messages"; +"ChatSettings.AutoDownloadVideoMessages" = "Video Messages"; +"ChatSettings.AutoDownloadReset" = "Reset Auto-Download Settings"; + +"AutoDownloadSettings.Title" = "Auto-Download"; + +"AutoDownloadSettings.PhotosTitle" = "Photos"; +"AutoDownloadSettings.VideosTitle" = "Videos"; +"AutoDownloadSettings.DocumentsTitle" = "Documents"; +"AutoDownloadSettings.VoiceMessagesTitle" = "Voice Messages"; +"AutoDownloadSettings.VideoMessagesTitle" = "Video Messages"; + +"AutoDownloadSettings.Cellular" = "CELLULAR"; +"AutoDownloadSettings.WiFi" = "WI-FI"; +"AutoDownloadSettings.Contacts" = "Contacts"; +"AutoDownloadSettings.PrivateChats" = "Other Private Chats"; +"AutoDownloadSettings.GroupChats" = "Group Chats"; +"AutoDownloadSettings.Channels" = "Channels"; +"AutoDownloadSettings.LimitBySize" = "LIMIT BY SIZE"; +"AutoDownloadSettings.UpTo" = "up to %@"; +"AutoDownloadSettings.Unlimited" = "unlimited"; + +"AutoDownloadSettings.Reset" = "Reset"; +"AutoDownloadSettings.ResetHelp" = "Undo all custom auto-download settings."; + +"SaveIncomingPhotosSettings.Title" = "Save Incoming Photos"; +"SaveIncomingPhotosSettings.From" = "SAVE INCOMING PHOTOS FROM"; + +"Channel.AdminLog.ChannelEmptyText" = "No service actions were taken by the channel's subscribers and admins in the last 48 hours."; +"Channel.AdminLogFilter.EventsNewSubscribers" = "New Subscribers"; +"Channel.AdminLogFilter.EventsLeavingSubscribers" = "Subscribers Removed"; + +"Conversation.ClearPrivateHistory" = "This will delete all messages and media in this chat from your Telegram cloud. Your chat partner will still have them."; +"Conversation.ClearGroupHistory" = "This will delete all messages and media in this chat from your Telegram cloud. Other members of the group will still have them."; +"Conversation.ClearSecretHistory" = "This will delete all messages and media in this chat for both you and your chat partner."; +"Conversation.ClearSelfHistory" = "This will delete all messages and media in this chat from your Telegram cloud."; + +"MediaPicker.LivePhotoDescription" = "The live photo will be sent as a GIF."; + +"Settings.Appearance" = "Appearance"; +"Appearance.Title" = "Appearance"; +"Appearance.TextSize" = "TEXT SIZE"; +"Appearance.Preview" = "CHAT PREVIEW"; +"Appearance.ColorTheme" = "COLOR THEME"; +"Appearance.ThemeDayClassic" = "Day Classic"; +"Appearance.ThemeDay" = "Day"; +"Appearance.ThemeNight" = "Night"; +"Appearance.ThemeNightBlue" = "Night Blue"; +"Appearance.PreviewReplyAuthor" = "Lucio"; +"Appearance.PreviewReplyText" = "Reinhart, we need to find you some..."; +"Appearance.PreviewIncomingText" = "Ah you kids today with techno music! Enjoy the classics, like Hasselhoff!"; +"Appearance.PreviewOutgoingText" = "I can't take you seriously right now. Sorry.."; +"Appearance.AccentColor" = "Accent Color"; +"Appearance.PickAccentColor" = "Pick an Accent Color"; + +"Appearance.AutoNightTheme" = "Auto-Night Theme"; +"Appearance.AutoNightThemeDisabled" = "Disabled"; + +"AutoNightTheme.Title" = "Auto-Night Theme"; +"AutoNightTheme.Disabled" = "Disabled"; +"AutoNightTheme.Scheduled" = "Scheduled"; +"AutoNightTheme.Automatic" = "Automatic"; + +"AutoNightTheme.ScheduleSection" = "SCHEDULE"; +"AutoNightTheme.UseSunsetSunrise" = "Use Location Sunset & Sunrise"; +"AutoNightTheme.ScheduledFrom" = "From"; +"AutoNightTheme.ScheduledTo" = "To"; + +"AutoNightTheme.UpdateLocation" = "Update Location"; +"AutoNightTheme.LocationHelp" = "Calculating sunset & sunrise times requires a one-time check of your approximate location. Note that this location is stored locally on your device only.\n\nSunset: %@\nSunrise: %@"; +"AutoNightTheme.NotAvailable" = "N/A"; + +"AutoNightTheme.AutomaticSection" = "BRIGHTNESS THRESHOLD"; +"AutoNightTheme.AutomaticHelp" = "Switch to night theme when brightness is %@%% or less. Auto-brightness should be enabled for this feature to work correctly."; + +"AutoNightTheme.PreferredTheme" = "PREFERRED THEME"; + +"AuthSessions.Sessions" = "Sessions"; +"AuthSessions.LoggedIn" = "Websites"; +"AuthSessions.LogOutApplications" = "Disconnect All Websites"; +"AuthSessions.LogOutApplicationsHelp" = "You can log in on websites that support signing in with Telegram."; +"AuthSessions.LoggedInWithTelegram" = "CONNECTED WEBSITES"; +"AuthSessions.LogOut" = "Disconnect"; +"AuthSessions.Message" = "You allowed this bot to message you when you logged in on %@."; + +"Conversation.ContextMenuReport" = "Report"; + +"Stickers.Search" = "Search Stickers"; +"Stickers.NoStickersFound" = "No Stickers Found"; + +"Camera.Discard" = "Discard All"; + +"Stickers.SuggestStickers" = "Suggest Stickers by Emoji"; +"Stickers.SuggestAll" = "All Sets"; +"Stickers.SuggestAdded" = "My Sets"; +"Stickers.SuggestNone" = "None"; + +"Settings.Proxy" = "Proxy"; +"Settings.ProxyDisabled" = "Disabled"; +"Settings.ProxyConnecting" = "Connecting..."; +"Settings.ProxyConnected" = "Connected"; + +"SocksProxySetup.UseProxy" = "Use Proxy"; +"SocksProxySetup.SavedProxies" = "SAVED PROXIES"; +"SocksProxySetup.AddProxy" = "Add Proxy"; +"SocksProxySetup.SaveProxy" = "Save Proxy"; +"SocksProxySetup.ConnectAndSave" = "Connect Proxy"; +"SocksProxySetup.AddProxyTitle" = "Add Proxy"; +"SocksProxySetup.ProxyDetailsTitle" = "Proxy Details"; +"SocksProxySetup.ProxyStatusChecking" = "checking..."; +"SocksProxySetup.ProxyStatusPing" = "%@ ms ping"; +"SocksProxySetup.ProxyStatusUnavailable" = "unavailable"; +"SocksProxySetup.ProxyStatusConnecting" = "connecting"; +"SocksProxySetup.ProxyStatusConnected" = "connected"; + +"SocksProxySetup.ProxyType" = "TYPE"; +"SocksProxySetup.ProxySocks5" = "SOCKS5"; +"SocksProxySetup.ProxyTelegram" = "MTProto"; +"SocksProxySetup.HostnamePlaceholder" = "Server"; +"SocksProxySetup.PortPlaceholder" = "Port"; +"SocksProxySetup.UsernamePlaceholder" = "Username"; +"SocksProxySetup.PasswordPlaceholder" = "Password"; +"SocksProxySetup.Secret" = "Secret"; +"SocksProxySetup.SecretPlaceholder" = "Secret"; +"SocksProxySetup.RequiredCredentials" = "CREDENTIALS"; + +"SocksProxySetup.Connecting" = "Connecting..."; +"SocksProxySetup.FailedToConnect" = "Failed to connect"; + +"SocksProxySetup.ProxyEnabled" = "Proxy\nEnabled"; + +"DialogList.AdLabel" = "Proxy Sponsor"; +"DialogList.AdNoticeAlert" = "The proxy you are using displays a sponsored channel in your chat list."; +"SocksProxySetup.AdNoticeHelp" = "This proxy may display a sponsored channel in your chat list. This doesn't reveal your Telegram traffic."; + +"SocksProxySetup.ShareProxyList" = "Share Proxy List"; + +"Privacy.SecretChatsTitle" = "SECRET CHATS"; +"Privacy.SecretChatsLinkPreviews" = "Link Previews"; +"Privacy.SecretChatsLinkPreviewsHelp" = "Link previews will be generated on Telegram servers. We do not store data about the links you send."; + +"Privacy.ContactsTitle" = "CONTACTS"; +"Privacy.ContactsSync" = "Sync Contacts"; +"Privacy.ContactsSyncHelp" = "Turn on to continuously sync contacts from this device with your account."; +"Privacy.ContactsReset" = "Delete Synced Contacts"; +"Privacy.ContactsResetConfirmation" = "This will remove your contacts from the Telegram servers.\nIf 'Sync Contacts' is enabled, contacts will be re-synced."; + +"Login.TermsOfServiceDecline" = "Decline"; +"Login.TermsOfServiceAgree" = "Agree & Continue"; + +"Login.TermsOfService.ProceedBot" = "Please agree and proceed to %@."; + +"Login.TermsOfServiceSignupDecline" = "We're very sorry, but this means you can't sign up for Telegram.\n\nUnlike others, we don't use your data for ad targeting or other commercial purposes. Telegram only stores the information it needs to function as a feature-rich cloud service. You can adjust how we use your data (e.g., delete synced contacts) in Privacy & Security settings.\n\nBut if you're generally not OK with Telegram's modest needs, it won't be possible for us to provide this service."; + +"UserInfo.BotPrivacy" = "Privacy Policy"; + +"PrivacyPolicy.Title" = "Privacy Policy and Terms of Service"; +"PrivacyPolicy.Decline" = "Decline"; +"PrivacyPolicy.Accept" = "Agree & Continue"; + +"PrivacyPolicy.AgeVerificationTitle" = "Age Verification"; +"PrivacyPolicy.AgeVerificationMessage" = "Tap Agree to confirm that you are %@ or over."; +"PrivacyPolicy.AgeVerificationAgree" = "Agree"; + +"PrivacyPolicy.DeclineTitle" = "Decline"; +"PrivacyPolicy.DeclineMessage" = "We're very sorry, but this means we must part ways here. Unlike others, we don't use your data for ad targeting or other commercial purposes. Telegram only stores the information it needs to function as a feature-rich cloud service. You can adjust how we use your data (e.g., delete synced contacts) in Privacy & Security settings.\n\nBut if you're generally not OK with Telegram's modest needs, it won't be possible for us to provide this service."; +"PrivacyPolicy.DeclineDeclineAndDelete" = "Decline and Delete"; + +"PrivacyPolicy.DeclineLastWarning" = "Warning, this will irreversibly delete your Telegram account along with all the data you store in the Telegram cloud.\n\nWe will provide a tool to download your data before June, 23 – so you may want to wait a little before deleting."; +"PrivacyPolicy.DeclineDeleteNow" = "Delete Now"; + +"Settings.Passport" = "Telegram Passport"; + +"Passport.Title" = "Passport"; + +"Passport.RequestHeader" = "%@ requests access to your personal data to sign you up for their services."; + +"Passport.InfoTitle" = "What is Telegram Passport?"; +"Passport.InfoText" = "With **Telegram Passport** you can easily sign up for websites and services that require identity verification.\n\nYour information, personal data, and documents are protected by end-to-end encryption. Nobody, including Telegram, can access them without your permission."; +"Passport.InfoLearnMore" = "Learn More"; +"Passport.InfoFAQ_URL" = "https://telegram.org/faq#passport"; + +"Passport.PassportInformation" = "PASSPORT INFORMATION"; +"Passport.RequestedInformation" = "REQUESTED INFORMATION"; +"Passport.FieldIdentity" = "Identity Document"; +"Passport.FieldIdentityDetailsHelp" = "Fill in your personal details"; +"Passport.FieldIdentityUploadHelp" = "Upload a scan of your passport or other ID"; +"Passport.FieldIdentitySelfieHelp" = "Take a selfie with your document"; +"Passport.FieldAddress" = "Residential Address"; +"Passport.FieldAddressHelp" = "Please provide your address"; +"Passport.FieldAddressUploadHelp" = "Upload proof of your address"; +"Passport.FieldPhone" = "Phone Number"; +"Passport.FieldPhoneHelp" = "Provide your contact phone number"; +"Passport.FieldEmail" = "Email Address"; +"Passport.FieldEmailHelp" = "Provide your contact email address"; +"Passport.PrivacyPolicy" = "You accept the [%1$@ Privacy Policy] and allow their @%2$@ to send you messages."; +"Passport.AcceptHelp" = "You are sending your documents directly to %1$@ and allowing their @%2$@ to send you messages."; +"Passport.Authorize" = "Authorize"; + +"Passport.DeletePassport" = "Delete Telegram Passport"; +"Passport.DeletePassportConfirmation" = "Are you sure you want to delete your Telegram Passport? All details will be lost."; + +"Passport.PasswordHelp" = "Please enter your Telegram Password\nto decrypt your data"; +"Passport.PasswordPlaceholder" = "Enter your password"; +"Passport.InvalidPasswordError" = "Invalid password. Please try again."; +"Passport.FloodError" = "Limit exceeded. Please try again later."; +"Passport.UpdateRequiredError" = "Sorry, your Telegram app is out of date and can’t handle this request. Please update Telegram."; + +"Passport.ForgottenPassword" = "Forgotten Password"; +"Passport.PasswordReset" = "All documents uploaded to your Telegram Passport will be lost. You will be able to upload new documents."; + +"Passport.PasswordDescription" = "Please create a password to secure your personal data with end-to-end encryption.\n\nThis password will also be required whenever you log in to Telegram on a new device."; +"Passport.PasswordCreate" = "Create a Password"; +"Passport.PasswordCompleteSetup" = "Complete Password Setup"; +"Passport.PasswordNext" = "Next"; + +"Passport.DeletePersonalDetails" = "Delete Personal Details"; +"Passport.DeletePersonalDetailsConfirmation" = "Are you sure you want to delete personal details?"; + +"Passport.DeleteAddress" = "Delete Address"; +"Passport.DeleteAddressConfirmation" = "Are you sure you want to delete address?"; + +"Passport.DeleteDocument" = "Delete Document"; +"Passport.DeleteDocumentConfirmation" = "Are you sure you want to delete this document? All details will be lost."; + +"Passport.Scans" = "SCANS"; +"Passport.Scans.Upload" = "Upload Scan"; +"Passport.Scans.UploadNew" = "Upload Additional Scan"; +"Passport.Scans.ScanIndex" = "Scan %@"; + +"Passport.Identity.TypePersonalDetails" = "Personal Details"; +"Passport.Identity.TypePassport" = "Passport"; +"Passport.Identity.TypePassportUploadScan" = "Upload a scan of your passport"; +"Passport.Identity.TypeInternalPassport" = "Internal Passport"; +"Passport.Identity.TypeInternalPassportUploadScan" = "Upload a scan of your internal passport"; +"Passport.Identity.TypeIdentityCard" = "Identity Card"; +"Passport.Identity.TypeIdentityCardUploadScan" = "Upload a scan of your identity card"; +"Passport.Identity.TypeDriversLicense" = "Driver's License"; +"Passport.Identity.TypeDriversLicenseUploadScan" = "Upload a scan of your driver's license"; + +"Passport.Identity.AddPersonalDetails" = "Add Personal Details"; +"Passport.Identity.AddPassport" = "Add Passport"; +"Passport.Identity.AddInternalPassport" = "Add Internal Passport"; +"Passport.Identity.AddIdentityCard" = "Add Identity Card"; +"Passport.Identity.AddDriversLicense" = "Add Driver's License"; + +"Passport.Identity.EditPersonalDetails" = "Edit Personal Details"; +"Passport.Identity.EditPassport" = "Edit Passport"; +"Passport.Identity.EditInternalPassport" = "Edit Internal Passport"; +"Passport.Identity.EditIdentityCard" = "Edit Identity Card"; +"Passport.Identity.EditDriversLicense" = "Edit Driver's License"; + +"Passport.Identity.DocumentDetails" = "DOCUMENT DETAILS"; +"Passport.Identity.Name" = "First Name"; +"Passport.Identity.NamePlaceholder" = "First Name"; +"Passport.Identity.MiddleName" = "Middle Name"; +"Passport.Identity.MiddleNamePlaceholder" = "Middle Name"; +"Passport.Identity.Surname" = "Last Name"; +"Passport.Identity.SurnamePlaceholder" = "Last Name"; +"Passport.Identity.DateOfBirth" = "Date of Birth"; +"Passport.Identity.DateOfBirthPlaceholder" = "Date of Birth"; +"Passport.Identity.Gender" = "Gender"; +"Passport.Identity.GenderPlaceholder" = "Gender"; +"Passport.Identity.GenderMale" = "Male"; +"Passport.Identity.GenderFemale" = "Female"; +"Passport.Identity.Country" = "Citizenship"; +"Passport.Identity.CountryPlaceholder" = "Citizenship"; +"Passport.Identity.ResidenceCountry" = "Residence"; +"Passport.Identity.ResidenceCountryPlaceholder" = "Residence"; +"Passport.Identity.DocumentNumber" = "Document #"; +"Passport.Identity.DocumentNumberPlaceholder" = "Document Number"; +"Passport.Identity.IssueDate" = "Issue Date"; +"Passport.Identity.IssueDatePlaceholder" = "Issue Date"; +"Passport.Identity.ExpiryDate" = "Expiry Date"; +"Passport.Identity.ExpiryDatePlaceholder" = "Expiry Date"; +"Passport.Identity.ExpiryDateNone" = "None"; +"Passport.Identity.DoesNotExpire" = "Does Not Expire"; + +"Passport.Identity.FilesTitle" = "REQUESTED FILES"; +"Passport.Identity.ScansHelp" = "The document must contain your photograph, first and last name, date of birth, document number, country of issue, and expiry date."; +"Passport.Identity.FilesView" = "View"; +"Passport.Identity.FilesUploadNew" = "Upload New"; +"Passport.Identity.MainPage" = "Main Page"; +"Passport.Identity.MainPageHelp" = "Upload a main page photo of the document"; +"Passport.Identity.FrontSide" = "Front Side"; +"Passport.Identity.FrontSideHelp" = "Upload a front side photo of the document"; +"Passport.Identity.ReverseSide" = "Reverse Side"; +"Passport.Identity.ReverseSideHelp" = "Upload a reverse side photo of the document"; +"Passport.Identity.Selfie" = "Selfie"; +"Passport.Identity.SelfieHelp" = "Upload a selfie holding this document"; +"Passport.Identity.Translation" = "Translation"; +"Passport.Identity.TranslationHelp" = "Upload a translation of this document"; + +"Passport.Address.TypeResidentialAddress" = "Residential Address"; +"Passport.Address.TypePassportRegistration" = "Passport Registration"; +"Passport.Address.TypeUtilityBill" = "Utility Bill"; +"Passport.Address.TypeBankStatement" = "Bank Statement"; +"Passport.Address.TypeRentalAgreement" = "Tenancy Agreement"; +"Passport.Address.TypeTemporaryRegistration" = "Temporary Registration"; + +"Passport.Address.AddResidentialAddress" = "Add Residential Address"; +"Passport.Address.AddPassportRegistration" = "Add Passport Registration"; +"Passport.Address.AddUtilityBill" = "Add Utility Bill"; +"Passport.Address.AddBankStatement" = "Add Bank Statement"; +"Passport.Address.AddRentalAgreement" = "Add Tenancy Agreement"; +"Passport.Address.AddTemporaryRegistration" = "Add Temporary Registration"; + +"Passport.Address.EditResidentialAddress" = "Edit Residential Address"; +"Passport.Address.EditPassportRegistration" = "Edit Passport Registration"; +"Passport.Address.EditUtilityBill" = "Edit Utility Bill"; +"Passport.Address.EditBankStatement" = "Edit Bank Statement"; +"Passport.Address.EditRentalAgreement" = "Edit Tenancy Agreement"; +"Passport.Address.EditTemporaryRegistration" = "Edit Temporary Registration"; + +"Passport.Address.Address" = "ADDRESS"; +"Passport.Address.Street" = "Street"; +"Passport.Address.Street1Placeholder" = "Street and number, P.O. box"; +"Passport.Address.Street2Placeholder" = "Apt., suite, unit, building, floor"; +"Passport.Address.Postcode" = "Postcode"; +"Passport.Address.PostcodePlaceholder" = "Postcode"; +"Passport.Address.City" = "City"; +"Passport.Address.CityPlaceholder" = "City"; +"Passport.Address.Region" = "Region"; +"Passport.Address.RegionPlaceholder" = "State / Province / Region"; +"Passport.Address.Country" = "Country"; +"Passport.Address.CountryPlaceholder" = "Country"; + +"Passport.Address.ScansHelp" = "The document must contain your first and last name, your residential address, a stamp / barcode / QR code / logo, and issue date, no more than 3 months ago."; + +"Passport.Phone.Title" = "Phone Number"; +"Passport.Phone.UseTelegramNumber" = "Use %@"; +"Passport.Phone.UseTelegramNumberHelp" = "Use the same phone number as on Telegram."; +"Passport.Phone.EnterOtherNumber" = "OR ENTER NEW PHONE NUMBER"; +"Passport.Phone.Help" = "Note: You will receive a confirmation code on the phone number you provide."; +"Passport.Phone.Delete" = "Delete Phone Number"; + +"Passport.Email.Title" = "Email"; +"Passport.Email.UseTelegramEmail" = "Use %@"; +"Passport.Email.UseTelegramEmailHelp" = "Use the same address as on Telegram."; +"Passport.Email.EnterOtherEmail" = "OR ENTER NEW EMAIL ADDRESS"; +"Passport.Email.EmailPlaceholder" = "Enter your email address"; +"Passport.Email.Help" = "Note: You will receive a confirmation code to the email address you provide."; +"Passport.Email.Delete" = "Delete Email Address"; +"Passport.Email.CodeHelp" = "Please enter the confirmation code we've just sent to %@"; + +"Notification.PassportValuesSentMessage" = "%1$@ received the following documents: %2$@"; +"Notification.PassportValuePersonalDetails" = "personal details"; +"Notification.PassportValueProofOfIdentity" = "proof of identity"; +"Notification.PassportValueAddress" = "your address"; +"Notification.PassportValueProofOfAddress" = "proof of address"; +"Notification.PassportValuePhone" = "phone number"; +"Notification.PassportValueEmail" = "email address"; + +"FastTwoStepSetup.HintSection" = "HINT"; +"FastTwoStepSetup.HintPlaceholder" = "Enter a hint"; +"FastTwoStepSetup.HintHelp" = "Please create an optional hint for your password."; + +"Passport.DiscardMessageTitle" = "Discard Changes"; +"Passport.DiscardMessageDescription" = "Are you sure you want to discard all changes?"; +"Passport.DiscardMessageAction" = "Discard"; + +"Passport.ScanPassport" = "Scan Your Passport"; +"Passport.ScanPassportHelp" = "Scan your passport or identity card with machine-readable zone to fill personal details automatically."; + +"TwoStepAuth.PasswordRemovePassportConfirmation" = "Are you sure you want to disable your password?\n\nWarning! All data saved in your Telegram Passport will be lost!"; + +"Application.Update" = "Update"; + +"Conversation.EditingMessagePanelMedia" = "Tap to edit media"; +"Conversation.EditingMessageMediaChange" = "Change Photo or Video"; +"Conversation.EditingMessageMediaEditCurrentPhoto" = "Edit Current Photo"; +"Conversation.EditingMessageMediaEditCurrentVideo" = "Edit Current Video"; + +"Conversation.InputTextCaptionPlaceholder" = "Caption"; + +"Conversation.ViewContactDetails" = "VIEW CONTACT"; + +"DialogList.Read" = "Read"; +"DialogList.Unread" = "Unread"; + +"ContactInfo.Title" = "Contact Info"; +"ContactInfo.PhoneLabelHome" = "home"; +"ContactInfo.PhoneLabelWork" = "work"; +"ContactInfo.PhoneLabelMobile" = "mobile"; +"ContactInfo.PhoneLabelMain" = "main"; +"ContactInfo.PhoneLabelHomeFax" = "home fax"; +"ContactInfo.PhoneLabelWorkFax" = "work fax"; +"ContactInfo.PhoneLabelPager" = "pager"; +"ContactInfo.PhoneLabelOther" = "other"; +"ContactInfo.URLLabelHomepage" = "homepage"; +"ContactInfo.BirthdayLabel" = "birthday"; +"ContactInfo.Job" = "job"; + +"UserInfo.NotificationsDefault" = "Default"; +"UserInfo.NotificationsDefaultSound" = "Default (%@)"; + +"DialogList.ProxyConnectionIssuesTooltip" = "Can’t connect to your preferred proxy.\nTap to change settings."; + +"Conversation.TapAndHoldToRecord" = "Tap and hold to record"; + +"Privacy.TopPeers" = "Suggest Frequent Contacts"; +"Privacy.TopPeersHelp" = "Display people you message frequently at the top of the search section for quick access."; +"Privacy.TopPeersWarning" = "This will delete all data about the people you message frequently as well the inline bots you are likely to use."; +"Privacy.TopPeersDelete" = "Delete"; + +"Conversation.EditingCaptionPanelTitle" = "Edit Caption"; + +"Passport.CorrectErrors" = "Tap to correct errors"; + +"Passport.NotLoggedInMessage" = "Please log in to your account to use Telegram Passport"; + +"Update.Title" = "Telegram Update"; +"Update.AppVersion" = "Telegram %@"; +"Update.UpdateApp" = "Update Telegram"; +"Update.Skip" = "Skip"; + +"ReportPeer.ReasonCopyright" = "Copyright"; + +"PrivacySettings.DataSettings" = "Data Settings"; +"PrivacySettings.DataSettingsHelp" = "Control which of your data is stored in the cloud and used by Telegram to enable advanced features."; + +"PrivateDataSettings.Title" = "Data Settings"; +"Privacy.ChatsTitle" = "CHATS"; +"Privacy.DeleteDrafts" = "Delete All Cloud Drafts"; + +"UserInfo.NotificationsDefaultEnabled" = "Default (Enabled)"; +"UserInfo.NotificationsDefaultDisabled" = "Default (Disabled)"; + +"Notifications.MessageNotificationsExceptions" = "Exceptions"; +"Notifications.GroupNotificationsExceptions" = "Exceptions"; + +"Notifications.ExceptionsNone" = "None"; +"Notifications.Exceptions_1" = "%@ chat"; +"Notifications.Exceptions_2" = "%@ chats"; +"Notifications.Exceptions_3_10" = "%@ chats"; +"Notifications.Exceptions_any" = "%@ chats"; +"Notifications.Exceptions_many" = "%@ chats"; +"Notifications.Exceptions_0" = "%@ chats"; + +"Notifications.ExceptionMuteExpires.Minutes_1" = "In 1 minute"; +"Notifications.ExceptionMuteExpires.Minutes_2" = "In 2 minutes"; +"Notifications.ExceptionMuteExpires.Minutes_3_10" = "In %@ minutes"; +"Notifications.ExceptionMuteExpires.Minutes_any" = "In %@ minutes"; +"Notifications.ExceptionMuteExpires.Minutes_many" = "In %@ minutes"; +"Notifications.ExceptionMuteExpires.Minutes_0" = "In %@ minutes"; + +"Notifications.ExceptionMuteExpires.Hours_1" = "In 1 hour"; +"Notifications.ExceptionMuteExpires.Hours_2" = "In 2 hours"; +"Notifications.ExceptionMuteExpires.Hours_3_10" = "In %@ hours"; +"Notifications.ExceptionMuteExpires.Hours_any" = "In %@ hours"; +"Notifications.ExceptionMuteExpires.Hours_many" = "In %@ hours"; +"Notifications.ExceptionMuteExpires.Hours_0" = "In %@ hours"; + +"Notifications.ExceptionMuteExpires.Days_1" = "In 1 day"; +"Notifications.ExceptionMuteExpires.Days_2" = "In 2 days"; +"Notifications.ExceptionMuteExpires.Days_3_10" = "In %@ days"; +"Notifications.ExceptionMuteExpires.Days_any" = "In %@ days"; +"Notifications.ExceptionMuteExpires.Days_many" = "In %@ days"; +"Notifications.ExceptionMuteExpires.Days_0" = "In %@ days"; + +"Notifications.ExceptionsTitle" = "Exceptions"; +"Notifications.ExceptionsChangeSound" = "Change Sound (%@)"; +"Notifications.ExceptionsDefaultSound" = "Default"; +"Notifications.ExceptionsMuted" = "Muted"; +"Notifications.ExceptionsUnmuted" = "Unmuted"; +"Notifications.AddExceptionTitle" = "Add Exception"; + +"Notifications.ExceptionsMessagePlaceholder" = "This section will list all private chats with non-default notification settings."; +"Notifications.ExceptionsGroupPlaceholder" = "This section will list all groups and channels with non-default notification settings."; + +"Passport.Identity.LatinNameHelp" = "Enter your name using the Latin alphabet"; +"Passport.Identity.NativeNameTitle" = "YOUR NAME IN %@"; +"Passport.Identity.NativeNameGenericTitle" = "NAME IN DOCUMENT LANGUAGE"; +"Passport.Identity.NativeNameHelp" = "Your name in the language of the country that issued the document."; +"Passport.Identity.NativeNameGenericHelp" = "Your name in the language of the country (%@) that issued the document."; + +"Passport.Identity.Translations" = "TRANSLATION"; +"Passport.Identity.TranslationsHelp" = "Upload scans of verified translation of the document."; +"Passport.FieldIdentityTranslationHelp" = "Upload a translation of your document"; +"Passport.FieldAddressTranslationHelp" = "Upload a translation of your document"; + +"Passport.FieldOneOf.Or" = "%1$@ or %2$@"; +"Passport.Identity.UploadOneOfScan" = "Upload a scan of your %@"; +"Passport.Address.UploadOneOfScan" = "Upload a scan of your %@"; + +"Passport.Address.TypeUtilityBillUploadScan" = "Upload a scan of your utiliity bill"; +"Passport.Address.TypeBankStatementUploadScan" = "Upload a scan of your bank statement"; +"Passport.Address.TypeRentalAgreementUploadScan" = "Upload a scan of your tenancy agreement"; +"Passport.Address.TypePassportRegistrationUploadScan" = "Upload a scan of your passport registration"; +"Passport.Address.TypeTemporaryRegistrationUploadScan" = "Upload a scan of your temporary registration"; + +"Passport.Identity.OneOfTypePassport" = "passport"; +"Passport.Identity.OneOfTypeInternalPassport" = "internal passport"; +"Passport.Identity.OneOfTypeIdentityCard" = "identity card"; +"Passport.Identity.OneOfTypeDriversLicense" = "driver's license"; + +"Passport.Address.OneOfTypePassportRegistration" = "passport registration"; +"Passport.Address.OneOfTypeUtilityBill" = "utility bill"; +"Passport.Address.OneOfTypeBankStatement" = "bank statement"; +"Passport.Address.OneOfTypeRentalAgreement" = "tenancy agreement"; +"Passport.Address.OneOfTypeTemporaryRegistration" = "temporary registration"; + +"Passport.FieldOneOf.Delimeter" = ", "; +"Passport.FieldOneOf.FinalDelimeter" = " or "; + +"Passport.Scans_1" = "%@ scan"; +"Passport.Scans_2" = "%@ scans"; +"Passport.Scans_3_10" = "%@ scans"; +"Passport.Scans_any" = "%@ scans"; +"Passport.Scans_many" = "%@ scans"; +"Passport.Scans_0" = "%@ scans"; + +"NotificationsSound.None" = "None"; +"NotificationsSound.Note" = "Note"; +"NotificationsSound.Aurora" = "Aurora"; +"NotificationsSound.Bamboo" = "Bamboo"; +"NotificationsSound.Chord" = "Chord"; +"NotificationsSound.Circles" = "Circles"; +"NotificationsSound.Complete" = "Complete"; +"NotificationsSound.Hello" = "Hello"; +"NotificationsSound.Input" = "Input"; +"NotificationsSound.Keys" = "Keys"; +"NotificationsSound.Popcorn" = "Popcorn"; +"NotificationsSound.Pulse" = "Pulse"; +"NotificationsSound.Synth" = "Synth"; + +"NotificationsSound.Tritone" = "Tri-tone"; +"NotificationsSound.Tremolo" = "Tremolo"; +"NotificationsSound.Alert" = "Alert"; +"NotificationsSound.Bell" = "Bell"; +"NotificationsSound.Calypso" = "Calypso"; +"NotificationsSound.Chime" = "Chime"; +"NotificationsSound.Glass" = "Glass"; +"NotificationsSound.Telegraph" = "Telegraph"; + +"Settings.CopyPhoneNumber" = "Copy Phone Number"; +"Settings.CopyUsername" = "Copy Username"; + +"Passport.Language.ar" = "Arabic"; +"Passport.Language.az" = "Azerbaijani"; +"Passport.Language.bg" = "Bulgarian"; +"Passport.Language.bn" = "Bangla"; +"Passport.Language.cs" = "Czech"; +"Passport.Language.da" = "Danish"; +"Passport.Language.de" = "German"; +"Passport.Language.dv" = "Divehi"; +"Passport.Language.dz" = "Dzongkha"; +"Passport.Language.el" = "Greek"; +"Passport.Language.en" = "English"; +"Passport.Language.es" = "Spanish"; +"Passport.Language.et" = "Estonian"; +"Passport.Language.fa" = "Persian"; +"Passport.Language.fr" = "French"; +"Passport.Language.he" = "Hebrew"; +"Passport.Language.hr" = "Croatian"; +"Passport.Language.hu" = "Hungarian"; +"Passport.Language.hy" = "Armenian"; +"Passport.Language.id" = "Indonesian"; +"Passport.Language.is" = "Icelandic"; +"Passport.Language.it" = "Italian"; +"Passport.Language.ja" = "Japanese"; +"Passport.Language.ka" = "Georgian"; +"Passport.Language.km" = "Khmer"; +"Passport.Language.ko" = "Korean"; +"Passport.Language.lo" = "Lao"; +"Passport.Language.lt" = "Lithuanian"; +"Passport.Language.lv" = "Latvian"; +"Passport.Language.mk" = "Macedonian"; +"Passport.Language.mn" = "Mongolian"; +"Passport.Language.ms" = "Malay"; +"Passport.Language.my" = "Burmese"; +"Passport.Language.ne" = "Nepali"; +"Passport.Language.nl" = "Dutch"; +"Passport.Language.pl" = "Polish"; +"Passport.Language.pt" = "Portuguese"; +"Passport.Language.ro" = "Romanian"; +"Passport.Language.ru" = "Russian"; +"Passport.Language.sk" = "Slovak"; +"Passport.Language.sl" = "Slovenian"; +"Passport.Language.th" = "Thai"; +"Passport.Language.tk" = "Turkmen"; +"Passport.Language.tr" = "Turkish"; +"Passport.Language.uk" = "Ukrainian"; +"Passport.Language.uz" = "Uzbek"; +"Passport.Language.vi" = "Vietnamese"; + +"Conversation.EmptyGifPanelPlaceholder" = "You have no saved GIFs yet.\nEnter @gif to search."; +"DialogList.MultipleTyping" = "%@ and %@"; +"Contacts.NotRegisteredSection" = "Phonebook"; + +"SocksProxySetup.PasteFromClipboard" = "Paste From Clipboard"; + +"Share.AuthTitle" = "Log in to Telegram"; +"Share.AuthDescription" = "Open Telegram and log in to share."; + +"Notifications.DisplayNamesOnLockScreen" = "Names on lock-screen"; +"Notifications.DisplayNamesOnLockScreenInfo" = "Display names in notifications on the lock screen."; + +"Notifications.Badge" = "BADGE COUNTER"; +"Notifications.Badge.IncludeMutedChats" = "Include Muted Chats"; +"Notifications.Badge.IncludePublicGroups" = "Include Public Groups"; +"Notifications.Badge.IncludeChannels" = "Include Channels"; +"Notifications.Badge.CountUnreadMessages" = "Count Unread Messages"; +"Notifications.Badge.CountUnreadMessages.InfoOff" = "Switch on to show the number of unread messages instead of chats."; +"Notifications.Badge.CountUnreadMessages.InfoOn" = "Switch off to show the number of unread chats instead of messages."; + +"Appearance.ReduceMotion" = "Reduce Motion"; +"Appearance.ReduceMotionInfo" = "Disable animations in message bubbles and in the chats list."; + +"Appearance.Animations" = "ANIMATIONS"; + +"Weekday.Monday" = "Monday"; +"Weekday.Tuesday" = "Tuesday"; +"Weekday.Wednesday" = "Wednesday"; +"Weekday.Thursday" = "Thursday"; +"Weekday.Friday" = "Friday"; +"Weekday.Saturday" = "Saturday"; +"Weekday.Sunday" = "Sunday"; + +"Watch.Message.Call" = "Call"; +"Watch.Message.Game" = "Game"; +"Watch.Message.Invoice" = "Invoice"; + +"Notifications.ExceptionsResetToDefaults" = "Reset to Defaults"; + +"AuthSessions.IncompleteAttempts" = "INCOMPLETE LOGIN ATTEMPTS"; +"AuthSessions.IncompleteAttemptsInfo" = "These devices have no access to your account. The code was entered correctly, but no correct password was given."; + +"AuthSessions.Terminate" = "Terminate"; + +"ApplyLanguage.ChangeLanguageAlreadyActive" = "The language %1$@ is already active."; +"ApplyLanguage.ChangeLanguageTitle" = "Change Language?"; +"ApplyLanguage.ChangeLanguageUnofficialText" = "You are about to apply a custom language pack **%1$@** that is %2$@% complete.\n\nThis will translate the entire interface. You can suggest corrections in the [translation panel]().\n\nYou can change your language back at any time in Settings."; +"ApplyLanguage.ChangeLanguageOfficialText" = "You are about to apply a language pack **%1$@**.\n\nThis will translate the entire interface. You can suggest corrections in the [translation panel]().\n\nYou can change your language back at any time in Settings."; +"ApplyLanguage.ChangeLanguageAction" = "Change"; +"ApplyLanguage.ApplyLanguageAction" = "Change"; +"ApplyLanguage.UnsufficientDataTitle" = "Insufficient Data"; +"ApplyLanguage.UnsufficientDataText" = "Unfortunately, this custom language pack (%1$@) doesn't conain data for Telegram iOS. You can contribute to this language pack using the [translations platform]()"; +"ApplyLanguage.LanguageNotSupportedError" = "Sorry, this language doesn't seem to exist."; +"ApplyLanguage.ApplySuccess" = "Language changed"; + +"TextFormat.Bold" = "Bold"; +"TextFormat.Italic" = "Italic"; +"TextFormat.Monospace" = "Monospace"; + +"TwoStepAuth.SetupPasswordTitle" = "Create a Password"; +"TwoStepAuth.SetupPasswordDescription" = "Please create a password which will be used to protect your data."; +"TwoStepAuth.ChangePassword" = "Change Password"; +"TwoStepAuth.ChangePasswordDescription" = "Please enter a new password which will be used to protect your data."; +"TwoStepAuth.ReEnterPasswordTitle" = "Re-enter your Password"; +"TwoStepAuth.ReEnterPasswordDescription" = "Please confirm your password."; +"TwoStepAuth.AddHintTitle" = "Add a Hint"; +"TwoStepAuth.AddHintDescription" = "You can create an optional hint for your password."; +"TwoStepAuth.HintPlaceholder" = "Hint"; +"TwoStepAuth.RecoveryEmailTitle" = "Recovery Email"; +"TwoStepAuth.RecoveryEmailAddDescription" = "Please add your valid e-mail. It is the only way to recover a forgotten password."; +"TwoStepAuth.RecoveryEmailChangeDescription" = "Please enter your new recovery email. It is the only way to recover a forgotten password."; +"TwoStepAuth.ChangeEmail" = "Change Email"; +"TwoStepAuth.ConfirmEmailDescription" = "Please enter the code we've just emailed at %1$@."; +"TwoStepAuth.ConfirmEmailCodePlaceholder" = "Code"; +"TwoStepAuth.ConfirmEmailResendCode" = "Resend Code"; + +"TwoStepAuth.SetupPendingEmail" = "Your recovery email %@ needs to be confirmed and is not yet active.\n\nPlease check your email and enter the confirmation code to complete Two-Step Verification setup. Be sure to check the spam folder as well."; +"TwoStepAuth.SetupResendEmailCode" = "Resend Code"; +"TwoStepAuth.SetupResendEmailCodeAlert" = "The code has been sent. Please check your e-mail. Be sure to check the spam folder as well."; +"TwoStepAuth.EnterEmailCode" = "Enter Code"; + +"TwoStepAuth.EnabledSuccess" = "Two-Step verification\nis enabled."; +"TwoStepAuth.DisableSuccess" = "Two-Step verification\nis disabled."; +"TwoStepAuth.PasswordChangeSuccess" = "Your password\nhas been changed."; +"TwoStepAuth.EmailAddSuccess" = "Your recovery e-mail\nhas been added."; +"TwoStepAuth.EmailChangeSuccess" = "Your recovery e-mail\nhas been changed."; + +"Conversation.SendMessageErrorGroupRestricted" = "Sorry, you are currently restricted from posting to public groups."; + +"InstantPage.TapToOpenLink" = "Tap to open the link:"; +"InstantPage.RelatedArticleAuthorAndDateTitle" = "%1$@ • %2$@"; + diff --git a/Telegram-iOS/es.lproj/AppIntentVocabulary.plist b/Telegram-iOS/es.lproj/AppIntentVocabulary.plist new file mode 100644 index 0000000000..fd11102f14 --- /dev/null +++ b/Telegram-iOS/es.lproj/AppIntentVocabulary.plist @@ -0,0 +1,17 @@ + + + + + IntentPhrases + + + IntentName + INSendMessageIntent + IntentExamples + + Envía un mensaje de Telegram a Alicia diciéndole que estarás allí en 15 minutos + + + + + diff --git a/Telegram-iOS/es.lproj/InfoPlist.strings b/Telegram-iOS/es.lproj/InfoPlist.strings new file mode 100644 index 0000000000..54c7b1d54b --- /dev/null +++ b/Telegram-iOS/es.lproj/InfoPlist.strings @@ -0,0 +1,14 @@ +/* Localized versions of Info.plist keys */ + +"NSContactsUsageDescription" = "Telegram subirá continuamente tus contactos a sus servidores fuertemente cifrados, para permitirte interactuar con tus amigos en todos tus dispositivos."; +"NSLocationWhenInUseUsageDescription" = "Cuando envías tu ubicación a tus amigos, Telegram necesita acceso para mostrarles un mapa."; +"NSLocationAlwaysUsageDescription" = "Cuando envías tu ubicación a tus amigos, Telegram necesita acceso para mostrarles un mapa. También es requerido para enviar ubicaciones desde un Apple Watch."; +"NSCameraUsageDescription" = "Es requerido para que puedas hacer fotos y vídeos."; +"NSPhotoLibraryUsageDescription" = "Es requerido para que puedas compartir fotos y vídeos desde tu biblioteca de fotos."; +"NSMicrophoneUsageDescription" = "Es requerido para que puedas grabar y compartir mensajes de voz y vídeos con sonido."; +"NSSiriUsageDescription" = "Puedes usar Siri para enviar mensajes."; + +"NSLocationAlwaysAndWhenInUseUsageDescription" = "Cuando eliges compartir tu ubicación en tiempo real con amigos en un chat, Telegram necesita acceso en segundo plano a tu ubicación para mantenerla actualizada mientras la función esté en uso."; +"NSLocationAlwaysUsageDescription" = "Cuando envías tu ubicación a tus amigos, Telegram necesita acceso para mostrarles un mapa. También es requerido para enviar ubicaciones desde un Apple Watch."; +"NSLocationWhenInUseUsageDescription" = "Cuando envías tu ubicación a tus amigos, Telegram necesita acceso para mostrarles un mapa."; + diff --git a/Telegram-iOS/es.lproj/Localizable.strings b/Telegram-iOS/es.lproj/Localizable.strings new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Telegram-iOS/it.lproj/AppIntentVocabulary.plist b/Telegram-iOS/it.lproj/AppIntentVocabulary.plist new file mode 100644 index 0000000000..8710a6c624 --- /dev/null +++ b/Telegram-iOS/it.lproj/AppIntentVocabulary.plist @@ -0,0 +1,17 @@ + + + + + IntentPhrases + + + IntentName + INSendMessageIntent + IntentExamples + + Invia un messaggio su Telegram (Telegramma) ad Alex dicendo che sarò lì tra 10 minuti + + + + + diff --git a/Telegram-iOS/it.lproj/InfoPlist.strings b/Telegram-iOS/it.lproj/InfoPlist.strings new file mode 100644 index 0000000000..773512d3bf --- /dev/null +++ b/Telegram-iOS/it.lproj/InfoPlist.strings @@ -0,0 +1,14 @@ +/* Localized versions of Info.plist keys */ + +"NSContactsUsageDescription" = "Telegram caricherà continuamente i tuoi contatti sui suoi server cloud altamente criptati per farti connettere con i tuoi amici da tutti i tuoi dispositivi."; +"NSLocationWhenInUseUsageDescription" = "Quando invii la tua posizione ai tuoi amici, Telegram ha bisogno di accedere per mostrare loro la mappa."; +"NSLocationAlwaysUsageDescription" = "Quando invii la tua posizione ai tuoi amici, Telegram ha bisogno di accedere per mostrare loro la mappa. Ti serve anche per inviare posizioni da Apple Watch."; +"NSCameraUsageDescription" = "Ci serve per farti scattare, registrare e condividere foto e video."; +"NSPhotoLibraryUsageDescription" = "Ci serve per farti condividere foto e video dalla tua libreria foto."; +"NSMicrophoneUsageDescription" = "Ci serve per farti registrare e condividere messaggi vocali e video con il sonoro."; +"NSSiriUsageDescription" = "Puoi usare Siri per inviare messaggi."; + +"NSLocationAlwaysAndWhenInUseUsageDescription" = "Quando scegli di condividere la tua Posizione Attuale con gli amici in una chat, Telegram ha bisogno dell'accesso in background alla tua posizione per tenerli aggiornati durante la durata della condivisione della posizione."; +"NSLocationAlwaysUsageDescription" = "Quando invii la tua posizione ai tuoi amici, Telegram ha bisogno di accedere per mostrare loro la mappa. Ti serve anche per inviare posizioni da Apple Watch."; +"NSLocationWhenInUseUsageDescription" = "Quando invii la tua posizione ai tuoi amici, Telegram ha bisogno di accedere per mostrare loro la mappa."; + diff --git a/Telegram-iOS/it.lproj/Localizable.strings b/Telegram-iOS/it.lproj/Localizable.strings new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Telegram-iOS/ko.lproj/AppIntentVocabulary.plist b/Telegram-iOS/ko.lproj/AppIntentVocabulary.plist new file mode 100644 index 0000000000..932e5f6d28 --- /dev/null +++ b/Telegram-iOS/ko.lproj/AppIntentVocabulary.plist @@ -0,0 +1,17 @@ + + + + + IntentPhrases + + + IntentName + INSendMessageIntent + IntentExamples + + 앨리스에게 나 15분 안에 도착한다고 Telegram 메시지 보내줘 + + + + + diff --git a/Telegram-iOS/ko.lproj/InfoPlist.strings b/Telegram-iOS/ko.lproj/InfoPlist.strings new file mode 100644 index 0000000000..33a523ce44 --- /dev/null +++ b/Telegram-iOS/ko.lproj/InfoPlist.strings @@ -0,0 +1,16 @@ +/* Localized versions of Info.plist keys */ + +"CFBundleDisplayName" = "텔레그램"; + +"NSContactsUsageDescription" = "Telegram will continuously upload your contacts to its heavily encrypted cloud servers to let you connect with your friends across all your devices."; +"NSLocationWhenInUseUsageDescription" = "친구에게 회원님의 위치를 전송할 경우 위치를 지도에 표시하기 위해 텔레그램이 위치 정보에 접근할 수 있어야 합니다."; +"NSLocationAlwaysUsageDescription" = "친구에게 회원님의 위치를 전송할 경우 위치를 지도에 표시하기 위해 텔레그램이 위치 정보에 접근할 수 있어야 합니다. 애플워치에 위치 전송을 위해서도 필요합니다."; +"NSCameraUsageDescription" = "사진과 비디오 촬영을 위하여 필요합니다."; +"NSPhotoLibraryUsageDescription" = "촬영한 사진과 비디오를 공유하기 위하여 필요합니다."; +"NSMicrophoneUsageDescription" = "음성메시지와 비디오 촬영시 음성 녹음을 위하여 필요합니다."; +"NSSiriUsageDescription" = "시리를 통하여 메시지 전송이 가능합니다."; + +"NSLocationAlwaysAndWhenInUseUsageDescription" = "When you choose to share your Live Location with friends in a chat, Telegram needs background access to your location to keep them updated for the duration of the live sharing."; +"NSLocationAlwaysUsageDescription" = "친구에게 회원님의 위치를 전송할 경우 위치를 지도에 표시하기 위해 텔레그램이 위치 정보에 접근할 수 있어야 합니다. 애플워치에 위치 전송을 위해서도 필요합니다."; +"NSLocationWhenInUseUsageDescription" = "친구에게 회원님의 위치를 전송할 경우 위치를 지도에 표시하기 위해 텔레그램이 위치 정보에 접근할 수 있어야 합니다."; + diff --git a/Telegram-iOS/ko.lproj/Localizable.strings b/Telegram-iOS/ko.lproj/Localizable.strings new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Telegram-iOS/main.m b/Telegram-iOS/main.m new file mode 100644 index 0000000000..81d976a847 --- /dev/null +++ b/Telegram-iOS/main.m @@ -0,0 +1,7 @@ +#import + +int main(int argc, char *argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, @"Application", @"AppDelegate"); + } +} diff --git a/Telegram-iOS/nl.lproj/AppIntentVocabulary.plist b/Telegram-iOS/nl.lproj/AppIntentVocabulary.plist new file mode 100644 index 0000000000..5c709b84ee --- /dev/null +++ b/Telegram-iOS/nl.lproj/AppIntentVocabulary.plist @@ -0,0 +1,17 @@ + + + + + IntentPhrases + + + IntentName + INSendMessageIntent + IntentExamples + + Stuur een Telegram-bericht naar Maartje met ik ben er over 15 minuten. + + + + + diff --git a/Telegram-iOS/nl.lproj/InfoPlist.strings b/Telegram-iOS/nl.lproj/InfoPlist.strings new file mode 100644 index 0000000000..2f2bf97fa2 --- /dev/null +++ b/Telegram-iOS/nl.lproj/InfoPlist.strings @@ -0,0 +1,14 @@ +/* Localized versions of Info.plist keys */ + +"NSContactsUsageDescription" = "Telegram synchroniseert je contacten continu naar onze zwaar versleutelde Cloud-servers, zodat je contact kunt opnemen met je vrienden vanaf al je apparaten."; +"NSLocationWhenInUseUsageDescription" = "Als je je locatie met vrienden wilt delen heeft Telegram toegang nodig om ze een kaart te kunnen tonen."; +"NSLocationAlwaysUsageDescription" = "Telegram heeft toegang nodig om een kaart aan je vrienden te tonen als je jouw locatie met ze deelt. Je hebt dit ook nodig om locaties te sturen vanaf een Apple Watch."; +"NSCameraUsageDescription" = "We hebben dit nodig zodat je foto's en video's kunt maken en delen."; +"NSPhotoLibraryUsageDescription" = "We hebben dit nodig zodat je foto's en video's kunt delen vanuit je fotobibliotheek."; +"NSMicrophoneUsageDescription" = "We hebben dit nodig zodat je spraakberichten en video's met geluid kunt opnemen en delen."; +"NSSiriUsageDescription" = "Je kunt Siri gebruiken om berichten te sturen"; + +"NSLocationAlwaysAndWhenInUseUsageDescription" = "Als je ervoor kiest om je huidige locatie te delen met vrienden in een chat heeft Telegram achtergrondtoegang tot je locatie nodig om deze bij te werken tijdens het live-delen."; +"NSLocationAlwaysUsageDescription" = "Telegram heeft toegang nodig om een kaart aan je vrienden te tonen als je jouw locatie met ze deelt. Je hebt dit ook nodig om locaties te sturen vanaf een Apple Watch."; +"NSLocationWhenInUseUsageDescription" = "Als je je locatie met vrienden wilt delen heeft Telegram toegang nodig om ze een kaart te kunnen tonen."; + diff --git a/Telegram-iOS/nl.lproj/Localizable.strings b/Telegram-iOS/nl.lproj/Localizable.strings new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Telegram-iOS/pt.lproj/AppIntentVocabulary.plist b/Telegram-iOS/pt.lproj/AppIntentVocabulary.plist new file mode 100644 index 0000000000..018471dbc6 --- /dev/null +++ b/Telegram-iOS/pt.lproj/AppIntentVocabulary.plist @@ -0,0 +1,17 @@ + + + + + IntentPhrases + + + IntentName + INSendMessageIntent + IntentExamples + + Enviar uma mensagem no Telegram para a Alice dizendo que eu chegarei lá em 15 minutos + + + + + diff --git a/Telegram-iOS/pt.lproj/InfoPlist.strings b/Telegram-iOS/pt.lproj/InfoPlist.strings new file mode 100644 index 0000000000..52d513f9fa --- /dev/null +++ b/Telegram-iOS/pt.lproj/InfoPlist.strings @@ -0,0 +1,14 @@ +/* Localized versions of Info.plist keys */ + +"NSContactsUsageDescription" = "Telegram atualizará continuamente os seus contatos em servidores na nuvem fortemente criptografados para que você se conecte com seus amigos através de todos os seus dispositivos."; +"NSLocationWhenInUseUsageDescription" = "Para enviar sua localização para seus amigos o Telegram precisa de permissão para mostrá-los o mapa."; +"NSLocationAlwaysUsageDescription" = "Para enviar sua localização para seus amigos, o Telegram precisa de permissão para mostrá-los o mapa. Você também precisará disso para enviar localizações de um Apple Watch."; +"NSCameraUsageDescription" = "Precisamos acessar sua câmera para que você possa capturar fotos e vídeos."; +"NSPhotoLibraryUsageDescription" = "Precisamos disso para que você possa compartilhar fotos e vídeos de sua galeria."; +"NSMicrophoneUsageDescription" = "Precisamos disso para que você possa gravar mensagens de voz e vídeos com som."; +"NSSiriUsageDescription" = "Você pode usar a Siri para enviar mensagens."; + +"NSLocationAlwaysAndWhenInUseUsageDescription" = "Quando você escolhe compartilhar sua Localização em Tempo Real com amigos no chat, o Telegram precisa de acesso à sua localização em segundo plano para mantê-los atualizados durante o compartilhamento."; +"NSLocationAlwaysUsageDescription" = "Para enviar sua localização para seus amigos, o Telegram precisa de permissão para mostrá-los o mapa. Você também precisará disso para enviar localizações de um Apple Watch."; +"NSLocationWhenInUseUsageDescription" = "Para enviar sua localização para seus amigos o Telegram precisa de permissão para mostrá-los o mapa."; + diff --git a/Telegram-iOS/pt.lproj/Localizable.strings b/Telegram-iOS/pt.lproj/Localizable.strings new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Telegram-iOS/ru.lproj/AppIntentVocabulary.plist b/Telegram-iOS/ru.lproj/AppIntentVocabulary.plist new file mode 100644 index 0000000000..72fd63d738 --- /dev/null +++ b/Telegram-iOS/ru.lproj/AppIntentVocabulary.plist @@ -0,0 +1,17 @@ + + + + + IntentPhrases + + + IntentName + INSendMessageIntent + IntentExamples + + Отправить Алисе сообщение в Telegram: я буду через 10 минут + + + + + diff --git a/Telegram-iOS/ru.lproj/InfoPlist.strings b/Telegram-iOS/ru.lproj/InfoPlist.strings new file mode 100644 index 0000000000..4490f7aabf --- /dev/null +++ b/Telegram-iOS/ru.lproj/InfoPlist.strings @@ -0,0 +1,13 @@ +/* Localized versions of Info.plist keys */ + +"NSContactsUsageDescription" = "Актуальная информация о ваших контактах будет храниться зашифрованной в облаке Telegram, чтобы вы могли связаться с друзьями с любого устройства."; +"NSLocationWhenInUseUsageDescription" = "Когда вы отправляете друзьям геопозицию, Telegram нужно разрешение, чтобы показать им карту."; +"NSCameraUsageDescription" = "Это необходимо, чтобы вы могли делиться снятыми фотографиями и видео."; +"NSPhotoLibraryUsageDescription" = "Это необходимо, чтобы вы могли делиться фото и видео из библиотеки устройства."; +"NSPhotoLibraryAddUsageDescription" = "Это необходимо, чтобы вы могли сохранять фото и видео в библиотеку устройства."; +"NSMicrophoneUsageDescription" = "Это необходимо, чтобы вы могли делиться голосовыми сообщениями и видео со звуком."; +"NSSiriUsageDescription" = "Вы можете использовать Siri для отправки сообщений"; +"NSLocationAlwaysAndWhenInUseUsageDescription" = "Фоновый доступ к геопозиции требуется, чтобы обновлять вашу геопозицию, когда вы транслируете её в чат с друзьями. "; +"NSLocationAlwaysUsageDescription" = "Фоновый доступ к геопозиции требуется, чтобы обновлять вашу геопозицию, когда вы транслируете её в чат с друзьями. Он также необходим для отправки геопозиции с Apple Watch."; +"NSLocationWhenInUseUsageDescription" = "Когда вы отправляете друзьям геопозицию, Telegram нужно разрешение, чтобы показать им карту."; +"NSFaceIDUsageDescription" = "Вы можете разблокировать приложение с помощью Face ID."; diff --git a/Telegram-iOS/ru.lproj/Localizable.strings b/Telegram-iOS/ru.lproj/Localizable.strings new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Telegram-iOSTests/Info.plist b/Telegram-iOSTests/Info.plist new file mode 100644 index 0000000000..3d965abe01 --- /dev/null +++ b/Telegram-iOSTests/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 624 + + diff --git a/Telegram-iOSTests/ListViewTests.swift b/Telegram-iOSTests/ListViewTests.swift new file mode 100644 index 0000000000..c5da8cd790 --- /dev/null +++ b/Telegram-iOSTests/ListViewTests.swift @@ -0,0 +1,29 @@ +import XCTest +import Display + +class ListViewTests: XCTestCase { + var listView: ListView! + + override func setUp() { + super.setUp() + + // Put setup code here. This method is called before the invocation of each test method in the class. + + // In UI tests it is usually best to stop immediately when a failure occurs. + continueAfterFailure = false + // UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method. + XCUIApplication().launch() + + // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + super.tearDown() + } + + func testExample() { + // Use recording to get started writing UI tests. + // Use XCTAssert and related functions to verify your tests produce the correct results. + } +} diff --git a/Telegram-iOSTests/Telegram_iOSTests.swift b/Telegram-iOSTests/Telegram_iOSTests.swift new file mode 100644 index 0000000000..eb6bb07928 --- /dev/null +++ b/Telegram-iOSTests/Telegram_iOSTests.swift @@ -0,0 +1,36 @@ +// +// Telegram_iOSTests.swift +// Telegram-iOSTests +// +// Created by Peter on 10/06/15. +// Copyright (c) 2015 Telegram. All rights reserved. +// + +import UIKit +import XCTest + +class Telegram_iOSTests: XCTestCase { + + override func setUp() { + super.setUp() + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + super.tearDown() + } + + func testExample() { + // This is an example of a functional test case. + XCTAssert(true, "Pass") + } + + func testPerformanceExample() { + // This is an example of a performance test case. + self.measure { + // Put the code you want to measure the time of here. + } + } + +} diff --git a/Watch/App/Assets.xcassets/AppIcon.appiconset/CompanionIcon@2x.png b/Watch/App/Assets.xcassets/AppIcon.appiconset/CompanionIcon@2x.png new file mode 100644 index 0000000000..e90a582eee Binary files /dev/null and b/Watch/App/Assets.xcassets/AppIcon.appiconset/CompanionIcon@2x.png differ diff --git a/Watch/App/Assets.xcassets/AppIcon.appiconset/CompanionIcon@3x.png b/Watch/App/Assets.xcassets/AppIcon.appiconset/CompanionIcon@3x.png new file mode 100644 index 0000000000..fef48bc05e Binary files /dev/null and b/Watch/App/Assets.xcassets/AppIcon.appiconset/CompanionIcon@3x.png differ diff --git a/Watch/App/Assets.xcassets/AppIcon.appiconset/Contents.json b/Watch/App/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..9409bb80cc --- /dev/null +++ b/Watch/App/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,92 @@ +{ + "images" : [ + { + "size" : "24x24", + "idiom" : "watch", + "filename" : "NotificationIcon38@2x.png", + "scale" : "2x", + "role" : "notificationCenter", + "subtype" : "38mm" + }, + { + "size" : "27.5x27.5", + "idiom" : "watch", + "filename" : "NotificationIcon42@2x.png", + "scale" : "2x", + "role" : "notificationCenter", + "subtype" : "42mm" + }, + { + "size" : "29x29", + "idiom" : "watch", + "filename" : "CompanionIcon@2x.png", + "role" : "companionSettings", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "watch", + "filename" : "CompanionIcon@3x.png", + "role" : "companionSettings", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "watch", + "filename" : "Icon@2x.png", + "scale" : "2x", + "role" : "appLauncher", + "subtype" : "38mm" + }, + { + "size" : "44x44", + "idiom" : "watch", + "filename" : "Home88@2x.png", + "scale" : "2x", + "role" : "appLauncher", + "subtype" : "40mm" + }, + { + "size" : "50x50", + "idiom" : "watch", + "filename" : "Home100@2x.png", + "scale" : "2x", + "role" : "appLauncher", + "subtype" : "44mm" + }, + { + "size" : "86x86", + "idiom" : "watch", + "filename" : "ShortLook38@2x.png", + "scale" : "2x", + "role" : "quickLook", + "subtype" : "38mm" + }, + { + "size" : "98x98", + "idiom" : "watch", + "filename" : "ShortLook42@2x.png", + "scale" : "2x", + "role" : "quickLook", + "subtype" : "42mm" + }, + { + "size" : "108x108", + "idiom" : "watch", + "filename" : "ShortLook44@2x.png", + "scale" : "2x", + "role" : "quickLook", + "subtype" : "44mm" + }, + { + "size" : "1024x1024", + "idiom" : "watch-marketing", + "filename" : "T-Ipad_1024.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/AppIcon.appiconset/Home100@2x.png b/Watch/App/Assets.xcassets/AppIcon.appiconset/Home100@2x.png new file mode 100644 index 0000000000..9a05846398 Binary files /dev/null and b/Watch/App/Assets.xcassets/AppIcon.appiconset/Home100@2x.png differ diff --git a/Watch/App/Assets.xcassets/AppIcon.appiconset/Home88@2x.png b/Watch/App/Assets.xcassets/AppIcon.appiconset/Home88@2x.png new file mode 100644 index 0000000000..3d6be85c5f Binary files /dev/null and b/Watch/App/Assets.xcassets/AppIcon.appiconset/Home88@2x.png differ diff --git a/Watch/App/Assets.xcassets/AppIcon.appiconset/Icon@2x.png b/Watch/App/Assets.xcassets/AppIcon.appiconset/Icon@2x.png new file mode 100644 index 0000000000..9c3c5f0da3 Binary files /dev/null and b/Watch/App/Assets.xcassets/AppIcon.appiconset/Icon@2x.png differ diff --git a/Watch/App/Assets.xcassets/AppIcon.appiconset/NotificationIcon38@2x.png b/Watch/App/Assets.xcassets/AppIcon.appiconset/NotificationIcon38@2x.png new file mode 100644 index 0000000000..519faf6972 Binary files /dev/null and b/Watch/App/Assets.xcassets/AppIcon.appiconset/NotificationIcon38@2x.png differ diff --git a/Watch/App/Assets.xcassets/AppIcon.appiconset/NotificationIcon42@2x.png b/Watch/App/Assets.xcassets/AppIcon.appiconset/NotificationIcon42@2x.png new file mode 100644 index 0000000000..aba7a5cea2 Binary files /dev/null and b/Watch/App/Assets.xcassets/AppIcon.appiconset/NotificationIcon42@2x.png differ diff --git a/Watch/App/Assets.xcassets/AppIcon.appiconset/ShortLook38@2x.png b/Watch/App/Assets.xcassets/AppIcon.appiconset/ShortLook38@2x.png new file mode 100644 index 0000000000..f2b13939cc Binary files /dev/null and b/Watch/App/Assets.xcassets/AppIcon.appiconset/ShortLook38@2x.png differ diff --git a/Watch/App/Assets.xcassets/AppIcon.appiconset/ShortLook42@2x.png b/Watch/App/Assets.xcassets/AppIcon.appiconset/ShortLook42@2x.png new file mode 100644 index 0000000000..a9077f67e3 Binary files /dev/null and b/Watch/App/Assets.xcassets/AppIcon.appiconset/ShortLook42@2x.png differ diff --git a/Watch/App/Assets.xcassets/AppIcon.appiconset/ShortLook44@2x.png b/Watch/App/Assets.xcassets/AppIcon.appiconset/ShortLook44@2x.png new file mode 100644 index 0000000000..62fbc1ed28 Binary files /dev/null and b/Watch/App/Assets.xcassets/AppIcon.appiconset/ShortLook44@2x.png differ diff --git a/Watch/App/Assets.xcassets/AppIcon.appiconset/T-Ipad_1024.png b/Watch/App/Assets.xcassets/AppIcon.appiconset/T-Ipad_1024.png new file mode 100644 index 0000000000..31cfe6b631 Binary files /dev/null and b/Watch/App/Assets.xcassets/AppIcon.appiconset/T-Ipad_1024.png differ diff --git a/Watch/App/Assets.xcassets/BotCommandIcon.imageset/BotCommandIcon@2x.png b/Watch/App/Assets.xcassets/BotCommandIcon.imageset/BotCommandIcon@2x.png new file mode 100644 index 0000000000..cd5238fd84 Binary files /dev/null and b/Watch/App/Assets.xcassets/BotCommandIcon.imageset/BotCommandIcon@2x.png differ diff --git a/Watch/App/Assets.xcassets/BotCommandIcon.imageset/Contents.json b/Watch/App/Assets.xcassets/BotCommandIcon.imageset/Contents.json new file mode 100644 index 0000000000..1bf7aa6b2f --- /dev/null +++ b/Watch/App/Assets.xcassets/BotCommandIcon.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BotCommandIcon@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BotKeyboardIcon.imageset/BotKeyboardIcon@2x.png b/Watch/App/Assets.xcassets/BotKeyboardIcon.imageset/BotKeyboardIcon@2x.png new file mode 100644 index 0000000000..6209c6a50e Binary files /dev/null and b/Watch/App/Assets.xcassets/BotKeyboardIcon.imageset/BotKeyboardIcon@2x.png differ diff --git a/Watch/App/Assets.xcassets/BotKeyboardIcon.imageset/Contents.json b/Watch/App/Assets.xcassets/BotKeyboardIcon.imageset/Contents.json new file mode 100644 index 0000000000..9e7d0399c2 --- /dev/null +++ b/Watch/App/Assets.xcassets/BotKeyboardIcon.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BotKeyboardIcon@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleNotification.imageset/BubbleNotification@2x.png b/Watch/App/Assets.xcassets/BubbleNotification.imageset/BubbleNotification@2x.png new file mode 100644 index 0000000000..8a5f958718 Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleNotification.imageset/BubbleNotification@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleNotification.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleNotification.imageset/Contents.json new file mode 100644 index 0000000000..d9071d6f3f --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleNotification.imageset/Contents.json @@ -0,0 +1,35 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "resizing" : { + "mode" : "9-part", + "center" : { + "mode" : "stretch", + "width" : 1, + "height" : 27 + }, + "capInsets" : { + "bottom" : 31, + "top" : 59, + "right" : 26, + "left" : 26 + } + }, + "idiom" : "universal", + "filename" : "BubbleNotification@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner0.imageset/BubbleSpinner0@2x.png b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner0.imageset/BubbleSpinner0@2x.png new file mode 100644 index 0000000000..92cf05c1ed Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner0.imageset/BubbleSpinner0@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner0.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner0.imageset/Contents.json new file mode 100644 index 0000000000..d99d28ab45 --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner0.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinner0@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner1.imageset/BubbleSpinner1@2x.png b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner1.imageset/BubbleSpinner1@2x.png new file mode 100644 index 0000000000..1c7a2aee4f Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner1.imageset/BubbleSpinner1@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner1.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner1.imageset/Contents.json new file mode 100644 index 0000000000..c579976d93 --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner1.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinner1@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner10.imageset/BubbleSpinner10@2x.png b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner10.imageset/BubbleSpinner10@2x.png new file mode 100644 index 0000000000..69562e7247 Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner10.imageset/BubbleSpinner10@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner10.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner10.imageset/Contents.json new file mode 100644 index 0000000000..08d84211bd --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner10.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinner10@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner11.imageset/BubbleSpinner11@2x.png b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner11.imageset/BubbleSpinner11@2x.png new file mode 100644 index 0000000000..c6f088efa3 Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner11.imageset/BubbleSpinner11@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner11.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner11.imageset/Contents.json new file mode 100644 index 0000000000..ceeeeca014 --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner11.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinner11@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner12.imageset/BubbleSpinner12@2x.png b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner12.imageset/BubbleSpinner12@2x.png new file mode 100644 index 0000000000..d9c2a27aa6 Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner12.imageset/BubbleSpinner12@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner12.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner12.imageset/Contents.json new file mode 100644 index 0000000000..e83d05d915 --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner12.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinner12@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner13.imageset/BubbleSpinner13@2x.png b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner13.imageset/BubbleSpinner13@2x.png new file mode 100644 index 0000000000..afacad5c23 Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner13.imageset/BubbleSpinner13@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner13.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner13.imageset/Contents.json new file mode 100644 index 0000000000..97100d0005 --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner13.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinner13@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner14.imageset/BubbleSpinner14@2x.png b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner14.imageset/BubbleSpinner14@2x.png new file mode 100644 index 0000000000..451a7a35d7 Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner14.imageset/BubbleSpinner14@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner14.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner14.imageset/Contents.json new file mode 100644 index 0000000000..2d723e2cd0 --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner14.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinner14@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner15.imageset/BubbleSpinner15@2x.png b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner15.imageset/BubbleSpinner15@2x.png new file mode 100644 index 0000000000..ca3ab6b0dd Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner15.imageset/BubbleSpinner15@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner15.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner15.imageset/Contents.json new file mode 100644 index 0000000000..413bb2b618 --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner15.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinner15@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner16.imageset/BubbleSpinner16@2x.png b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner16.imageset/BubbleSpinner16@2x.png new file mode 100644 index 0000000000..b4ece2ea2a Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner16.imageset/BubbleSpinner16@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner16.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner16.imageset/Contents.json new file mode 100644 index 0000000000..39e0c46a4d --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner16.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinner16@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner17.imageset/BubbleSpinner17@2x.png b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner17.imageset/BubbleSpinner17@2x.png new file mode 100644 index 0000000000..a32a96d932 Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner17.imageset/BubbleSpinner17@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner17.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner17.imageset/Contents.json new file mode 100644 index 0000000000..e8650d9954 --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner17.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinner17@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner18.imageset/BubbleSpinner18@2x.png b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner18.imageset/BubbleSpinner18@2x.png new file mode 100644 index 0000000000..6d3ad03dc9 Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner18.imageset/BubbleSpinner18@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner18.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner18.imageset/Contents.json new file mode 100644 index 0000000000..e894486150 --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner18.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinner18@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner19.imageset/BubbleSpinner19@2x.png b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner19.imageset/BubbleSpinner19@2x.png new file mode 100644 index 0000000000..3b76c00bfb Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner19.imageset/BubbleSpinner19@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner19.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner19.imageset/Contents.json new file mode 100644 index 0000000000..e557093087 --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner19.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinner19@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner2.imageset/BubbleSpinner2@2x.png b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner2.imageset/BubbleSpinner2@2x.png new file mode 100644 index 0000000000..198287d78b Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner2.imageset/BubbleSpinner2@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner2.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner2.imageset/Contents.json new file mode 100644 index 0000000000..7d3429955e --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner2.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinner2@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner20.imageset/BubbleSpinner20@2x.png b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner20.imageset/BubbleSpinner20@2x.png new file mode 100644 index 0000000000..c0cb2c4e55 Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner20.imageset/BubbleSpinner20@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner20.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner20.imageset/Contents.json new file mode 100644 index 0000000000..fc07fb7134 --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner20.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinner20@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner21.imageset/BubbleSpinner21@2x.png b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner21.imageset/BubbleSpinner21@2x.png new file mode 100644 index 0000000000..1bcc4a1fd2 Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner21.imageset/BubbleSpinner21@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner21.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner21.imageset/Contents.json new file mode 100644 index 0000000000..faf4db6eca --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner21.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinner21@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner22.imageset/BubbleSpinner22@2x.png b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner22.imageset/BubbleSpinner22@2x.png new file mode 100644 index 0000000000..1cb8b58dab Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner22.imageset/BubbleSpinner22@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner22.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner22.imageset/Contents.json new file mode 100644 index 0000000000..0b4a359828 --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner22.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinner22@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner23.imageset/BubbleSpinner23@2x.png b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner23.imageset/BubbleSpinner23@2x.png new file mode 100644 index 0000000000..021728c015 Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner23.imageset/BubbleSpinner23@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner23.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner23.imageset/Contents.json new file mode 100644 index 0000000000..48418cd4d9 --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner23.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinner23@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner24.imageset/BubbleSpinner24@2x.png b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner24.imageset/BubbleSpinner24@2x.png new file mode 100644 index 0000000000..3f9c60a242 Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner24.imageset/BubbleSpinner24@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner24.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner24.imageset/Contents.json new file mode 100644 index 0000000000..9d662a2a2b --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner24.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinner24@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner25.imageset/BubbleSpinner25@2x.png b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner25.imageset/BubbleSpinner25@2x.png new file mode 100644 index 0000000000..f5f0dd8700 Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner25.imageset/BubbleSpinner25@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner25.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner25.imageset/Contents.json new file mode 100644 index 0000000000..6bb3f6d2e7 --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner25.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinner25@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner26.imageset/BubbleSpinner26@2x.png b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner26.imageset/BubbleSpinner26@2x.png new file mode 100644 index 0000000000..fbc576b516 Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner26.imageset/BubbleSpinner26@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner26.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner26.imageset/Contents.json new file mode 100644 index 0000000000..f149071d0f --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner26.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinner26@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner27.imageset/BubbleSpinner27@2x.png b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner27.imageset/BubbleSpinner27@2x.png new file mode 100644 index 0000000000..d8af386ceb Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner27.imageset/BubbleSpinner27@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner27.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner27.imageset/Contents.json new file mode 100644 index 0000000000..0f216bdc45 --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner27.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinner27@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner28.imageset/BubbleSpinner28@2x.png b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner28.imageset/BubbleSpinner28@2x.png new file mode 100644 index 0000000000..5cc728db81 Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner28.imageset/BubbleSpinner28@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner28.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner28.imageset/Contents.json new file mode 100644 index 0000000000..3f24f07f08 --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner28.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinner28@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner29.imageset/BubbleSpinner29@2x.png b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner29.imageset/BubbleSpinner29@2x.png new file mode 100644 index 0000000000..d7d149c913 Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner29.imageset/BubbleSpinner29@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner29.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner29.imageset/Contents.json new file mode 100644 index 0000000000..4bf5f7ecda --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner29.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinner29@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner3.imageset/BubbleSpinner3@2x.png b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner3.imageset/BubbleSpinner3@2x.png new file mode 100644 index 0000000000..616cf55a6d Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner3.imageset/BubbleSpinner3@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner3.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner3.imageset/Contents.json new file mode 100644 index 0000000000..767c64b482 --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner3.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinner3@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner30.imageset/BubbleSpinner30@2x.png b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner30.imageset/BubbleSpinner30@2x.png new file mode 100644 index 0000000000..e23584880f Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner30.imageset/BubbleSpinner30@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner30.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner30.imageset/Contents.json new file mode 100644 index 0000000000..8d799395ce --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner30.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinner30@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner31.imageset/BubbleSpinner31@2x.png b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner31.imageset/BubbleSpinner31@2x.png new file mode 100644 index 0000000000..34669cca99 Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner31.imageset/BubbleSpinner31@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner31.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner31.imageset/Contents.json new file mode 100644 index 0000000000..001ad12ce5 --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner31.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinner31@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner32.imageset/BubbleSpinner32@2x.png b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner32.imageset/BubbleSpinner32@2x.png new file mode 100644 index 0000000000..dbd25ae4bd Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner32.imageset/BubbleSpinner32@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner32.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner32.imageset/Contents.json new file mode 100644 index 0000000000..4f2d6a7f9b --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner32.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinner32@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner33.imageset/BubbleSpinner33@2x.png b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner33.imageset/BubbleSpinner33@2x.png new file mode 100644 index 0000000000..595339b79f Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner33.imageset/BubbleSpinner33@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner33.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner33.imageset/Contents.json new file mode 100644 index 0000000000..fe9b99c1e3 --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner33.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinner33@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner34.imageset/BubbleSpinner34@2x.png b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner34.imageset/BubbleSpinner34@2x.png new file mode 100644 index 0000000000..ac0d9b462e Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner34.imageset/BubbleSpinner34@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner34.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner34.imageset/Contents.json new file mode 100644 index 0000000000..68fbce58be --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner34.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinner34@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner35.imageset/BubbleSpinner35@2x.png b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner35.imageset/BubbleSpinner35@2x.png new file mode 100644 index 0000000000..0cc356c77c Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner35.imageset/BubbleSpinner35@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner35.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner35.imageset/Contents.json new file mode 100644 index 0000000000..e8f0f0bf8f --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner35.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinner35@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner36.imageset/BubbleSpinner36@2x.png b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner36.imageset/BubbleSpinner36@2x.png new file mode 100644 index 0000000000..89839ad03e Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner36.imageset/BubbleSpinner36@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner36.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner36.imageset/Contents.json new file mode 100644 index 0000000000..57009f9a25 --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner36.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinner36@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner37.imageset/BubbleSpinner37@2x.png b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner37.imageset/BubbleSpinner37@2x.png new file mode 100644 index 0000000000..a6c45ea28a Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner37.imageset/BubbleSpinner37@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner37.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner37.imageset/Contents.json new file mode 100644 index 0000000000..3cf020344d --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner37.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinner37@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner38.imageset/BubbleSpinner38@2x.png b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner38.imageset/BubbleSpinner38@2x.png new file mode 100644 index 0000000000..6a156f5fa1 Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner38.imageset/BubbleSpinner38@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner38.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner38.imageset/Contents.json new file mode 100644 index 0000000000..561548b4d6 --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner38.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinner38@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner4.imageset/BubbleSpinner4@2x.png b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner4.imageset/BubbleSpinner4@2x.png new file mode 100644 index 0000000000..a88e8427b6 Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner4.imageset/BubbleSpinner4@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner4.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner4.imageset/Contents.json new file mode 100644 index 0000000000..259c984db0 --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner4.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinner4@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner5.imageset/BubbleSpinner5@2x.png b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner5.imageset/BubbleSpinner5@2x.png new file mode 100644 index 0000000000..e63d66c59c Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner5.imageset/BubbleSpinner5@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner5.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner5.imageset/Contents.json new file mode 100644 index 0000000000..89fd50f24e --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner5.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinner5@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner6.imageset/BubbleSpinner6@2x.png b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner6.imageset/BubbleSpinner6@2x.png new file mode 100644 index 0000000000..ed724cb3a8 Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner6.imageset/BubbleSpinner6@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner6.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner6.imageset/Contents.json new file mode 100644 index 0000000000..0140edf583 --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner6.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinner6@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner7.imageset/BubbleSpinner7@2x.png b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner7.imageset/BubbleSpinner7@2x.png new file mode 100644 index 0000000000..02ed2d1ab1 Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner7.imageset/BubbleSpinner7@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner7.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner7.imageset/Contents.json new file mode 100644 index 0000000000..ced0657b17 --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner7.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinner7@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner8.imageset/BubbleSpinner8@2x.png b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner8.imageset/BubbleSpinner8@2x.png new file mode 100644 index 0000000000..b6fed3fc3b Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner8.imageset/BubbleSpinner8@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner8.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner8.imageset/Contents.json new file mode 100644 index 0000000000..99aaa82423 --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner8.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinner8@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner9.imageset/BubbleSpinner9@2x.png b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner9.imageset/BubbleSpinner9@2x.png new file mode 100644 index 0000000000..3f8068eadf Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner9.imageset/BubbleSpinner9@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner9.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner9.imageset/Contents.json new file mode 100644 index 0000000000..4f76740419 --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinner/BubbleSpinner9.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinner9@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinner/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinner/Contents.json new file mode 100644 index 0000000000..da4a164c91 --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinner/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming0.imageset/BubbleSpinnerIncoming0@2x.png b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming0.imageset/BubbleSpinnerIncoming0@2x.png new file mode 100644 index 0000000000..a717896d6f Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming0.imageset/BubbleSpinnerIncoming0@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming0.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming0.imageset/Contents.json new file mode 100644 index 0000000000..b3c1067acb --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming0.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinnerIncoming0@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming1.imageset/BubbleSpinnerIncoming1@2x.png b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming1.imageset/BubbleSpinnerIncoming1@2x.png new file mode 100644 index 0000000000..a6a7371bf9 Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming1.imageset/BubbleSpinnerIncoming1@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming1.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming1.imageset/Contents.json new file mode 100644 index 0000000000..5e937defdd --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming1.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinnerIncoming1@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming10.imageset/BubbleSpinnerIncoming10@2x.png b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming10.imageset/BubbleSpinnerIncoming10@2x.png new file mode 100644 index 0000000000..cfd00c2784 Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming10.imageset/BubbleSpinnerIncoming10@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming10.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming10.imageset/Contents.json new file mode 100644 index 0000000000..362e97b41a --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming10.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinnerIncoming10@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming11.imageset/BubbleSpinnerIncoming11@2x.png b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming11.imageset/BubbleSpinnerIncoming11@2x.png new file mode 100644 index 0000000000..71c78da335 Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming11.imageset/BubbleSpinnerIncoming11@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming11.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming11.imageset/Contents.json new file mode 100644 index 0000000000..08be1ec345 --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming11.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinnerIncoming11@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming12.imageset/BubbleSpinnerIncoming12@2x.png b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming12.imageset/BubbleSpinnerIncoming12@2x.png new file mode 100644 index 0000000000..9c099a4cc5 Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming12.imageset/BubbleSpinnerIncoming12@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming12.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming12.imageset/Contents.json new file mode 100644 index 0000000000..b89f9c88b3 --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming12.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinnerIncoming12@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming13.imageset/BubbleSpinnerIncoming13@2x.png b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming13.imageset/BubbleSpinnerIncoming13@2x.png new file mode 100644 index 0000000000..65412c4737 Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming13.imageset/BubbleSpinnerIncoming13@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming13.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming13.imageset/Contents.json new file mode 100644 index 0000000000..bf730b056c --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming13.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinnerIncoming13@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming14.imageset/BubbleSpinnerIncoming14@2x.png b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming14.imageset/BubbleSpinnerIncoming14@2x.png new file mode 100644 index 0000000000..19689b851d Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming14.imageset/BubbleSpinnerIncoming14@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming14.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming14.imageset/Contents.json new file mode 100644 index 0000000000..d181c02cf6 --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming14.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinnerIncoming14@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming15.imageset/BubbleSpinnerIncoming15@2x.png b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming15.imageset/BubbleSpinnerIncoming15@2x.png new file mode 100644 index 0000000000..785986182c Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming15.imageset/BubbleSpinnerIncoming15@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming15.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming15.imageset/Contents.json new file mode 100644 index 0000000000..5730ade29a --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming15.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinnerIncoming15@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming16.imageset/BubbleSpinnerIncoming16@2x.png b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming16.imageset/BubbleSpinnerIncoming16@2x.png new file mode 100644 index 0000000000..a341ee7985 Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming16.imageset/BubbleSpinnerIncoming16@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming16.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming16.imageset/Contents.json new file mode 100644 index 0000000000..d12bb19b28 --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming16.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinnerIncoming16@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming17.imageset/BubbleSpinnerIncoming17@2x.png b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming17.imageset/BubbleSpinnerIncoming17@2x.png new file mode 100644 index 0000000000..3c1d8f0a3c Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming17.imageset/BubbleSpinnerIncoming17@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming17.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming17.imageset/Contents.json new file mode 100644 index 0000000000..791e4c8eda --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming17.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinnerIncoming17@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming18.imageset/BubbleSpinnerIncoming18@2x.png b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming18.imageset/BubbleSpinnerIncoming18@2x.png new file mode 100644 index 0000000000..9795f26284 Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming18.imageset/BubbleSpinnerIncoming18@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming18.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming18.imageset/Contents.json new file mode 100644 index 0000000000..36f6ab9642 --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming18.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinnerIncoming18@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming19.imageset/BubbleSpinnerIncoming19@2x.png b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming19.imageset/BubbleSpinnerIncoming19@2x.png new file mode 100644 index 0000000000..aedb2e12f2 Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming19.imageset/BubbleSpinnerIncoming19@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming19.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming19.imageset/Contents.json new file mode 100644 index 0000000000..c5c3522819 --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming19.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinnerIncoming19@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming2.imageset/BubbleSpinnerIncoming2@2x.png b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming2.imageset/BubbleSpinnerIncoming2@2x.png new file mode 100644 index 0000000000..8f25fb5a8d Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming2.imageset/BubbleSpinnerIncoming2@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming2.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming2.imageset/Contents.json new file mode 100644 index 0000000000..3073523c85 --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming2.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinnerIncoming2@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming20.imageset/BubbleSpinnerIncoming20@2x.png b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming20.imageset/BubbleSpinnerIncoming20@2x.png new file mode 100644 index 0000000000..b06645ebd2 Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming20.imageset/BubbleSpinnerIncoming20@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming20.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming20.imageset/Contents.json new file mode 100644 index 0000000000..40f51b23bc --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming20.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinnerIncoming20@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming21.imageset/BubbleSpinnerIncoming21@2x.png b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming21.imageset/BubbleSpinnerIncoming21@2x.png new file mode 100644 index 0000000000..e35849965d Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming21.imageset/BubbleSpinnerIncoming21@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming21.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming21.imageset/Contents.json new file mode 100644 index 0000000000..84d230a178 --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming21.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinnerIncoming21@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming22.imageset/BubbleSpinnerIncoming22@2x.png b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming22.imageset/BubbleSpinnerIncoming22@2x.png new file mode 100644 index 0000000000..cd8e22bdb7 Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming22.imageset/BubbleSpinnerIncoming22@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming22.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming22.imageset/Contents.json new file mode 100644 index 0000000000..bf6442f778 --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming22.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinnerIncoming22@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming23.imageset/BubbleSpinnerIncoming23@2x.png b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming23.imageset/BubbleSpinnerIncoming23@2x.png new file mode 100644 index 0000000000..9ffb5a68de Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming23.imageset/BubbleSpinnerIncoming23@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming23.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming23.imageset/Contents.json new file mode 100644 index 0000000000..a045366747 --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming23.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinnerIncoming23@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming24.imageset/BubbleSpinnerIncoming24@2x.png b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming24.imageset/BubbleSpinnerIncoming24@2x.png new file mode 100644 index 0000000000..d46b8e334d Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming24.imageset/BubbleSpinnerIncoming24@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming24.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming24.imageset/Contents.json new file mode 100644 index 0000000000..606098b634 --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming24.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinnerIncoming24@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming25.imageset/BubbleSpinnerIncoming25@2x.png b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming25.imageset/BubbleSpinnerIncoming25@2x.png new file mode 100644 index 0000000000..85de7aae82 Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming25.imageset/BubbleSpinnerIncoming25@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming25.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming25.imageset/Contents.json new file mode 100644 index 0000000000..dc8d326b9c --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming25.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinnerIncoming25@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming26.imageset/BubbleSpinnerIncoming26@2x.png b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming26.imageset/BubbleSpinnerIncoming26@2x.png new file mode 100644 index 0000000000..644a69ee09 Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming26.imageset/BubbleSpinnerIncoming26@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming26.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming26.imageset/Contents.json new file mode 100644 index 0000000000..5456d476d8 --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming26.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinnerIncoming26@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming27.imageset/BubbleSpinnerIncoming27@2x.png b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming27.imageset/BubbleSpinnerIncoming27@2x.png new file mode 100644 index 0000000000..c35db82324 Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming27.imageset/BubbleSpinnerIncoming27@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming27.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming27.imageset/Contents.json new file mode 100644 index 0000000000..30275c0658 --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming27.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinnerIncoming27@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming28.imageset/BubbleSpinnerIncoming28@2x.png b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming28.imageset/BubbleSpinnerIncoming28@2x.png new file mode 100644 index 0000000000..a1d7199f23 Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming28.imageset/BubbleSpinnerIncoming28@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming28.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming28.imageset/Contents.json new file mode 100644 index 0000000000..5fe5b110a7 --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming28.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinnerIncoming28@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming29.imageset/BubbleSpinnerIncoming29@2x.png b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming29.imageset/BubbleSpinnerIncoming29@2x.png new file mode 100644 index 0000000000..bb5b8e9efd Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming29.imageset/BubbleSpinnerIncoming29@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming29.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming29.imageset/Contents.json new file mode 100644 index 0000000000..2ee4a09a66 --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming29.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinnerIncoming29@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming3.imageset/BubbleSpinnerIncoming3@2x.png b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming3.imageset/BubbleSpinnerIncoming3@2x.png new file mode 100644 index 0000000000..f01d558243 Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming3.imageset/BubbleSpinnerIncoming3@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming3.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming3.imageset/Contents.json new file mode 100644 index 0000000000..5656a4c3ee --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming3.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinnerIncoming3@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming30.imageset/BubbleSpinnerIncoming30@2x.png b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming30.imageset/BubbleSpinnerIncoming30@2x.png new file mode 100644 index 0000000000..6faa14f01c Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming30.imageset/BubbleSpinnerIncoming30@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming30.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming30.imageset/Contents.json new file mode 100644 index 0000000000..54637c6de4 --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming30.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinnerIncoming30@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming31.imageset/BubbleSpinnerIncoming31@2x.png b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming31.imageset/BubbleSpinnerIncoming31@2x.png new file mode 100644 index 0000000000..f35878ff36 Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming31.imageset/BubbleSpinnerIncoming31@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming31.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming31.imageset/Contents.json new file mode 100644 index 0000000000..2865d3c5db --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming31.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinnerIncoming31@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming32.imageset/BubbleSpinnerIncoming32@2x.png b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming32.imageset/BubbleSpinnerIncoming32@2x.png new file mode 100644 index 0000000000..9ba83b780b Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming32.imageset/BubbleSpinnerIncoming32@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming32.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming32.imageset/Contents.json new file mode 100644 index 0000000000..381cfb92ea --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming32.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinnerIncoming32@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming33.imageset/BubbleSpinnerIncoming33@2x.png b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming33.imageset/BubbleSpinnerIncoming33@2x.png new file mode 100644 index 0000000000..bc7a34454a Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming33.imageset/BubbleSpinnerIncoming33@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming33.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming33.imageset/Contents.json new file mode 100644 index 0000000000..8407c49090 --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming33.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinnerIncoming33@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming34.imageset/BubbleSpinnerIncoming34@2x.png b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming34.imageset/BubbleSpinnerIncoming34@2x.png new file mode 100644 index 0000000000..314d29e771 Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming34.imageset/BubbleSpinnerIncoming34@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming34.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming34.imageset/Contents.json new file mode 100644 index 0000000000..3701de1bdf --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming34.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinnerIncoming34@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming35.imageset/BubbleSpinnerIncoming35@2x.png b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming35.imageset/BubbleSpinnerIncoming35@2x.png new file mode 100644 index 0000000000..8e6dcbfc6b Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming35.imageset/BubbleSpinnerIncoming35@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming35.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming35.imageset/Contents.json new file mode 100644 index 0000000000..1a078b2d8a --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming35.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinnerIncoming35@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming36.imageset/BubbleSpinnerIncoming36@2x.png b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming36.imageset/BubbleSpinnerIncoming36@2x.png new file mode 100644 index 0000000000..d5e312d326 Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming36.imageset/BubbleSpinnerIncoming36@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming36.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming36.imageset/Contents.json new file mode 100644 index 0000000000..80007c70c5 --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming36.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinnerIncoming36@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming37.imageset/BubbleSpinnerIncoming37@2x.png b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming37.imageset/BubbleSpinnerIncoming37@2x.png new file mode 100644 index 0000000000..8bf8125c89 Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming37.imageset/BubbleSpinnerIncoming37@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming37.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming37.imageset/Contents.json new file mode 100644 index 0000000000..4cf10ade96 --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming37.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinnerIncoming37@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming38.imageset/BubbleSpinnerIncoming38@2x.png b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming38.imageset/BubbleSpinnerIncoming38@2x.png new file mode 100644 index 0000000000..fb331bcafb Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming38.imageset/BubbleSpinnerIncoming38@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming38.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming38.imageset/Contents.json new file mode 100644 index 0000000000..692506755c --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming38.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinnerIncoming38@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming4.imageset/BubbleSpinnerIncoming4@2x.png b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming4.imageset/BubbleSpinnerIncoming4@2x.png new file mode 100644 index 0000000000..36494758f9 Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming4.imageset/BubbleSpinnerIncoming4@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming4.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming4.imageset/Contents.json new file mode 100644 index 0000000000..48eb058fba --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming4.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinnerIncoming4@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming5.imageset/BubbleSpinnerIncoming5@2x.png b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming5.imageset/BubbleSpinnerIncoming5@2x.png new file mode 100644 index 0000000000..e5dfc99901 Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming5.imageset/BubbleSpinnerIncoming5@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming5.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming5.imageset/Contents.json new file mode 100644 index 0000000000..b319581d6c --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming5.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinnerIncoming5@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming6.imageset/BubbleSpinnerIncoming6@2x.png b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming6.imageset/BubbleSpinnerIncoming6@2x.png new file mode 100644 index 0000000000..d3c2486003 Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming6.imageset/BubbleSpinnerIncoming6@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming6.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming6.imageset/Contents.json new file mode 100644 index 0000000000..6d37501432 --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming6.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinnerIncoming6@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming7.imageset/BubbleSpinnerIncoming7@2x.png b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming7.imageset/BubbleSpinnerIncoming7@2x.png new file mode 100644 index 0000000000..07870976b8 Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming7.imageset/BubbleSpinnerIncoming7@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming7.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming7.imageset/Contents.json new file mode 100644 index 0000000000..b33e9a08f5 --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming7.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinnerIncoming7@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming8.imageset/BubbleSpinnerIncoming8@2x.png b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming8.imageset/BubbleSpinnerIncoming8@2x.png new file mode 100644 index 0000000000..b5d9feb135 Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming8.imageset/BubbleSpinnerIncoming8@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming8.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming8.imageset/Contents.json new file mode 100644 index 0000000000..2eaa924b1a --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming8.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinnerIncoming8@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming9.imageset/BubbleSpinnerIncoming9@2x.png b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming9.imageset/BubbleSpinnerIncoming9@2x.png new file mode 100644 index 0000000000..7ff0a4bfe1 Binary files /dev/null and b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming9.imageset/BubbleSpinnerIncoming9@2x.png differ diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming9.imageset/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming9.imageset/Contents.json new file mode 100644 index 0000000000..06083ec9b9 --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/BubbleSpinnerIncoming9.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "BubbleSpinnerIncoming9@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/Contents.json b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/Contents.json new file mode 100644 index 0000000000..da4a164c91 --- /dev/null +++ b/Watch/App/Assets.xcassets/BubbleSpinnerIncoming/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/ChatBubbleChannel.imageset/BubbleChannel@2x.png b/Watch/App/Assets.xcassets/ChatBubbleChannel.imageset/BubbleChannel@2x.png new file mode 100644 index 0000000000..f82c16246c Binary files /dev/null and b/Watch/App/Assets.xcassets/ChatBubbleChannel.imageset/BubbleChannel@2x.png differ diff --git a/Watch/App/Assets.xcassets/ChatBubbleChannel.imageset/Contents.json b/Watch/App/Assets.xcassets/ChatBubbleChannel.imageset/Contents.json new file mode 100644 index 0000000000..d5bbcec899 --- /dev/null +++ b/Watch/App/Assets.xcassets/ChatBubbleChannel.imageset/Contents.json @@ -0,0 +1,35 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "resizing" : { + "mode" : "9-part", + "center" : { + "mode" : "stretch", + "width" : 1, + "height" : 1 + }, + "cap-insets" : { + "bottom" : 32, + "top" : 26, + "right" : 26, + "left" : 26 + } + }, + "idiom" : "universal", + "filename" : "BubbleChannel@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/ChatBubbleIncoming.imageset/BubbleIncoming@2x.png b/Watch/App/Assets.xcassets/ChatBubbleIncoming.imageset/BubbleIncoming@2x.png new file mode 100644 index 0000000000..99d5ad1633 Binary files /dev/null and b/Watch/App/Assets.xcassets/ChatBubbleIncoming.imageset/BubbleIncoming@2x.png differ diff --git a/Watch/App/Assets.xcassets/ChatBubbleIncoming.imageset/Contents.json b/Watch/App/Assets.xcassets/ChatBubbleIncoming.imageset/Contents.json new file mode 100644 index 0000000000..bdada4786e --- /dev/null +++ b/Watch/App/Assets.xcassets/ChatBubbleIncoming.imageset/Contents.json @@ -0,0 +1,35 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "resizing" : { + "mode" : "9-part", + "center" : { + "mode" : "stretch", + "width" : 1, + "height" : 1 + }, + "cap-insets" : { + "bottom" : 32, + "top" : 26, + "right" : 26, + "left" : 26 + } + }, + "idiom" : "universal", + "filename" : "BubbleIncoming@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/ChatBubbleOutgoing.imageset/BubbleOutgoing@2x.png b/Watch/App/Assets.xcassets/ChatBubbleOutgoing.imageset/BubbleOutgoing@2x.png new file mode 100644 index 0000000000..ad2492b911 Binary files /dev/null and b/Watch/App/Assets.xcassets/ChatBubbleOutgoing.imageset/BubbleOutgoing@2x.png differ diff --git a/Watch/App/Assets.xcassets/ChatBubbleOutgoing.imageset/Contents.json b/Watch/App/Assets.xcassets/ChatBubbleOutgoing.imageset/Contents.json new file mode 100644 index 0000000000..3953427d48 --- /dev/null +++ b/Watch/App/Assets.xcassets/ChatBubbleOutgoing.imageset/Contents.json @@ -0,0 +1,35 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "resizing" : { + "mode" : "9-part", + "center" : { + "mode" : "stretch", + "width" : 1, + "height" : 1 + }, + "cap-insets" : { + "bottom" : 32, + "top" : 26, + "right" : 26, + "left" : 26 + } + }, + "idiom" : "universal", + "filename" : "BubbleOutgoing@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/Complication.complicationset/Circular.imageset/Contents.json b/Watch/App/Assets.xcassets/Complication.complicationset/Circular.imageset/Contents.json new file mode 100644 index 0000000000..f0ef076b2a --- /dev/null +++ b/Watch/App/Assets.xcassets/Complication.complicationset/Circular.imageset/Contents.json @@ -0,0 +1,32 @@ +{ + "images" : [ + { + "idiom" : "watch", + "scale" : "2x" + }, + { + "idiom" : "watch", + "scale" : "2x", + "screen-width" : "<=145" + }, + { + "idiom" : "watch", + "scale" : "2x", + "screen-width" : ">161" + }, + { + "idiom" : "watch", + "scale" : "2x", + "screen-width" : ">145" + }, + { + "idiom" : "watch", + "scale" : "2x", + "screen-width" : ">183" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/Complication.complicationset/Contents.json b/Watch/App/Assets.xcassets/Complication.complicationset/Contents.json new file mode 100644 index 0000000000..1571c7e531 --- /dev/null +++ b/Watch/App/Assets.xcassets/Complication.complicationset/Contents.json @@ -0,0 +1,48 @@ +{ + "assets" : [ + { + "idiom" : "watch", + "filename" : "Circular.imageset", + "role" : "circular" + }, + { + "idiom" : "watch", + "filename" : "Extra Large.imageset", + "role" : "extra-large" + }, + { + "idiom" : "watch", + "filename" : "Graphic Bezel.imageset", + "role" : "graphic-bezel" + }, + { + "idiom" : "watch", + "filename" : "Graphic Circular.imageset", + "role" : "graphic-circular" + }, + { + "idiom" : "watch", + "filename" : "Graphic Corner.imageset", + "role" : "graphic-corner" + }, + { + "idiom" : "watch", + "filename" : "Graphic Large Rectangular.imageset", + "role" : "graphic-large-rectangular" + }, + { + "idiom" : "watch", + "filename" : "Modular.imageset", + "role" : "modular" + }, + { + "idiom" : "watch", + "filename" : "Utilitarian.imageset", + "role" : "utilitarian" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/Complication.complicationset/Extra Large.imageset/Contents.json b/Watch/App/Assets.xcassets/Complication.complicationset/Extra Large.imageset/Contents.json new file mode 100644 index 0000000000..e011e32711 --- /dev/null +++ b/Watch/App/Assets.xcassets/Complication.complicationset/Extra Large.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "watch", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/Complication.complicationset/Graphic Bezel.imageset/Contents.json b/Watch/App/Assets.xcassets/Complication.complicationset/Graphic Bezel.imageset/Contents.json new file mode 100644 index 0000000000..e011e32711 --- /dev/null +++ b/Watch/App/Assets.xcassets/Complication.complicationset/Graphic Bezel.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "watch", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/Complication.complicationset/Graphic Circular.imageset/Contents.json b/Watch/App/Assets.xcassets/Complication.complicationset/Graphic Circular.imageset/Contents.json new file mode 100644 index 0000000000..e011e32711 --- /dev/null +++ b/Watch/App/Assets.xcassets/Complication.complicationset/Graphic Circular.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "watch", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/Complication.complicationset/Graphic Corner.imageset/Contents.json b/Watch/App/Assets.xcassets/Complication.complicationset/Graphic Corner.imageset/Contents.json new file mode 100644 index 0000000000..e011e32711 --- /dev/null +++ b/Watch/App/Assets.xcassets/Complication.complicationset/Graphic Corner.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "watch", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/Complication.complicationset/Graphic Large Rectangular.imageset/Contents.json b/Watch/App/Assets.xcassets/Complication.complicationset/Graphic Large Rectangular.imageset/Contents.json new file mode 100644 index 0000000000..e011e32711 --- /dev/null +++ b/Watch/App/Assets.xcassets/Complication.complicationset/Graphic Large Rectangular.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "watch", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/Complication.complicationset/Modular.imageset/Contents.json b/Watch/App/Assets.xcassets/Complication.complicationset/Modular.imageset/Contents.json new file mode 100644 index 0000000000..f0ef076b2a --- /dev/null +++ b/Watch/App/Assets.xcassets/Complication.complicationset/Modular.imageset/Contents.json @@ -0,0 +1,32 @@ +{ + "images" : [ + { + "idiom" : "watch", + "scale" : "2x" + }, + { + "idiom" : "watch", + "scale" : "2x", + "screen-width" : "<=145" + }, + { + "idiom" : "watch", + "scale" : "2x", + "screen-width" : ">161" + }, + { + "idiom" : "watch", + "scale" : "2x", + "screen-width" : ">145" + }, + { + "idiom" : "watch", + "scale" : "2x", + "screen-width" : ">183" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/Complication.complicationset/Utilitarian.imageset/Contents.json b/Watch/App/Assets.xcassets/Complication.complicationset/Utilitarian.imageset/Contents.json new file mode 100644 index 0000000000..f0ef076b2a --- /dev/null +++ b/Watch/App/Assets.xcassets/Complication.complicationset/Utilitarian.imageset/Contents.json @@ -0,0 +1,32 @@ +{ + "images" : [ + { + "idiom" : "watch", + "scale" : "2x" + }, + { + "idiom" : "watch", + "scale" : "2x", + "screen-width" : "<=145" + }, + { + "idiom" : "watch", + "scale" : "2x", + "screen-width" : ">161" + }, + { + "idiom" : "watch", + "scale" : "2x", + "screen-width" : ">145" + }, + { + "idiom" : "watch", + "scale" : "2x", + "screen-width" : ">183" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/Compose.imageset/Compose@2x.png b/Watch/App/Assets.xcassets/Compose.imageset/Compose@2x.png new file mode 100644 index 0000000000..876fe00f38 Binary files /dev/null and b/Watch/App/Assets.xcassets/Compose.imageset/Compose@2x.png differ diff --git a/Watch/App/Assets.xcassets/Compose.imageset/Contents.json b/Watch/App/Assets.xcassets/Compose.imageset/Contents.json new file mode 100644 index 0000000000..726f551a23 --- /dev/null +++ b/Watch/App/Assets.xcassets/Compose.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "Compose@2x.png" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/Contents.json b/Watch/App/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..da4a164c91 --- /dev/null +++ b/Watch/App/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/File.imageset/Contents.json b/Watch/App/Assets.xcassets/File.imageset/Contents.json new file mode 100644 index 0000000000..a4c13ef2ca --- /dev/null +++ b/Watch/App/Assets.xcassets/File.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "File@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/File.imageset/File@2x.png b/Watch/App/Assets.xcassets/File.imageset/File@2x.png new file mode 100644 index 0000000000..0f4d832b32 Binary files /dev/null and b/Watch/App/Assets.xcassets/File.imageset/File@2x.png differ diff --git a/Watch/App/Assets.xcassets/LocationIcon.imageset/Contents.json b/Watch/App/Assets.xcassets/LocationIcon.imageset/Contents.json new file mode 100644 index 0000000000..42b8ee51a2 --- /dev/null +++ b/Watch/App/Assets.xcassets/LocationIcon.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "LocationIcon@2x.png" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/LocationIcon.imageset/LocationIcon@2x.png b/Watch/App/Assets.xcassets/LocationIcon.imageset/LocationIcon@2x.png new file mode 100644 index 0000000000..6b1b104dd5 Binary files /dev/null and b/Watch/App/Assets.xcassets/LocationIcon.imageset/LocationIcon@2x.png differ diff --git a/Watch/App/Assets.xcassets/LoginIcon.imageset/Contents.json b/Watch/App/Assets.xcassets/LoginIcon.imageset/Contents.json new file mode 100644 index 0000000000..049173bcc1 --- /dev/null +++ b/Watch/App/Assets.xcassets/LoginIcon.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "LoginIcon@2x.png" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/LoginIcon.imageset/LoginIcon@2x.png b/Watch/App/Assets.xcassets/LoginIcon.imageset/LoginIcon@2x.png new file mode 100644 index 0000000000..4f8e0cacda Binary files /dev/null and b/Watch/App/Assets.xcassets/LoginIcon.imageset/LoginIcon@2x.png differ diff --git a/Watch/App/Assets.xcassets/MediaAudio.imageset/Contents.json b/Watch/App/Assets.xcassets/MediaAudio.imageset/Contents.json new file mode 100644 index 0000000000..5afc099170 --- /dev/null +++ b/Watch/App/Assets.xcassets/MediaAudio.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "MediaAudio@2x.png" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode", + "template-rendering-intent" : "template" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/MediaAudio.imageset/MediaAudio@2x.png b/Watch/App/Assets.xcassets/MediaAudio.imageset/MediaAudio@2x.png new file mode 100644 index 0000000000..f8cf3ee3d5 Binary files /dev/null and b/Watch/App/Assets.xcassets/MediaAudio.imageset/MediaAudio@2x.png differ diff --git a/Watch/App/Assets.xcassets/MediaAudioPlay.imageset/Contents.json b/Watch/App/Assets.xcassets/MediaAudioPlay.imageset/Contents.json new file mode 100644 index 0000000000..bafafdbb8a --- /dev/null +++ b/Watch/App/Assets.xcassets/MediaAudioPlay.imageset/Contents.json @@ -0,0 +1,24 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "MediaAudioPlay@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "template-rendering-intent" : "template" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/MediaAudioPlay.imageset/MediaAudioPlay@2x.png b/Watch/App/Assets.xcassets/MediaAudioPlay.imageset/MediaAudioPlay@2x.png new file mode 100644 index 0000000000..50157e58b1 Binary files /dev/null and b/Watch/App/Assets.xcassets/MediaAudioPlay.imageset/MediaAudioPlay@2x.png differ diff --git a/Watch/App/Assets.xcassets/MediaDocument.imageset/Contents.json b/Watch/App/Assets.xcassets/MediaDocument.imageset/Contents.json new file mode 100644 index 0000000000..4147b6e7c0 --- /dev/null +++ b/Watch/App/Assets.xcassets/MediaDocument.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "MediaDocument@2x.png" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode", + "template-rendering-intent" : "template" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/MediaDocument.imageset/MediaDocument@2x.png b/Watch/App/Assets.xcassets/MediaDocument.imageset/MediaDocument@2x.png new file mode 100644 index 0000000000..95d413c5a9 Binary files /dev/null and b/Watch/App/Assets.xcassets/MediaDocument.imageset/MediaDocument@2x.png differ diff --git a/Watch/App/Assets.xcassets/MediaLocation.imageset/Contents.json b/Watch/App/Assets.xcassets/MediaLocation.imageset/Contents.json new file mode 100644 index 0000000000..d164079b6e --- /dev/null +++ b/Watch/App/Assets.xcassets/MediaLocation.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "MediaLocation@2x.png" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode", + "template-rendering-intent" : "template" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/MediaLocation.imageset/MediaLocation@2x.png b/Watch/App/Assets.xcassets/MediaLocation.imageset/MediaLocation@2x.png new file mode 100644 index 0000000000..52240d4107 Binary files /dev/null and b/Watch/App/Assets.xcassets/MediaLocation.imageset/MediaLocation@2x.png differ diff --git a/Watch/App/Assets.xcassets/MediaPhoto.imageset/Contents.json b/Watch/App/Assets.xcassets/MediaPhoto.imageset/Contents.json new file mode 100644 index 0000000000..438ebfecfb --- /dev/null +++ b/Watch/App/Assets.xcassets/MediaPhoto.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "MediaPhoto@2x.png" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode", + "template-rendering-intent" : "template" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/MediaPhoto.imageset/MediaPhoto@2x.png b/Watch/App/Assets.xcassets/MediaPhoto.imageset/MediaPhoto@2x.png new file mode 100644 index 0000000000..3044d9f5b5 Binary files /dev/null and b/Watch/App/Assets.xcassets/MediaPhoto.imageset/MediaPhoto@2x.png differ diff --git a/Watch/App/Assets.xcassets/MediaVideo.imageset/Contents.json b/Watch/App/Assets.xcassets/MediaVideo.imageset/Contents.json new file mode 100644 index 0000000000..9c6e52e1c1 --- /dev/null +++ b/Watch/App/Assets.xcassets/MediaVideo.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "MediaVideo@2x.png" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode", + "template-rendering-intent" : "template" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/MediaVideo.imageset/MediaVideo@2x.png b/Watch/App/Assets.xcassets/MediaVideo.imageset/MediaVideo@2x.png new file mode 100644 index 0000000000..3b1fabcf1a Binary files /dev/null and b/Watch/App/Assets.xcassets/MediaVideo.imageset/MediaVideo@2x.png differ diff --git a/Watch/App/Assets.xcassets/MessageStatusDot.imageset/Contents.json b/Watch/App/Assets.xcassets/MessageStatusDot.imageset/Contents.json new file mode 100644 index 0000000000..060e43fd8f --- /dev/null +++ b/Watch/App/Assets.xcassets/MessageStatusDot.imageset/Contents.json @@ -0,0 +1,24 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "MessageStatusDot@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "template-rendering-intent" : "template" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/MessageStatusDot.imageset/MessageStatusDot@2x.png b/Watch/App/Assets.xcassets/MessageStatusDot.imageset/MessageStatusDot@2x.png new file mode 100644 index 0000000000..7789866aef Binary files /dev/null and b/Watch/App/Assets.xcassets/MessageStatusDot.imageset/MessageStatusDot@2x.png differ diff --git a/Watch/App/Assets.xcassets/MicAccessIcon.imageset/Contents.json b/Watch/App/Assets.xcassets/MicAccessIcon.imageset/Contents.json new file mode 100644 index 0000000000..767bb793e0 --- /dev/null +++ b/Watch/App/Assets.xcassets/MicAccessIcon.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "MicIcon@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/MicAccessIcon.imageset/MicIcon@2x.png b/Watch/App/Assets.xcassets/MicAccessIcon.imageset/MicIcon@2x.png new file mode 100644 index 0000000000..8adf165069 Binary files /dev/null and b/Watch/App/Assets.xcassets/MicAccessIcon.imageset/MicIcon@2x.png differ diff --git a/Watch/App/Assets.xcassets/MicIcon.imageset/Contents.json b/Watch/App/Assets.xcassets/MicIcon.imageset/Contents.json new file mode 100644 index 0000000000..da7ad4620b --- /dev/null +++ b/Watch/App/Assets.xcassets/MicIcon.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "Mic@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/MicIcon.imageset/Mic@2x.png b/Watch/App/Assets.xcassets/MicIcon.imageset/Mic@2x.png new file mode 100644 index 0000000000..5865b02ea0 Binary files /dev/null and b/Watch/App/Assets.xcassets/MicIcon.imageset/Mic@2x.png differ diff --git a/Watch/App/Assets.xcassets/PasscodeIcon.imageset/Contents.json b/Watch/App/Assets.xcassets/PasscodeIcon.imageset/Contents.json new file mode 100644 index 0000000000..fc1be91b9b --- /dev/null +++ b/Watch/App/Assets.xcassets/PasscodeIcon.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "Passcode@2x.png" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/PasscodeIcon.imageset/Passcode@2x.png b/Watch/App/Assets.xcassets/PasscodeIcon.imageset/Passcode@2x.png new file mode 100644 index 0000000000..6b7cff1124 Binary files /dev/null and b/Watch/App/Assets.xcassets/PasscodeIcon.imageset/Passcode@2x.png differ diff --git a/Watch/App/Assets.xcassets/PickLocation.imageset/Contents.json b/Watch/App/Assets.xcassets/PickLocation.imageset/Contents.json new file mode 100644 index 0000000000..09b6fc2dce --- /dev/null +++ b/Watch/App/Assets.xcassets/PickLocation.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "PickLocation@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/PickLocation.imageset/PickLocation@2x.png b/Watch/App/Assets.xcassets/PickLocation.imageset/PickLocation@2x.png new file mode 100644 index 0000000000..2a58dd6d77 Binary files /dev/null and b/Watch/App/Assets.xcassets/PickLocation.imageset/PickLocation@2x.png differ diff --git a/Watch/App/Assets.xcassets/RemotePhone.imageset/Contents.json b/Watch/App/Assets.xcassets/RemotePhone.imageset/Contents.json new file mode 100644 index 0000000000..f3dbad873e --- /dev/null +++ b/Watch/App/Assets.xcassets/RemotePhone.imageset/Contents.json @@ -0,0 +1,24 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "Phone@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "template-rendering-intent" : "template" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/RemotePhone.imageset/Phone@2x.png b/Watch/App/Assets.xcassets/RemotePhone.imageset/Phone@2x.png new file mode 100644 index 0000000000..51c983610e Binary files /dev/null and b/Watch/App/Assets.xcassets/RemotePhone.imageset/Phone@2x.png differ diff --git a/Watch/App/Assets.xcassets/RemotePlayVideo.imageset/Contents.json b/Watch/App/Assets.xcassets/RemotePlayVideo.imageset/Contents.json new file mode 100644 index 0000000000..66bd7865d6 --- /dev/null +++ b/Watch/App/Assets.xcassets/RemotePlayVideo.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "RemotePlayVideo@2x.png" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/RemotePlayVideo.imageset/RemotePlayVideo@2x.png b/Watch/App/Assets.xcassets/RemotePlayVideo.imageset/RemotePlayVideo@2x.png new file mode 100644 index 0000000000..fe017d5529 Binary files /dev/null and b/Watch/App/Assets.xcassets/RemotePlayVideo.imageset/RemotePlayVideo@2x.png differ diff --git a/Watch/App/Assets.xcassets/SavedMessages.imageset/Contents.json b/Watch/App/Assets.xcassets/SavedMessages.imageset/Contents.json new file mode 100644 index 0000000000..9b13966774 --- /dev/null +++ b/Watch/App/Assets.xcassets/SavedMessages.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "SavedMessages@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/SavedMessages.imageset/SavedMessages@2x.png b/Watch/App/Assets.xcassets/SavedMessages.imageset/SavedMessages@2x.png new file mode 100644 index 0000000000..62fb50087c Binary files /dev/null and b/Watch/App/Assets.xcassets/SavedMessages.imageset/SavedMessages@2x.png differ diff --git a/Watch/App/Assets.xcassets/SavedMessagesAvatar.imageset/Contents.json b/Watch/App/Assets.xcassets/SavedMessagesAvatar.imageset/Contents.json new file mode 100644 index 0000000000..684cc27133 --- /dev/null +++ b/Watch/App/Assets.xcassets/SavedMessagesAvatar.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "SavedMessagesAvatar@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/SavedMessagesAvatar.imageset/SavedMessagesAvatar@2x.png b/Watch/App/Assets.xcassets/SavedMessagesAvatar.imageset/SavedMessagesAvatar@2x.png new file mode 100644 index 0000000000..8fa8e689b4 Binary files /dev/null and b/Watch/App/Assets.xcassets/SavedMessagesAvatar.imageset/SavedMessagesAvatar@2x.png differ diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner0.imageset/Contents.json b/Watch/App/Assets.xcassets/Spinner/Spinner0.imageset/Contents.json new file mode 100644 index 0000000000..e409cd4b37 --- /dev/null +++ b/Watch/App/Assets.xcassets/Spinner/Spinner0.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "Spinner0@2x.png" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner0.imageset/Spinner0@2x.png b/Watch/App/Assets.xcassets/Spinner/Spinner0.imageset/Spinner0@2x.png new file mode 100644 index 0000000000..2f7cb4d7cc Binary files /dev/null and b/Watch/App/Assets.xcassets/Spinner/Spinner0.imageset/Spinner0@2x.png differ diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner1.imageset/Contents.json b/Watch/App/Assets.xcassets/Spinner/Spinner1.imageset/Contents.json new file mode 100644 index 0000000000..8574ce5928 --- /dev/null +++ b/Watch/App/Assets.xcassets/Spinner/Spinner1.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "Spinner1@2x.png" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner1.imageset/Spinner1@2x.png b/Watch/App/Assets.xcassets/Spinner/Spinner1.imageset/Spinner1@2x.png new file mode 100644 index 0000000000..779c7cfb63 Binary files /dev/null and b/Watch/App/Assets.xcassets/Spinner/Spinner1.imageset/Spinner1@2x.png differ diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner10.imageset/Contents.json b/Watch/App/Assets.xcassets/Spinner/Spinner10.imageset/Contents.json new file mode 100644 index 0000000000..e850b80781 --- /dev/null +++ b/Watch/App/Assets.xcassets/Spinner/Spinner10.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "Spinner10@2x.png" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner10.imageset/Spinner10@2x.png b/Watch/App/Assets.xcassets/Spinner/Spinner10.imageset/Spinner10@2x.png new file mode 100644 index 0000000000..5f4fab4e3a Binary files /dev/null and b/Watch/App/Assets.xcassets/Spinner/Spinner10.imageset/Spinner10@2x.png differ diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner11.imageset/Contents.json b/Watch/App/Assets.xcassets/Spinner/Spinner11.imageset/Contents.json new file mode 100644 index 0000000000..a9ed89e6a7 --- /dev/null +++ b/Watch/App/Assets.xcassets/Spinner/Spinner11.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "Spinner11@2x.png" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner11.imageset/Spinner11@2x.png b/Watch/App/Assets.xcassets/Spinner/Spinner11.imageset/Spinner11@2x.png new file mode 100644 index 0000000000..a34aa75932 Binary files /dev/null and b/Watch/App/Assets.xcassets/Spinner/Spinner11.imageset/Spinner11@2x.png differ diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner12.imageset/Contents.json b/Watch/App/Assets.xcassets/Spinner/Spinner12.imageset/Contents.json new file mode 100644 index 0000000000..2433d6e27b --- /dev/null +++ b/Watch/App/Assets.xcassets/Spinner/Spinner12.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "Spinner12@2x.png" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner12.imageset/Spinner12@2x.png b/Watch/App/Assets.xcassets/Spinner/Spinner12.imageset/Spinner12@2x.png new file mode 100644 index 0000000000..2672a1ecb9 Binary files /dev/null and b/Watch/App/Assets.xcassets/Spinner/Spinner12.imageset/Spinner12@2x.png differ diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner13.imageset/Contents.json b/Watch/App/Assets.xcassets/Spinner/Spinner13.imageset/Contents.json new file mode 100644 index 0000000000..2e9cbe2585 --- /dev/null +++ b/Watch/App/Assets.xcassets/Spinner/Spinner13.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "Spinner13@2x.png" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner13.imageset/Spinner13@2x.png b/Watch/App/Assets.xcassets/Spinner/Spinner13.imageset/Spinner13@2x.png new file mode 100644 index 0000000000..f48c612fa3 Binary files /dev/null and b/Watch/App/Assets.xcassets/Spinner/Spinner13.imageset/Spinner13@2x.png differ diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner14.imageset/Contents.json b/Watch/App/Assets.xcassets/Spinner/Spinner14.imageset/Contents.json new file mode 100644 index 0000000000..8ca24578f5 --- /dev/null +++ b/Watch/App/Assets.xcassets/Spinner/Spinner14.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "Spinner14@2x.png" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner14.imageset/Spinner14@2x.png b/Watch/App/Assets.xcassets/Spinner/Spinner14.imageset/Spinner14@2x.png new file mode 100644 index 0000000000..b085173e1c Binary files /dev/null and b/Watch/App/Assets.xcassets/Spinner/Spinner14.imageset/Spinner14@2x.png differ diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner15.imageset/Contents.json b/Watch/App/Assets.xcassets/Spinner/Spinner15.imageset/Contents.json new file mode 100644 index 0000000000..9ed5fd44cc --- /dev/null +++ b/Watch/App/Assets.xcassets/Spinner/Spinner15.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "Spinner15@2x.png" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner15.imageset/Spinner15@2x.png b/Watch/App/Assets.xcassets/Spinner/Spinner15.imageset/Spinner15@2x.png new file mode 100644 index 0000000000..520d61ceda Binary files /dev/null and b/Watch/App/Assets.xcassets/Spinner/Spinner15.imageset/Spinner15@2x.png differ diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner16.imageset/Contents.json b/Watch/App/Assets.xcassets/Spinner/Spinner16.imageset/Contents.json new file mode 100644 index 0000000000..54779a1bbe --- /dev/null +++ b/Watch/App/Assets.xcassets/Spinner/Spinner16.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "Spinner16@2x.png" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner16.imageset/Spinner16@2x.png b/Watch/App/Assets.xcassets/Spinner/Spinner16.imageset/Spinner16@2x.png new file mode 100644 index 0000000000..7f95ba3ecd Binary files /dev/null and b/Watch/App/Assets.xcassets/Spinner/Spinner16.imageset/Spinner16@2x.png differ diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner17.imageset/Contents.json b/Watch/App/Assets.xcassets/Spinner/Spinner17.imageset/Contents.json new file mode 100644 index 0000000000..8e0762fb8e --- /dev/null +++ b/Watch/App/Assets.xcassets/Spinner/Spinner17.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "Spinner17@2x.png" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner17.imageset/Spinner17@2x.png b/Watch/App/Assets.xcassets/Spinner/Spinner17.imageset/Spinner17@2x.png new file mode 100644 index 0000000000..97fc3e70ba Binary files /dev/null and b/Watch/App/Assets.xcassets/Spinner/Spinner17.imageset/Spinner17@2x.png differ diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner18.imageset/Contents.json b/Watch/App/Assets.xcassets/Spinner/Spinner18.imageset/Contents.json new file mode 100644 index 0000000000..bb24e8c1f4 --- /dev/null +++ b/Watch/App/Assets.xcassets/Spinner/Spinner18.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "Spinner18@2x.png" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner18.imageset/Spinner18@2x.png b/Watch/App/Assets.xcassets/Spinner/Spinner18.imageset/Spinner18@2x.png new file mode 100644 index 0000000000..2c4d0f0a6d Binary files /dev/null and b/Watch/App/Assets.xcassets/Spinner/Spinner18.imageset/Spinner18@2x.png differ diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner19.imageset/Contents.json b/Watch/App/Assets.xcassets/Spinner/Spinner19.imageset/Contents.json new file mode 100644 index 0000000000..6394c30ea5 --- /dev/null +++ b/Watch/App/Assets.xcassets/Spinner/Spinner19.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "Spinner19@2x.png" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner19.imageset/Spinner19@2x.png b/Watch/App/Assets.xcassets/Spinner/Spinner19.imageset/Spinner19@2x.png new file mode 100644 index 0000000000..a47787295c Binary files /dev/null and b/Watch/App/Assets.xcassets/Spinner/Spinner19.imageset/Spinner19@2x.png differ diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner2.imageset/Contents.json b/Watch/App/Assets.xcassets/Spinner/Spinner2.imageset/Contents.json new file mode 100644 index 0000000000..21d2275892 --- /dev/null +++ b/Watch/App/Assets.xcassets/Spinner/Spinner2.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "Spinner2@2x.png" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner2.imageset/Spinner2@2x.png b/Watch/App/Assets.xcassets/Spinner/Spinner2.imageset/Spinner2@2x.png new file mode 100644 index 0000000000..1be589d7b9 Binary files /dev/null and b/Watch/App/Assets.xcassets/Spinner/Spinner2.imageset/Spinner2@2x.png differ diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner20.imageset/Contents.json b/Watch/App/Assets.xcassets/Spinner/Spinner20.imageset/Contents.json new file mode 100644 index 0000000000..08ec4ab2ba --- /dev/null +++ b/Watch/App/Assets.xcassets/Spinner/Spinner20.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "Spinner20@2x.png" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner20.imageset/Spinner20@2x.png b/Watch/App/Assets.xcassets/Spinner/Spinner20.imageset/Spinner20@2x.png new file mode 100644 index 0000000000..d724449f09 Binary files /dev/null and b/Watch/App/Assets.xcassets/Spinner/Spinner20.imageset/Spinner20@2x.png differ diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner21.imageset/Contents.json b/Watch/App/Assets.xcassets/Spinner/Spinner21.imageset/Contents.json new file mode 100644 index 0000000000..4fd6dd1142 --- /dev/null +++ b/Watch/App/Assets.xcassets/Spinner/Spinner21.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "Spinner21@2x.png" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner21.imageset/Spinner21@2x.png b/Watch/App/Assets.xcassets/Spinner/Spinner21.imageset/Spinner21@2x.png new file mode 100644 index 0000000000..0d0e8dc203 Binary files /dev/null and b/Watch/App/Assets.xcassets/Spinner/Spinner21.imageset/Spinner21@2x.png differ diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner22.imageset/Contents.json b/Watch/App/Assets.xcassets/Spinner/Spinner22.imageset/Contents.json new file mode 100644 index 0000000000..6f4cb8b45a --- /dev/null +++ b/Watch/App/Assets.xcassets/Spinner/Spinner22.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "Spinner22@2x.png" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner22.imageset/Spinner22@2x.png b/Watch/App/Assets.xcassets/Spinner/Spinner22.imageset/Spinner22@2x.png new file mode 100644 index 0000000000..6068e1e3ef Binary files /dev/null and b/Watch/App/Assets.xcassets/Spinner/Spinner22.imageset/Spinner22@2x.png differ diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner23.imageset/Contents.json b/Watch/App/Assets.xcassets/Spinner/Spinner23.imageset/Contents.json new file mode 100644 index 0000000000..659d607c34 --- /dev/null +++ b/Watch/App/Assets.xcassets/Spinner/Spinner23.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "Spinner23@2x.png" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner23.imageset/Spinner23@2x.png b/Watch/App/Assets.xcassets/Spinner/Spinner23.imageset/Spinner23@2x.png new file mode 100644 index 0000000000..f789abca8c Binary files /dev/null and b/Watch/App/Assets.xcassets/Spinner/Spinner23.imageset/Spinner23@2x.png differ diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner24.imageset/Contents.json b/Watch/App/Assets.xcassets/Spinner/Spinner24.imageset/Contents.json new file mode 100644 index 0000000000..346eb77184 --- /dev/null +++ b/Watch/App/Assets.xcassets/Spinner/Spinner24.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "Spinner24@2x.png" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner24.imageset/Spinner24@2x.png b/Watch/App/Assets.xcassets/Spinner/Spinner24.imageset/Spinner24@2x.png new file mode 100644 index 0000000000..143ecad718 Binary files /dev/null and b/Watch/App/Assets.xcassets/Spinner/Spinner24.imageset/Spinner24@2x.png differ diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner25.imageset/Contents.json b/Watch/App/Assets.xcassets/Spinner/Spinner25.imageset/Contents.json new file mode 100644 index 0000000000..3591232a1c --- /dev/null +++ b/Watch/App/Assets.xcassets/Spinner/Spinner25.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "Spinner25@2x.png" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner25.imageset/Spinner25@2x.png b/Watch/App/Assets.xcassets/Spinner/Spinner25.imageset/Spinner25@2x.png new file mode 100644 index 0000000000..1bf4759663 Binary files /dev/null and b/Watch/App/Assets.xcassets/Spinner/Spinner25.imageset/Spinner25@2x.png differ diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner26.imageset/Contents.json b/Watch/App/Assets.xcassets/Spinner/Spinner26.imageset/Contents.json new file mode 100644 index 0000000000..b10711abac --- /dev/null +++ b/Watch/App/Assets.xcassets/Spinner/Spinner26.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "Spinner26@2x.png" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner26.imageset/Spinner26@2x.png b/Watch/App/Assets.xcassets/Spinner/Spinner26.imageset/Spinner26@2x.png new file mode 100644 index 0000000000..a240a194ae Binary files /dev/null and b/Watch/App/Assets.xcassets/Spinner/Spinner26.imageset/Spinner26@2x.png differ diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner27.imageset/Contents.json b/Watch/App/Assets.xcassets/Spinner/Spinner27.imageset/Contents.json new file mode 100644 index 0000000000..26c9e04b9c --- /dev/null +++ b/Watch/App/Assets.xcassets/Spinner/Spinner27.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "Spinner27@2x.png" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner27.imageset/Spinner27@2x.png b/Watch/App/Assets.xcassets/Spinner/Spinner27.imageset/Spinner27@2x.png new file mode 100644 index 0000000000..ae0d7d6338 Binary files /dev/null and b/Watch/App/Assets.xcassets/Spinner/Spinner27.imageset/Spinner27@2x.png differ diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner28.imageset/Contents.json b/Watch/App/Assets.xcassets/Spinner/Spinner28.imageset/Contents.json new file mode 100644 index 0000000000..2986916312 --- /dev/null +++ b/Watch/App/Assets.xcassets/Spinner/Spinner28.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "Spinner28@2x.png" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner28.imageset/Spinner28@2x.png b/Watch/App/Assets.xcassets/Spinner/Spinner28.imageset/Spinner28@2x.png new file mode 100644 index 0000000000..eb0510e6a3 Binary files /dev/null and b/Watch/App/Assets.xcassets/Spinner/Spinner28.imageset/Spinner28@2x.png differ diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner29.imageset/Contents.json b/Watch/App/Assets.xcassets/Spinner/Spinner29.imageset/Contents.json new file mode 100644 index 0000000000..ad6fd50395 --- /dev/null +++ b/Watch/App/Assets.xcassets/Spinner/Spinner29.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "Spinner29@2x.png" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner29.imageset/Spinner29@2x.png b/Watch/App/Assets.xcassets/Spinner/Spinner29.imageset/Spinner29@2x.png new file mode 100644 index 0000000000..f2885c2d03 Binary files /dev/null and b/Watch/App/Assets.xcassets/Spinner/Spinner29.imageset/Spinner29@2x.png differ diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner3.imageset/Contents.json b/Watch/App/Assets.xcassets/Spinner/Spinner3.imageset/Contents.json new file mode 100644 index 0000000000..4c004602af --- /dev/null +++ b/Watch/App/Assets.xcassets/Spinner/Spinner3.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "Spinner3@2x.png" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner3.imageset/Spinner3@2x.png b/Watch/App/Assets.xcassets/Spinner/Spinner3.imageset/Spinner3@2x.png new file mode 100644 index 0000000000..b1d2c367de Binary files /dev/null and b/Watch/App/Assets.xcassets/Spinner/Spinner3.imageset/Spinner3@2x.png differ diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner30.imageset/Contents.json b/Watch/App/Assets.xcassets/Spinner/Spinner30.imageset/Contents.json new file mode 100644 index 0000000000..f32c99cd5d --- /dev/null +++ b/Watch/App/Assets.xcassets/Spinner/Spinner30.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "Spinner30@2x.png" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner30.imageset/Spinner30@2x.png b/Watch/App/Assets.xcassets/Spinner/Spinner30.imageset/Spinner30@2x.png new file mode 100644 index 0000000000..e91f53100b Binary files /dev/null and b/Watch/App/Assets.xcassets/Spinner/Spinner30.imageset/Spinner30@2x.png differ diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner31.imageset/Contents.json b/Watch/App/Assets.xcassets/Spinner/Spinner31.imageset/Contents.json new file mode 100644 index 0000000000..fb424dfbf9 --- /dev/null +++ b/Watch/App/Assets.xcassets/Spinner/Spinner31.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "Spinner31@2x.png" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner31.imageset/Spinner31@2x.png b/Watch/App/Assets.xcassets/Spinner/Spinner31.imageset/Spinner31@2x.png new file mode 100644 index 0000000000..82655750f6 Binary files /dev/null and b/Watch/App/Assets.xcassets/Spinner/Spinner31.imageset/Spinner31@2x.png differ diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner32.imageset/Contents.json b/Watch/App/Assets.xcassets/Spinner/Spinner32.imageset/Contents.json new file mode 100644 index 0000000000..509c38170a --- /dev/null +++ b/Watch/App/Assets.xcassets/Spinner/Spinner32.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "Spinner32@2x.png" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner32.imageset/Spinner32@2x.png b/Watch/App/Assets.xcassets/Spinner/Spinner32.imageset/Spinner32@2x.png new file mode 100644 index 0000000000..9b91c3df55 Binary files /dev/null and b/Watch/App/Assets.xcassets/Spinner/Spinner32.imageset/Spinner32@2x.png differ diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner33.imageset/Contents.json b/Watch/App/Assets.xcassets/Spinner/Spinner33.imageset/Contents.json new file mode 100644 index 0000000000..32fa1feb79 --- /dev/null +++ b/Watch/App/Assets.xcassets/Spinner/Spinner33.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "Spinner33@2x.png" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner33.imageset/Spinner33@2x.png b/Watch/App/Assets.xcassets/Spinner/Spinner33.imageset/Spinner33@2x.png new file mode 100644 index 0000000000..5f166ccfec Binary files /dev/null and b/Watch/App/Assets.xcassets/Spinner/Spinner33.imageset/Spinner33@2x.png differ diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner34.imageset/Contents.json b/Watch/App/Assets.xcassets/Spinner/Spinner34.imageset/Contents.json new file mode 100644 index 0000000000..ad41eee0b5 --- /dev/null +++ b/Watch/App/Assets.xcassets/Spinner/Spinner34.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "Spinner34@2x.png" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner34.imageset/Spinner34@2x.png b/Watch/App/Assets.xcassets/Spinner/Spinner34.imageset/Spinner34@2x.png new file mode 100644 index 0000000000..afd7a979d1 Binary files /dev/null and b/Watch/App/Assets.xcassets/Spinner/Spinner34.imageset/Spinner34@2x.png differ diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner35.imageset/Contents.json b/Watch/App/Assets.xcassets/Spinner/Spinner35.imageset/Contents.json new file mode 100644 index 0000000000..1af1d5325c --- /dev/null +++ b/Watch/App/Assets.xcassets/Spinner/Spinner35.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "Spinner35@2x.png" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner35.imageset/Spinner35@2x.png b/Watch/App/Assets.xcassets/Spinner/Spinner35.imageset/Spinner35@2x.png new file mode 100644 index 0000000000..4b7b5b2f7a Binary files /dev/null and b/Watch/App/Assets.xcassets/Spinner/Spinner35.imageset/Spinner35@2x.png differ diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner36.imageset/Contents.json b/Watch/App/Assets.xcassets/Spinner/Spinner36.imageset/Contents.json new file mode 100644 index 0000000000..dcd23d529a --- /dev/null +++ b/Watch/App/Assets.xcassets/Spinner/Spinner36.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "Spinner36@2x.png" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner36.imageset/Spinner36@2x.png b/Watch/App/Assets.xcassets/Spinner/Spinner36.imageset/Spinner36@2x.png new file mode 100644 index 0000000000..416b2912b3 Binary files /dev/null and b/Watch/App/Assets.xcassets/Spinner/Spinner36.imageset/Spinner36@2x.png differ diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner37.imageset/Contents.json b/Watch/App/Assets.xcassets/Spinner/Spinner37.imageset/Contents.json new file mode 100644 index 0000000000..584e4b35ce --- /dev/null +++ b/Watch/App/Assets.xcassets/Spinner/Spinner37.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "Spinner37@2x.png" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner37.imageset/Spinner37@2x.png b/Watch/App/Assets.xcassets/Spinner/Spinner37.imageset/Spinner37@2x.png new file mode 100644 index 0000000000..63125944e6 Binary files /dev/null and b/Watch/App/Assets.xcassets/Spinner/Spinner37.imageset/Spinner37@2x.png differ diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner38.imageset/Contents.json b/Watch/App/Assets.xcassets/Spinner/Spinner38.imageset/Contents.json new file mode 100644 index 0000000000..379e84be11 --- /dev/null +++ b/Watch/App/Assets.xcassets/Spinner/Spinner38.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "Spinner38@2x.png" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner38.imageset/Spinner38@2x.png b/Watch/App/Assets.xcassets/Spinner/Spinner38.imageset/Spinner38@2x.png new file mode 100644 index 0000000000..5eacec6546 Binary files /dev/null and b/Watch/App/Assets.xcassets/Spinner/Spinner38.imageset/Spinner38@2x.png differ diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner4.imageset/Contents.json b/Watch/App/Assets.xcassets/Spinner/Spinner4.imageset/Contents.json new file mode 100644 index 0000000000..6c4cfa0f62 --- /dev/null +++ b/Watch/App/Assets.xcassets/Spinner/Spinner4.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "Spinner4@2x.png" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner4.imageset/Spinner4@2x.png b/Watch/App/Assets.xcassets/Spinner/Spinner4.imageset/Spinner4@2x.png new file mode 100644 index 0000000000..37e04adb95 Binary files /dev/null and b/Watch/App/Assets.xcassets/Spinner/Spinner4.imageset/Spinner4@2x.png differ diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner5.imageset/Contents.json b/Watch/App/Assets.xcassets/Spinner/Spinner5.imageset/Contents.json new file mode 100644 index 0000000000..d95dd090ba --- /dev/null +++ b/Watch/App/Assets.xcassets/Spinner/Spinner5.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "Spinner5@2x.png" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner5.imageset/Spinner5@2x.png b/Watch/App/Assets.xcassets/Spinner/Spinner5.imageset/Spinner5@2x.png new file mode 100644 index 0000000000..c5cc8010bf Binary files /dev/null and b/Watch/App/Assets.xcassets/Spinner/Spinner5.imageset/Spinner5@2x.png differ diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner6.imageset/Contents.json b/Watch/App/Assets.xcassets/Spinner/Spinner6.imageset/Contents.json new file mode 100644 index 0000000000..3446eae09f --- /dev/null +++ b/Watch/App/Assets.xcassets/Spinner/Spinner6.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "Spinner6@2x.png" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner6.imageset/Spinner6@2x.png b/Watch/App/Assets.xcassets/Spinner/Spinner6.imageset/Spinner6@2x.png new file mode 100644 index 0000000000..3a3b033ae4 Binary files /dev/null and b/Watch/App/Assets.xcassets/Spinner/Spinner6.imageset/Spinner6@2x.png differ diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner7.imageset/Contents.json b/Watch/App/Assets.xcassets/Spinner/Spinner7.imageset/Contents.json new file mode 100644 index 0000000000..5ab3e70b59 --- /dev/null +++ b/Watch/App/Assets.xcassets/Spinner/Spinner7.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "Spinner7@2x.png" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner7.imageset/Spinner7@2x.png b/Watch/App/Assets.xcassets/Spinner/Spinner7.imageset/Spinner7@2x.png new file mode 100644 index 0000000000..4f3aacac96 Binary files /dev/null and b/Watch/App/Assets.xcassets/Spinner/Spinner7.imageset/Spinner7@2x.png differ diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner8.imageset/Contents.json b/Watch/App/Assets.xcassets/Spinner/Spinner8.imageset/Contents.json new file mode 100644 index 0000000000..7a02258a9f --- /dev/null +++ b/Watch/App/Assets.xcassets/Spinner/Spinner8.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "Spinner8@2x.png" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner8.imageset/Spinner8@2x.png b/Watch/App/Assets.xcassets/Spinner/Spinner8.imageset/Spinner8@2x.png new file mode 100644 index 0000000000..2832056f79 Binary files /dev/null and b/Watch/App/Assets.xcassets/Spinner/Spinner8.imageset/Spinner8@2x.png differ diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner9.imageset/Contents.json b/Watch/App/Assets.xcassets/Spinner/Spinner9.imageset/Contents.json new file mode 100644 index 0000000000..1d5bc21c62 --- /dev/null +++ b/Watch/App/Assets.xcassets/Spinner/Spinner9.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "Spinner9@2x.png" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/Spinner/Spinner9.imageset/Spinner9@2x.png b/Watch/App/Assets.xcassets/Spinner/Spinner9.imageset/Spinner9@2x.png new file mode 100644 index 0000000000..af38e110c8 Binary files /dev/null and b/Watch/App/Assets.xcassets/Spinner/Spinner9.imageset/Spinner9@2x.png differ diff --git a/Watch/App/Assets.xcassets/StatusDot.imageset/Contents.json b/Watch/App/Assets.xcassets/StatusDot.imageset/Contents.json new file mode 100644 index 0000000000..8441497eec --- /dev/null +++ b/Watch/App/Assets.xcassets/StatusDot.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "StatusDot@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/StatusDot.imageset/StatusDot@2x.png b/Watch/App/Assets.xcassets/StatusDot.imageset/StatusDot@2x.png new file mode 100644 index 0000000000..93d74983e6 Binary files /dev/null and b/Watch/App/Assets.xcassets/StatusDot.imageset/StatusDot@2x.png differ diff --git a/Watch/App/Assets.xcassets/StickerIcon.imageset/Contents.json b/Watch/App/Assets.xcassets/StickerIcon.imageset/Contents.json new file mode 100644 index 0000000000..ce090e6c56 --- /dev/null +++ b/Watch/App/Assets.xcassets/StickerIcon.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "StickerIcon@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/StickerIcon.imageset/StickerIcon@2x.png b/Watch/App/Assets.xcassets/StickerIcon.imageset/StickerIcon@2x.png new file mode 100644 index 0000000000..ad86cc062b Binary files /dev/null and b/Watch/App/Assets.xcassets/StickerIcon.imageset/StickerIcon@2x.png differ diff --git a/Watch/App/Assets.xcassets/VerifiedProfile.imageset/Contents.json b/Watch/App/Assets.xcassets/VerifiedProfile.imageset/Contents.json new file mode 100644 index 0000000000..72f58ba414 --- /dev/null +++ b/Watch/App/Assets.xcassets/VerifiedProfile.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "VerifiedProfile@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Watch/App/Assets.xcassets/VerifiedProfile.imageset/VerifiedProfile@2x.png b/Watch/App/Assets.xcassets/VerifiedProfile.imageset/VerifiedProfile@2x.png new file mode 100644 index 0000000000..8267867a6f Binary files /dev/null and b/Watch/App/Assets.xcassets/VerifiedProfile.imageset/VerifiedProfile@2x.png differ diff --git a/Watch/App/Base.lproj/Interface.storyboard b/Watch/App/Base.lproj/Interface.storyboard new file mode 100644 index 0000000000..dc860eb1cc --- /dev/null +++ b/Watch/App/Base.lproj/Interface.storyboard @@ -0,0 +1,2003 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + +
+ + + + + + + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+
+ +
+
+ +
diff --git a/Watch/App/Info.plist b/Watch/App/Info.plist new file mode 100644 index 0000000000..b5f1564b8c --- /dev/null +++ b/Watch/App/Info.plist @@ -0,0 +1,33 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + $(APP_NAME) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 5.0.17 + CFBundleVersion + 624 + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + + WKCompanionAppBundleIdentifier + $(APP_BUNDLE_ID) + WKWatchKitApp + + + diff --git a/Watch/Bridge/TGBridgeActionMediaAttachment.h b/Watch/Bridge/TGBridgeActionMediaAttachment.h new file mode 100644 index 0000000000..2c1fb44db1 --- /dev/null +++ b/Watch/Bridge/TGBridgeActionMediaAttachment.h @@ -0,0 +1,36 @@ +#import "TGBridgeMediaAttachment.h" + +typedef NS_ENUM(NSUInteger, TGBridgeMessageAction) { + TGBridgeMessageActionNone = 0, + TGBridgeMessageActionChatEditTitle = 1, + TGBridgeMessageActionChatAddMember = 2, + TGBridgeMessageActionChatDeleteMember = 3, + TGBridgeMessageActionCreateChat = 4, + TGBridgeMessageActionChatEditPhoto = 5, + TGBridgeMessageActionContactRequest = 6, + TGBridgeMessageActionAcceptContactRequest = 7, + TGBridgeMessageActionContactRegistered = 8, + TGBridgeMessageActionUserChangedPhoto = 9, + TGBridgeMessageActionEncryptedChatRequest = 10, + TGBridgeMessageActionEncryptedChatAccept = 11, + TGBridgeMessageActionEncryptedChatDecline = 12, + TGBridgeMessageActionEncryptedChatMessageLifetime = 13, + TGBridgeMessageActionEncryptedChatScreenshot = 14, + TGBridgeMessageActionEncryptedChatMessageScreenshot = 15, + TGBridgeMessageActionCreateBroadcastList = 16, + TGBridgeMessageActionJoinedByLink = 17, + TGBridgeMessageActionChannelCreated = 18, + TGBridgeMessageActionChannelCommentsStatusChanged = 19, + TGBridgeMessageActionChannelInviter = 20, + TGBridgeMessageActionGroupMigratedTo = 21, + TGBridgeMessageActionGroupDeactivated = 22, + TGBridgeMessageActionGroupActivated = 23, + TGBridgeMessageActionChannelMigratedFrom = 24 +}; + +@interface TGBridgeActionMediaAttachment : TGBridgeMediaAttachment + +@property (nonatomic, assign) TGBridgeMessageAction actionType; +@property (nonatomic, strong) NSDictionary *actionData; + +@end diff --git a/Watch/Bridge/TGBridgeActionMediaAttachment.m b/Watch/Bridge/TGBridgeActionMediaAttachment.m new file mode 100644 index 0000000000..763cf8a1eb --- /dev/null +++ b/Watch/Bridge/TGBridgeActionMediaAttachment.m @@ -0,0 +1,33 @@ +#import "TGBridgeActionMediaAttachment.h" +#import "TGBridgeImageMediaAttachment.h" + +const NSInteger TGBridgeActionMediaAttachmentType = 0x1167E28B; + +NSString *const TGBridgeActionMediaTypeKey = @"actionType"; +NSString *const TGBridgeActionMediaDataKey = @"actionData"; + +@implementation TGBridgeActionMediaAttachment + +- (instancetype)initWithCoder:(NSCoder *)aDecoder +{ + self = [super init]; + if (self != nil) + { + _actionType = (TGBridgeMessageAction)[aDecoder decodeInt32ForKey:TGBridgeActionMediaTypeKey]; + _actionData = [aDecoder decodeObjectForKey:TGBridgeActionMediaDataKey]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder +{ + [aCoder encodeInt32:self.actionType forKey:TGBridgeActionMediaTypeKey]; + [aCoder encodeObject:self.actionData forKey:TGBridgeActionMediaDataKey]; +} + ++ (NSInteger)mediaType +{ + return TGBridgeActionMediaAttachmentType; +} + +@end diff --git a/Watch/Bridge/TGBridgeAudioMediaAttachment.h b/Watch/Bridge/TGBridgeAudioMediaAttachment.h new file mode 100644 index 0000000000..01b70fe351 --- /dev/null +++ b/Watch/Bridge/TGBridgeAudioMediaAttachment.h @@ -0,0 +1,16 @@ +#import "TGBridgeMediaAttachment.h" + +@interface TGBridgeAudioMediaAttachment : TGBridgeMediaAttachment + +@property (nonatomic, assign) int64_t audioId; +@property (nonatomic, assign) int64_t accessHash; +@property (nonatomic, assign) int32_t datacenterId; + +@property (nonatomic, assign) int64_t localAudioId; + +@property (nonatomic, assign) int32_t duration; +@property (nonatomic, assign) int32_t fileSize; + +- (int64_t)identifier; + +@end diff --git a/Watch/Bridge/TGBridgeAudioMediaAttachment.m b/Watch/Bridge/TGBridgeAudioMediaAttachment.m new file mode 100644 index 0000000000..3f4096a897 --- /dev/null +++ b/Watch/Bridge/TGBridgeAudioMediaAttachment.m @@ -0,0 +1,65 @@ +#import "TGBridgeAudioMediaAttachment.h" + +const NSInteger TGBridgeAudioMediaAttachmentType = 0x3A0E7A32; + +NSString *const TGBridgeAudioMediaAudioIdKey = @"audioId"; +NSString *const TGBridgeAudioMediaAccessHashKey = @"accessHash"; +NSString *const TGBridgeAudioMediaLocalIdKey = @"localId"; +NSString *const TGBridgeAudioMediaDatacenterIdKey = @"datacenterId"; +NSString *const TGBridgeAudioMediaDurationKey = @"duration"; +NSString *const TGBridgeAudioMediaFileSizeKey = @"fileSize"; + +@implementation TGBridgeAudioMediaAttachment + +- (instancetype)initWithCoder:(NSCoder *)aDecoder +{ + self = [super init]; + if (self != nil) + { + _audioId = [aDecoder decodeInt64ForKey:TGBridgeAudioMediaAudioIdKey]; + _accessHash = [aDecoder decodeInt64ForKey:TGBridgeAudioMediaAccessHashKey]; + _localAudioId = [aDecoder decodeInt64ForKey:TGBridgeAudioMediaLocalIdKey]; + _datacenterId = [aDecoder decodeInt32ForKey:TGBridgeAudioMediaDatacenterIdKey]; + _duration = [aDecoder decodeInt32ForKey:TGBridgeAudioMediaDurationKey]; + _fileSize = [aDecoder decodeInt32ForKey:TGBridgeAudioMediaFileSizeKey]; + } + return self; +} + +- (void)encodeWithCoder:(nonnull NSCoder *)aCoder +{ + [aCoder encodeInt64:self.audioId forKey:TGBridgeAudioMediaAudioIdKey]; + [aCoder encodeInt64:self.accessHash forKey:TGBridgeAudioMediaAccessHashKey]; + [aCoder encodeInt64:self.localAudioId forKey:TGBridgeAudioMediaLocalIdKey]; + [aCoder encodeInt32:self.datacenterId forKey:TGBridgeAudioMediaDatacenterIdKey]; + [aCoder encodeInt32:self.duration forKey:TGBridgeAudioMediaDurationKey]; + [aCoder encodeInt32:self.fileSize forKey:TGBridgeAudioMediaFileSizeKey]; +} + +- (int64_t)identifier +{ + if (self.localAudioId != 0) + return self.localAudioId; + + return self.audioId; +} + +- (BOOL)isEqual:(id)object +{ + if (object == self) + return YES; + + if (!object || ![object isKindOfClass:[self class]]) + return NO; + + TGBridgeAudioMediaAttachment *audio = (TGBridgeAudioMediaAttachment *)object; + + return (self.audioId == audio.audioId || self.localAudioId == audio.localAudioId); +} + ++ (NSInteger)mediaType +{ + return TGBridgeAudioMediaAttachmentType; +} + +@end diff --git a/Watch/Bridge/TGBridgeAudioSignals.h b/Watch/Bridge/TGBridgeAudioSignals.h new file mode 100644 index 0000000000..1fdda22e1f --- /dev/null +++ b/Watch/Bridge/TGBridgeAudioSignals.h @@ -0,0 +1,11 @@ +#import + +@class TGBridgeMediaAttachment; + +@interface TGBridgeAudioSignals : NSObject + ++ (SSignal *)audioForAttachment:(TGBridgeMediaAttachment *)attachment conversationId:(int64_t)conversationId messageId:(int32_t)messageId; + ++ (SSignal *)sentAudioForConversationId:(int64_t)conversationId; + +@end diff --git a/Watch/Bridge/TGBridgeAudioSignals.m b/Watch/Bridge/TGBridgeAudioSignals.m new file mode 100644 index 0000000000..36a0961e06 --- /dev/null +++ b/Watch/Bridge/TGBridgeAudioSignals.m @@ -0,0 +1,163 @@ +#import "TGBridgeAudioSignals.h" +#import "TGBridgeSubscriptions.h" +#import "TGBridgeAudioMediaAttachment.h" +#import "TGBridgeClient.h" +#import "TGFileCache.h" + +#import "TGBridgeMessage.h" + +#import "TGExtensionDelegate.h" +#import + +@interface TGBridgeAudioManager : NSObject +{ + NSMutableArray *_pendingUrls; + OSSpinLock _pendingUrlsLock; +} +@end + +@implementation TGBridgeAudioManager + +- (instancetype)init +{ + self = [super init]; + if (self != nil) + { + _pendingUrls = [[NSMutableArray alloc] init]; + } + return self; +} + +- (void)addUrl:(NSString *)url +{ + OSSpinLockLock(&_pendingUrlsLock); + [_pendingUrls addObject:url]; + OSSpinLockUnlock(&_pendingUrlsLock); +} + +- (void)removeUrl:(NSString *)url +{ + OSSpinLockLock(&_pendingUrlsLock); + [_pendingUrls removeObject:url]; + OSSpinLockUnlock(&_pendingUrlsLock); +} + +- (bool)hasUrl:(NSString *)url +{ + OSSpinLockLock(&_pendingUrlsLock); + bool contains = [_pendingUrls containsObject:url]; + OSSpinLockUnlock(&_pendingUrlsLock); + + return contains; +} + +@end + + +@implementation TGBridgeAudioSignals + ++ (SSignal *)audioForAttachment:(TGBridgeMediaAttachment *)attachment conversationId:(int64_t)conversationId messageId:(int32_t)messageId +{ + NSString *url = [NSString stringWithFormat:@"audio_%lld_%d", conversationId, messageId]; + SSignal *remoteSignal = [[[[TGBridgeClient instance] requestSignalWithSubscription:[[TGBridgeAudioSubscription alloc] initWithAttachment:attachment peerId:conversationId messageId:messageId]] onDispose:^ + { + // cancel download + }] mapToSignal:^SSignal *(__unused id next) + { + return [self _downloadedFileWithUrl:url]; + }]; + + return [[self _cachedOrPendingWithUrl:url] catch:^SSignal *(id error) + { + return remoteSignal; + }]; +} + ++ (SSignal *)_loadCachedWithUrl:(NSString *)url +{ + return [[SSignal alloc] initWithGenerator:^id(SSubscriber *subscriber) + { + TGFileCache *audioCache = [TGExtensionDelegate instance].audioCache; + if ([audioCache hasDataForKey:url]) + { + [subscriber putNext:[audioCache urlForKey:url]]; + [subscriber putCompletion]; + } + else + { + [subscriber putError:nil]; + } + + return nil; + }]; +} + ++ (SSignal *)_downloadedFileWithUrl:(NSString *)url +{ + return [[self _loadCachedWithUrl:url] catch:^SSignal *(id error) + { + return [[[TGBridgeClient instance] fileSignalForKey:url] take:1]; + }]; +} + ++ (SSignal *)_cachedOrPendingWithUrl:(NSString *)url +{ + return [[self _loadCachedWithUrl:url] catch:^SSignal *(id error) + { + if ([[self audioManager] hasUrl:url]) + return [self _downloadedFileWithUrl:url]; + + return [SSignal fail:nil]; + }]; +} + ++ (SSignal *)sentAudioForConversationId:(int64_t)conversationId +{ + return [[[TGBridgeClient instance] requestSignalWithSubscription:[[TGBridgeAudioSentSubscription alloc] initWithConversationId:conversationId]] onNext:^(TGBridgeMessage *next) + { + int64_t identifier = 0; + int64_t localIdentifier = 0; + for (TGBridgeMediaAttachment *attachment in next.media) + { + if ([attachment isKindOfClass:[TGBridgeAudioMediaAttachment class]]) + { + identifier = ((TGBridgeAudioMediaAttachment *)attachment).audioId; + localIdentifier = ((TGBridgeAudioMediaAttachment *)attachment).localAudioId; + } + else if ([attachment isKindOfClass:[TGBridgeDocumentMediaAttachment class]]) + { + identifier = ((TGBridgeDocumentMediaAttachment *)attachment).documentId; + localIdentifier = ((TGBridgeDocumentMediaAttachment *)attachment).localDocumentId; + } + } + + if (identifier != 0 && localIdentifier != 0) + { + TGFileCache *audioCache = [[TGExtensionDelegate instance] audioCache]; + + NSString *localId = [NSString stringWithFormat:@"%lld", localIdentifier]; + NSString *audioId = [NSString stringWithFormat:@"%lld", identifier]; + + if ([audioCache hasDataForKey:localId] && ![audioCache hasDataForKey:audioId]) + { + NSURL *localUrl = [audioCache urlForKey:localId]; + NSURL *remoteUrl = [audioCache urlForKey:audioId]; + + [[NSFileManager defaultManager] moveItemAtURL:localUrl toURL:remoteUrl error:nil]; + } + } + }]; +} + ++ (TGBridgeAudioManager *)audioManager +{ + static dispatch_once_t onceToken; + static TGBridgeAudioManager *manager; + dispatch_once(&onceToken, ^ + { + manager = [[TGBridgeAudioManager alloc] init]; + }); + return manager; +} + +@end diff --git a/Watch/Bridge/TGBridgeBotCommandInfo.h b/Watch/Bridge/TGBridgeBotCommandInfo.h new file mode 100644 index 0000000000..fe6f72e1a0 --- /dev/null +++ b/Watch/Bridge/TGBridgeBotCommandInfo.h @@ -0,0 +1,12 @@ +#import + +@interface TGBridgeBotCommandInfo : NSObject +{ + NSString *_command; + NSString *_commandDescription; +} + +@property (nonatomic, readonly) NSString *command; +@property (nonatomic, readonly) NSString *commandDescription; + +@end diff --git a/Watch/Bridge/TGBridgeBotCommandInfo.m b/Watch/Bridge/TGBridgeBotCommandInfo.m new file mode 100644 index 0000000000..0f1e005861 --- /dev/null +++ b/Watch/Bridge/TGBridgeBotCommandInfo.m @@ -0,0 +1,25 @@ +#import "TGBridgeBotCommandInfo.h" + +NSString *const TGBridgeBotCommandInfoCommandKey = @"command"; +NSString *const TGBridgeBotCommandDescriptionKey = @"commandDescription"; + +@implementation TGBridgeBotCommandInfo + +- (instancetype)initWithCoder:(NSCoder *)aDecoder +{ + self = [super init]; + if (self != nil) + { + _command = [aDecoder decodeObjectForKey:TGBridgeBotCommandInfoCommandKey]; + _commandDescription = [aDecoder decodeObjectForKey:TGBridgeBotCommandDescriptionKey]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder +{ + [aCoder encodeObject:self.command forKey:TGBridgeBotCommandInfoCommandKey]; + [aCoder encodeObject:self.commandDescription forKey:TGBridgeBotCommandDescriptionKey]; +} + +@end diff --git a/Watch/Bridge/TGBridgeBotInfo+TGBotInfo.h b/Watch/Bridge/TGBridgeBotInfo+TGBotInfo.h new file mode 100644 index 0000000000..be8b1097ba --- /dev/null +++ b/Watch/Bridge/TGBridgeBotInfo+TGBotInfo.h @@ -0,0 +1,9 @@ +#import "TGBridgeBotInfo.h" + +@class TGBotInfo; + +@interface TGBridgeBotInfo (TGBotInfo) + ++ (TGBridgeBotInfo *)botInfoWithTGBotInfo:(TGBotInfo *)botInfo userId:(int32_t)userId; + +@end diff --git a/Watch/Bridge/TGBridgeBotInfo+TGBotInfo.m b/Watch/Bridge/TGBridgeBotInfo+TGBotInfo.m new file mode 100644 index 0000000000..8f48d0d515 --- /dev/null +++ b/Watch/Bridge/TGBridgeBotInfo+TGBotInfo.m @@ -0,0 +1,29 @@ +#import "TGBridgeBotInfo+TGBotInfo.h" + +#import + +#import "TGBridgeBotCommandInfo+TGBotCommandInfo.h" + +@implementation TGBridgeBotInfo (TGBotInfo) + ++ (TGBridgeBotInfo *)botInfoWithTGBotInfo:(TGBotInfo *)botInfo userId:(int32_t)userId +{ + TGBridgeBotInfo *bridgeBotInfo = [[TGBridgeBotInfo alloc] init]; + bridgeBotInfo->_version = botInfo.version; + bridgeBotInfo->_userId = userId; + bridgeBotInfo->_shortDescription = botInfo.shortDescription; + bridgeBotInfo->_botDescription = botInfo.botDescription; + + NSMutableArray *commandList = [[NSMutableArray alloc] init]; + for (TGBotComandInfo *commandInfo in botInfo.commandList) + { + TGBridgeBotCommandInfo *bridgeCommandInfo = [TGBridgeBotCommandInfo botCommandInfoWithTGBotCommandInfo:commandInfo]; + if (bridgeCommandInfo != nil) + [commandList addObject:bridgeCommandInfo]; + } + bridgeBotInfo->_commandList = commandList; + + return bridgeBotInfo; +} + +@end diff --git a/Watch/Bridge/TGBridgeBotInfo.h b/Watch/Bridge/TGBridgeBotInfo.h new file mode 100644 index 0000000000..0dafae5cef --- /dev/null +++ b/Watch/Bridge/TGBridgeBotInfo.h @@ -0,0 +1,12 @@ +#import + +@interface TGBridgeBotInfo : NSObject +{ + NSString *_shortDescription; + NSArray *_commandList; +} + +@property (nonatomic, readonly) NSString *shortDescription; +@property (nonatomic, readonly) NSArray *commandList; + +@end diff --git a/Watch/Bridge/TGBridgeBotInfo.m b/Watch/Bridge/TGBridgeBotInfo.m new file mode 100644 index 0000000000..996abb3a95 --- /dev/null +++ b/Watch/Bridge/TGBridgeBotInfo.m @@ -0,0 +1,25 @@ +#import "TGBridgeBotInfo.h" + +NSString *const TGBridgeBotInfoShortDescriptionKey = @"shortDescription"; +NSString *const TGBridgeBotInfoCommandListKey = @"commandList"; + +@implementation TGBridgeBotInfo + +- (instancetype)initWithCoder:(NSCoder *)aDecoder +{ + self = [super init]; + if (self != nil) + { + _shortDescription = [aDecoder decodeObjectForKey:TGBridgeBotInfoShortDescriptionKey]; + _commandList = [aDecoder decodeObjectForKey:TGBridgeBotInfoCommandListKey]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder +{ + [aCoder encodeObject:self.shortDescription forKey:TGBridgeBotInfoShortDescriptionKey]; + [aCoder encodeObject:self.commandList forKey:TGBridgeBotInfoCommandListKey]; +} + +@end diff --git a/Watch/Bridge/TGBridgeBotReplyMarkup.h b/Watch/Bridge/TGBridgeBotReplyMarkup.h new file mode 100644 index 0000000000..f0cf962cde --- /dev/null +++ b/Watch/Bridge/TGBridgeBotReplyMarkup.h @@ -0,0 +1,46 @@ +#import + +@class TGBridgeMessage; + +@interface TGBridgeBotReplyMarkupButton : NSObject +{ + NSString *_text; +} + +@property (nonatomic, readonly) NSString *text; + +- (instancetype)initWithText:(NSString *)text; + +@end + + +@interface TGBridgeBotReplyMarkupRow : NSObject +{ + NSArray *_buttons; +} + +@property (nonatomic, readonly) NSArray *buttons; + +- (instancetype)initWithButtons:(NSArray *)buttons; + +@end + + +@interface TGBridgeBotReplyMarkup : NSObject +{ + int32_t _userId; + int32_t _messageId; + TGBridgeMessage *_message; + bool _hideKeyboardOnActivation; + bool _alreadyActivated; + NSArray *_rows; +} + +@property (nonatomic, readonly) int32_t userId; +@property (nonatomic, readonly) int32_t messageId; +@property (nonatomic, readonly) TGBridgeMessage *message; +@property (nonatomic, readonly) bool hideKeyboardOnActivation; +@property (nonatomic, readonly) bool alreadyActivated; +@property (nonatomic, readonly) NSArray *rows; + +@end diff --git a/Watch/Bridge/TGBridgeBotReplyMarkup.m b/Watch/Bridge/TGBridgeBotReplyMarkup.m new file mode 100644 index 0000000000..241798f4dc --- /dev/null +++ b/Watch/Bridge/TGBridgeBotReplyMarkup.m @@ -0,0 +1,101 @@ +#import "TGBridgeBotReplyMarkup.h" + +NSString *const TGBridgeBotReplyMarkupButtonText = @"text"; + +@implementation TGBridgeBotReplyMarkupButton + +- (instancetype)initWithText:(NSString *)text +{ + self = [super init]; + if (self != nil) + { + _text = text; + } + return self; +} + +- (instancetype)initWithCoder:(NSCoder *)aDecoder +{ + self = [super init]; + if (self != nil) + { + _text = [aDecoder decodeObjectForKey:TGBridgeBotReplyMarkupButtonText]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder +{ + [aCoder encodeObject:self.text forKey:TGBridgeBotReplyMarkupButtonText]; +} + +@end + + +NSString *const TGBridgeBotReplyMarkupRowButtons = @"buttons"; + +@implementation TGBridgeBotReplyMarkupRow + +- (instancetype)initWithButtons:(NSArray *)buttons +{ + self = [super init]; + if (self != nil) + { + _buttons = buttons; + } + return self; +} + +- (instancetype)initWithCoder:(NSCoder *)aDecoder +{ + self = [super init]; + if (self != nil) + { + _buttons = [aDecoder decodeObjectForKey:TGBridgeBotReplyMarkupRowButtons]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder +{ + [aCoder encodeObject:self.buttons forKey:TGBridgeBotReplyMarkupRowButtons]; +} + +@end + + +NSString *const TGBridgeBotReplyMarkupUserId = @"userId"; +NSString *const TGBridgeBotReplyMarkupMessageId = @"messageId"; +NSString *const TGBridgeBotReplyMarkupMessage = @"message"; +NSString *const TGBridgeBotReplyMarkupHideKeyboardOnActivation = @"hideKeyboardOnActivation"; +NSString *const TGBridgeBotReplyMarkupAlreadyActivated = @"alreadyActivated"; +NSString *const TGBridgeBotReplyMarkupRows = @"rows"; + +@implementation TGBridgeBotReplyMarkup + +- (instancetype)initWithCoder:(NSCoder *)aDecoder +{ + self = [super init]; + if (self != nil) + { + _userId = [aDecoder decodeInt32ForKey:TGBridgeBotReplyMarkupUserId]; + _messageId = [aDecoder decodeInt32ForKey:TGBridgeBotReplyMarkupMessageId]; + _message = [aDecoder decodeObjectForKey:TGBridgeBotReplyMarkupMessage]; + _hideKeyboardOnActivation = [aDecoder decodeBoolForKey:TGBridgeBotReplyMarkupHideKeyboardOnActivation]; + _alreadyActivated = [aDecoder decodeBoolForKey:TGBridgeBotReplyMarkupAlreadyActivated]; + _rows = [aDecoder decodeObjectForKey:TGBridgeBotReplyMarkupRows]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder +{ + [aCoder encodeInt32:self.userId forKey:TGBridgeBotReplyMarkupUserId]; + [aCoder encodeInt32:self.messageId forKey:TGBridgeBotReplyMarkupMessageId]; + [aCoder encodeObject:self.message forKey:TGBridgeBotReplyMarkupMessage]; + [aCoder encodeBool:self.hideKeyboardOnActivation forKey:TGBridgeBotReplyMarkupHideKeyboardOnActivation]; + [aCoder encodeBool:self.alreadyActivated forKey:TGBridgeBotReplyMarkupAlreadyActivated]; + [aCoder encodeObject:self.rows forKey:TGBridgeBotReplyMarkupRows]; +} + +@end diff --git a/Watch/Bridge/TGBridgeBotSignals.h b/Watch/Bridge/TGBridgeBotSignals.h new file mode 100644 index 0000000000..a3930248fa --- /dev/null +++ b/Watch/Bridge/TGBridgeBotSignals.h @@ -0,0 +1,8 @@ +#import + +@interface TGBridgeBotSignals : NSObject + ++ (SSignal *)botInfoForUserId:(int32_t)userId; ++ (SSignal *)botReplyMarkupForPeerId:(int64_t)peerId; + +@end diff --git a/Watch/Bridge/TGBridgeBotSignals.m b/Watch/Bridge/TGBridgeBotSignals.m new file mode 100644 index 0000000000..9af378c047 --- /dev/null +++ b/Watch/Bridge/TGBridgeBotSignals.m @@ -0,0 +1,55 @@ +#import "TGBridgeBotSignals.h" +#import "TGBridgeUserCache.h" + +#import "TGBridgeClient.h" +#import "TGBridgeSubscriptions.h" + +#import "TGBridgeUser.h" +#import "TGBridgeBotInfo.h" + +@implementation TGBridgeBotSignals + ++ (SSignal *)botInfoForUserId:(int32_t)userId +{ + SSignal *cachedSignal = [[SSignal alloc] initWithGenerator:^id(SSubscriber *subscriber) + { + TGBridgeUser *user = [[TGBridgeUserCache instance] userWithId:userId]; + TGBridgeBotInfo *botInfo = [[TGBridgeUserCache instance] botInfoForUserId:userId]; + + if (botInfo == nil) + { + [subscriber putError:nil]; + } + else + { + [subscriber putNext:botInfo]; + } + + return nil; + }]; + + return [cachedSignal catch:^SSignal *(__unused id error) + { + return [[[TGBridgeClient instance] requestSignalWithSubscription:[[TGBridgeUserBotInfoSubscription alloc] initWithUserIds:@[ @(userId) ]]] mapToSignal:^SSignal *(NSDictionary *bots) + { + TGBridgeBotInfo *botInfo = bots[@(userId)]; + + if (botInfo != nil) + { + [[TGBridgeUserCache instance] storeBotInfo:botInfo forUserId:userId]; + return [SSignal single:botInfo]; + } + else + { + return [SSignal fail:nil]; + } + }]; + }]; +} + ++ (SSignal *)botReplyMarkupForPeerId:(int64_t)peerId +{ + return [[TGBridgeClient instance] requestSignalWithSubscription:[[TGBridgeBotReplyMarkupSubscription alloc] initWithPeerId:peerId]]; +} + +@end diff --git a/Watch/Bridge/TGBridgeChat+TGTableItem.h b/Watch/Bridge/TGBridgeChat+TGTableItem.h new file mode 100644 index 0000000000..4b036bf928 --- /dev/null +++ b/Watch/Bridge/TGBridgeChat+TGTableItem.h @@ -0,0 +1,6 @@ +#import "TGBridgeChat.h" +#import "WKInterfaceTable+TGDataDrivenTable.h" + +@interface TGBridgeChat (TGTableItem) + +@end diff --git a/Watch/Bridge/TGBridgeChat+TGTableItem.m b/Watch/Bridge/TGBridgeChat+TGTableItem.m new file mode 100644 index 0000000000..bc1cb4d106 --- /dev/null +++ b/Watch/Bridge/TGBridgeChat+TGTableItem.m @@ -0,0 +1,10 @@ +#import "TGBridgeChat+TGTableItem.h" + +@implementation TGBridgeChat (TGTableItem) + +- (NSString *)uniqueIdentifier +{ + return [NSString stringWithFormat:@"%lld", self.identifier]; +} + +@end diff --git a/Watch/Bridge/TGBridgeChat.h b/Watch/Bridge/TGBridgeChat.h new file mode 100644 index 0000000000..6f94f3df87 --- /dev/null +++ b/Watch/Bridge/TGBridgeChat.h @@ -0,0 +1,46 @@ +#import "TGBridgeCommon.h" +#import "TGBridgeMessage.h" + +@interface TGBridgeChat : NSObject + +@property (nonatomic) int64_t identifier; +@property (nonatomic) NSTimeInterval date; +@property (nonatomic) int32_t fromUid; +@property (nonatomic, strong) NSString *text; + +@property (nonatomic, strong) NSArray *media; + +@property (nonatomic) bool outgoing; +@property (nonatomic) bool unread; +@property (nonatomic) bool deliveryError; +@property (nonatomic) TGBridgeMessageDeliveryState deliveryState; + +@property (nonatomic) int32_t unreadCount; + +@property (nonatomic) bool isBroadcast; + +@property (nonatomic, strong) NSString *groupTitle; +@property (nonatomic, strong) NSString *groupPhotoSmall; +@property (nonatomic, strong) NSString *groupPhotoBig; + +@property (nonatomic) bool isGroup; +@property (nonatomic) bool hasLeftGroup; +@property (nonatomic) bool isKickedFromGroup; + +@property (nonatomic) bool isChannel; +@property (nonatomic) bool isChannelGroup; + +@property (nonatomic, strong) NSString *userName; +@property (nonatomic, strong) NSString *about; +@property (nonatomic) bool verified; + +@property (nonatomic) int32_t participantsCount; +@property (nonatomic, strong) NSArray *participants; + +- (NSIndexSet *)involvedUserIds; +- (NSIndexSet *)participantsUserIds; + +@end + +extern NSString *const TGBridgeChatKey; +extern NSString *const TGBridgeChatsArrayKey; diff --git a/Watch/Bridge/TGBridgeChat.m b/Watch/Bridge/TGBridgeChat.m new file mode 100644 index 0000000000..4ea9552f08 --- /dev/null +++ b/Watch/Bridge/TGBridgeChat.m @@ -0,0 +1,139 @@ +#import "TGBridgeChat.h" +#import "TGBridgePeerIdAdapter.h" + +NSString *const TGBridgeChatIdentifierKey = @"identifier"; +NSString *const TGBridgeChatDateKey = @"date"; +NSString *const TGBridgeChatFromUidKey = @"fromUid"; +NSString *const TGBridgeChatTextKey = @"text"; +NSString *const TGBridgeChatOutgoingKey = @"outgoing"; +NSString *const TGBridgeChatUnreadKey = @"unread"; +NSString *const TGBridgeChatMediaKey = @"media"; +NSString *const TGBridgeChatUnreadCountKey = @"unreadCount"; +NSString *const TGBridgeChatGroupTitleKey = @"groupTitle"; +NSString *const TGBridgeChatGroupPhotoSmallKey = @"groupPhotoSmall"; +NSString *const TGBridgeChatGroupPhotoBigKey = @"groupPhotoBig"; +NSString *const TGBridgeChatIsGroupKey = @"isGroup"; +NSString *const TGBridgeChatHasLeftGroupKey = @"hasLeftGroup"; +NSString *const TGBridgeChatIsKickedFromGroupKey = @"isKickedFromGroup"; +NSString *const TGBridgeChatIsChannelKey = @"isChannel"; +NSString *const TGBridgeChatIsChannelGroupKey = @"isChannelGroup"; +NSString *const TGBridgeChatUserNameKey = @"userName"; +NSString *const TGBridgeChatAboutKey = @"about"; +NSString *const TGBridgeChatVerifiedKey = @"verified"; +NSString *const TGBridgeChatGroupParticipantsCountKey = @"participantsCount"; +NSString *const TGBridgeChatGroupParticipantsKey = @"participants"; +NSString *const TGBridgeChatDeliveryStateKey = @"deliveryState"; +NSString *const TGBridgeChatDeliveryErrorKey = @"deliveryError"; + +NSString *const TGBridgeChatKey = @"chat"; +NSString *const TGBridgeChatsArrayKey = @"chats"; + +@implementation TGBridgeChat + +- (instancetype)initWithCoder:(NSCoder *)aDecoder +{ + self = [super init]; + if (self != nil) + { + _identifier = [aDecoder decodeInt64ForKey:TGBridgeChatIdentifierKey]; + _date = [aDecoder decodeDoubleForKey:TGBridgeChatDateKey]; + _fromUid = [aDecoder decodeInt32ForKey:TGBridgeChatFromUidKey]; + _text = [aDecoder decodeObjectForKey:TGBridgeChatTextKey]; + _outgoing = [aDecoder decodeBoolForKey:TGBridgeChatOutgoingKey]; + _unread = [aDecoder decodeBoolForKey:TGBridgeChatUnreadKey]; + _unreadCount = [aDecoder decodeInt32ForKey:TGBridgeChatUnreadCountKey]; + _deliveryState = [aDecoder decodeInt32ForKey:TGBridgeChatDeliveryStateKey]; + _deliveryError = [aDecoder decodeBoolForKey:TGBridgeChatDeliveryErrorKey]; + _media = [aDecoder decodeObjectForKey:TGBridgeChatMediaKey]; + + _groupTitle = [aDecoder decodeObjectForKey:TGBridgeChatGroupTitleKey]; + _groupPhotoSmall = [aDecoder decodeObjectForKey:TGBridgeChatGroupPhotoSmallKey]; + _groupPhotoBig = [aDecoder decodeObjectForKey:TGBridgeChatGroupPhotoBigKey]; + _isGroup = [aDecoder decodeBoolForKey:TGBridgeChatIsGroupKey]; + _hasLeftGroup = [aDecoder decodeBoolForKey:TGBridgeChatHasLeftGroupKey]; + _isKickedFromGroup = [aDecoder decodeBoolForKey:TGBridgeChatIsKickedFromGroupKey]; + _isChannel = [aDecoder decodeBoolForKey:TGBridgeChatIsChannelKey]; + _isChannelGroup = [aDecoder decodeBoolForKey:TGBridgeChatIsChannelGroupKey]; + _userName = [aDecoder decodeObjectForKey:TGBridgeChatUserNameKey]; + _about = [aDecoder decodeObjectForKey:TGBridgeChatAboutKey]; + _verified = [aDecoder decodeBoolForKey:TGBridgeChatVerifiedKey]; + _participantsCount = [aDecoder decodeInt32ForKey:TGBridgeChatGroupParticipantsCountKey]; + _participants = [aDecoder decodeObjectForKey:TGBridgeChatGroupParticipantsKey]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder +{ + [aCoder encodeInt64:self.identifier forKey:TGBridgeChatIdentifierKey]; + [aCoder encodeDouble:self.date forKey:TGBridgeChatDateKey]; + [aCoder encodeInt32:self.fromUid forKey:TGBridgeChatFromUidKey]; + [aCoder encodeObject:self.text forKey:TGBridgeChatTextKey]; + [aCoder encodeBool:self.outgoing forKey:TGBridgeChatOutgoingKey]; + [aCoder encodeBool:self.unread forKey:TGBridgeChatUnreadKey]; + [aCoder encodeInt32:self.unreadCount forKey:TGBridgeChatUnreadCountKey]; + [aCoder encodeInt32:self.deliveryState forKey:TGBridgeChatDeliveryStateKey]; + [aCoder encodeBool:self.deliveryError forKey:TGBridgeChatDeliveryErrorKey]; + [aCoder encodeObject:self.media forKey:TGBridgeChatMediaKey]; + + [aCoder encodeObject:self.groupTitle forKey:TGBridgeChatGroupTitleKey]; + [aCoder encodeObject:self.groupPhotoSmall forKey:TGBridgeChatGroupPhotoSmallKey]; + [aCoder encodeObject:self.groupPhotoBig forKey:TGBridgeChatGroupPhotoBigKey]; + + [aCoder encodeBool:self.isGroup forKey:TGBridgeChatIsGroupKey]; + [aCoder encodeBool:self.hasLeftGroup forKey:TGBridgeChatHasLeftGroupKey]; + [aCoder encodeBool:self.isKickedFromGroup forKey:TGBridgeChatIsKickedFromGroupKey]; + + [aCoder encodeBool:self.isChannel forKey:TGBridgeChatIsChannelKey]; + [aCoder encodeBool:self.isChannelGroup forKey:TGBridgeChatIsChannelGroupKey]; + [aCoder encodeObject:self.userName forKey:TGBridgeChatUserNameKey]; + [aCoder encodeObject:self.about forKey:TGBridgeChatAboutKey]; + [aCoder encodeBool:self.verified forKey:TGBridgeChatVerifiedKey]; + + [aCoder encodeInt32:self.participantsCount forKey:TGBridgeChatGroupParticipantsCountKey]; + [aCoder encodeObject:self.participants forKey:TGBridgeChatGroupParticipantsKey]; +} + +- (NSIndexSet *)involvedUserIds +{ + NSMutableIndexSet *userIds = [[NSMutableIndexSet alloc] init]; + if (!self.isGroup && !self.isChannel && self.identifier != 0) + [userIds addIndex:(int32_t)self.identifier]; + if ((!self.isChannel || self.isChannelGroup) && self.fromUid != self.identifier && self.fromUid != 0 && !TGPeerIdIsChannel(self.fromUid) && self.fromUid > 0) + [userIds addIndex:(int32_t)self.fromUid]; + + for (TGBridgeMediaAttachment *attachment in self.media) + { + if ([attachment isKindOfClass:[TGBridgeActionMediaAttachment class]]) + { + TGBridgeActionMediaAttachment *actionAttachment = (TGBridgeActionMediaAttachment *)attachment; + if (actionAttachment.actionData[@"uid"] != nil) + [userIds addIndex:[actionAttachment.actionData[@"uid"] integerValue]]; + } + } + + return userIds; +} + +- (NSIndexSet *)participantsUserIds +{ + NSMutableIndexSet *userIds = [[NSMutableIndexSet alloc] init]; + + for (NSNumber *uid in self.participants) + [userIds addIndex:uid.unsignedIntegerValue]; + + return userIds; +} + +- (BOOL)isEqual:(id)object +{ + if (object == self) + return YES; + + if (!object || ![object isKindOfClass:[self class]]) + return NO; + + return self.identifier == ((TGBridgeChat *)object).identifier; +} + +@end diff --git a/Watch/Bridge/TGBridgeChatListSignals.h b/Watch/Bridge/TGBridgeChatListSignals.h new file mode 100644 index 0000000000..3daf6aa4a4 --- /dev/null +++ b/Watch/Bridge/TGBridgeChatListSignals.h @@ -0,0 +1,7 @@ +#import + +@interface TGBridgeChatListSignals : NSObject + ++ (SSignal *)chatListWithLimit:(NSUInteger)limit; + +@end diff --git a/Watch/Bridge/TGBridgeChatListSignals.m b/Watch/Bridge/TGBridgeChatListSignals.m new file mode 100644 index 0000000000..61edb4ecec --- /dev/null +++ b/Watch/Bridge/TGBridgeChatListSignals.m @@ -0,0 +1,14 @@ +#import "TGBridgeChatListSignals.h" +#import "TGBridgeSubscriptions.h" +#import "TGBridgeChat.h" +#import "TGBridgeUser.h" +#import "TGBridgeClient.h" + +@implementation TGBridgeChatListSignals + ++ (SSignal *)chatListWithLimit:(NSUInteger)limit; +{ + return [[TGBridgeClient instance] requestSignalWithSubscription:[[TGBridgeChatListSubscription alloc] initWithLimit:limit]]; +} + +@end diff --git a/Watch/Bridge/TGBridgeChatMessageListSignals.h b/Watch/Bridge/TGBridgeChatMessageListSignals.h new file mode 100644 index 0000000000..a80d648aec --- /dev/null +++ b/Watch/Bridge/TGBridgeChatMessageListSignals.h @@ -0,0 +1,11 @@ +#import + +@interface TGBridgeChatMessageListSignals : NSObject + ++ (SSignal *)chatMessageListViewWithPeerId:(int64_t)peerId atMessageId:(int32_t)messageId rangeMessageCount:(NSUInteger)rangeMessageCount; + ++ (SSignal *)chatMessageWithPeerId:(int64_t)peerId messageId:(int32_t)messageId; + ++ (SSignal *)readChatMessageListWithPeerId:(int64_t)peerId messageId:(int32_t)messageId; + +@end diff --git a/Watch/Bridge/TGBridgeChatMessageListSignals.m b/Watch/Bridge/TGBridgeChatMessageListSignals.m new file mode 100644 index 0000000000..b06600c807 --- /dev/null +++ b/Watch/Bridge/TGBridgeChatMessageListSignals.m @@ -0,0 +1,24 @@ +#import "TGBridgeChatMessageListSignals.h" +#import "TGBridgeSubscriptions.h" +#import "TGBridgeMessage.h" +#import "TGBridgeUser.h" +#import "TGBridgeClient.h" + +@implementation TGBridgeChatMessageListSignals + ++ (SSignal *)chatMessageListViewWithPeerId:(int64_t)peerId atMessageId:(int32_t)messageId rangeMessageCount:(NSUInteger)rangeMessageCount +{ + return [[TGBridgeClient instance] requestSignalWithSubscription:[[TGBridgeChatMessageListSubscription alloc] initWithPeerId:peerId atMessageId:messageId rangeMessageCount:rangeMessageCount]]; +} + ++ (SSignal *)chatMessageWithPeerId:(int64_t)peerId messageId:(int32_t)messageId +{ + return [[TGBridgeClient instance] requestSignalWithSubscription:[[TGBridgeChatMessageSubscription alloc] initWithPeerId:peerId messageId:messageId]]; +} + ++ (SSignal *)readChatMessageListWithPeerId:(int64_t)peerId messageId:(int32_t)messageId +{ + return [[TGBridgeClient instance] requestSignalWithSubscription:[[TGBridgeReadChatMessageListSubscription alloc] initWithPeerId:peerId messageId:messageId]]; +} + +@end diff --git a/Watch/Bridge/TGBridgeChatMessages.h b/Watch/Bridge/TGBridgeChatMessages.h new file mode 100644 index 0000000000..04e7220be7 --- /dev/null +++ b/Watch/Bridge/TGBridgeChatMessages.h @@ -0,0 +1,14 @@ +#import "TGBridgeCommon.h" + +@class SSignal; + +@interface TGBridgeChatMessages : NSObject +{ + NSArray *_messages; +} + +@property (nonatomic, readonly) NSArray *messages; + +@end + +extern NSString *const TGBridgeChatMessageListViewKey; diff --git a/Watch/Bridge/TGBridgeChatMessages.m b/Watch/Bridge/TGBridgeChatMessages.m new file mode 100644 index 0000000000..c7b64e2c13 --- /dev/null +++ b/Watch/Bridge/TGBridgeChatMessages.m @@ -0,0 +1,27 @@ +#import "TGBridgeChatMessages.h" +#import "TGBridgeMessage.h" + +NSString *const TGBridgeChatMessageListViewMessagesKey = @"messages"; +NSString *const TGBridgeChatMessageListViewEarlierMessageIdKey = @"earlier"; +NSString *const TGBridgeChatMessageListViewLaterMessageIdKey = @"later"; + +NSString *const TGBridgeChatMessageListViewKey = @"messageListView"; + +@implementation TGBridgeChatMessages + +- (instancetype)initWithCoder:(NSCoder *)aDecoder +{ + self = [super init]; + if (self != nil) + { + _messages = [aDecoder decodeObjectForKey:TGBridgeChatMessageListViewMessagesKey]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder +{ + [aCoder encodeObject:self.messages forKey:TGBridgeChatMessageListViewMessagesKey]; +} + +@end diff --git a/Watch/Bridge/TGBridgeClient.h b/Watch/Bridge/TGBridgeClient.h new file mode 100644 index 0000000000..eb7661b36f --- /dev/null +++ b/Watch/Bridge/TGBridgeClient.h @@ -0,0 +1,33 @@ +#import + +@class TGBridgeSubscription; + +@interface TGBridgeClient : NSObject + +- (SSignal *)requestSignalWithSubscription:(TGBridgeSubscription *)subscription; +- (SSignal *)contextSignal; + +- (SSignal *)fileSignalForKey:(NSString *)key; +- (NSArray *)stickerPacks; + +- (void)handleDidBecomeActive; +- (void)handleWillResignActive; + +- (void)sendFileWithURL:(NSURL *)url metadata:(NSDictionary *)metadata; + +- (void)updateReachability; +- (bool)isServerReachable; +- (bool)isActuallyReachable; +- (SSignal *)actualReachabilitySignal; +- (SSignal *)reachabilitySignal; + +- (SSignal *)userInfoSignal; + +- (SSignal *)sendMessageData:(NSData *)messageData; +- (void)sendRawMessageData:(NSData *)messageData replyHandler:(void (^)(NSData *))replyHandler errorHandler:(void (^)(NSError *))errorHandler; + +- (void)transferUserInfo:(NSDictionary *)userInfo; + ++ (instancetype)instance; + +@end diff --git a/Watch/Bridge/TGBridgeClient.m b/Watch/Bridge/TGBridgeClient.m new file mode 100644 index 0000000000..92bb803261 --- /dev/null +++ b/Watch/Bridge/TGBridgeClient.m @@ -0,0 +1,657 @@ +#import "TGBridgeClient.h" +#import "TGBridgeCommon.h" +#import "TGBridgeChat.h" +#import "TGWatchCommon.h" + +#import + +#import "TGBridgeContext.h" +#import "TGFileCache.h" + +#import "TGBridgeStickersSignals.h" +#import "TGBridgePresetsSignals.h" + +#import "TGExtensionDelegate.h" + +#import + +NSString *const TGBridgeContextDomain = @"com.telegram.BridgeContext"; + +const NSTimeInterval TGBridgeClientTimerInterval = 4.0; +const NSTimeInterval TGBridgeClientWakeInterval = 2.0; + +@interface TGBridgeClient () +{ + int32_t _sessionId; + bool _reachable; + + bool _processingNotification; + + SMulticastSignalManager *_signalManager; + SMulticastSignalManager *_fileSignalManager; + SVariable *_context; + + SPipe *_actualReachabilityPipe; + SPipe *_reachabilityPipe; + + SPipe *_userInfoPipe; + + dispatch_queue_t _contextQueue; + + OSSpinLock _outgoingQueueLock; + NSMutableArray *_outgoingMessageQueue; + + NSArray *_stickerPacks; + OSSpinLock _stickerPacksLock; + + NSMutableDictionary *_subscriptions; + + NSTimeInterval _lastForegroundEntry; + STimer *_timer; + + bool _sentFirstPing; + bool _isActive; +} + +@property (nonatomic, readonly) WCSession *session; + +@end + +@implementation TGBridgeClient + +- (instancetype)init +{ + self = [super init]; + if (self != nil) + { + int32_t sessionId = 0; + arc4random_buf(&sessionId, sizeof(int32_t)); + _sessionId = sessionId; + + _contextQueue = dispatch_queue_create(TGBridgeContextDomain.UTF8String, nil); + + _signalManager = [[SMulticastSignalManager alloc] init]; + _fileSignalManager = [[SMulticastSignalManager alloc] init]; + _context = [[SVariable alloc] init]; + _userInfoPipe = [[SPipe alloc] init]; + _actualReachabilityPipe = [[SPipe alloc] init]; + _reachabilityPipe = [[SPipe alloc] init]; + _reachable = true; + + _outgoingMessageQueue = [[NSMutableArray alloc] init]; + _subscriptions = [[NSMutableDictionary alloc] init]; + + self.session.delegate = self; + [self.session activateSession]; + + TGLog(@"BridgeClient: initialized"); + + [self ping]; + } + return self; +} + +- (void)transferUserInfo:(NSDictionary *)userInfo +{ + [self.session transferUserInfo:userInfo]; +} + +- (SSignal *)requestSignalWithSubscription:(TGBridgeSubscription *)subscription +{ + if (!_sentFirstPing) + [self ping]; + + NSData *messageData = [NSKeyedArchiver archivedDataWithRootObject:subscription]; + void (^transcribe)(id, SSubscriber *, bool *) = ^(id message, SSubscriber *subscriber, bool *completed) + { + NSLog(@"BridgeClient: received %p %@", subscription, NSStringFromClass(subscription.class)); + + TGBridgeResponse *response = nil; + if ([message isKindOfClass:[TGBridgeResponse class]]) + { + response = message; + } + else if ([message isKindOfClass:[NSData class]]) + { + @try + { + id unarchivedMessage = [NSKeyedUnarchiver unarchiveObjectWithData:message]; + if ([unarchivedMessage isKindOfClass:[TGBridgeResponse class]]) + response = (TGBridgeResponse *)unarchivedMessage; + } + @catch (NSException *exception) + { + + } + } + + if (response == nil) + return; + + switch (response.type) + { + case TGBridgeResponseTypeNext: + [subscriber putNext:response.next]; + break; + + case TGBridgeResponseTypeFailed: + [subscriber putError:response.error]; + break; + + case TGBridgeResponseTypeCompleted: + if (completed != NULL) + *completed = true; + + [subscriber putCompletion]; + break; + + default: + break; + } + }; + + __weak TGBridgeClient *weakSelf = self; + return [[[[SSignal alloc] initWithGenerator:^id(SSubscriber *subscriber) + { + NSLog(@"BridgeClient: requestSub %p %@", subscription, NSStringFromClass(subscription.class)); + + SDisposableSet *combinedDisposable = [[SDisposableSet alloc] init]; + SMetaDisposable *currentDisposable = [[SMetaDisposable alloc] init]; + + __block bool completed = false; + [combinedDisposable add:currentDisposable]; + + void (^afterSendMessage)(void) = ^ + { + __strong TGBridgeClient *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [combinedDisposable add:[[strongSelf->_signalManager multicastedPipeForKey:[NSString stringWithFormat:@"%lld", subscription.identifier]] startWithNext:^(id next) + { + transcribe(next, subscriber, NULL); + } error:^(id error) + { + [subscriber putError:error]; + } completed:^ + { + [subscriber putCompletion]; + }]]; + }; + + [currentDisposable setDisposable:[[[self sendMessageData:messageData] onStart:^ + { + __strong TGBridgeClient *strongSelf = weakSelf; + if (strongSelf != nil) + strongSelf->_subscriptions[@(subscription.identifier)] = subscription; + }] startWithNext:^(id next) + { + __strong TGBridgeClient *strongSelf = weakSelf; + if (strongSelf != nil) + transcribe(next, subscriber, &completed); + } error:^(NSError *error) + { + if ([error isKindOfClass:[NSError class]] && error.domain == WCErrorDomain) + { + __strong TGBridgeClient *strongSelf = weakSelf; + if (strongSelf != nil) + [strongSelf _enqueueMessage:messageData]; + + afterSendMessage(); + } + else + { + [subscriber putError:error]; + } + } completed:^ + { + if (completed) + return; + + afterSendMessage(); + }]]; + + return combinedDisposable; + }] onCompletion:^ + { + __strong TGBridgeClient *strongSelf = weakSelf; + if (strongSelf != nil) + [strongSelf->_subscriptions removeObjectForKey:@(subscription.identifier)]; + }] onDispose:^ + { + __strong TGBridgeClient *strongSelf = weakSelf; + if (strongSelf != nil) + { + [strongSelf->_subscriptions removeObjectForKey:@(subscription.identifier)]; + [strongSelf unsubscribe:subscription.identifier]; + } + }]; +} + +- (void)unsubscribe:(int64_t)identifier +{ + TGBridgeDisposal *disposal = [[TGBridgeDisposal alloc] initWithIdentifier:identifier]; + NSData *message = [NSKeyedArchiver archivedDataWithRootObject:disposal]; + [self.session sendMessageData:message replyHandler:nil errorHandler:^(NSError *error) + { + [self _logError:error]; + }]; +} + +- (SSignal *)sendMessageData:(NSData *)messageData +{ + return [[SSignal alloc] initWithGenerator:^id(SSubscriber *subscriber) + { + [self.session sendMessageData:messageData replyHandler:^(NSData *replyMessageData) + { + if (replyMessageData.length > 0) + [subscriber putNext:replyMessageData]; + [subscriber putCompletion]; + } errorHandler:^(NSError * _Nonnull error) + { + [self _logError:error]; + [subscriber putError:error]; + }]; + return nil; + }]; +} + +- (void)sendRawMessageData:(NSData *)messageData replyHandler:(void (^)(NSData *))replyHandler errorHandler:(void (^)(NSError *))errorHandler +{ + [self.session sendMessageData:messageData replyHandler:replyHandler errorHandler:errorHandler]; +} + +#pragma mark - + +- (SSignal *)contextSignal +{ + return _context.signal; +} + +#pragma mark - + +- (SSignal *)fileSignalForKey:(NSString *)key +{ + return [_fileSignalManager multicastedPipeForKey:key]; +} + +- (void)sendFileWithURL:(NSURL *)url metadata:(NSDictionary *)metadata +{ + [self.session transferFile:url metadata:metadata]; +} + +#pragma mark - + +- (NSArray *)stickerPacks +{ + OSSpinLockLock(&_stickerPacksLock); + if (_stickerPacks != nil) + { + NSArray *stickerPacks = [_stickerPacks copy]; + OSSpinLockUnlock(&_stickerPacksLock); + + return stickerPacks; + } + else + { + NSArray *stickerPacks = [self readStickerPacks]; + if (stickerPacks == nil) + stickerPacks = [NSArray array]; + + _stickerPacks = stickerPacks; + + OSSpinLockUnlock(&_stickerPacksLock); + + return stickerPacks; + } +} + +- (NSArray *)readStickerPacks +{ + NSURL *url = [TGBridgeStickersSignals stickerPacksURL]; + + NSData *data = [[NSData alloc] initWithContentsOfURL:url]; + if (data == nil) + return nil; + + NSArray *stickerPacks = nil; + @try + { + stickerPacks = [NSKeyedUnarchiver unarchiveObjectWithData:data]; + } + @catch (NSException *exception) + { + + } + + if (![stickerPacks isKindOfClass:[NSArray class]]) + return nil; + + return stickerPacks; +} + +#pragma mark - + +- (void)session:(WCSession *)session didReceiveMessageData:(NSData *)messageData +{ + [self handleReceivedData:messageData replyHandler:nil]; +} + +- (void)session:(WCSession *)session didReceiveMessageData:(NSData *)messageData replyHandler:(nonnull void (^)(NSData * _Nonnull))replyHandler +{ + [self handleReceivedData:messageData replyHandler:replyHandler]; +} + +- (void)handleReceivedData:(NSData *)messageData replyHandler:(void (^)(NSData *))replyHandler +{ + id message = nil; + @try + { + message = [NSKeyedUnarchiver unarchiveObjectWithData:messageData]; + } + @catch (NSException *exception) + { + + } + + if ([message isKindOfClass:[TGBridgeResponse class]]) + { + TGBridgeResponse *response = (TGBridgeResponse *)message; + [_signalManager putNext:response toMulticastedPipeForKey:[NSString stringWithFormat:@"%lld", response.subscriptionIdentifier]]; + } + else if ([message isKindOfClass:[TGBridgeSubscriptionListRequest class]]) + { + [self refreshSubscriptions]; + } + else if ([message isKindOfClass:[TGBridgeFile class]]) + { + TGBridgeFile *file = (TGBridgeFile *)message; + + NSString *type = file.metadata[TGBridgeIncomingFileTypeKey]; + NSString *identifier = file.metadata[TGBridgeIncomingFileIdentifierKey]; + if (identifier == nil) + return; + + if ([type isEqualToString:TGBridgeIncomingFileTypeImage]) + { + NSLog(@"Received message image file: %@", identifier); + [[TGExtensionDelegate instance].imageCache cacheData:file.data key:identifier synchronous:true unserializeBlock:^id(NSData *data) + { + return data; + } completion:^(NSURL *url) + { + [_fileSignalManager putNext:url toMulticastedPipeForKey:identifier]; + }]; + } + } +} + +- (void)session:(WCSession *)session didReceiveApplicationContext:(NSDictionary *)applicationContext +{ + TGBridgeContext *context = [[TGBridgeContext alloc] initWithDictionary:applicationContext]; + [_context set:[SSignal single:context]]; +} + +- (void)session:(WCSession *)session didReceiveFile:(WCSessionFile *)file +{ + NSString *type = file.metadata[TGBridgeIncomingFileTypeKey]; + NSString *identifier = file.metadata[TGBridgeIncomingFileIdentifierKey]; + if (identifier == nil) + return; + + if ([identifier isEqualToString:@"stickers"]) + { + NSURL *stickerPacksURL = [TGBridgeStickersSignals stickerPacksURL]; + if ([[NSFileManager defaultManager] fileExistsAtPath:stickerPacksURL.path]) + [[NSFileManager defaultManager] removeItemAtURL:stickerPacksURL error:nil]; + + [[NSFileManager defaultManager] moveItemAtURL:file.fileURL toURL:stickerPacksURL error:nil]; + + NSArray *stickerPacks = [self readStickerPacks]; + OSSpinLockLock(&_stickerPacksLock); + _stickerPacks = stickerPacks; + OSSpinLockUnlock(&_stickerPacksLock); + + [_fileSignalManager putNext:stickerPacks toMulticastedPipeForKey:identifier]; + } + else if ([identifier isEqualToString:@"localization"]) + { + [[TGExtensionDelegate instance] setCustomLocalizationFile:file.fileURL]; + } + else if ([identifier isEqualToString:@"presets"]) + { + NSURL *presetsURL = [TGBridgePresetsSignals presetsURL]; + if ([[NSFileManager defaultManager] fileExistsAtPath:presetsURL.path]) + [[NSFileManager defaultManager] removeItemAtURL:presetsURL error:nil]; + + [[NSFileManager defaultManager] moveItemAtURL:file.fileURL toURL:presetsURL error:nil]; + } + else if ([type isEqualToString:TGBridgeIncomingFileTypeImage]) + { + NSLog(@"Received image file: %@", identifier); + [[TGExtensionDelegate instance].imageCache cacheFileAtURL:file.fileURL key:identifier synchronous:true unserializeBlock:^id(NSData *data) + { + return data; + } completion:^(NSURL *url) + { + [_fileSignalManager putNext:url toMulticastedPipeForKey:identifier]; + }]; + } + else if ([type isEqualToString:TGBridgeIncomingFileTypeAudio]) + { + NSLog(@"Received audio file: %@", identifier); + [[TGExtensionDelegate instance].audioCache cacheFileAtURL:file.fileURL key:identifier synchronous:true unserializeBlock:nil completion:^(NSURL *url) + { + [_fileSignalManager putNext:url toMulticastedPipeForKey:identifier]; + }]; + } +} + +- (void)sessionReachabilityDidChange:(WCSession *)session +{ + bool reachable = session.isReachable; + if (!reachable) + { + TGDispatchAfter(4.5, dispatch_get_main_queue(), ^ + { + bool newReachable = session.isReachable; + if (newReachable == reachable && newReachable != _reachable) + { + _reachable = newReachable; + _reachabilityPipe.sink(@(newReachable)); + } + }); + } + else if (_reachable != reachable) + { + _reachable = reachable; + _reachabilityPipe.sink(@(reachable)); + + [self ping]; + } + + if (reachable && !_processingNotification) + [self sendQueuedMessages]; +} + +- (void)session:(WCSession *)session didReceiveUserInfo:(NSDictionary *)userInfo +{ + _userInfoPipe.sink(userInfo); +} + +- (void)session:(nonnull WCSession *)session activationDidCompleteWithState:(WCSessionActivationState)activationState error:(nullable NSError *)error { + if (activationState == WCSessionActivationStateActivated) { + TGBridgeContext *context = [[TGBridgeContext alloc] initWithDictionary:session.receivedApplicationContext]; + [_context set:[SSignal single:context]]; + } else { + TGLog(@"[BridgeClient] inactive session state"); + } +} + + +- (SSignal *)userInfoSignal +{ + return _userInfoPipe.signalProducer(); +} + +#pragma mark - + +- (void)_enqueueMessage:(NSData *)message +{ + TGLog(@"[BridgeClient] Enqued failed message"); + + OSSpinLockLock(&_outgoingQueueLock); + [_outgoingMessageQueue addObject:message]; + OSSpinLockUnlock(&_outgoingQueueLock); +} + +- (void)sendQueuedMessages +{ + OSSpinLockLock(&_outgoingQueueLock); + + if (_outgoingMessageQueue.count > 0) + { + TGLog(@"[BridgeClient] Sending queued messages"); + + for (NSData *messageData in _outgoingMessageQueue) + [self.session sendMessageData:messageData replyHandler:nil errorHandler:nil]; + + [_outgoingMessageQueue removeAllObjects]; + } + OSSpinLockUnlock(&_outgoingQueueLock); +} + +#pragma mark - + +- (void)ping +{ + if (!_isActive || _processingNotification) + return; + + TGBridgePing *ping = [[TGBridgePing alloc] initWithSessionId:_sessionId]; + NSData *message = [NSKeyedArchiver archivedDataWithRootObject:ping]; + [self.session sendMessageData:message replyHandler:^(NSData *replyData) + { + _sentFirstPing = true; + } errorHandler:^(NSError *error) + { + [self _logError:error]; + }]; +} + +- (void)refreshSubscriptions +{ + NSArray *activeSubscriptions = [_subscriptions allValues]; + NSMutableArray *subscriptions = [[NSMutableArray alloc] init]; + for (TGBridgeSubscription *subscription in activeSubscriptions) + { + if (subscription.renewable) + [subscriptions addObject:subscription]; + } + + TGBridgeSubscriptionList *subscriptionsList = [[TGBridgeSubscriptionList alloc] initWithArray:subscriptions]; + NSData *message = [NSKeyedArchiver archivedDataWithRootObject:subscriptionsList]; + [self.session sendMessageData:message replyHandler:nil errorHandler:^(NSError *error) + { + [self _logError:error]; + }]; +} + +#pragma mark - + +- (void)handleDidBecomeActive +{ + _isActive = true; + + NSTimeInterval currentTime = [[NSDate date] timeIntervalSinceReferenceDate]; + if (_lastForegroundEntry == 0 || currentTime - _lastForegroundEntry > TGBridgeClientWakeInterval) + { + if (_lastForegroundEntry != 0) + [self ping]; + + _lastForegroundEntry = currentTime; + } + + if (_timer == nil) + { + __weak TGBridgeClient *weakSelf = self; + NSTimeInterval interval = _lastForegroundEntry == 0 ? TGBridgeClientTimerInterval : MAX(MIN(TGBridgeClientTimerInterval - currentTime - _lastForegroundEntry, TGBridgeClientTimerInterval), 1); + + __block void (^completion)(void) = ^ + { + __strong TGBridgeClient *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf ping]; + + strongSelf->_lastForegroundEntry = [[NSDate date] timeIntervalSinceReferenceDate]; + strongSelf->_timer = [[STimer alloc] initWithTimeout:TGBridgeClientTimerInterval repeat:false completion:completion queue:[SQueue mainQueue]]; + [strongSelf->_timer start]; + }; + + _timer = [[STimer alloc] initWithTimeout:interval repeat:false completion:completion queue:[SQueue mainQueue]]; + [_timer start]; + } +} + +- (void)handleWillResignActive +{ + _isActive = false; + + [_timer invalidate]; + _timer = nil; +} + +#pragma mark - + +- (void)updateReachability +{ + if (self.session.isReachable && !_reachable) + _reachable = true; +} + +- (bool)isServerReachable +{ + return _reachable; +} + +- (bool)isActuallyReachable +{ + return self.session.isReachable; +} + +- (SSignal *)actualReachabilitySignal +{ + return [[SSignal single:@(self.session.isReachable)] then:_actualReachabilityPipe.signalProducer()]; +} + +- (SSignal *)reachabilitySignal +{ + return [[SSignal single:@(self.session.isReachable)] then:_reachabilityPipe.signalProducer()]; +} + +- (void)_logError:(NSError *)error +{ + NSLog(@"%@", error); +} + +#pragma mark - + +- (WCSession *)session +{ + return [WCSession defaultSession]; +} + ++ (instancetype)instance +{ + static dispatch_once_t onceToken; + static TGBridgeClient *instance; + dispatch_once(&onceToken, ^ + { + instance = [[TGBridgeClient alloc] init]; + }); + return instance; +} + +@end diff --git a/Watch/Bridge/TGBridgeCommon.h b/Watch/Bridge/TGBridgeCommon.h new file mode 100644 index 0000000000..fe0510c041 --- /dev/null +++ b/Watch/Bridge/TGBridgeCommon.h @@ -0,0 +1,95 @@ +#import + +extern NSString *const TGBridgeIncomingFileTypeKey; +extern NSString *const TGBridgeIncomingFileIdentifierKey; +extern NSString *const TGBridgeIncomingFileRandomIdKey; +extern NSString *const TGBridgeIncomingFilePeerIdKey; +extern NSString *const TGBridgeIncomingFileReplyToMidKey; + +extern NSString *const TGBridgeIncomingFileTypeAudio; +extern NSString *const TGBridgeIncomingFileTypeImage; + +@interface TGBridgeSubscription : NSObject + +@property (nonatomic, readonly) int64_t identifier; +@property (nonatomic, readonly, strong) NSString *name; + +@property (nonatomic, readonly) bool isOneTime; +@property (nonatomic, readonly) bool renewable; +@property (nonatomic, readonly) bool dropPreviouslyQueued; +@property (nonatomic, readonly) bool synchronous; + +- (void)_serializeParametersWithCoder:(NSCoder *)aCoder; +- (void)_unserializeParametersWithCoder:(NSCoder *)aDecoder; + ++ (NSString *)subscriptionName; + +@end + + +@interface TGBridgeDisposal : NSObject + +@property (nonatomic, readonly) int64_t identifier; + +- (instancetype)initWithIdentifier:(int64_t)identifier; + +@end + + +@interface TGBridgeFile : NSObject + +@property (nonatomic, readonly, strong) NSData *data; +@property (nonatomic, readonly, strong) NSDictionary *metadata; + +- (instancetype)initWithData:(NSData *)data metadata:(NSDictionary *)metadata; + +@end + + +@interface TGBridgePing : NSObject + +@property (nonatomic, readonly) int32_t sessionId; + +- (instancetype)initWithSessionId:(int32_t)sessionId; + +@end + + +@interface TGBridgeSubscriptionListRequest : NSObject + +@property (nonatomic, readonly) int32_t sessionId; + +- (instancetype)initWithSessionId:(int32_t)sessionId; + +@end + + +@interface TGBridgeSubscriptionList : NSObject + +@property (nonatomic, readonly, strong) NSArray *subscriptions; + +- (instancetype)initWithArray:(NSArray *)array; + +@end + + +typedef NS_ENUM(int32_t, TGBridgeResponseType) { + TGBridgeResponseTypeUndefined, + TGBridgeResponseTypeNext, + TGBridgeResponseTypeFailed, + TGBridgeResponseTypeCompleted +}; + +@interface TGBridgeResponse : NSObject + +@property (nonatomic, readonly) int64_t subscriptionIdentifier; + +@property (nonatomic, readonly) TGBridgeResponseType type; +@property (nonatomic, readonly, strong) id next; +@property (nonatomic, readonly, strong) NSString *error; + ++ (TGBridgeResponse *)single:(id)next forSubscription:(TGBridgeSubscription *)subscription; ++ (TGBridgeResponse *)fail:(id)error forSubscription:(TGBridgeSubscription *)subscription; ++ (TGBridgeResponse *)completeForSubscription:(TGBridgeSubscription *)subscription; + +@end diff --git a/Watch/Bridge/TGBridgeCommon.m b/Watch/Bridge/TGBridgeCommon.m new file mode 100644 index 0000000000..ae0cf5300b --- /dev/null +++ b/Watch/Bridge/TGBridgeCommon.m @@ -0,0 +1,295 @@ +#import "TGBridgeCommon.h" + +NSString *const TGBridgeIncomingFileTypeKey = @"type"; +NSString *const TGBridgeIncomingFileIdentifierKey = @"identifier"; +NSString *const TGBridgeIncomingFileRandomIdKey = @"randomId"; +NSString *const TGBridgeIncomingFilePeerIdKey = @"peerId"; +NSString *const TGBridgeIncomingFileReplyToMidKey = @"replyToMid"; +NSString *const TGBridgeIncomingFileTypeAudio = @"audio"; +NSString *const TGBridgeIncomingFileTypeImage = @"image"; + +NSString *const TGBridgeResponseSubscriptionIdentifier = @"identifier"; +NSString *const TGBridgeResponseTypeKey = @"type"; +NSString *const TGBridgeResponseNextKey = @"next"; +NSString *const TGBridgeResponseErrorKey = @"error"; + +@implementation TGBridgeResponse + +- (instancetype)initWithCoder:(NSCoder *)aDecoder +{ + self = [super init]; + if (self != nil) + { + _subscriptionIdentifier = [aDecoder decodeInt64ForKey:TGBridgeResponseSubscriptionIdentifier]; + _type = [aDecoder decodeInt32ForKey:TGBridgeResponseTypeKey]; + _next = [aDecoder decodeObjectForKey:TGBridgeResponseNextKey]; + _error = [aDecoder decodeObjectForKey:TGBridgeResponseErrorKey]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder +{ + [aCoder encodeInt64:self.subscriptionIdentifier forKey:TGBridgeResponseSubscriptionIdentifier]; + [aCoder encodeInt32:self.type forKey:TGBridgeResponseTypeKey]; + [aCoder encodeObject:self.next forKey:TGBridgeResponseNextKey]; + [aCoder encodeObject:self.error forKey:TGBridgeResponseErrorKey]; +} + ++ (TGBridgeResponse *)single:(id)next forSubscription:(TGBridgeSubscription *)subscription +{ + TGBridgeResponse *response = [[TGBridgeResponse alloc] init]; + response->_subscriptionIdentifier = subscription.identifier; + response->_type = TGBridgeResponseTypeNext; + response->_next = next; + return response; +} + ++ (TGBridgeResponse *)fail:(id)error forSubscription:(TGBridgeSubscription *)subscription +{ + TGBridgeResponse *response = [[TGBridgeResponse alloc] init]; + response->_subscriptionIdentifier = subscription.identifier; + response->_type = TGBridgeResponseTypeFailed; + response->_error = error; + return response; +} + ++ (TGBridgeResponse *)completeForSubscription:(TGBridgeSubscription *)subscription +{ + TGBridgeResponse *response = [[TGBridgeResponse alloc] init]; + response->_subscriptionIdentifier = subscription.identifier; + response->_type = TGBridgeResponseTypeCompleted; + return response; +} + +@end + + +NSString *const TGBridgeSubscriptionIdentifierKey = @"identifier"; +NSString *const TGBridgeSubscriptionNameKey = @"name"; +NSString *const TGBridgeSubscriptionParametersKey = @"parameters"; + +@implementation TGBridgeSubscription + +- (instancetype)init +{ + self = [super init]; + if (self != nil) + { + int64_t randomId = 0; + arc4random_buf(&randomId, sizeof(int64_t)); + _identifier = randomId; + _name = [[self class] subscriptionName]; + } + return self; +} + +- (instancetype)initWithCoder:(NSCoder *)aDecoder +{ + self = [super init]; + if (self != nil) + { + _identifier = [aDecoder decodeInt64ForKey:TGBridgeSubscriptionIdentifierKey]; + _name = [aDecoder decodeObjectForKey:TGBridgeSubscriptionNameKey]; + [self _unserializeParametersWithCoder:aDecoder]; + } + return self; +} + +- (bool)synchronous +{ + return false; +} + +- (bool)renewable +{ + return true; +} + +- (bool)dropPreviouslyQueued +{ + return false; +} + +- (void)_serializeParametersWithCoder:(NSCoder *)__unused aCoder +{ + +} + +- (void)_unserializeParametersWithCoder:(NSCoder *)__unused aDecoder +{ + +} + +- (void)encodeWithCoder:(NSCoder *)aCoder +{ + [aCoder encodeInt64:self.identifier forKey:TGBridgeSubscriptionIdentifierKey]; + [aCoder encodeObject:self.name forKey:TGBridgeSubscriptionNameKey]; + [self _serializeParametersWithCoder:aCoder]; +} + ++ (NSString *)subscriptionName +{ + return nil; +} + +@end + + +@implementation TGBridgeDisposal + +- (instancetype)initWithIdentifier:(int64_t)identifier +{ + self = [super init]; + if (self != nil) + { + _identifier = identifier; + } + return self; +} + +- (instancetype)initWithCoder:(NSCoder *)aDecoder +{ + self = [super init]; + if (self != nil) + { + _identifier = [aDecoder decodeInt64ForKey:TGBridgeSubscriptionIdentifierKey]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder +{ + [aCoder encodeInt64:self.identifier forKey:TGBridgeSubscriptionIdentifierKey]; +} + +@end + +NSString *const TGBridgeFileDataKey = @"data"; +NSString *const TGBridgeFileMetadataKey = @"metadata"; + +@implementation TGBridgeFile + +- (instancetype)initWithData:(NSData *)data metadata:(NSDictionary *)metadata +{ + self = [super init]; + if (self != nil) + { + _data = data; + _metadata = metadata; + } + return self; +} + +- (instancetype)initWithCoder:(NSCoder *)aDecoder +{ + self = [super init]; + if (self != nil) + { + _data = [aDecoder decodeObjectForKey:TGBridgeFileDataKey]; + _metadata = [aDecoder decodeObjectForKey:TGBridgeFileMetadataKey]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder +{ + [aCoder encodeObject:self.data forKey:TGBridgeFileDataKey]; + [aCoder encodeObject:self.metadata forKey:TGBridgeFileMetadataKey]; +} + +@end + + +NSString *const TGBridgeSessionIdKey = @"sessionId"; + +@implementation TGBridgePing + +- (instancetype)initWithSessionId:(int32_t)sessionId +{ + self = [super init]; + if (self != nil) + { + _sessionId = sessionId; + } + return self; +} + +- (instancetype)initWithCoder:(NSCoder *)aDecoder +{ + self = [super init]; + if (self != nil) + { + _sessionId = [aDecoder decodeInt32ForKey:TGBridgeSessionIdKey]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder +{ + [aCoder encodeInt32:self.sessionId forKey:TGBridgeSessionIdKey]; +} + +@end + + +@implementation TGBridgeSubscriptionListRequest + +- (instancetype)initWithSessionId:(int32_t)sessionId +{ + self = [super init]; + if (self != nil) + { + _sessionId = sessionId; + } + return self; +} + +- (instancetype)initWithCoder:(NSCoder *)aDecoder +{ + self = [super init]; + if (self != nil) + { + _sessionId = [aDecoder decodeInt32ForKey:TGBridgeSessionIdKey]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder +{ + [aCoder encodeInt32:self.sessionId forKey:TGBridgeSessionIdKey]; +} + +@end + + +NSString *const TGBridgeSubscriptionListSubscriptionsKey = @"subscriptions"; + +@implementation TGBridgeSubscriptionList + +- (instancetype)initWithArray:(NSArray *)array +{ + self = [super init]; + if (self != nil) + { + _subscriptions = array; + } + return self; +} + +- (instancetype)initWithCoder:(NSCoder *)aDecoder +{ + self = [super init]; + if (self != nil) + { + _subscriptions = [aDecoder decodeObjectForKey:TGBridgeSubscriptionListSubscriptionsKey]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder +{ + [aCoder encodeObject:self.subscriptions forKey:TGBridgeSubscriptionListSubscriptionsKey]; +} + +@end diff --git a/Watch/Bridge/TGBridgeContactMediaAttachment.h b/Watch/Bridge/TGBridgeContactMediaAttachment.h new file mode 100644 index 0000000000..4d247e8d0f --- /dev/null +++ b/Watch/Bridge/TGBridgeContactMediaAttachment.h @@ -0,0 +1,13 @@ +#import "TGBridgeMediaAttachment.h" + +@interface TGBridgeContactMediaAttachment : TGBridgeMediaAttachment + +@property (nonatomic, assign) int32_t uid; +@property (nonatomic, strong) NSString *firstName; +@property (nonatomic, strong) NSString *lastName; +@property (nonatomic, strong) NSString *phoneNumber; +@property (nonatomic, strong) NSString *prettyPhoneNumber; + +- (NSString *)displayName; + +@end diff --git a/Watch/Bridge/TGBridgeContactMediaAttachment.m b/Watch/Bridge/TGBridgeContactMediaAttachment.m new file mode 100644 index 0000000000..50fb4aab15 --- /dev/null +++ b/Watch/Bridge/TGBridgeContactMediaAttachment.m @@ -0,0 +1,63 @@ +#import "TGBridgeContactMediaAttachment.h" + +#import "../Extension/TGStringUtils.h" + +const NSInteger TGBridgeContactMediaAttachmentType = 0xB90A5663; + +NSString *const TGBridgeContactMediaUidKey = @"uid"; +NSString *const TGBridgeContactMediaFirstNameKey = @"firstName"; +NSString *const TGBridgeContactMediaLastNameKey = @"lastName"; +NSString *const TGBridgeContactMediaPhoneNumberKey = @"phoneNumber"; +NSString *const TGBridgeContactMediaPrettyPhoneNumberKey = @"prettyPhoneNumber"; + +@implementation TGBridgeContactMediaAttachment + +- (instancetype)initWithCoder:(NSCoder *)aDecoder +{ + self = [super init]; + if (self != nil) + { + _uid = [aDecoder decodeInt32ForKey:TGBridgeContactMediaUidKey]; + _firstName = [aDecoder decodeObjectForKey:TGBridgeContactMediaFirstNameKey]; + _lastName = [aDecoder decodeObjectForKey:TGBridgeContactMediaLastNameKey]; + _phoneNumber = [aDecoder decodeObjectForKey:TGBridgeContactMediaPhoneNumberKey]; + _prettyPhoneNumber = [aDecoder decodeObjectForKey:TGBridgeContactMediaPrettyPhoneNumberKey]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder +{ + [aCoder encodeInt32:self.uid forKey:TGBridgeContactMediaUidKey]; + [aCoder encodeObject:self.firstName forKey:TGBridgeContactMediaFirstNameKey]; + [aCoder encodeObject:self.lastName forKey:TGBridgeContactMediaLastNameKey]; + [aCoder encodeObject:self.phoneNumber forKey:TGBridgeContactMediaPhoneNumberKey]; + [aCoder encodeObject:self.prettyPhoneNumber forKey:TGBridgeContactMediaPrettyPhoneNumberKey]; +} + +- (NSString *)displayName +{ + NSString *firstName = self.firstName; + NSString *lastName = self.lastName; + + if (firstName != nil && firstName.length != 0 && lastName != nil && lastName.length != 0) + { + if (TGIsKorean()) + return [[NSString alloc] initWithFormat:@"%@ %@", lastName, firstName]; + else + return [[NSString alloc] initWithFormat:@"%@ %@", firstName, lastName]; + } + else if (firstName != nil && firstName.length != 0) + return firstName; + else if (lastName != nil && lastName.length != 0) + return lastName; + + return @""; +} + ++ (NSInteger)mediaType +{ + return TGBridgeContactMediaAttachmentType; +} + +@end diff --git a/Watch/Bridge/TGBridgeContactsSignals.h b/Watch/Bridge/TGBridgeContactsSignals.h new file mode 100644 index 0000000000..af6ab71ad0 --- /dev/null +++ b/Watch/Bridge/TGBridgeContactsSignals.h @@ -0,0 +1,7 @@ +#import + +@interface TGBridgeContactsSignals : NSObject + ++ (SSignal *)searchContactsWithQuery:(NSString *)query; + +@end diff --git a/Watch/Bridge/TGBridgeContactsSignals.m b/Watch/Bridge/TGBridgeContactsSignals.m new file mode 100644 index 0000000000..6c16d039ba --- /dev/null +++ b/Watch/Bridge/TGBridgeContactsSignals.m @@ -0,0 +1,13 @@ +#import "TGBridgeContactsSignals.h" +#import "TGBridgeSubscriptions.h" +#import "TGBridgeUser.h" +#import "TGBridgeClient.h" + +@implementation TGBridgeContactsSignals + ++ (SSignal *)searchContactsWithQuery:(NSString *)query +{ + return [[TGBridgeClient instance] requestSignalWithSubscription:[[TGBridgeContactsSubscription alloc] initWithQuery:query]]; +} + +@end diff --git a/Watch/Bridge/TGBridgeContext.h b/Watch/Bridge/TGBridgeContext.h new file mode 100644 index 0000000000..45225a04f6 --- /dev/null +++ b/Watch/Bridge/TGBridgeContext.h @@ -0,0 +1,18 @@ +#import + +@interface TGBridgeContext : NSObject + +@property (nonatomic, readonly) bool authorized; +@property (nonatomic, readonly) int32_t userId; +@property (nonatomic, readonly) bool micAccessAllowed; +@property (nonatomic, readonly) NSDictionary *preheatData; +@property (nonatomic, readonly) NSInteger preheatVersion; + +- (instancetype)initWithDictionary:(NSDictionary *)dictionary; +- (NSDictionary *)dictionary; + +- (TGBridgeContext *)updatedWithAuthorized:(bool)authorized peerId:(int32_t)peerId; +- (TGBridgeContext *)updatedWithPreheatData:(NSDictionary *)data; +- (TGBridgeContext *)updatedWithMicAccessAllowed:(bool)allowed; + +@end diff --git a/Watch/Bridge/TGBridgeContext.m b/Watch/Bridge/TGBridgeContext.m new file mode 100644 index 0000000000..4ae8cde2ff --- /dev/null +++ b/Watch/Bridge/TGBridgeContext.m @@ -0,0 +1,101 @@ +#import "TGBridgeContext.h" +#import "TGBridgeCommon.h" +#import "TGWatchCommon.h" + +NSString *const TGBridgeContextAuthorized = @"authorized"; +NSString *const TGBridgeContextUserId = @"userId"; +NSString *const TGBridgeContextMicAccessAllowed = @"micAccessAllowed"; +NSString *const TGBridgeContextStartupData = @"startupData"; +NSString *const TGBridgeContextStartupDataVersion = @"version"; + +@implementation TGBridgeContext + +- (instancetype)initWithDictionary:(NSDictionary *)dictionary +{ + self = [super init]; + if (self != nil) + { + _authorized = [dictionary[TGBridgeContextAuthorized] boolValue]; + _userId = [dictionary[TGBridgeContextUserId] int32Value]; + _micAccessAllowed = [dictionary[TGBridgeContextMicAccessAllowed] boolValue]; + + if (dictionary[TGBridgeContextStartupData] != nil) { + _preheatData = [NSKeyedUnarchiver unarchiveObjectWithData:dictionary[TGBridgeContextStartupData]]; + _preheatVersion = [dictionary[TGBridgeContextStartupDataVersion] integerValue]; + } + } + return self; +} + +- (NSDictionary *)dictionary +{ + NSMutableDictionary *dictionary = [[NSMutableDictionary alloc] init]; + dictionary[TGBridgeContextAuthorized] = @(self.authorized); + dictionary[TGBridgeContextUserId] = @(self.userId); + dictionary[TGBridgeContextMicAccessAllowed] = @(self.micAccessAllowed); + if (self.preheatData != nil) { + dictionary[TGBridgeContextStartupData] = [NSKeyedArchiver archivedDataWithRootObject:self.preheatData]; + dictionary[TGBridgeContextStartupDataVersion] = @(self.preheatVersion); + } + return dictionary; +} + +- (TGBridgeContext *)updatedWithAuthorized:(bool)authorized peerId:(int32_t)peerId +{ + TGBridgeContext *context = [[TGBridgeContext alloc] init]; + context->_authorized = authorized; + context->_userId = peerId; + context->_micAccessAllowed = self.micAccessAllowed; + if (authorized) { + context->_preheatData = self.preheatData; + context->_preheatVersion = self.preheatVersion; + } + return context; +} + +- (TGBridgeContext *)updatedWithPreheatData:(NSDictionary *)data +{ + TGBridgeContext *context = [[TGBridgeContext alloc] init]; + context->_authorized = self.authorized; + context->_userId = self.userId; + context->_micAccessAllowed = self.micAccessAllowed; + if (data != nil) { + context->_preheatData = data; + context->_preheatVersion = (int32_t)[NSDate date].timeIntervalSinceReferenceDate; + } + return context; +} + +- (TGBridgeContext *)updatedWithMicAccessAllowed:(bool)allowed +{ + TGBridgeContext *context = [[TGBridgeContext alloc] init]; + context->_authorized = self.authorized; + context->_userId = self.userId; + context->_micAccessAllowed = allowed; + context->_preheatData = self.preheatData; + context->_preheatVersion = self.preheatVersion; + return context; +} + +- (BOOL)isEqual:(id)object +{ + if (object == self) + return true; + + if (!object || ![object isKindOfClass:[self class]]) + return false; + + TGBridgeContext *context = (TGBridgeContext *)object; + if (context.authorized != self.authorized) + return false; + if (context.userId != self.userId) + return false; + if (context.micAccessAllowed != self.micAccessAllowed) + return false; + if (context.preheatVersion != self.preheatVersion) + return false; + + return true; +} + +@end diff --git a/Watch/Bridge/TGBridgeConversationSignals.h b/Watch/Bridge/TGBridgeConversationSignals.h new file mode 100644 index 0000000000..277f9345b0 --- /dev/null +++ b/Watch/Bridge/TGBridgeConversationSignals.h @@ -0,0 +1,7 @@ +#import + +@interface TGBridgeConversationSignals : NSObject + ++ (SSignal *)conversationWithPeerId:(int64_t)peerId; + +@end diff --git a/Watch/Bridge/TGBridgeConversationSignals.m b/Watch/Bridge/TGBridgeConversationSignals.m new file mode 100644 index 0000000000..6927c25b2e --- /dev/null +++ b/Watch/Bridge/TGBridgeConversationSignals.m @@ -0,0 +1,14 @@ +#import "TGBridgeConversationSignals.h" +#import "TGBridgeSubscriptions.h" +#import "TGBridgeChat.h" +#import "TGBridgeUser.h" +#import "TGBridgeClient.h" + +@implementation TGBridgeConversationSignals + ++ (SSignal *)conversationWithPeerId:(int64_t)peerId +{ + return [[TGBridgeClient instance] requestSignalWithSubscription:[[TGBridgeConversationSubscription alloc] initWithPeerId:peerId]]; +} + +@end diff --git a/Watch/Bridge/TGBridgeDocumentMediaAttachment.h b/Watch/Bridge/TGBridgeDocumentMediaAttachment.h new file mode 100644 index 0000000000..328483412a --- /dev/null +++ b/Watch/Bridge/TGBridgeDocumentMediaAttachment.h @@ -0,0 +1,23 @@ +#import "TGBridgeMediaAttachment.h" + +@interface TGBridgeDocumentMediaAttachment : TGBridgeMediaAttachment + +@property (nonatomic, assign) int64_t documentId; +@property (nonatomic, assign) int64_t localDocumentId; +@property (nonatomic, assign) int32_t fileSize; + +@property (nonatomic, strong) NSString *fileName; +@property (nonatomic, strong) NSValue *imageSize; +@property (nonatomic, assign) bool isAnimated; +@property (nonatomic, assign) bool isSticker; +@property (nonatomic, strong) NSString *stickerAlt; +@property (nonatomic, assign) int64_t stickerPackId; +@property (nonatomic, assign) int64_t stickerPackAccessHash; + +@property (nonatomic, assign) bool isVoice; +@property (nonatomic, assign) bool isAudio; +@property (nonatomic, strong) NSString *title; +@property (nonatomic, strong) NSString *performer; +@property (nonatomic, assign) int32_t duration; + +@end diff --git a/Watch/Bridge/TGBridgeDocumentMediaAttachment.m b/Watch/Bridge/TGBridgeDocumentMediaAttachment.m new file mode 100644 index 0000000000..8d492ae704 --- /dev/null +++ b/Watch/Bridge/TGBridgeDocumentMediaAttachment.m @@ -0,0 +1,84 @@ +#import "TGBridgeDocumentMediaAttachment.h" + +const NSInteger TGBridgeDocumentMediaAttachmentType = 0xE6C64318; + +NSString *const TGBridgeDocumentMediaDocumentIdKey = @"documentId"; +NSString *const TGBridgeDocumentMediaLocalDocumentIdKey = @"localDocumentId"; +NSString *const TGBridgeDocumentMediaFileSizeKey = @"fileSize"; +NSString *const TGBridgeDocumentMediaFileNameKey = @"fileName"; +NSString *const TGBridgeDocumentMediaImageSizeKey = @"imageSize"; +NSString *const TGBridgeDocumentMediaAnimatedKey = @"animated"; +NSString *const TGBridgeDocumentMediaStickerKey = @"sticker"; +NSString *const TGBridgeDocumentMediaStickerAltKey = @"stickerAlt"; +NSString *const TGBridgeDocumentMediaStickerPackIdKey = @"stickerPackId"; +NSString *const TGBridgeDocumentMediaStickerPackAccessHashKey = @"stickerPackAccessHash"; +NSString *const TGBridgeDocumentMediaAudioKey = @"audio"; +NSString *const TGBridgeDocumentMediaAudioTitleKey = @"title"; +NSString *const TGBridgeDocumentMediaAudioPerformerKey = @"performer"; +NSString *const TGBridgeDocumentMediaAudioVoice = @"voice"; +NSString *const TGBridgeDocumentMediaAudioDuration = @"duration"; + +@implementation TGBridgeDocumentMediaAttachment + +- (instancetype)initWithCoder:(NSCoder *)aDecoder +{ + self = [super init]; + if (self != nil) + { + _documentId = [aDecoder decodeInt64ForKey:TGBridgeDocumentMediaDocumentIdKey]; + _localDocumentId = [aDecoder decodeInt64ForKey:TGBridgeDocumentMediaLocalDocumentIdKey]; + _fileSize = [aDecoder decodeInt32ForKey:TGBridgeDocumentMediaFileSizeKey]; + _fileName = [aDecoder decodeObjectForKey:TGBridgeDocumentMediaFileNameKey]; + _imageSize = [aDecoder decodeObjectForKey:TGBridgeDocumentMediaImageSizeKey]; + _isAnimated = [aDecoder decodeBoolForKey:TGBridgeDocumentMediaAnimatedKey]; + _isSticker = [aDecoder decodeBoolForKey:TGBridgeDocumentMediaStickerKey]; + _stickerAlt = [aDecoder decodeObjectForKey:TGBridgeDocumentMediaStickerAltKey]; + _stickerPackId = [aDecoder decodeInt64ForKey:TGBridgeDocumentMediaStickerPackIdKey]; + _stickerPackAccessHash = [aDecoder decodeInt64ForKey:TGBridgeDocumentMediaStickerPackAccessHashKey]; + _isAudio = [aDecoder decodeBoolForKey:TGBridgeDocumentMediaAudioKey]; + _title = [aDecoder decodeObjectForKey:TGBridgeDocumentMediaAudioTitleKey]; + _performer = [aDecoder decodeObjectForKey:TGBridgeDocumentMediaAudioPerformerKey]; + _isVoice = [aDecoder decodeBoolForKey:TGBridgeDocumentMediaAudioVoice]; + _duration = [aDecoder decodeInt32ForKey:TGBridgeDocumentMediaAudioDuration]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder +{ + [aCoder encodeInt64:self.documentId forKey:TGBridgeDocumentMediaDocumentIdKey]; + [aCoder encodeInt64:self.localDocumentId forKey:TGBridgeDocumentMediaLocalDocumentIdKey]; + [aCoder encodeInt32:self.fileSize forKey:TGBridgeDocumentMediaFileSizeKey]; + [aCoder encodeObject:self.fileName forKey:TGBridgeDocumentMediaFileNameKey]; + [aCoder encodeObject:self.imageSize forKey:TGBridgeDocumentMediaImageSizeKey]; + [aCoder encodeBool:self.isAnimated forKey:TGBridgeDocumentMediaAnimatedKey]; + [aCoder encodeBool:self.isSticker forKey:TGBridgeDocumentMediaStickerKey]; + [aCoder encodeObject:self.stickerAlt forKey:TGBridgeDocumentMediaStickerAltKey]; + [aCoder encodeInt64:self.stickerPackId forKey:TGBridgeDocumentMediaStickerPackIdKey]; + [aCoder encodeInt64:self.stickerPackAccessHash forKey:TGBridgeDocumentMediaStickerPackAccessHashKey]; + [aCoder encodeBool:self.isAudio forKey:TGBridgeDocumentMediaAudioKey]; + [aCoder encodeObject:self.title forKey:TGBridgeDocumentMediaAudioTitleKey]; + [aCoder encodeObject:self.performer forKey:TGBridgeDocumentMediaAudioPerformerKey]; + [aCoder encodeBool:self.isVoice forKey:TGBridgeDocumentMediaAudioVoice]; + [aCoder encodeInt32:self.duration forKey:TGBridgeDocumentMediaAudioDuration]; +} + +- (BOOL)isEqual:(id)object +{ + if (object == self) + return YES; + + if (!object || ![object isKindOfClass:[self class]]) + return NO; + + TGBridgeDocumentMediaAttachment *document = (TGBridgeDocumentMediaAttachment *)object; + + return (self.localDocumentId == 0 && self.documentId == document.documentId) || (self.localDocumentId != 0 && self.localDocumentId == document.localDocumentId); +} + ++ (NSInteger)mediaType +{ + return TGBridgeDocumentMediaAttachmentType; +} + +@end diff --git a/Watch/Bridge/TGBridgeForwardedMessageMediaAttachment.h b/Watch/Bridge/TGBridgeForwardedMessageMediaAttachment.h new file mode 100644 index 0000000000..16e1b648ca --- /dev/null +++ b/Watch/Bridge/TGBridgeForwardedMessageMediaAttachment.h @@ -0,0 +1,9 @@ +#import "TGBridgeMediaAttachment.h" + +@interface TGBridgeForwardedMessageMediaAttachment : TGBridgeMediaAttachment + +@property (nonatomic, assign) int64_t peerId; +@property (nonatomic, assign) int32_t mid; +@property (nonatomic, assign) int32_t date; + +@end diff --git a/Watch/Bridge/TGBridgeForwardedMessageMediaAttachment.m b/Watch/Bridge/TGBridgeForwardedMessageMediaAttachment.m new file mode 100644 index 0000000000..169e261cff --- /dev/null +++ b/Watch/Bridge/TGBridgeForwardedMessageMediaAttachment.m @@ -0,0 +1,35 @@ +#import "TGBridgeForwardedMessageMediaAttachment.h" + +const NSInteger TGBridgeForwardedMessageMediaAttachmentType = 0xAA1050C1; + +NSString *const TGBridgeForwardedMessageMediaPeerIdKey = @"peerId"; +NSString *const TGBridgeForwardedMessageMediaMidKey = @"mid"; +NSString *const TGBridgeForwardedMessageMediaDateKey = @"date"; + +@implementation TGBridgeForwardedMessageMediaAttachment + +- (nullable instancetype)initWithCoder:(nonnull NSCoder *)aDecoder +{ + self = [super init]; + if (self != nil) + { + _peerId = [aDecoder decodeInt64ForKey:TGBridgeForwardedMessageMediaPeerIdKey]; + _mid = [aDecoder decodeInt32ForKey:TGBridgeForwardedMessageMediaMidKey]; + _date = [aDecoder decodeInt32ForKey:TGBridgeForwardedMessageMediaDateKey]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder +{ + [aCoder encodeInt64:self.peerId forKey:TGBridgeForwardedMessageMediaPeerIdKey]; + [aCoder encodeInt32:self.mid forKey:TGBridgeForwardedMessageMediaMidKey]; + [aCoder encodeInt32:self.date forKey:TGBridgeForwardedMessageMediaDateKey]; +} + ++ (NSInteger)mediaType +{ + return TGBridgeForwardedMessageMediaAttachmentType; +} + +@end diff --git a/Watch/Bridge/TGBridgeImageInfo+TGImageInfo.h b/Watch/Bridge/TGBridgeImageInfo+TGImageInfo.h new file mode 100644 index 0000000000..b1786a480d --- /dev/null +++ b/Watch/Bridge/TGBridgeImageInfo+TGImageInfo.h @@ -0,0 +1,11 @@ +#import "TGBridgeImageInfo.h" + +@class TGImageInfo; + +@interface TGBridgeImageInfo (TGImageInfo) + ++ (TGBridgeImageInfo *)imageInfoWithTGImageInfo:(TGImageInfo *)imageInfo; + ++ (TGImageInfo *)tgImageInfoWithBridgeImageInfo:(TGBridgeImageInfo *)bridgeImageInfo; + +@end diff --git a/Watch/Bridge/TGBridgeImageInfo+TGImageInfo.m b/Watch/Bridge/TGBridgeImageInfo+TGImageInfo.m new file mode 100644 index 0000000000..705a71d383 --- /dev/null +++ b/Watch/Bridge/TGBridgeImageInfo+TGImageInfo.m @@ -0,0 +1,43 @@ +#import "TGBridgeImageInfo+TGImageInfo.h" + +#import + +@implementation TGBridgeImageInfo (TGImageInfo) + ++ (TGBridgeImageInfo *)imageInfoWithTGImageInfo:(TGImageInfo *)imageInfo +{ + if (imageInfo == nil) + return nil; + + TGBridgeImageInfo *bridgeImageInfo = [[TGBridgeImageInfo alloc] init]; + NSDictionary *allSizes = imageInfo.allSizes; + + NSMutableArray *bridgeEntries = [[NSMutableArray alloc] init]; + for (NSString *url in allSizes.allKeys) + { + TGBridgeImageSizeInfo *bridgeEntry = [[TGBridgeImageSizeInfo alloc] init]; + bridgeEntry.url = url; + bridgeEntry.dimensions = [allSizes[url] CGSizeValue]; + + [bridgeEntries addObject:bridgeEntry]; + } + + bridgeImageInfo->_entries = bridgeEntries; + + return bridgeImageInfo; +} + ++ (TGImageInfo *)tgImageInfoWithBridgeImageInfo:(TGBridgeImageInfo *)bridgeImageInfo +{ + if (bridgeImageInfo == nil) + return nil; + + TGImageInfo *imageInfo = [[TGImageInfo alloc] init]; + + for (TGBridgeImageSizeInfo *entry in bridgeImageInfo.entries) + [imageInfo addImageWithSize:entry.dimensions url:entry.url]; + + return imageInfo; +} + +@end diff --git a/Watch/Bridge/TGBridgeImageMediaAttachment.h b/Watch/Bridge/TGBridgeImageMediaAttachment.h new file mode 100644 index 0000000000..aff935c69f --- /dev/null +++ b/Watch/Bridge/TGBridgeImageMediaAttachment.h @@ -0,0 +1,9 @@ +#import "TGBridgeMediaAttachment.h" +#import + +@interface TGBridgeImageMediaAttachment : TGBridgeMediaAttachment + +@property (nonatomic, assign) int64_t imageId; +@property (nonatomic, assign) CGSize dimensions; + +@end diff --git a/Watch/Bridge/TGBridgeImageMediaAttachment.m b/Watch/Bridge/TGBridgeImageMediaAttachment.m new file mode 100644 index 0000000000..8ab5ec7044 --- /dev/null +++ b/Watch/Bridge/TGBridgeImageMediaAttachment.m @@ -0,0 +1,33 @@ +#import "TGBridgeImageMediaAttachment.h" +#import + +const NSInteger TGBridgeImageMediaAttachmentType = 0x269BD8A8; + +NSString *const TGBridgeImageMediaImageIdKey = @"imageId"; +NSString *const TGBridgeImageMediaDimensionsKey = @"dimensions"; + +@implementation TGBridgeImageMediaAttachment + +- (instancetype)initWithCoder:(NSCoder *)aDecoder +{ + self = [super init]; + if (self != nil) + { + _imageId = [aDecoder decodeInt64ForKey:TGBridgeImageMediaImageIdKey]; + _dimensions = [aDecoder decodeCGSizeForKey:TGBridgeImageMediaDimensionsKey]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder +{ + [aCoder encodeInt64:self.imageId forKey:TGBridgeImageMediaImageIdKey]; + [aCoder encodeCGSize:self.dimensions forKey:TGBridgeImageMediaDimensionsKey]; +} + ++ (NSInteger)mediaType +{ + return TGBridgeImageMediaAttachmentType; +} + +@end diff --git a/Watch/Bridge/TGBridgeLocationMediaAttachment.h b/Watch/Bridge/TGBridgeLocationMediaAttachment.h new file mode 100644 index 0000000000..c1a7bf54eb --- /dev/null +++ b/Watch/Bridge/TGBridgeLocationMediaAttachment.h @@ -0,0 +1,19 @@ +#import "TGBridgeMediaAttachment.h" + +@interface TGBridgeVenueAttachment : NSObject + +@property (nonatomic, strong) NSString *title; +@property (nonatomic, strong) NSString *address; +@property (nonatomic, strong) NSString *provider; +@property (nonatomic, strong) NSString *venueId; + +@end + +@interface TGBridgeLocationMediaAttachment : TGBridgeMediaAttachment + +@property (nonatomic, assign) double latitude; +@property (nonatomic, assign) double longitude; + +@property (nonatomic, strong) TGBridgeVenueAttachment *venue; + +@end diff --git a/Watch/Bridge/TGBridgeLocationMediaAttachment.m b/Watch/Bridge/TGBridgeLocationMediaAttachment.m new file mode 100644 index 0000000000..f6762eb549 --- /dev/null +++ b/Watch/Bridge/TGBridgeLocationMediaAttachment.m @@ -0,0 +1,95 @@ +#import "TGBridgeLocationMediaAttachment.h" + +const NSInteger TGBridgeLocationMediaAttachmentType = 0x0C9ED06E; + +NSString *const TGBridgeLocationMediaLatitudeKey = @"lat"; +NSString *const TGBridgeLocationMediaLongitudeKey = @"lon"; +NSString *const TGBridgeLocationMediaVenueKey = @"venue"; + +NSString *const TGBridgeVenueTitleKey = @"title"; +NSString *const TGBridgeVenueAddressKey = @"address"; +NSString *const TGBridgeVenueProviderKey = @"provider"; +NSString *const TGBridgeVenueIdKey = @"venueId"; + +@implementation TGBridgeVenueAttachment + +- (instancetype)initWithCoder:(NSCoder *)aDecoder +{ + self = [super init]; + if (self != nil) + { + _title = [aDecoder decodeObjectForKey:TGBridgeVenueTitleKey]; + _address = [aDecoder decodeObjectForKey:TGBridgeVenueAddressKey]; + _provider = [aDecoder decodeObjectForKey:TGBridgeVenueProviderKey]; + _venueId = [aDecoder decodeObjectForKey:TGBridgeVenueIdKey]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder +{ + [aCoder encodeObject:self.title forKey:TGBridgeVenueTitleKey]; + [aCoder encodeObject:self.address forKey:TGBridgeVenueAddressKey]; + [aCoder encodeObject:self.provider forKey:TGBridgeVenueProviderKey]; + [aCoder encodeObject:self.venueId forKey:TGBridgeVenueIdKey]; +} + +- (BOOL)isEqual:(id)object +{ + if (object == self) + return YES; + + if (!object || ![object isKindOfClass:[self class]]) + return NO; + + TGBridgeVenueAttachment *venue = (TGBridgeVenueAttachment *)object; + + return [self.title isEqualToString:venue.title] && [self.address isEqualToString:venue.address] && [self.provider isEqualToString:venue.provider] && [self.venueId isEqualToString:venue.venueId]; +} + +@end + + +@implementation TGBridgeLocationMediaAttachment + +- (instancetype)initWithCoder:(NSCoder *)aDecoder +{ + self = [super init]; + if (self != nil) + { + _latitude = [aDecoder decodeDoubleForKey:TGBridgeLocationMediaLatitudeKey]; + _longitude = [aDecoder decodeDoubleForKey:TGBridgeLocationMediaLongitudeKey]; + _venue = [aDecoder decodeObjectForKey:TGBridgeLocationMediaVenueKey]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder +{ + [aCoder encodeDouble:self.latitude forKey:TGBridgeLocationMediaLatitudeKey]; + [aCoder encodeDouble:self.longitude forKey:TGBridgeLocationMediaLongitudeKey]; + [aCoder encodeObject:self.venue forKey:TGBridgeLocationMediaVenueKey]; +} + +- (BOOL)isEqual:(id)object +{ + if (object == self) + return YES; + + if (!object || ![object isKindOfClass:[self class]]) + return NO; + + TGBridgeLocationMediaAttachment *location = (TGBridgeLocationMediaAttachment *)object; + + bool equalCoord = (fabs(self.latitude - location.latitude) < DBL_EPSILON && fabs(self.longitude - location.longitude) < DBL_EPSILON); + bool equalVenue = (self.venue == nil && location.venue == nil) || ([self.venue isEqual:location.venue]); + + return equalCoord || equalVenue; +} + ++ (NSInteger)mediaType +{ + return TGBridgeLocationMediaAttachmentType; +} + +@end diff --git a/Watch/Bridge/TGBridgeLocationSignals.h b/Watch/Bridge/TGBridgeLocationSignals.h new file mode 100644 index 0000000000..e7f6acc15e --- /dev/null +++ b/Watch/Bridge/TGBridgeLocationSignals.h @@ -0,0 +1,11 @@ +#import + +@interface TGBridgeLocationSignals : NSObject + ++ (SSignal *)currentLocation; ++ (SSignal *)nearbyVenuesWithLimit:(NSUInteger)limit; + +@end + +extern NSString *const TGBridgeLocationAccessRequiredKey; +extern NSString *const TGBridgeLocationLoadingKey; diff --git a/Watch/Bridge/TGBridgeLocationSignals.m b/Watch/Bridge/TGBridgeLocationSignals.m new file mode 100644 index 0000000000..fe850eaab7 --- /dev/null +++ b/Watch/Bridge/TGBridgeLocationSignals.m @@ -0,0 +1,192 @@ +#import "TGBridgeLocationSignals.h" +#import "TGBridgeCommon.h" +#import "TGBridgeSubscriptions.h" +#import "TGBridgeLocationVenue.h" +#import "TGBridgeClient.h" + +#import + +NSString *const TGBridgeLocationAccessRequiredKey = @"access"; +NSString *const TGBridgeLocationLoadingKey = @"loading"; + +@interface TGLocationManagerAdapter : NSObject +{ + CLLocationManager *_locationManager; +} + +@property (nonatomic, copy) void (^authorizationStatusChanged)(TGLocationManagerAdapter *sender, CLAuthorizationStatus status); +@property (nonatomic, copy) void (^locationChanged)(CLLocation *location); + +@end + +@implementation TGLocationManagerAdapter + +- (instancetype)init +{ + self = [super init]; + if (self != nil) + { + _locationManager = [[CLLocationManager alloc] init]; + _locationManager.delegate = self; + _locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters; + _locationManager.distanceFilter = 20; + //_locationManager.activityType = CLActivityTypeOther; + } + return self; +} + +- (void)dealloc +{ + _locationManager.delegate = nil; +} + +- (void)requestAuthorizationWithCompletion:(void (^)(TGLocationManagerAdapter *, CLAuthorizationStatus ))completion +{ + self.authorizationStatusChanged = completion; + + CLAuthorizationStatus status = [self authorizationStatus]; + if (status == kCLAuthorizationStatusNotDetermined || status == kCLAuthorizationStatusAuthorizedWhenInUse) + [_locationManager requestAlwaysAuthorization]; + else + self.authorizationStatusChanged(self, [self authorizationStatus]); +} + +- (CLAuthorizationStatus)authorizationStatus +{ + return [CLLocationManager authorizationStatus]; +} + +- (void)locationManager:(CLLocationManager *)manager didChangeAuthorizationStatus:(CLAuthorizationStatus)status +{ + if (status != kCLAuthorizationStatusNotDetermined && self.authorizationStatusChanged != nil) + self.authorizationStatusChanged(self, status); +} + +- (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error +{ + +} + +- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations +{ + CLLocation *location = locations.lastObject; + + if (self.locationChanged != nil) + self.locationChanged(location); +} + +- (void)startUpdating +{ + [_locationManager requestLocation]; +} + +- (void)stopUpdating +{ + [_locationManager stopUpdatingLocation]; +} + +@end + +@implementation TGBridgeLocationSignals + ++ (SSignal *)currentLocation +{ + return [[SSignal alloc] initWithGenerator:^id(SSubscriber *subscriber) + { + SDisposableSet *compositeDisposable = [[SDisposableSet alloc] init]; + + TGLocationManagerAdapter *adapter = [[TGLocationManagerAdapter alloc] init]; + if (adapter.authorizationStatus == kCLAuthorizationStatusAuthorizedAlways) + { + [adapter startUpdating]; + } + else if (adapter.authorizationStatus == kCLAuthorizationStatusNotDetermined || adapter.authorizationStatus == kCLAuthorizationStatusAuthorizedWhenInUse) + { + [subscriber putNext:TGBridgeLocationAccessRequiredKey]; + + SMetaDisposable *accessDisposable = [[SMetaDisposable alloc] init]; + + [accessDisposable setDisposable:[[[SSignal complete] delay:1.0f onQueue:[SQueue mainQueue]] startWithNext:nil completed:^ + { + [adapter requestAuthorizationWithCompletion:^(TGLocationManagerAdapter *sender, CLAuthorizationStatus status) + { + if (status == kCLAuthorizationStatusAuthorizedAlways) + { + [subscriber putNext:TGBridgeLocationLoadingKey]; + [sender startUpdating]; + } + else + { + [subscriber putNext:TGBridgeLocationAccessRequiredKey]; + } + }]; + }]]; + + [compositeDisposable add:accessDisposable]; + } + else if (adapter.authorizationStatus != kCLAuthorizationStatusAuthorizedAlways) + { + [subscriber putNext:TGBridgeLocationAccessRequiredKey]; + } + + adapter.locationChanged = ^(CLLocation *location) + { + if (location != nil && location.horizontalAccuracy > 0) + { + [subscriber putNext:location]; + [subscriber putCompletion]; + } + }; + + SBlockDisposable *adapterDisposable = [[SBlockDisposable alloc] initWithBlock:^ + { + [adapter stopUpdating]; + }]; + [compositeDisposable add:adapterDisposable]; + + return compositeDisposable; + }]; +} + ++ (SSignal *)nearbyVenuesWithLimit:(NSUInteger)limit +{ + return [[SSignal alloc] initWithGenerator:^id(SSubscriber *subscriber) + { + SMetaDisposable *disposable = [[SMetaDisposable alloc] init]; + [disposable setDisposable:[[[self currentLocation] mapToSignal:^SSignal *(id next) + { + if ([next isKindOfClass:[NSString class]]) + { + return [SSignal single:next]; + } + else if ([next isKindOfClass:[CLLocation class]]) + { + CLLocation *location = (CLLocation *)next; + return [[SSignal single:next] then:[self _nearbyVenuesWithCoordinate:location.coordinate limit:limit]]; + } + + return nil; + }] startWithNext:^(id next) + { + if ([next isKindOfClass:[NSArray class]]) + { + [subscriber putNext:next]; + [subscriber putCompletion]; + [disposable dispose]; + } + else + { + [subscriber putNext:next]; + } + }]]; + + return nil; + }]; +} + ++ (SSignal *)_nearbyVenuesWithCoordinate:(CLLocationCoordinate2D)coordinate limit:(NSUInteger)limit +{ + return [[TGBridgeClient instance] requestSignalWithSubscription:[[TGBridgeNearbyVenuesSubscription alloc] initWithCoordinate:coordinate limit:limit]]; +} + +@end diff --git a/Watch/Bridge/TGBridgeLocationVenue+TGTableItem.h b/Watch/Bridge/TGBridgeLocationVenue+TGTableItem.h new file mode 100644 index 0000000000..06f1293d86 --- /dev/null +++ b/Watch/Bridge/TGBridgeLocationVenue+TGTableItem.h @@ -0,0 +1,6 @@ +#import "TGBridgeLocationVenue.h" +#import "WKInterfaceTable+TGDataDrivenTable.h" + +@interface TGBridgeLocationVenue (TGTableItem) + +@end diff --git a/Watch/Bridge/TGBridgeLocationVenue+TGTableItem.m b/Watch/Bridge/TGBridgeLocationVenue+TGTableItem.m new file mode 100644 index 0000000000..47dc339a1f --- /dev/null +++ b/Watch/Bridge/TGBridgeLocationVenue+TGTableItem.m @@ -0,0 +1,10 @@ +#import "TGBridgeLocationVenue+TGTableItem.h" + +@implementation TGBridgeLocationVenue (TGTableItem) + +- (NSString *)uniqueIdentifier +{ + return self.identifier; +} + +@end diff --git a/Watch/Bridge/TGBridgeLocationVenue.h b/Watch/Bridge/TGBridgeLocationVenue.h new file mode 100644 index 0000000000..c626f76f9c --- /dev/null +++ b/Watch/Bridge/TGBridgeLocationVenue.h @@ -0,0 +1,15 @@ +#import + +@class TGBridgeLocationMediaAttachment; + +@interface TGBridgeLocationVenue : NSObject + +@property (nonatomic) CLLocationCoordinate2D coordinate; +@property (nonatomic, strong) NSString *identifier; +@property (nonatomic, strong) NSString *provider; +@property (nonatomic, strong) NSString *name; +@property (nonatomic, strong) NSString *address; + +- (TGBridgeLocationMediaAttachment *)locationAttachment; + +@end diff --git a/Watch/Bridge/TGBridgeLocationVenue.m b/Watch/Bridge/TGBridgeLocationVenue.m new file mode 100644 index 0000000000..01c8fc8c1a --- /dev/null +++ b/Watch/Bridge/TGBridgeLocationVenue.m @@ -0,0 +1,66 @@ +#import "TGBridgeLocationVenue.h" + +#import "TGBridgeLocationMediaAttachment.h" + +NSString *const TGBridgeLocationVenueLatitudeKey = @"lat"; +NSString *const TGBridgeLocationVenueLongitudeKey = @"lon"; +NSString *const TGBridgeLocationVenueIdentifierKey = @"identifier"; +NSString *const TGBridgeLocationVenueProviderKey = @"provider"; +NSString *const TGBridgeLocationVenueNameKey = @"name"; +NSString *const TGBridgeLocationVenueAddressKey = @"address"; + +@implementation TGBridgeLocationVenue + +- (instancetype)initWithCoder:(NSCoder *)aDecoder +{ + self = [super init]; + if (self != nil) + { + _coordinate = CLLocationCoordinate2DMake([aDecoder decodeDoubleForKey:TGBridgeLocationVenueLatitudeKey], [aDecoder decodeDoubleForKey:TGBridgeLocationVenueLongitudeKey]); + _identifier = [aDecoder decodeObjectForKey:TGBridgeLocationVenueIdentifierKey]; + _provider = [aDecoder decodeObjectForKey:TGBridgeLocationVenueProviderKey]; + _name = [aDecoder decodeObjectForKey:TGBridgeLocationVenueNameKey]; + _address = [aDecoder decodeObjectForKey:TGBridgeLocationVenueAddressKey]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder +{ + [aCoder encodeDouble:self.coordinate.latitude forKey:TGBridgeLocationVenueLatitudeKey]; + [aCoder encodeDouble:self.coordinate.longitude forKey:TGBridgeLocationVenueLongitudeKey]; + [aCoder encodeObject:self.identifier forKey:TGBridgeLocationVenueIdentifierKey]; + [aCoder encodeObject:self.provider forKey:TGBridgeLocationVenueProviderKey]; + [aCoder encodeObject:self.name forKey:TGBridgeLocationVenueNameKey]; + [aCoder encodeObject:self.address forKey:TGBridgeLocationVenueAddressKey]; +} + +- (TGBridgeLocationMediaAttachment *)locationAttachment +{ + TGBridgeLocationMediaAttachment *attachment = [[TGBridgeLocationMediaAttachment alloc] init]; + attachment.latitude = self.coordinate.latitude; + attachment.longitude = self.coordinate.longitude; + + TGBridgeVenueAttachment *venueAttachment = [[TGBridgeVenueAttachment alloc] init]; + venueAttachment.title = self.name; + venueAttachment.address = self.address; + venueAttachment.provider = self.provider; + venueAttachment.venueId = self.identifier; + + attachment.venue = venueAttachment; + + return attachment; +} + +- (BOOL)isEqual:(id)object +{ + if (object == self) + return YES; + + if (!object || ![object isKindOfClass:[self class]]) + return NO; + + return [self.identifier isEqualToString:((TGBridgeLocationVenue *)object).identifier]; +} + +@end diff --git a/Watch/Bridge/TGBridgeMediaAttachment.h b/Watch/Bridge/TGBridgeMediaAttachment.h new file mode 100644 index 0000000000..4833e13b91 --- /dev/null +++ b/Watch/Bridge/TGBridgeMediaAttachment.h @@ -0,0 +1,11 @@ +#import "TGBridgeCommon.h" + +@interface TGBridgeMediaAttachment : NSObject + +@property (nonatomic, readonly) NSInteger mediaType; + ++ (NSInteger)mediaType; + +@end + +extern NSString *const TGBridgeMediaAttachmentTypeKey; diff --git a/Watch/Bridge/TGBridgeMediaAttachment.m b/Watch/Bridge/TGBridgeMediaAttachment.m new file mode 100644 index 0000000000..971a23b64d --- /dev/null +++ b/Watch/Bridge/TGBridgeMediaAttachment.m @@ -0,0 +1,32 @@ +#import "TGBridgeMediaAttachment.h" + +NSString *const TGBridgeMediaAttachmentTypeKey = @"type"; + +@implementation TGBridgeMediaAttachment + +- (instancetype)initWithCoder:(NSCoder *)__unused aDecoder +{ + self = [super init]; + if (self != nil) + { + + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)__unused aCoder +{ + +} + +- (NSInteger)mediaType +{ + return 0; +} + ++ (NSInteger)mediaType +{ + return 0; +} + +@end diff --git a/Watch/Bridge/TGBridgeMediaSignals.h b/Watch/Bridge/TGBridgeMediaSignals.h new file mode 100644 index 0000000000..0157c31bc8 --- /dev/null +++ b/Watch/Bridge/TGBridgeMediaSignals.h @@ -0,0 +1,23 @@ +#import +#import "TGBridgeSubscriptions.h" + +@class TGBridgeImageMediaAttachment; +@class TGBridgeVideoMediaAttachment; +@class TGBridgeDocumentMediaAttachment; + +typedef enum +{ + TGMediaStickerImageTypeList, + TGMediaStickerImageTypeNormal, + TGMediaStickerImageTypeInput +} TGMediaStickerImageType; + +@interface TGBridgeMediaSignals : NSObject + ++ (SSignal *)thumbnailWithPeerId:(int64_t)peerId messageId:(int32_t)messageId size:(CGSize)size notification:(bool)notification; ++ (SSignal *)avatarWithPeerId:(int64_t)peerId url:(NSString *)url type:(TGBridgeMediaAvatarType)type; + ++ (SSignal *)stickerWithDocumentId:(int64_t)documentId packId:(int64_t)packId accessHash:(int64_t)accessHash type:(TGMediaStickerImageType)type; ++ (SSignal *)stickerWithDocumentId:(int64_t)documentId peerId:(int64_t)peerId messageId:(int32_t)messageId type:(TGMediaStickerImageType)type notification:(bool)notification; + +@end diff --git a/Watch/Bridge/TGBridgeMediaSignals.m b/Watch/Bridge/TGBridgeMediaSignals.m new file mode 100644 index 0000000000..d915ed246a --- /dev/null +++ b/Watch/Bridge/TGBridgeMediaSignals.m @@ -0,0 +1,209 @@ +#import "TGBridgeMediaSignals.h" +#import "TGBridgeImageMediaAttachment.h" +#import "TGBridgeVideoMediaAttachment.h" +#import "TGBridgeDocumentMediaAttachment.h" +#import "TGBridgeClient.h" +#import "TGFileCache.h" + +#import "TGGeometry.h" +#import "TGWatchCommon.h" + +#import "TGExtensionDelegate.h" +#import + +@interface TGBridgeMediaManager : NSObject +{ + NSMutableArray *_pendingUrls; + OSSpinLock _pendingUrlsLock; +} +@end + +@implementation TGBridgeMediaManager + +- (instancetype)init +{ + self = [super init]; + if (self != nil) + { + _pendingUrls = [[NSMutableArray alloc] init]; + } + return self; +} + +- (void)addUrl:(NSString *)url +{ + if (url == nil) + return; + + OSSpinLockLock(&_pendingUrlsLock); + [_pendingUrls addObject:url]; + OSSpinLockUnlock(&_pendingUrlsLock); +} + +- (void)removeUrl:(NSString *)url +{ + if (url == nil) + return; + + OSSpinLockLock(&_pendingUrlsLock); + [_pendingUrls removeObject:url]; + OSSpinLockUnlock(&_pendingUrlsLock); +} + +- (bool)hasUrl:(NSString *)url +{ + if (url == nil) + return false; + + OSSpinLockLock(&_pendingUrlsLock); + bool contains = [_pendingUrls containsObject:url]; + OSSpinLockUnlock(&_pendingUrlsLock); + + return contains; +} + +@end + + +@implementation TGBridgeMediaSignals + ++ (SSignal *)thumbnailWithPeerId:(int64_t)peerId messageId:(int32_t)messageId size:(CGSize)size notification:(bool)notification +{ + TGBridgeSubscription *subscription = [[TGBridgeMediaThumbnailSubscription alloc] initWithPeerId:peerId messageId:messageId size:size notification:notification]; + NSString *imageUrl = [NSString stringWithFormat:@"%lld_%d", peerId, messageId]; + return [self _requestImageWithUrl:imageUrl subscription:subscription]; +} + ++ (SSignal *)avatarWithPeerId:(int64_t)peerId url:(NSString *)url type:(TGBridgeMediaAvatarType)type +{ + NSString *imageUrl = [NSString stringWithFormat:@"%@_%lu", url, (unsigned long)type]; + TGBridgeSubscription *subscription = [[TGBridgeMediaAvatarSubscription alloc] initWithPeerId:peerId url:url type:type]; + return [self _requestImageWithUrl:imageUrl subscription:subscription]; +} + ++ (CGSize)_imageSizeForStickerType:(TGMediaStickerImageType)avatarType +{ + switch (avatarType) + { + case TGMediaStickerImageTypeList: + return CGSizeMake(19, 19); + + case TGMediaStickerImageTypeNormal: + case TGMediaStickerImageTypeInput: + { + return TGWatchStickerSizeForScreen(TGWatchScreenType()); + } + + default: + break; + } + + return CGSizeMake(72, 72); +} + ++ (SSignal *)stickerWithDocumentId:(int64_t)documentId packId:(int64_t)packId accessHash:(int64_t)accessHash type:(TGMediaStickerImageType)type +{ + CGSize imageSize = [self _imageSizeForStickerType:type]; + NSString *imageUrl = [NSString stringWithFormat:@"sticker_%lld_%dx%d_0", documentId, (int)imageSize.width, (int)imageSize.height]; + + TGBridgeSubscription *subscription = [[TGBridgeMediaStickerSubscription alloc] initWithDocumentId:documentId stickerPackId:packId stickerPackAccessHash:accessHash stickerPeerId:0 stickerMessageId:0 notification:false size:imageSize]; + + return [self _requestImageWithUrl:imageUrl subscription:subscription]; +} + ++ (SSignal *)stickerWithDocumentId:(int64_t)documentId peerId:(int64_t)peerId messageId:(int32_t)messageId type:(TGMediaStickerImageType)type notification:(bool)notification +{ + CGSize imageSize = [self _imageSizeForStickerType:type]; + NSString *imageUrl = [NSString stringWithFormat:@"sticker_%lld_%dx%d_%d", documentId, (int)imageSize.width, (int)imageSize.height, notification]; + + TGBridgeSubscription *subscription = [[TGBridgeMediaStickerSubscription alloc] initWithDocumentId:documentId stickerPackId:0 stickerPackAccessHash:0 stickerPeerId:peerId stickerMessageId:messageId notification:notification size:imageSize]; + + return [self _requestImageWithUrl:imageUrl subscription:subscription]; +} + ++ (id(^)(NSData *))_imageUnserializeBlock +{ + return ^id(NSData *data) + { + return data; + }; +} + ++ (SSignal *)_requestImageWithUrl:(NSString *)url subscription:(TGBridgeSubscription *)subscription +{ + SSignal *remoteSignal = [[[[[TGBridgeClient instance] requestSignalWithSubscription:subscription] onStart:^ + { + if (![[self mediaManager] hasUrl:url]) + [[self mediaManager] addUrl:url]; + }] onDispose:^ + { + [[self mediaManager] removeUrl:url]; + }] mapToSignal:^SSignal *(id next) + { + return [[self _downloadedFileWithUrl:url] onNext:^(id next) + { + [[self mediaManager] removeUrl:url]; + }]; + }]; + + return [[self _cachedOrPendingWithUrl:url] catch:^SSignal *(id error) + { + return remoteSignal; + }]; +} + ++ (SSignal *)_loadCachedWithUrl:(NSString *)url memoryOnly:(bool)memoryOnly unserializeBlock:(UIImage *(^)(NSData *))unserializeBlock +{ + return [[SSignal alloc] initWithGenerator:^id(SSubscriber *subscriber) + { + [[TGExtensionDelegate instance].imageCache fetchDataForKey:url memoryOnly:memoryOnly synchronous:false unserializeBlock:unserializeBlock completion:^(id image) + { + if (image != nil) + { + [subscriber putNext:image]; + [subscriber putCompletion]; + } + else + { + [subscriber putError:nil]; + } + }]; + + return nil; + }]; +} + ++ (SSignal *)_downloadedFileWithUrl:(NSString *)url +{ + return [[self _loadCachedWithUrl:url memoryOnly:true unserializeBlock:nil] catch:^SSignal *(id error) + { + return [[[[TGBridgeClient instance] fileSignalForKey:url] take:1] map:^NSData *(NSURL *url) + { + return [NSData dataWithContentsOfURL:url]; + }]; + }]; +} + ++ (SSignal *)_cachedOrPendingWithUrl:(NSString *)url +{ + return [[self _loadCachedWithUrl:url memoryOnly:false unserializeBlock:[self _imageUnserializeBlock]] catch:^SSignal *(id error) + { + if ([[self mediaManager] hasUrl:url]) + return [self _downloadedFileWithUrl:url]; + + return [SSignal fail:nil]; + }]; +} + ++ (TGBridgeMediaManager *)mediaManager +{ + static dispatch_once_t onceToken; + static TGBridgeMediaManager *manager; + dispatch_once(&onceToken, ^ + { + manager = [[TGBridgeMediaManager alloc] init]; + }); + return manager; +} + +@end diff --git a/Watch/Bridge/TGBridgeMessage+TGTableItem.h b/Watch/Bridge/TGBridgeMessage+TGTableItem.h new file mode 100644 index 0000000000..253c3d9449 --- /dev/null +++ b/Watch/Bridge/TGBridgeMessage+TGTableItem.h @@ -0,0 +1,6 @@ +#import "TGBridgeMessage.h" +#import "WKInterfaceTable+TGDataDrivenTable.h" + +@interface TGBridgeMessage (TGTableItem) + +@end diff --git a/Watch/Bridge/TGBridgeMessage+TGTableItem.m b/Watch/Bridge/TGBridgeMessage+TGTableItem.m new file mode 100644 index 0000000000..fa4c28700d --- /dev/null +++ b/Watch/Bridge/TGBridgeMessage+TGTableItem.m @@ -0,0 +1,10 @@ +#import "TGBridgeMessage+TGTableItem.h" + +@implementation TGBridgeMessage (TGTableItem) + +- (NSString *)uniqueIdentifier +{ + return [NSString stringWithFormat:@"%d", self.identifier]; +} + +@end diff --git a/Watch/Bridge/TGBridgeMessage.h b/Watch/Bridge/TGBridgeMessage.h new file mode 100644 index 0000000000..46d46a00b8 --- /dev/null +++ b/Watch/Bridge/TGBridgeMessage.h @@ -0,0 +1,65 @@ +#import "TGBridgeCommon.h" +#import "TGBridgeImageMediaAttachment.h" +#import "TGBridgeVideoMediaAttachment.h" +#import "TGBridgeAudioMediaAttachment.h" +#import "TGBridgeDocumentMediaAttachment.h" +#import "TGBridgeLocationMediaAttachment.h" +#import "TGBridgeContactMediaAttachment.h" +#import "TGBridgeActionMediaAttachment.h" +#import "TGBridgeReplyMessageMediaAttachment.h" +#import "TGBridgeForwardedMessageMediaAttachment.h" +#import "TGBridgeWebPageMediaAttachment.h" +#import "TGBridgeMessageEntitiesAttachment.h" +#import "TGBridgeUnsupportedMediaAttachment.h" + +typedef enum { + TGBridgeTextCheckingResultTypeUndefined, + TGBridgeTextCheckingResultTypeBold, + TGBridgeTextCheckingResultTypeItalic, + TGBridgeTextCheckingResultTypeCode, + TGBridgeTextCheckingResultTypePre +} TGBridgeTextCheckingResultType; + +@interface TGBridgeTextCheckingResult : NSObject + +@property (nonatomic, assign) TGBridgeTextCheckingResultType type; +@property (nonatomic, assign) NSRange range; + +@end + + +typedef NS_ENUM(NSUInteger, TGBridgeMessageDeliveryState) { + TGBridgeMessageDeliveryStateDelivered = 0, + TGBridgeMessageDeliveryStatePending = 1, + TGBridgeMessageDeliveryStateFailed = 2 +}; + +@interface TGBridgeMessage : NSObject + +@property (nonatomic) int32_t identifier; +@property (nonatomic) NSTimeInterval date; +@property (nonatomic) int64_t randomId; +@property (nonatomic) bool unread; +@property (nonatomic) bool deliveryError; +@property (nonatomic) TGBridgeMessageDeliveryState deliveryState; +@property (nonatomic) bool outgoing; +@property (nonatomic) int64_t fromUid; +@property (nonatomic) int64_t toUid; +@property (nonatomic) int64_t cid; +@property (nonatomic, strong) NSString *text; +@property (nonatomic, strong) NSArray *media; +@property (nonatomic) bool forceReply; + +- (NSIndexSet *)involvedUserIds; +- (NSArray *)textCheckingResults; + ++ (instancetype)temporaryNewMessageForText:(NSString *)text userId:(int32_t)userId; ++ (instancetype)temporaryNewMessageForText:(NSString *)text userId:(int32_t)userId replyToMessage:(TGBridgeMessage *)replyToMessage; ++ (instancetype)temporaryNewMessageForSticker:(TGBridgeDocumentMediaAttachment *)sticker userId:(int32_t)userId; ++ (instancetype)temporaryNewMessageForLocation:(TGBridgeLocationMediaAttachment *)location userId:(int32_t)userId; ++ (instancetype)temporaryNewMessageForAudioWithDuration:(int32_t)duration userId:(int32_t)userId localAudioId:(int64_t)localAudioId; + +@end + +extern NSString *const TGBridgeMessageKey; +extern NSString *const TGBridgeMessagesArrayKey; diff --git a/Watch/Bridge/TGBridgeMessage.m b/Watch/Bridge/TGBridgeMessage.m new file mode 100644 index 0000000000..1793d98851 --- /dev/null +++ b/Watch/Bridge/TGBridgeMessage.m @@ -0,0 +1,238 @@ +#import "TGBridgeMessage.h" +#import "TGWatchCommon.h" +#import "TGBridgePeerIdAdapter.h" + +NSString *const TGBridgeMessageIdentifierKey = @"identifier"; +NSString *const TGBridgeMessageDateKey = @"date"; +NSString *const TGBridgeMessageRandomIdKey = @"randomId"; +NSString *const TGBridgeMessageFromUidKey = @"fromUid"; +NSString *const TGBridgeMessageCidKey = @"cid"; +NSString *const TGBridgeMessageTextKey = @"text"; +NSString *const TGBridgeMessageUnreadKey = @"unread"; +NSString *const TGBridgeMessageOutgoingKey = @"outgoing"; +NSString *const TGBridgeMessageMediaKey = @"media"; +NSString *const TGBridgeMessageDeliveryStateKey = @"deliveryState"; +NSString *const TGBridgeMessageForceReplyKey = @"forceReply"; + +NSString *const TGBridgeMessageKey = @"message"; +NSString *const TGBridgeMessagesArrayKey = @"messages"; + +@interface TGBridgeMessage () +{ + NSArray *_textCheckingResults; +} +@end + +@implementation TGBridgeMessage + +- (instancetype)initWithCoder:(NSCoder *)aDecoder +{ + self = [super init]; + if (self != nil) + { + _identifier = [aDecoder decodeInt32ForKey:TGBridgeMessageIdentifierKey]; + _date = [aDecoder decodeDoubleForKey:TGBridgeMessageDateKey]; + _randomId = [aDecoder decodeInt64ForKey:TGBridgeMessageRandomIdKey]; + _fromUid = [aDecoder decodeInt64ForKey:TGBridgeMessageFromUidKey]; + _cid = [aDecoder decodeInt64ForKey:TGBridgeMessageCidKey]; + _text = [aDecoder decodeObjectForKey:TGBridgeMessageTextKey]; + _outgoing = [aDecoder decodeBoolForKey:TGBridgeMessageOutgoingKey]; + _unread = [aDecoder decodeBoolForKey:TGBridgeMessageUnreadKey]; + _deliveryState = [aDecoder decodeInt32ForKey:TGBridgeMessageDeliveryStateKey]; + _media = [aDecoder decodeObjectForKey:TGBridgeMessageMediaKey]; + _forceReply = [aDecoder decodeBoolForKey:TGBridgeMessageForceReplyKey]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder +{ + [aCoder encodeInt32:self.identifier forKey:TGBridgeMessageIdentifierKey]; + [aCoder encodeDouble:self.date forKey:TGBridgeMessageDateKey]; + [aCoder encodeInt64:self.randomId forKey:TGBridgeMessageRandomIdKey]; + [aCoder encodeInt64:self.fromUid forKey:TGBridgeMessageFromUidKey]; + [aCoder encodeInt64:self.cid forKey:TGBridgeMessageCidKey]; + [aCoder encodeObject:self.text forKey:TGBridgeMessageTextKey]; + [aCoder encodeBool:self.outgoing forKey:TGBridgeMessageOutgoingKey]; + [aCoder encodeBool:self.unread forKey:TGBridgeMessageUnreadKey]; + [aCoder encodeInt32:self.deliveryState forKey:TGBridgeMessageDeliveryStateKey]; + [aCoder encodeObject:self.media forKey:TGBridgeMessageMediaKey]; + [aCoder encodeBool:self.forceReply forKey:TGBridgeMessageForceReplyKey]; +} + +- (NSIndexSet *)involvedUserIds +{ + NSMutableIndexSet *userIds = [[NSMutableIndexSet alloc] init]; + if (!TGPeerIdIsChannel(self.fromUid)) + [userIds addIndex:(int32_t)self.fromUid]; + + for (TGBridgeMediaAttachment *attachment in self.media) + { + if ([attachment isKindOfClass:[TGBridgeContactMediaAttachment class]]) + { + TGBridgeContactMediaAttachment *contactAttachment = (TGBridgeContactMediaAttachment *)attachment; + if (contactAttachment.uid != 0) + [userIds addIndex:contactAttachment.uid]; + } + else if ([attachment isKindOfClass:[TGBridgeForwardedMessageMediaAttachment class]]) + { + TGBridgeForwardedMessageMediaAttachment *forwardAttachment = (TGBridgeForwardedMessageMediaAttachment *)attachment; + if (forwardAttachment.peerId != 0 && !TGPeerIdIsChannel(forwardAttachment.peerId)) + [userIds addIndex:(int32_t)forwardAttachment.peerId]; + } + else if ([attachment isKindOfClass:[TGBridgeReplyMessageMediaAttachment class]]) + { + TGBridgeReplyMessageMediaAttachment *replyAttachment = (TGBridgeReplyMessageMediaAttachment *)attachment; + if (replyAttachment.message != nil && !TGPeerIdIsChannel(replyAttachment.message.fromUid)) + [userIds addIndex:(int32_t)replyAttachment.message.fromUid]; + } + else if ([attachment isKindOfClass:[TGBridgeActionMediaAttachment class]]) + { + TGBridgeActionMediaAttachment *actionAttachment = (TGBridgeActionMediaAttachment *)attachment; + if (actionAttachment.actionData[@"uid"] != nil) + [userIds addIndex:[actionAttachment.actionData[@"uid"] int32Value]]; + } + } + + return userIds; +} + +- (NSArray *)textCheckingResults +{ + if (_textCheckingResults == nil) + { + NSMutableArray *results = [[NSMutableArray alloc] init]; + + NSArray *entities = nil; + for (TGBridgeMediaAttachment *attachment in self.media) + { + if ([attachment isKindOfClass:[TGBridgeMessageEntitiesAttachment class]]) + { + entities = ((TGBridgeMessageEntitiesAttachment *)attachment).entities; + break; + } + } + + for (TGBridgeMessageEntity *entity in entities) + { + TGBridgeTextCheckingResult *result = [[TGBridgeTextCheckingResult alloc] init]; + result.range = entity.range; + + if ([entity isKindOfClass:[TGBridgeMessageEntityBold class]]) + result.type = TGBridgeTextCheckingResultTypeBold; + else if ([entity isKindOfClass:[TGBridgeMessageEntityItalic class]]) + result.type = TGBridgeTextCheckingResultTypeItalic; + else if ([entity isKindOfClass:[TGBridgeMessageEntityCode class]]) + result.type = TGBridgeTextCheckingResultTypeCode; + else if ([entity isKindOfClass:[TGBridgeMessageEntityPre class]]) + result.type = TGBridgeTextCheckingResultTypePre; + + if (result.type != TGBridgeTextCheckingResultTypeUndefined) + [results addObject:result]; + } + + _textCheckingResults = results; + } + + return _textCheckingResults; +} + +- (BOOL)isEqual:(id)object +{ + if (object == self) + return YES; + + if (!object || ![object isKindOfClass:[self class]]) + return NO; + + TGBridgeMessage *message = (TGBridgeMessage *)object; + + if (self.randomId != 0) + return self.randomId == message.randomId; + else + return self.identifier == message.identifier; +} + ++ (instancetype)temporaryNewMessageForText:(NSString *)text userId:(int32_t)userId +{ + return [self temporaryNewMessageForText:text userId:userId replyToMessage:nil]; +} + ++ (instancetype)temporaryNewMessageForText:(NSString *)text userId:(int32_t)userId replyToMessage:(TGBridgeMessage *)replyToMessage +{ + int64_t randomId = 0; + arc4random_buf(&randomId, 8); + + int32_t messageId = 0; + arc4random_buf(&messageId, 4); + + TGBridgeMessage *message = [[TGBridgeMessage alloc] init]; + message->_identifier = -abs(messageId); + message->_fromUid = userId; + message->_randomId = randomId; + message->_unread = true; + message->_outgoing = true; + message->_deliveryState = TGBridgeMessageDeliveryStatePending; + message->_text = text; + message->_date = [[NSDate date] timeIntervalSince1970]; + + if (replyToMessage != nil) + { + TGBridgeReplyMessageMediaAttachment *replyAttachment = [[TGBridgeReplyMessageMediaAttachment alloc] init]; + replyAttachment.mid = replyToMessage.identifier; + replyAttachment.message = replyToMessage; + + message->_media = @[ replyToMessage ]; + } + + return message; +} + ++ (instancetype)temporaryNewMessageForSticker:(TGBridgeDocumentMediaAttachment *)sticker userId:(int32_t)userId +{ + return [self _temporaryNewMessageForMediaAttachment:sticker userId:userId]; +} + ++ (instancetype)temporaryNewMessageForLocation:(TGBridgeLocationMediaAttachment *)location userId:(int32_t)userId +{ + return [self _temporaryNewMessageForMediaAttachment:location userId:userId]; +} + ++ (instancetype)temporaryNewMessageForAudioWithDuration:(int32_t)duration userId:(int32_t)userId localAudioId:(int64_t)localAudioId +{ + TGBridgeDocumentMediaAttachment *document = [[TGBridgeDocumentMediaAttachment alloc] init]; + document.isAudio = true; + document.isVoice = true; + document.localDocumentId = localAudioId; + document.duration = duration; + + return [self _temporaryNewMessageForMediaAttachment:document userId:userId]; +} + ++ (instancetype)_temporaryNewMessageForMediaAttachment:(TGBridgeMediaAttachment *)attachment userId:(int32_t)userId +{ + int64_t randomId = 0; + arc4random_buf(&randomId, 8); + + int32_t messageId = 0; + arc4random_buf(&messageId, 4); + + TGBridgeMessage *message = [[TGBridgeMessage alloc] init]; + message->_identifier = -abs(messageId); + message->_fromUid = userId; + message->_unread = true; + message->_outgoing = true; + message->_deliveryState = TGBridgeMessageDeliveryStatePending; + message->_date = [[NSDate date] timeIntervalSince1970]; + + message->_media = @[ attachment ]; + + return message; +} + +@end + + +@implementation TGBridgeTextCheckingResult + +@end diff --git a/Watch/Bridge/TGBridgeMessageEntities.h b/Watch/Bridge/TGBridgeMessageEntities.h new file mode 100644 index 0000000000..669ff3b21d --- /dev/null +++ b/Watch/Bridge/TGBridgeMessageEntities.h @@ -0,0 +1,59 @@ +#import + +@interface TGBridgeMessageEntity : NSObject + +@property (nonatomic, assign) NSRange range; + ++ (instancetype)entitityWithRange:(NSRange)range; + +@end + + +@interface TGBridgeMessageEntityUrl : TGBridgeMessageEntity + +@end + + +@interface TGBridgeMessageEntityEmail : TGBridgeMessageEntity + +@end + + +@interface TGBridgeMessageEntityTextUrl : TGBridgeMessageEntity + +@end + + +@interface TGBridgeMessageEntityMention : TGBridgeMessageEntity + +@end + + +@interface TGBridgeMessageEntityHashtag : TGBridgeMessageEntity + +@end + + +@interface TGBridgeMessageEntityBotCommand : TGBridgeMessageEntity + +@end + + +@interface TGBridgeMessageEntityBold : TGBridgeMessageEntity + +@end + + +@interface TGBridgeMessageEntityItalic : TGBridgeMessageEntity + +@end + + +@interface TGBridgeMessageEntityCode : TGBridgeMessageEntity + +@end + + +@interface TGBridgeMessageEntityPre : TGBridgeMessageEntity + +@end diff --git a/Watch/Bridge/TGBridgeMessageEntities.m b/Watch/Bridge/TGBridgeMessageEntities.m new file mode 100644 index 0000000000..8d639a9468 --- /dev/null +++ b/Watch/Bridge/TGBridgeMessageEntities.m @@ -0,0 +1,83 @@ +#import "TGBridgeMessageEntities.h" + +NSString *const TGBridgeMessageEntityLocationKey = @"loc"; +NSString *const TGBridgeMessageEntityLengthKey = @"len"; + +@implementation TGBridgeMessageEntity + +- (instancetype)initWithCoder:(NSCoder *)aDecoder +{ + self = [super init]; + if (self != nil) + { + NSUInteger loc = [aDecoder decodeIntegerForKey:TGBridgeMessageEntityLocationKey]; + NSUInteger len = [aDecoder decodeIntegerForKey:TGBridgeMessageEntityLengthKey]; + _range = NSMakeRange(loc, len); + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder +{ + [aCoder encodeInteger:self.range.location forKey:TGBridgeMessageEntityLocationKey]; + [aCoder encodeInteger:self.range.length forKey:TGBridgeMessageEntityLengthKey]; +} + ++ (instancetype)entitityWithRange:(NSRange)range +{ + TGBridgeMessageEntity *entity = [[self alloc] init]; + entity.range = range; + return entity; +} + +@end + + +@implementation TGBridgeMessageEntityUrl + +@end + + +@implementation TGBridgeMessageEntityEmail + +@end + + +@implementation TGBridgeMessageEntityTextUrl + +@end + + +@implementation TGBridgeMessageEntityMention + +@end + + +@implementation TGBridgeMessageEntityHashtag + +@end + + +@implementation TGBridgeMessageEntityBotCommand + +@end + + +@implementation TGBridgeMessageEntityBold + +@end + + +@implementation TGBridgeMessageEntityItalic + +@end + + +@implementation TGBridgeMessageEntityCode + +@end + + +@implementation TGBridgeMessageEntityPre + +@end diff --git a/Watch/Bridge/TGBridgeMessageEntitiesAttachment.h b/Watch/Bridge/TGBridgeMessageEntitiesAttachment.h new file mode 100644 index 0000000000..7913d8316e --- /dev/null +++ b/Watch/Bridge/TGBridgeMessageEntitiesAttachment.h @@ -0,0 +1,8 @@ +#import "TGBridgeMediaAttachment.h" +#import "TGBridgeMessageEntities.h" + +@interface TGBridgeMessageEntitiesAttachment : TGBridgeMediaAttachment + +@property (nonatomic, strong) NSArray *entities; + +@end diff --git a/Watch/Bridge/TGBridgeMessageEntitiesAttachment.m b/Watch/Bridge/TGBridgeMessageEntitiesAttachment.m new file mode 100644 index 0000000000..fb5cb3b7d7 --- /dev/null +++ b/Watch/Bridge/TGBridgeMessageEntitiesAttachment.m @@ -0,0 +1,30 @@ +#import "TGBridgeMessageEntitiesAttachment.h" + +const NSInteger TGBridgeMessageEntitiesAttachmentType = 0x8c2e3cce; + +NSString *const TGBridgeMessageEntitiesKey = @"entities"; + +@implementation TGBridgeMessageEntitiesAttachment + +- (instancetype)initWithCoder:(NSCoder *)aDecoder +{ + self = [super init]; + if (self != nil) + { + _entities = [aDecoder decodeObjectForKey:TGBridgeMessageEntitiesKey]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder +{ + [aCoder encodeObject:self.entities forKey:TGBridgeMessageEntitiesKey]; +} + ++ (NSInteger)mediaType +{ + return TGBridgeMessageEntitiesAttachmentType; +} + + +@end diff --git a/Watch/Bridge/TGBridgeMessageEntity+TGMessageEntity.h b/Watch/Bridge/TGBridgeMessageEntity+TGMessageEntity.h new file mode 100644 index 0000000000..645a079e35 --- /dev/null +++ b/Watch/Bridge/TGBridgeMessageEntity+TGMessageEntity.h @@ -0,0 +1,9 @@ +#import "TGBridgeMessageEntities.h" + +#import + +@interface TGBridgeMessageEntity (TGMessageEntity) + ++ (TGBridgeMessageEntity *)entityWithTGMessageEntity:(TGMessageEntity *)entity; + +@end diff --git a/Watch/Bridge/TGBridgeMessageEntity+TGMessageEntity.m b/Watch/Bridge/TGBridgeMessageEntity+TGMessageEntity.m new file mode 100644 index 0000000000..4fccef8cd1 --- /dev/null +++ b/Watch/Bridge/TGBridgeMessageEntity+TGMessageEntity.m @@ -0,0 +1,38 @@ +#import "TGBridgeMessageEntity+TGMessageEntity.h" + +#import + +@implementation TGBridgeMessageEntity (TGMessageEntity) + ++ (TGBridgeMessageEntity *)entityWithTGMessageEntity:(TGMessageEntity *)entity +{ + Class bridgeEntityClass = nil; + + if ([entity isKindOfClass:[TGMessageEntityUrl class]]) + bridgeEntityClass = [TGBridgeMessageEntityUrl class]; + else if ([entity isKindOfClass:[TGMessageEntityEmail class]]) + bridgeEntityClass = [TGBridgeMessageEntityEmail class]; + else if ([entity isKindOfClass:[TGMessageEntityTextUrl class]]) + bridgeEntityClass = [TGBridgeMessageEntityTextUrl class]; + else if ([entity isKindOfClass:[TGMessageEntityMention class]]) + bridgeEntityClass = [TGBridgeMessageEntityMention class]; + else if ([entity isKindOfClass:[TGMessageEntityHashtag class]]) + bridgeEntityClass = [TGBridgeMessageEntityHashtag class]; + else if ([entity isKindOfClass:[TGMessageEntityBotCommand class]]) + bridgeEntityClass = [TGBridgeMessageEntityBotCommand class]; + else if ([entity isKindOfClass:[TGMessageEntityBold class]]) + bridgeEntityClass = [TGBridgeMessageEntityBold class]; + else if ([entity isKindOfClass:[TGMessageEntityItalic class]]) + bridgeEntityClass = [TGBridgeMessageEntityItalic class]; + else if ([entity isKindOfClass:[TGMessageEntityCode class]]) + bridgeEntityClass = [TGBridgeMessageEntityCode class]; + else if ([entity isKindOfClass:[TGMessageEntityPre class]]) + bridgeEntityClass = [TGBridgeMessageEntityPre class]; + + if (bridgeEntityClass != nil) + return [bridgeEntityClass entitityWithRange:entity.range]; + + return nil; +} + +@end diff --git a/Watch/Bridge/TGBridgePeerIdAdapter.h b/Watch/Bridge/TGBridgePeerIdAdapter.h new file mode 100644 index 0000000000..c5f0ac92fc --- /dev/null +++ b/Watch/Bridge/TGBridgePeerIdAdapter.h @@ -0,0 +1,52 @@ +#ifndef Telegraph_TGPeerIdAdapter_h +#define Telegraph_TGPeerIdAdapter_h + +static inline bool TGPeerIdIsGroup(int64_t peerId) { + return peerId < 0 && peerId > INT32_MIN; +} + +static inline bool TGPeerIdIsUser(int64_t peerId) { + return peerId > 0 && peerId < INT32_MAX; +} + +static inline bool TGPeerIdIsChannel(int64_t peerId) { + return peerId <= ((int64_t)INT32_MIN) * 2 && peerId > ((int64_t)INT32_MIN) * 3; +} + +static inline bool TGPeerIdIsAdminLog(int64_t peerId) { + return peerId <= ((int64_t)INT32_MIN) * 3 && peerId > ((int64_t)INT32_MIN) * 4; +} + +static inline int32_t TGChannelIdFromPeerId(int64_t peerId) { + if (TGPeerIdIsChannel(peerId)) { + return (int32_t)(((int64_t)INT32_MIN) * 2 - peerId); + } else { + return 0; + } +} + +static inline int64_t TGPeerIdFromChannelId(int32_t channelId) { + return ((int64_t)INT32_MIN) * 2 - ((int64_t)channelId); +} + +static inline int64_t TGPeerIdFromAdminLogId(int32_t channelId) { + return ((int64_t)INT32_MIN) * 3 - ((int64_t)channelId); +} + +static inline int64_t TGPeerIdFromGroupId(int32_t groupId) { + return -groupId; +} + +static inline int32_t TGGroupIdFromPeerId(int64_t peerId) { + if (TGPeerIdIsGroup(peerId)) { + return (int32_t)-peerId; + } else { + return 0; + } +} + +static inline bool TGPeerIdIsSecretChat(int64_t peerId) { + return peerId <= ((int64_t)INT32_MIN) && peerId > ((int64_t)INT32_MIN) * 2; +} + +#endif diff --git a/Watch/Bridge/TGBridgePeerNotificationSettings.h b/Watch/Bridge/TGBridgePeerNotificationSettings.h new file mode 100644 index 0000000000..262908e368 --- /dev/null +++ b/Watch/Bridge/TGBridgePeerNotificationSettings.h @@ -0,0 +1,7 @@ +#import "TGBridgeCommon.h" + +@interface TGBridgePeerNotificationSettings : NSObject + +@property (nonatomic, assign) int32_t muteFor; + +@end diff --git a/Watch/Bridge/TGBridgePeerNotificationSettings.m b/Watch/Bridge/TGBridgePeerNotificationSettings.m new file mode 100644 index 0000000000..662c2f4ca4 --- /dev/null +++ b/Watch/Bridge/TGBridgePeerNotificationSettings.m @@ -0,0 +1,22 @@ +#import "TGBridgePeerNotificationSettings.h" + +NSString *const TGBridgePeerNotificationSettingsMuteForKey = @"muteFor"; + +@implementation TGBridgePeerNotificationSettings + +- (instancetype)initWithCoder:(NSCoder *)aDecoder +{ + self = [super init]; + if (self != nil) + { + _muteFor = [aDecoder decodeInt32ForKey:TGBridgePeerNotificationSettingsMuteForKey]; + } + return self; +} + +- (void)encodeWithCoder:(nonnull NSCoder *)aCoder +{ + [aCoder encodeInt32:self.muteFor forKey:TGBridgePeerNotificationSettingsMuteForKey]; +} + +@end diff --git a/Watch/Bridge/TGBridgePeerSettingsSignals.h b/Watch/Bridge/TGBridgePeerSettingsSignals.h new file mode 100644 index 0000000000..be84781194 --- /dev/null +++ b/Watch/Bridge/TGBridgePeerSettingsSignals.h @@ -0,0 +1,11 @@ +#import +#import "TGBridgePeerNotificationSettings.h" + +@interface TGBridgePeerSettingsSignals : NSObject + ++ (SSignal *)peerSettingsWithPeerId:(int64_t)peerId; + ++ (SSignal *)toggleMutedWithPeerId:(int64_t)peerId; ++ (SSignal *)updateBlockStatusWithPeerId:(int64_t)peerId blocked:(bool)blocked; + +@end diff --git a/Watch/Bridge/TGBridgePeerSettingsSignals.m b/Watch/Bridge/TGBridgePeerSettingsSignals.m new file mode 100644 index 0000000000..1a7978870c --- /dev/null +++ b/Watch/Bridge/TGBridgePeerSettingsSignals.m @@ -0,0 +1,22 @@ +#import "TGBridgePeerSettingsSignals.h" +#import "TGBridgeSubscriptions.h" +#import "TGBridgeClient.h" + +@implementation TGBridgePeerSettingsSignals + ++ (SSignal *)peerSettingsWithPeerId:(int64_t)peerId; +{ + return [[TGBridgeClient instance] requestSignalWithSubscription:[[TGBridgePeerSettingsSubscription alloc] initWithPeerId:peerId]]; +} + ++ (SSignal *)toggleMutedWithPeerId:(int64_t)peerId +{ + return [[TGBridgeClient instance] requestSignalWithSubscription:[[TGBridgePeerUpdateNotificationSettingsSubscription alloc] initWithPeerId:peerId]]; +} + ++ (SSignal *)updateBlockStatusWithPeerId:(int64_t)peerId blocked:(bool)blocked +{ + return [[TGBridgeClient instance] requestSignalWithSubscription:[[TGBridgePeerUpdateBlockStatusSubscription alloc] initWithPeerId:peerId blocked:blocked]]; +} + +@end diff --git a/Watch/Bridge/TGBridgePresetsSignals.h b/Watch/Bridge/TGBridgePresetsSignals.h new file mode 100644 index 0000000000..231e1e29c6 --- /dev/null +++ b/Watch/Bridge/TGBridgePresetsSignals.h @@ -0,0 +1,7 @@ +#import + +@interface TGBridgePresetsSignals : NSObject + ++ (NSURL *)presetsURL; + +@end diff --git a/Watch/Bridge/TGBridgePresetsSignals.m b/Watch/Bridge/TGBridgePresetsSignals.m new file mode 100644 index 0000000000..3ce4121769 --- /dev/null +++ b/Watch/Bridge/TGBridgePresetsSignals.m @@ -0,0 +1,17 @@ +#import "TGBridgePresetsSignals.h" + +@implementation TGBridgePresetsSignals + ++ (NSURL *)presetsURL +{ + static dispatch_once_t onceToken; + static NSURL *presetsURL; + dispatch_once(&onceToken, ^ + { + NSString *documentsPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, true)[0]; + presetsURL = [[NSURL alloc] initFileURLWithPath:[documentsPath stringByAppendingPathComponent:@"presets.data"]]; + }); + return presetsURL; +} + +@end diff --git a/Watch/Bridge/TGBridgeRemoteSignals.h b/Watch/Bridge/TGBridgeRemoteSignals.h new file mode 100644 index 0000000000..9948d40899 --- /dev/null +++ b/Watch/Bridge/TGBridgeRemoteSignals.h @@ -0,0 +1,7 @@ +#import + +@interface TGBridgeRemoteSignals : NSObject + ++ (SSignal *)openRemoteMessageWithPeerId:(int64_t)peerId messageId:(int32_t)messageId type:(int32_t)type autoPlay:(bool)autoPlay; + +@end diff --git a/Watch/Bridge/TGBridgeRemoteSignals.m b/Watch/Bridge/TGBridgeRemoteSignals.m new file mode 100644 index 0000000000..24fa1e0b99 --- /dev/null +++ b/Watch/Bridge/TGBridgeRemoteSignals.m @@ -0,0 +1,13 @@ +#import "TGBridgeRemoteSignals.h" +#import "TGBridgeSubscriptions.h" +#import "TGBridgeClient.h" + +@implementation TGBridgeRemoteSignals + ++ (SSignal *)openRemoteMessageWithPeerId:(int64_t)peerId messageId:(int32_t)messageId type:(int32_t)type autoPlay:(bool)autoPlay +{ + autoPlay = false; + return [[TGBridgeClient instance] requestSignalWithSubscription:[[TGBridgeRemoteSubscription alloc] initWithPeerId:peerId messageId:messageId type:type autoPlay:autoPlay]]; +} + +@end diff --git a/Watch/Bridge/TGBridgeReplyMarkupMediaAttachment.h b/Watch/Bridge/TGBridgeReplyMarkupMediaAttachment.h new file mode 100644 index 0000000000..c0114eb3fa --- /dev/null +++ b/Watch/Bridge/TGBridgeReplyMarkupMediaAttachment.h @@ -0,0 +1,9 @@ +#import "TGBridgeMediaAttachment.h" + +@class TGBridgeBotReplyMarkup; + +@interface TGBridgeReplyMarkupMediaAttachment : TGBridgeMediaAttachment + +@property (nonatomic, strong) TGBridgeBotReplyMarkup *replyMarkup; + +@end diff --git a/Watch/Bridge/TGBridgeReplyMarkupMediaAttachment.m b/Watch/Bridge/TGBridgeReplyMarkupMediaAttachment.m new file mode 100644 index 0000000000..1299c900cd --- /dev/null +++ b/Watch/Bridge/TGBridgeReplyMarkupMediaAttachment.m @@ -0,0 +1,29 @@ +#import "TGBridgeReplyMarkupMediaAttachment.h" + +const NSInteger TGBridgeReplyMarkupMediaAttachmentType = 0x5678acc1; + +NSString *const TGBridgeReplyMarkupMediaMessageKey = @"replyMarkup"; + +@implementation TGBridgeReplyMarkupMediaAttachment + +- (instancetype)initWithCoder:(NSCoder *)aDecoder +{ + self = [super init]; + if (self != nil) + { + _replyMarkup = [aDecoder decodeObjectForKey:TGBridgeReplyMarkupMediaMessageKey]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder +{ + [aCoder encodeObject:self.replyMarkup forKey:TGBridgeReplyMarkupMediaMessageKey]; +} + ++ (NSInteger)mediaType +{ + return TGBridgeReplyMarkupMediaAttachmentType; +} + +@end \ No newline at end of file diff --git a/Watch/Bridge/TGBridgeReplyMessageMediaAttachment.h b/Watch/Bridge/TGBridgeReplyMessageMediaAttachment.h new file mode 100644 index 0000000000..5c2cd80f08 --- /dev/null +++ b/Watch/Bridge/TGBridgeReplyMessageMediaAttachment.h @@ -0,0 +1,10 @@ +#import "TGBridgeMediaAttachment.h" + +@class TGBridgeMessage; + +@interface TGBridgeReplyMessageMediaAttachment : TGBridgeMediaAttachment + +@property (nonatomic, assign) int32_t mid; +@property (nonatomic, strong) TGBridgeMessage *message; + +@end diff --git a/Watch/Bridge/TGBridgeReplyMessageMediaAttachment.m b/Watch/Bridge/TGBridgeReplyMessageMediaAttachment.m new file mode 100644 index 0000000000..fbc2456919 --- /dev/null +++ b/Watch/Bridge/TGBridgeReplyMessageMediaAttachment.m @@ -0,0 +1,33 @@ +#import "TGBridgeReplyMessageMediaAttachment.h" +#import "TGBridgeMessage.h" + +const NSInteger TGBridgeReplyMessageMediaAttachmentType = 414002169; + +NSString *const TGBridgeReplyMessageMediaMidKey = @"mid"; +NSString *const TGBridgeReplyMessageMediaMessageKey = @"message"; + +@implementation TGBridgeReplyMessageMediaAttachment + +- (instancetype)initWithCoder:(NSCoder *)aDecoder +{ + self = [super init]; + if (self != nil) + { + _mid = [aDecoder decodeInt32ForKey:TGBridgeReplyMessageMediaMidKey]; + _message = [aDecoder decodeObjectForKey:TGBridgeReplyMessageMediaMessageKey]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder +{ + [aCoder encodeInt32:self.mid forKey:TGBridgeReplyMessageMediaMidKey]; + [aCoder encodeObject:self.message forKey:TGBridgeReplyMessageMediaMessageKey]; +} + ++ (NSInteger)mediaType +{ + return TGBridgeReplyMessageMediaAttachmentType; +} + +@end diff --git a/Watch/Bridge/TGBridgeSendMessageSignals.h b/Watch/Bridge/TGBridgeSendMessageSignals.h new file mode 100644 index 0000000000..81eda0f5b6 --- /dev/null +++ b/Watch/Bridge/TGBridgeSendMessageSignals.h @@ -0,0 +1,16 @@ +#import + +#import "TGBridgeDocumentMediaAttachment.h" +#import "TGBridgeLocationMediaAttachment.h" + +@interface TGBridgeSendMessageSignals : NSObject + ++ (SSignal *)sendMessageWithPeerId:(int64_t)peerId text:(NSString *)text replyToMid:(int32_t)replyToMid; + ++ (SSignal *)sendMessageWithPeerId:(int64_t)peerId location:(TGBridgeLocationMediaAttachment *)location replyToMid:(int32_t)replyToMid; + ++ (SSignal *)sendMessageWithPeerId:(int64_t)peerId sticker:(TGBridgeDocumentMediaAttachment *)sticker replyToMid:(int32_t)replyToMid; + ++ (SSignal *)forwardMessageWithPeerId:(int64_t)peerId mid:(int32_t)mid targetPeerId:(int64_t)targetPeerId; + +@end diff --git a/Watch/Bridge/TGBridgeSendMessageSignals.m b/Watch/Bridge/TGBridgeSendMessageSignals.m new file mode 100644 index 0000000000..4a7bfa666d --- /dev/null +++ b/Watch/Bridge/TGBridgeSendMessageSignals.m @@ -0,0 +1,28 @@ +#import "TGBridgeSendMessageSignals.h" +#import "TGBridgeSubscriptions.h" +#import "TGBridgeMessage.h" +#import "TGBridgeClient.h" + +@implementation TGBridgeSendMessageSignals + ++ (SSignal *)sendMessageWithPeerId:(int64_t)peerId text:(NSString *)text replyToMid:(int32_t)replyToMid +{ + return [[TGBridgeClient instance] requestSignalWithSubscription:[[TGBridgeSendTextMessageSubscription alloc] initWithPeerId:peerId text:text replyToMid:replyToMid]]; +} + ++ (SSignal *)sendMessageWithPeerId:(int64_t)peerId location:(TGBridgeLocationMediaAttachment *)location replyToMid:(int32_t)replyToMid +{ + return [[TGBridgeClient instance] requestSignalWithSubscription:[[TGBridgeSendLocationMessageSubscription alloc] initWithPeerId:peerId location:location replyToMid:replyToMid]]; +} + ++ (SSignal *)sendMessageWithPeerId:(int64_t)peerId sticker:(TGBridgeDocumentMediaAttachment *)sticker replyToMid:(int32_t)replyToMid +{ + return [[TGBridgeClient instance] requestSignalWithSubscription:[[TGBridgeSendStickerMessageSubscription alloc] initWithPeerId:peerId document:sticker replyToMid:replyToMid]]; +} + ++ (SSignal *)forwardMessageWithPeerId:(int64_t)peerId mid:(int32_t)mid targetPeerId:(int64_t)targetPeerId +{ + return [[TGBridgeClient instance] requestSignalWithSubscription:[[TGBridgeSendForwardedMessageSubscription alloc] initWithPeerId:peerId messageId:mid targetPeerId:targetPeerId]]; +} + +@end diff --git a/Watch/Bridge/TGBridgeStateSignal.h b/Watch/Bridge/TGBridgeStateSignal.h new file mode 100644 index 0000000000..e76c94ba24 --- /dev/null +++ b/Watch/Bridge/TGBridgeStateSignal.h @@ -0,0 +1,15 @@ +#import + +typedef enum +{ + TGBridgeSynchronizationStateSynchronized, + TGBridgeSynchronizationStateWaitingForNetwork, + TGBridgeSynchronizationStateConnecting, + TGBridgeSynchronizationStateUpdating +} TGBridgeSynchronizationStateValue; + +@interface TGBridgeStateSignal : NSObject + ++ (SSignal *)synchronizationState; + +@end diff --git a/Watch/Bridge/TGBridgeStateSignal.m b/Watch/Bridge/TGBridgeStateSignal.m new file mode 100644 index 0000000000..915e7d0a43 --- /dev/null +++ b/Watch/Bridge/TGBridgeStateSignal.m @@ -0,0 +1,18 @@ +#import "TGBridgeStateSignal.h" +#import "TGBridgeSubscriptions.h" +#import "TGBridgeClient.h" + +@implementation TGBridgeStateSignal + ++ (SSignal *)synchronizationState +{ + return [[[TGBridgeClient instance] requestSignalWithSubscription:[[TGBridgeStateSubscription alloc] init]] map:^NSNumber *(id next) + { + if ([next isKindOfClass:[NSNumber class]]) + return next; + + return @(TGBridgeSynchronizationStateSynchronized); + }]; +} + +@end diff --git a/Watch/Bridge/TGBridgeStickerPack+TGStickerPack.h b/Watch/Bridge/TGBridgeStickerPack+TGStickerPack.h new file mode 100644 index 0000000000..1922da7917 --- /dev/null +++ b/Watch/Bridge/TGBridgeStickerPack+TGStickerPack.h @@ -0,0 +1,9 @@ +#import "TGBridgeStickerPack.h" + +@class TGStickerPack; + +@interface TGBridgeStickerPack (TGStickerPack) + ++ (TGBridgeStickerPack *)stickerPackWithTGStickerPack:(TGStickerPack *)stickerPack; + +@end diff --git a/Watch/Bridge/TGBridgeStickerPack+TGStickerPack.m b/Watch/Bridge/TGBridgeStickerPack+TGStickerPack.m new file mode 100644 index 0000000000..219e0cd78d --- /dev/null +++ b/Watch/Bridge/TGBridgeStickerPack+TGStickerPack.m @@ -0,0 +1,26 @@ +#import "TGBridgeStickerPack+TGStickerPack.h" +#import +#import "TGBridgeDocumentMediaAttachment+TGDocumentMediaAttachment.h" + +@implementation TGBridgeStickerPack (TGStickerPack) + ++ (TGBridgeStickerPack *)stickerPackWithTGStickerPack:(TGStickerPack *)stickerPack +{ + TGBridgeStickerPack *bridgeStickerPack = [[TGBridgeStickerPack alloc] init]; + bridgeStickerPack->_builtIn = [stickerPack.packReference isKindOfClass:[TGStickerPackBuiltinReference class]]; + bridgeStickerPack->_title = stickerPack.title; + + NSMutableArray *bridgeDocuments = [[NSMutableArray alloc] init]; + for (TGDocumentMediaAttachment *document in stickerPack.documents) + { + TGBridgeDocumentMediaAttachment *bridgeDocument = [TGBridgeDocumentMediaAttachment attachmentWithTGDocumentMediaAttachment:document]; + if (bridgeDocument != nil) + [bridgeDocuments addObject:bridgeDocument]; + } + + bridgeStickerPack->_documents = bridgeDocuments; + + return bridgeStickerPack; +} + +@end diff --git a/Watch/Bridge/TGBridgeStickerPack.h b/Watch/Bridge/TGBridgeStickerPack.h new file mode 100644 index 0000000000..e37f8b26cc --- /dev/null +++ b/Watch/Bridge/TGBridgeStickerPack.h @@ -0,0 +1,14 @@ +#import "TGBridgeCommon.h" + +@interface TGBridgeStickerPack : NSObject +{ + bool _builtIn; + NSString *_title; + NSArray *_documents; +} + +@property (nonatomic, readonly, getter=isBuiltIn) bool builtIn; +@property (nonatomic, readonly) NSString *title; +@property (nonatomic, readonly) NSArray *documents; + +@end diff --git a/Watch/Bridge/TGBridgeStickerPack.m b/Watch/Bridge/TGBridgeStickerPack.m new file mode 100644 index 0000000000..da2711aec8 --- /dev/null +++ b/Watch/Bridge/TGBridgeStickerPack.m @@ -0,0 +1,29 @@ +#import "TGBridgeStickerPack.h" +#import "TGBridgeDocumentMediaAttachment.h" + +NSString *const TGBridgeStickerPackBuiltInKey = @"builtin"; +NSString *const TGBridgeStickerPackTitleKey = @"title"; +NSString *const TGBridgeStickerPackDocumentsKey = @"documents"; + +@implementation TGBridgeStickerPack + +- (instancetype)initWithCoder:(NSCoder *)aDecoder +{ + self = [super init]; + if (self != nil) + { + _builtIn = [aDecoder decodeBoolForKey:TGBridgeStickerPackBuiltInKey]; + _title = [aDecoder decodeObjectForKey:TGBridgeStickerPackTitleKey]; + _documents = [aDecoder decodeObjectForKey:TGBridgeStickerPackDocumentsKey]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder +{ + [aCoder encodeBool:self.builtIn forKey:TGBridgeStickerPackBuiltInKey]; + [aCoder encodeObject:self.title forKey:TGBridgeStickerPackTitleKey]; + [aCoder encodeObject:self.documents forKey:TGBridgeStickerPackDocumentsKey]; +} + +@end diff --git a/Watch/Bridge/TGBridgeStickersSignals.h b/Watch/Bridge/TGBridgeStickersSignals.h new file mode 100644 index 0000000000..3b549b2aa8 --- /dev/null +++ b/Watch/Bridge/TGBridgeStickersSignals.h @@ -0,0 +1,10 @@ +#import + +@interface TGBridgeStickersSignals : NSObject + ++ (SSignal *)recentStickersWithLimit:(NSUInteger)limit; ++ (SSignal *)stickerPacks; + ++ (NSURL *)stickerPacksURL; + +@end diff --git a/Watch/Bridge/TGBridgeStickersSignals.m b/Watch/Bridge/TGBridgeStickersSignals.m new file mode 100644 index 0000000000..dd2e3afde3 --- /dev/null +++ b/Watch/Bridge/TGBridgeStickersSignals.m @@ -0,0 +1,49 @@ +#import "TGBridgeStickersSignals.h" +#import "TGBridgeSubscriptions.h" +#import "TGBridgeStickerPack.h" +#import "TGBridgeDocumentMediaAttachment.h" +#import "TGBridgeClient.h" + +@implementation TGBridgeStickersSignals + +static NSArray *cachedStickers = nil; + ++ (SSignal *)cachedRecentStickers +{ + return [SSignal single:cachedStickers]; +} + ++ (SSignal *)recentStickersWithLimit:(NSUInteger)limit +{ + return [[TGBridgeClient instance] requestSignalWithSubscription:[[TGBridgeRecentStickersSubscription alloc] initWithLimit:limit]]; +// return [[self cachedRecentStickers] mapToSignal:^SSignal *(NSArray *stickers) { +// SSignal *remote = [[TGBridgeClient instance] requestSignalWithSubscription:[[TGBridgeRecentStickersSubscription alloc] initWithLimit:limit]]; +// remote = [remote onNext:^(NSArray *stickers) { +// cachedStickers = stickers; +// }]; +// if (stickers != nil) { +// return [[SSignal single:stickers] then:remote]; +// } else { +// return remote; +// } +// }]; +} + ++ (SSignal *)stickerPacks +{ + return [[SSignal single:[[TGBridgeClient instance] stickerPacks]] then:[[TGBridgeClient instance] fileSignalForKey:@"stickers"]]; +} + ++ (NSURL *)stickerPacksURL +{ + static dispatch_once_t onceToken; + static NSURL *stickerPacksURL; + dispatch_once(&onceToken, ^ + { + NSString *documentsPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, true)[0]; + stickerPacksURL = [[NSURL alloc] initFileURLWithPath:[documentsPath stringByAppendingPathComponent:@"stickers.data"]]; + }); + return stickerPacksURL; +} + +@end diff --git a/Watch/Bridge/TGBridgeSubscriptions.h b/Watch/Bridge/TGBridgeSubscriptions.h new file mode 100644 index 0000000000..82f179715b --- /dev/null +++ b/Watch/Bridge/TGBridgeSubscriptions.h @@ -0,0 +1,268 @@ +#import "TGBridgeCommon.h" + +#import +#import + +@class TGBridgeMediaAttachment; +@class TGBridgeImageMediaAttachment; +@class TGBridgeVideoMediaAttachment; +@class TGBridgeDocumentMediaAttachment; +@class TGBridgeLocationMediaAttachment; +@class TGBridgePeerNotificationSettings; + +@interface TGBridgeAudioSubscription : TGBridgeSubscription + +@property (nonatomic, readonly) TGBridgeMediaAttachment *attachment; +@property (nonatomic, readonly) int64_t peerId; +@property (nonatomic, readonly) int32_t messageId; + +- (instancetype)initWithAttachment:(TGBridgeMediaAttachment *)attachment peerId:(int64_t)peerId messageId:(int32_t)messageId; + +@end + + +@interface TGBridgeAudioSentSubscription : TGBridgeSubscription + +@property (nonatomic, readonly) int64_t conversationId; + +- (instancetype)initWithConversationId:(int64_t)conversationId; + +@end + + +@interface TGBridgeChatListSubscription : TGBridgeSubscription + +@property (nonatomic, readonly) int32_t limit; + +- (instancetype)initWithLimit:(int32_t)limit; + +@end + + +@interface TGBridgeChatMessageListSubscription : TGBridgeSubscription + +@property (nonatomic, readonly) int64_t peerId; +@property (nonatomic, readonly) int32_t atMessageId; +@property (nonatomic, readonly) NSUInteger rangeMessageCount; + +- (instancetype)initWithPeerId:(int64_t)peerId atMessageId:(int32_t)messageId rangeMessageCount:(NSUInteger)rangeMessageCount; + +@end + + +@interface TGBridgeChatMessageSubscription : TGBridgeSubscription + +@property (nonatomic, readonly) int64_t peerId; +@property (nonatomic, readonly) int32_t messageId; + +- (instancetype)initWithPeerId:(int64_t)peerId messageId:(int32_t)messageId; + +@end + + +@interface TGBridgeReadChatMessageListSubscription : TGBridgeSubscription + +@property (nonatomic, readonly) int64_t peerId; +@property (nonatomic, readonly) int32_t messageId; + +- (instancetype)initWithPeerId:(int64_t)peerId messageId:(int32_t)messageId; + +@end + + +@interface TGBridgeContactsSubscription : TGBridgeSubscription + +@property (nonatomic, readonly) NSString *query; + +- (instancetype)initWithQuery:(NSString *)query; + +@end + + +@interface TGBridgeConversationSubscription : TGBridgeSubscription + +@property (nonatomic, readonly) int64_t peerId; + +- (instancetype)initWithPeerId:(int64_t)peerId; + +@end + + +@interface TGBridgeNearbyVenuesSubscription : TGBridgeSubscription + +@property (nonatomic, readonly) CLLocationCoordinate2D coordinate; +@property (nonatomic, readonly) int32_t limit; + +- (instancetype)initWithCoordinate:(CLLocationCoordinate2D)coordinate limit:(int32_t)limit; + +@end + + +@interface TGBridgeMediaThumbnailSubscription : TGBridgeSubscription + +@property (nonatomic, readonly) int64_t peerId; +@property (nonatomic, readonly) int32_t messageId; +@property (nonatomic, readonly) CGSize size; +@property (nonatomic, readonly) bool notification; + +- (instancetype)initWithPeerId:(int64_t)peerId messageId:(int32_t)messageId size:(CGSize)size notification:(bool)notification; + +@end + + +typedef NS_ENUM(NSUInteger, TGBridgeMediaAvatarType) { + TGBridgeMediaAvatarTypeSmall, + TGBridgeMediaAvatarTypeProfile, + TGBridgeMediaAvatarTypeLarge +}; + +@interface TGBridgeMediaAvatarSubscription : TGBridgeSubscription + +@property (nonatomic, readonly) int64_t peerId; +@property (nonatomic, readonly) NSString *url; +@property (nonatomic, readonly) TGBridgeMediaAvatarType type; + +- (instancetype)initWithPeerId:(int64_t)peerId url:(NSString *)url type:(TGBridgeMediaAvatarType)type; + +@end + +@interface TGBridgeMediaStickerSubscription : TGBridgeSubscription + +@property (nonatomic, readonly) int64_t documentId; +@property (nonatomic, readonly) int64_t stickerPackId; +@property (nonatomic, readonly) int64_t stickerPackAccessHash; +@property (nonatomic, readonly) int64_t stickerPeerId; +@property (nonatomic, readonly) int32_t stickerMessageId; +@property (nonatomic, readonly) bool notification; +@property (nonatomic, readonly) CGSize size; + +- (instancetype)initWithDocumentId:(int64_t)documentId stickerPackId:(int64_t)stickerPackId stickerPackAccessHash:(int64_t)stickerPackAccessHash stickerPeerId:(int64_t)stickerPeerId stickerMessageId:(int32_t)stickerMessageId notification:(bool)notification size:(CGSize)size; + +@end + + +@interface TGBridgePeerSettingsSubscription : TGBridgeSubscription + +@property (nonatomic, readonly) int64_t peerId; + +- (instancetype)initWithPeerId:(int64_t)peerId; + +@end + +@interface TGBridgePeerUpdateNotificationSettingsSubscription : TGBridgeSubscription + +@property (nonatomic, readonly) int64_t peerId; + +- (instancetype)initWithPeerId:(int64_t)peerId; + +@end + +@interface TGBridgePeerUpdateBlockStatusSubscription : TGBridgeSubscription + +@property (nonatomic, readonly) int64_t peerId; +@property (nonatomic, readonly) bool blocked; + +- (instancetype)initWithPeerId:(int64_t)peerId blocked:(bool)blocked; + +@end + + +@interface TGBridgeRemoteSubscription : TGBridgeSubscription + +@property (nonatomic, readonly) int64_t peerId; +@property (nonatomic, readonly) int32_t messageId; +@property (nonatomic, readonly) int32_t type; +@property (nonatomic, readonly) bool autoPlay; + +- (instancetype)initWithPeerId:(int64_t)peerId messageId:(int32_t)messageId type:(int32_t)type autoPlay:(bool)autoPlay; + +@end + + +@interface TGBridgeSendTextMessageSubscription : TGBridgeSubscription + +@property (nonatomic, readonly) int64_t peerId; +@property (nonatomic, readonly) NSString *text; +@property (nonatomic, readonly) int32_t replyToMid; + +- (instancetype)initWithPeerId:(int64_t)peerId text:(NSString *)text replyToMid:(int32_t)replyToMid; + +@end + + +@interface TGBridgeSendStickerMessageSubscription : TGBridgeSubscription + +@property (nonatomic, readonly) int64_t peerId; +@property (nonatomic, readonly) TGBridgeDocumentMediaAttachment *document; +@property (nonatomic, readonly) int32_t replyToMid; + +- (instancetype)initWithPeerId:(int64_t)peerId document:(TGBridgeDocumentMediaAttachment *)document replyToMid:(int32_t)replyToMid; + +@end + + +@interface TGBridgeSendLocationMessageSubscription : TGBridgeSubscription + +@property (nonatomic, readonly) int64_t peerId; +@property (nonatomic, readonly) TGBridgeLocationMediaAttachment *location; +@property (nonatomic, readonly) int32_t replyToMid; + +- (instancetype)initWithPeerId:(int64_t)peerId location:(TGBridgeLocationMediaAttachment *)location replyToMid:(int32_t)replyToMid; + +@end + + +@interface TGBridgeSendForwardedMessageSubscription : TGBridgeSubscription + +@property (nonatomic, readonly) int64_t peerId; +@property (nonatomic, readonly) int32_t messageId; +@property (nonatomic, readonly) int64_t targetPeerId; + +- (instancetype)initWithPeerId:(int64_t)peerId messageId:(int32_t)messageId targetPeerId:(int64_t)targetPeerId; + +@end + + +@interface TGBridgeStateSubscription : TGBridgeSubscription + +@end + + +@interface TGBridgeStickerPacksSubscription : TGBridgeSubscription + +@end + + +@interface TGBridgeRecentStickersSubscription : TGBridgeSubscription + +@property (nonatomic, readonly) int32_t limit; + +- (instancetype)initWithLimit:(int32_t)limit; + +@end + + +@interface TGBridgeUserInfoSubscription : TGBridgeSubscription + +@property (nonatomic, readonly) NSArray *userIds; + +- (instancetype)initWithUserIds:(NSArray *)userIds; + +@end + + +@interface TGBridgeUserBotInfoSubscription : TGBridgeSubscription + +@property (nonatomic, readonly) NSArray *userIds; + +- (instancetype)initWithUserIds:(NSArray *)userIds; + +@end + +@interface TGBridgeBotReplyMarkupSubscription : TGBridgeSubscription + +@property (nonatomic, readonly) int64_t peerId; + +- (instancetype)initWithPeerId:(int64_t)peerId; + +@end diff --git a/Watch/Bridge/TGBridgeSubscriptions.m b/Watch/Bridge/TGBridgeSubscriptions.m new file mode 100644 index 0000000000..8c4b50d224 --- /dev/null +++ b/Watch/Bridge/TGBridgeSubscriptions.m @@ -0,0 +1,1048 @@ +#import "TGBridgeSubscriptions.h" + +#import + +#import "TGBridgeImageMediaAttachment.h" +#import "TGBridgeVideoMediaAttachment.h" +#import "TGBridgeDocumentMediaAttachment.h" +#import "TGBridgeLocationMediaAttachment.h" +#import "TGBridgePeerNotificationSettings.h" + +NSString *const TGBridgeAudioSubscriptionName = @"media.audio"; +NSString *const TGBridgeAudioSubscriptionAttachmentKey = @"attachment"; +NSString *const TGBridgeAudioSubscriptionPeerIdKey = @"peerId"; +NSString *const TGBridgeAudioSubscriptionMessageIdKey = @"messageId"; + +@implementation TGBridgeAudioSubscription + +- (instancetype)initWithAttachment:(TGBridgeMediaAttachment *)attachment peerId:(int64_t)peerId messageId:(int32_t)messageId +{ + self = [super init]; + if (self != nil) + { + _attachment = attachment; + _peerId = peerId; + _messageId = messageId; + } + return self; +} + +- (void)_serializeParametersWithCoder:(NSCoder *)aCoder +{ + [aCoder encodeObject:self.attachment forKey:TGBridgeAudioSubscriptionAttachmentKey]; + [aCoder encodeInt64:self.peerId forKey:TGBridgeAudioSubscriptionPeerIdKey]; + [aCoder encodeInt32:self.messageId forKey:TGBridgeAudioSubscriptionMessageIdKey]; +} + +- (void)_unserializeParametersWithCoder:(NSCoder *)aDecoder +{ + _attachment = [aDecoder decodeObjectForKey:TGBridgeAudioSubscriptionAttachmentKey]; + _peerId = [aDecoder decodeInt64ForKey:TGBridgeAudioSubscriptionPeerIdKey]; + _messageId = [aDecoder decodeInt32ForKey:TGBridgeAudioSubscriptionMessageIdKey]; +} + ++ (NSString *)subscriptionName +{ + return TGBridgeAudioSubscriptionName; +} + +@end + + +NSString *const TGBridgeAudioSentSubscriptionName = @"media.audioSent"; +NSString *const TGBridgeAudioSentSubscriptionConversationIdKey = @"conversationId"; + +@implementation TGBridgeAudioSentSubscription + +- (instancetype)initWithConversationId:(int64_t)conversationId +{ + self = [super init]; + if (self != nil) + { + _conversationId = conversationId; + } + return self; +} + +- (void)_serializeParametersWithCoder:(NSCoder *)aCoder +{ + [aCoder encodeInt64:self.conversationId forKey:TGBridgeAudioSentSubscriptionConversationIdKey]; +} + +- (void)_unserializeParametersWithCoder:(NSCoder *)aDecoder +{ + _conversationId = [aDecoder decodeInt64ForKey:TGBridgeAudioSentSubscriptionConversationIdKey]; +} + ++ (NSString *)subscriptionName +{ + return TGBridgeAudioSentSubscriptionName; +} + +@end + + +NSString *const TGBridgeChatListSubscriptionName = @"chats.chatList"; +NSString *const TGBridgeChatListSubscriptionLimitKey = @"limit"; + +@implementation TGBridgeChatListSubscription + +- (instancetype)initWithLimit:(int32_t)limit +{ + self = [super init]; + if (self != nil) + { + _limit = limit; + } + return self; +} + +- (bool)dropPreviouslyQueued +{ + return true; +} + +- (void)_serializeParametersWithCoder:(NSCoder *)aCoder +{ + [aCoder encodeInt32:self.limit forKey:TGBridgeChatListSubscriptionLimitKey]; +} + +- (void)_unserializeParametersWithCoder:(NSCoder *)aDecoder +{ + _limit = [aDecoder decodeInt32ForKey:TGBridgeChatListSubscriptionLimitKey]; +} + ++ (NSString *)subscriptionName +{ + return TGBridgeChatListSubscriptionName; +} + +@end + + +NSString *const TGBridgeChatMessageListSubscriptionName = @"chats.chatMessageList"; +NSString *const TGBridgeChatMessageListSubscriptionPeerIdKey = @"peerId"; +NSString *const TGBridgeChatMessageListSubscriptionAtMessageIdKey = @"atMessageId"; +NSString *const TGBridgeChatMessageListSubscriptionRangeMessageCountKey = @"rangeMessageCount"; + +@implementation TGBridgeChatMessageListSubscription + +- (instancetype)initWithPeerId:(int64_t)peerId atMessageId:(int32_t)messageId rangeMessageCount:(NSUInteger)rangeMessageCount +{ + self = [super init]; + if (self != nil) + { + _peerId = peerId; + _atMessageId = messageId; + _rangeMessageCount = rangeMessageCount; + } + return self; +} + +- (bool)dropPreviouslyQueued +{ + return true; +} + +- (void)_serializeParametersWithCoder:(NSCoder *)aCoder +{ + [aCoder encodeInt64:self.peerId forKey:TGBridgeChatMessageListSubscriptionPeerIdKey]; + [aCoder encodeInt32:self.atMessageId forKey:TGBridgeChatMessageListSubscriptionAtMessageIdKey]; + [aCoder encodeInt32:(int32_t)self.rangeMessageCount forKey:TGBridgeChatMessageListSubscriptionRangeMessageCountKey]; +} + +- (void)_unserializeParametersWithCoder:(NSCoder *)aDecoder +{ + _peerId = [aDecoder decodeInt64ForKey:TGBridgeChatMessageListSubscriptionPeerIdKey]; + _atMessageId = [aDecoder decodeInt32ForKey:TGBridgeChatMessageListSubscriptionAtMessageIdKey]; + _rangeMessageCount = [aDecoder decodeInt32ForKey:TGBridgeChatMessageListSubscriptionRangeMessageCountKey]; +} + ++ (NSString *)subscriptionName +{ + return TGBridgeChatMessageListSubscriptionName; +} + +@end + + +NSString *const TGBridgeChatMessageSubscriptionName = @"chats.message"; +NSString *const TGBridgeChatMessageSubscriptionPeerIdKey = @"peerId"; +NSString *const TGBridgeChatMessageSubscriptionMessageIdKey = @"mid"; + +@implementation TGBridgeChatMessageSubscription + +- (instancetype)initWithPeerId:(int64_t)peerId messageId:(int32_t)messageId +{ + self = [super init]; + if (self != nil) + { + _peerId = peerId; + _messageId = messageId; + } + return self; +} + +- (bool)synchronous +{ + return true; +} + +- (bool)renewable +{ + return false; +} + +- (void)_serializeParametersWithCoder:(NSCoder *)aCoder +{ + [aCoder encodeInt64:self.peerId forKey:TGBridgeChatMessageSubscriptionPeerIdKey]; + [aCoder encodeInt32:self.messageId forKey:TGBridgeChatMessageSubscriptionMessageIdKey]; +} + +- (void)_unserializeParametersWithCoder:(NSCoder *)aDecoder +{ + _peerId = [aDecoder decodeInt64ForKey:TGBridgeChatMessageSubscriptionPeerIdKey]; + _messageId = [aDecoder decodeInt32ForKey:TGBridgeChatMessageSubscriptionMessageIdKey]; +} + ++ (NSString *)subscriptionName +{ + return TGBridgeChatMessageSubscriptionName; +} + +@end + + +NSString *const TGBridgeReadChatMessageListSubscriptionName = @"chats.readChatMessageList"; +NSString *const TGBridgeReadChatMessageListSubscriptionPeerIdKey = @"peerId"; +NSString *const TGBridgeReadChatMessageListSubscriptionMessageIdKey = @"mid"; + +@implementation TGBridgeReadChatMessageListSubscription + +- (instancetype)initWithPeerId:(int64_t)peerId messageId:(int32_t)messageId +{ + self = [super init]; + if (self != nil) + { + _peerId = peerId; + _messageId = messageId; + } + return self; +} + +- (void)_serializeParametersWithCoder:(NSCoder *)aCoder +{ + [aCoder encodeInt64:self.peerId forKey:TGBridgeReadChatMessageListSubscriptionPeerIdKey]; + [aCoder encodeInt32:self.messageId forKey:TGBridgeReadChatMessageListSubscriptionMessageIdKey]; +} + +- (void)_unserializeParametersWithCoder:(NSCoder *)aDecoder +{ + _peerId = [aDecoder decodeInt64ForKey:TGBridgeReadChatMessageListSubscriptionPeerIdKey]; + _messageId = [aDecoder decodeInt32ForKey:TGBridgeReadChatMessageListSubscriptionMessageIdKey]; +} + ++ (NSString *)subscriptionName +{ + return TGBridgeReadChatMessageListSubscriptionName; +} + +@end + + +NSString *const TGBridgeContactsSubscriptionName = @"contacts.search"; +NSString *const TGBridgeContactsSubscriptionQueryKey = @"query"; + +@implementation TGBridgeContactsSubscription + +- (instancetype)initWithQuery:(NSString *)query +{ + self = [super init]; + if (self != nil) + { + _query = query; + } + return self; +} + +- (void)_serializeParametersWithCoder:(NSCoder *)aCoder +{ + [aCoder encodeObject:self.query forKey:TGBridgeContactsSubscriptionQueryKey]; +} + +- (void)_unserializeParametersWithCoder:(NSCoder *)aDecoder +{ + _query = [aDecoder decodeObjectForKey:TGBridgeContactsSubscriptionQueryKey]; +} + ++ (NSString *)subscriptionName +{ + return TGBridgeContactsSubscriptionName; +} + +@end + + +NSString *const TGBridgeConversationSubscriptionName = @"chats.conversation"; +NSString *const TGBridgeConversationSubscriptionPeerIdKey = @"peerId"; + +@implementation TGBridgeConversationSubscription + +- (instancetype)initWithPeerId:(int64_t)peerId +{ + self = [super init]; + if (self != nil) + { + _peerId = peerId; + } + return self; +} + +- (bool)dropPreviouslyQueued +{ + return true; +} + +- (void)_serializeParametersWithCoder:(NSCoder *)aCoder +{ + [aCoder encodeInt64:self.peerId forKey:TGBridgeConversationSubscriptionPeerIdKey]; +} + +- (void)_unserializeParametersWithCoder:(NSCoder *)aDecoder +{ + _peerId = [aDecoder decodeInt64ForKey:TGBridgeConversationSubscriptionPeerIdKey]; +} + ++ (NSString *)subscriptionName +{ + return TGBridgeConversationSubscriptionName; +} + +@end + + +NSString *const TGBridgeNearbyVenuesSubscriptionName = @"location.nearbyVenues"; +NSString *const TGBridgeNearbyVenuesSubscriptionLatitudeKey = @"lat"; +NSString *const TGBridgeNearbyVenuesSubscriptionLongitudeKey = @"lon"; +NSString *const TGBridgeNearbyVenuesSubscriptionLimitKey = @"limit"; + +@implementation TGBridgeNearbyVenuesSubscription + +- (instancetype)initWithCoordinate:(CLLocationCoordinate2D)coordinate limit:(int32_t)limit +{ + self = [super init]; + if (self != nil) + { + _coordinate = coordinate; + _limit = limit; + } + return self; +} + +- (void)_serializeParametersWithCoder:(NSCoder *)aCoder +{ + [aCoder encodeDouble:self.coordinate.latitude forKey:TGBridgeNearbyVenuesSubscriptionLatitudeKey]; + [aCoder encodeDouble:self.coordinate.longitude forKey:TGBridgeNearbyVenuesSubscriptionLongitudeKey]; + [aCoder encodeInt32:self.limit forKey:TGBridgeNearbyVenuesSubscriptionLimitKey]; +} + +- (void)_unserializeParametersWithCoder:(NSCoder *)aDecoder +{ + _coordinate = CLLocationCoordinate2DMake([aDecoder decodeDoubleForKey:TGBridgeNearbyVenuesSubscriptionLatitudeKey], + [aDecoder decodeDoubleForKey:TGBridgeNearbyVenuesSubscriptionLongitudeKey]); + _limit = [aDecoder decodeInt32ForKey:TGBridgeNearbyVenuesSubscriptionLimitKey]; +} + ++ (NSString *)subscriptionName +{ + return TGBridgeNearbyVenuesSubscriptionName; +} + +@end + + +NSString *const TGBridgeMediaThumbnailSubscriptionName = @"media.thumbnail"; +NSString *const TGBridgeMediaThumbnailPeerIdKey = @"peerId"; +NSString *const TGBridgeMediaThumbnailMessageIdKey = @"mid"; +NSString *const TGBridgeMediaThumbnailSizeKey = @"size"; +NSString *const TGBridgeMediaThumbnailNotificationKey = @"notification"; + +@implementation TGBridgeMediaThumbnailSubscription + +- (instancetype)initWithPeerId:(int64_t)peerId messageId:(int32_t)messageId size:(CGSize)size notification:(bool)notification +{ + self = [super init]; + if (self != nil) + { + _peerId = peerId; + _messageId = messageId; + _size = size; + _notification = notification; + } + return self; +} + +- (bool)renewable +{ + return false; +} + +- (void)_serializeParametersWithCoder:(NSCoder *)aCoder +{ + [aCoder encodeInt64:self.peerId forKey:TGBridgeMediaThumbnailPeerIdKey]; + [aCoder encodeInt32:self.messageId forKey:TGBridgeMediaThumbnailMessageIdKey]; + [aCoder encodeCGSize:self.size forKey:TGBridgeMediaThumbnailSizeKey]; + [aCoder encodeBool:self.notification forKey:TGBridgeMediaThumbnailNotificationKey]; +} + +- (void)_unserializeParametersWithCoder:(NSCoder *)aDecoder +{ + _peerId = [aDecoder decodeInt64ForKey:TGBridgeMediaThumbnailPeerIdKey]; + _messageId = [aDecoder decodeInt32ForKey:TGBridgeMediaThumbnailMessageIdKey]; + _size = [aDecoder decodeCGSizeForKey:TGBridgeMediaThumbnailSizeKey]; + _notification = [aDecoder decodeBoolForKey:TGBridgeMediaThumbnailNotificationKey]; +} + ++ (NSString *)subscriptionName +{ + return TGBridgeMediaThumbnailSubscriptionName; +} + +@end + + +NSString *const TGBridgeMediaAvatarSubscriptionName = @"media.avatar"; +NSString *const TGBridgeMediaAvatarPeerIdKey = @"peerId"; +NSString *const TGBridgeMediaAvatarUrlKey = @"url"; +NSString *const TGBridgeMediaAvatarTypeKey = @"type"; + +@implementation TGBridgeMediaAvatarSubscription + +- (instancetype)initWithPeerId:(int64_t)peerId url:(NSString *)url type:(TGBridgeMediaAvatarType)type +{ + self = [super init]; + if (self != nil) + { + _peerId = peerId; + _url = url; + _type = type; + } + return self; +} + +- (bool)renewable +{ + return false; +} + +- (void)_serializeParametersWithCoder:(NSCoder *)aCoder +{ + [aCoder encodeInt64:self.peerId forKey:TGBridgeMediaAvatarPeerIdKey]; + [aCoder encodeObject:self.url forKey:TGBridgeMediaAvatarUrlKey]; + [aCoder encodeInt32:self.type forKey:TGBridgeMediaAvatarTypeKey]; +} + +- (void)_unserializeParametersWithCoder:(NSCoder *)aDecoder +{ + _peerId = [aDecoder decodeInt64ForKey:TGBridgeMediaAvatarPeerIdKey]; + _url = [aDecoder decodeObjectForKey:TGBridgeMediaAvatarUrlKey]; + _type = [aDecoder decodeInt32ForKey:TGBridgeMediaAvatarTypeKey]; +} + ++ (NSString *)subscriptionName +{ + return TGBridgeMediaAvatarSubscriptionName; +} + +@end + + +NSString *const TGBridgeMediaStickerSubscriptionName = @"media.sticker"; +NSString *const TGBridgeMediaStickerDocumentIdKey = @"documentId"; +NSString *const TGBridgeMediaStickerPackIdKey = @"packId"; +NSString *const TGBridgeMediaStickerPackAccessHashKey = @"accessHash"; +NSString *const TGBridgeMediaStickerPeerIdKey = @"peerId"; +NSString *const TGBridgeMediaStickerMessageIdKey = @"mid"; +NSString *const TGBridgeMediaStickerNotificationKey = @"notification"; +NSString *const TGBridgeMediaStickerSizeKey = @"size"; + +@implementation TGBridgeMediaStickerSubscription + +- (instancetype)initWithDocumentId:(int64_t)documentId stickerPackId:(int64_t)stickerPackId stickerPackAccessHash:(int64_t)stickerPackAccessHash stickerPeerId:(int64_t)stickerPeerId stickerMessageId:(int32_t)stickerMessageId notification:(bool)notification size:(CGSize)size +{ + self = [super init]; + if (self != nil) + { + _documentId = documentId; + _stickerPackId = stickerPackId; + _stickerPackAccessHash = stickerPackAccessHash; + _stickerPeerId = stickerPeerId; + _stickerMessageId = stickerMessageId; + _notification = notification; + _size = size; + } + return self; +} + +- (bool)renewable +{ + return false; +} + +- (void)_serializeParametersWithCoder:(NSCoder *)aCoder +{ + [aCoder encodeInt64:self.documentId forKey:TGBridgeMediaStickerDocumentIdKey]; + [aCoder encodeInt64:self.stickerPackId forKey:TGBridgeMediaStickerPackIdKey]; + [aCoder encodeInt64:self.stickerPackAccessHash forKey:TGBridgeMediaStickerPackAccessHashKey]; + [aCoder encodeInt64:self.stickerPeerId forKey:TGBridgeMediaStickerPeerIdKey]; + [aCoder encodeInt32:self.stickerMessageId forKey:TGBridgeMediaStickerMessageIdKey]; + [aCoder encodeBool:self.notification forKey:TGBridgeMediaStickerNotificationKey]; + [aCoder encodeCGSize:self.size forKey:TGBridgeMediaStickerSizeKey]; +} + +- (void)_unserializeParametersWithCoder:(NSCoder *)aDecoder +{ + _documentId = [aDecoder decodeInt64ForKey:TGBridgeMediaStickerDocumentIdKey]; + _stickerPackId = [aDecoder decodeInt64ForKey:TGBridgeMediaStickerPackIdKey]; + _stickerPackAccessHash = [aDecoder decodeInt64ForKey:TGBridgeMediaStickerPackAccessHashKey]; + _stickerPeerId = [aDecoder decodeInt64ForKey:TGBridgeMediaStickerPeerIdKey]; + _stickerMessageId = [aDecoder decodeInt32ForKey:TGBridgeMediaStickerMessageIdKey]; + _notification = [aDecoder decodeBoolForKey:TGBridgeMediaStickerNotificationKey]; + _size = [aDecoder decodeCGSizeForKey:TGBridgeMediaStickerSizeKey]; +} + ++ (NSString *)subscriptionName +{ + return TGBridgeMediaStickerSubscriptionName; +} + +@end + + +NSString *const TGBridgePeerSettingsSubscriptionName = @"peer.settings"; +NSString *const TGBridgePeerSettingsSubscriptionPeerIdKey = @"peerId"; + +@implementation TGBridgePeerSettingsSubscription + +- (instancetype)initWithPeerId:(int64_t)peerId +{ + self = [super init]; + if (self != nil) + { + _peerId = peerId; + } + return self; +} + +- (bool)dropPreviouslyQueued +{ + return true; +} + +- (void)_serializeParametersWithCoder:(NSCoder *)aCoder +{ + [aCoder encodeInt64:self.peerId forKey:TGBridgePeerSettingsSubscriptionPeerIdKey]; +} + +- (void)_unserializeParametersWithCoder:(NSCoder *)aDecoder +{ + _peerId = [aDecoder decodeInt64ForKey:TGBridgePeerSettingsSubscriptionPeerIdKey]; +} + ++ (NSString *)subscriptionName +{ + return TGBridgePeerSettingsSubscriptionName; +} + +@end + + +NSString *const TGBridgePeerUpdateNotificationSettingsSubscriptionName = @"peer.notificationSettings"; +NSString *const TGBridgePeerUpdateNotificationSettingsSubscriptionPeerIdKey = @"peerId"; + +@implementation TGBridgePeerUpdateNotificationSettingsSubscription + +- (instancetype)initWithPeerId:(int64_t)peerId +{ + self = [super init]; + if (self != nil) + { + _peerId = peerId; + } + return self; +} + +- (bool)renewable +{ + return false; +} + +- (void)_serializeParametersWithCoder:(NSCoder *)aCoder +{ + [aCoder encodeInt64:self.peerId forKey:TGBridgePeerUpdateNotificationSettingsSubscriptionPeerIdKey]; +} + +- (void)_unserializeParametersWithCoder:(NSCoder *)aDecoder +{ + _peerId = [aDecoder decodeInt64ForKey:TGBridgePeerUpdateNotificationSettingsSubscriptionPeerIdKey]; +} + ++ (NSString *)subscriptionName +{ + return TGBridgePeerUpdateNotificationSettingsSubscriptionName; +} + +@end + + +NSString *const TGBridgePeerUpdateBlockStatusSubscriptionName = @"peer.updateBlocked"; +NSString *const TGBridgePeerUpdateBlockStatusSubscriptionPeerIdKey = @"peerId"; +NSString *const TGBridgePeerUpdateBlockStatusSubscriptionBlockedKey = @"blocked"; + +@implementation TGBridgePeerUpdateBlockStatusSubscription + +- (instancetype)initWithPeerId:(int64_t)peerId blocked:(bool)blocked +{ + self = [super init]; + if (self != nil) + { + _peerId = peerId; + _blocked = blocked; + } + return self; +} + +- (bool)renewable +{ + return false; +} + +- (void)_serializeParametersWithCoder:(NSCoder *)aCoder +{ + [aCoder encodeInt64:self.peerId forKey:TGBridgePeerUpdateBlockStatusSubscriptionPeerIdKey]; + [aCoder encodeBool:self.blocked forKey:TGBridgePeerUpdateBlockStatusSubscriptionBlockedKey]; +} + +- (void)_unserializeParametersWithCoder:(NSCoder *)aDecoder +{ + _peerId = [aDecoder decodeInt64ForKey:TGBridgePeerUpdateBlockStatusSubscriptionPeerIdKey]; + _blocked = [aDecoder decodeBoolForKey:TGBridgePeerUpdateBlockStatusSubscriptionBlockedKey]; +} + ++ (NSString *)subscriptionName +{ + return TGBridgePeerUpdateBlockStatusSubscriptionName; +} + +@end + + +NSString *const TGBridgeRemoteSubscriptionName = @"remote.request"; +NSString *const TGBridgeRemotePeerIdKey = @"peerId"; +NSString *const TGBridgeRemoteMessageIdKey = @"mid"; +NSString *const TGBridgeRemoteTypeKey = @"mediaType"; +NSString *const TGBridgeRemoteAutoPlayKey = @"autoPlay"; + +@implementation TGBridgeRemoteSubscription + +- (instancetype)initWithPeerId:(int64_t)peerId messageId:(int32_t)messageId type:(int32_t)type autoPlay:(bool)autoPlay +{ + self = [super init]; + if (self != nil) + { + _peerId = peerId; + _messageId = messageId; + _type = type; + _autoPlay = autoPlay; + } + return self; +} + +- (bool)renewable +{ + return false; +} + +- (void)_serializeParametersWithCoder:(NSCoder *)aCoder +{ + [aCoder encodeInt64:self.peerId forKey:TGBridgeRemotePeerIdKey]; + [aCoder encodeInt32:self.messageId forKey:TGBridgeRemoteMessageIdKey]; + [aCoder encodeInt32:self.type forKey:TGBridgeRemoteTypeKey]; + [aCoder encodeBool:self.autoPlay forKey:TGBridgeRemoteAutoPlayKey]; +} + +- (void)_unserializeParametersWithCoder:(NSCoder *)aDecoder +{ + _peerId = [aDecoder decodeInt64ForKey:TGBridgeRemotePeerIdKey]; + _messageId = [aDecoder decodeInt32ForKey:TGBridgeRemoteMessageIdKey]; + _type = [aDecoder decodeInt32ForKey:TGBridgeRemoteTypeKey]; + _autoPlay = [aDecoder decodeBoolForKey:TGBridgeRemoteAutoPlayKey]; +} + ++ (NSString *)subscriptionName +{ + return TGBridgeRemoteSubscriptionName; +} + +@end + + +NSString *const TGBridgeSendTextMessageSubscriptionName = @"sendMessage.text"; +NSString *const TGBridgeSendTextMessageSubscriptionPeerIdKey = @"peerId"; +NSString *const TGBridgeSendTextMessageSubscriptionTextKey = @"text"; +NSString *const TGBridgeSendTextMessageSubscriptionReplyToMidKey = @"replyToMid"; + +@implementation TGBridgeSendTextMessageSubscription + +- (instancetype)initWithPeerId:(int64_t)peerId text:(NSString *)text replyToMid:(int32_t)replyToMid +{ + self = [super init]; + if (self != nil) + { + _peerId = peerId; + _text = text; + _replyToMid = replyToMid; + } + return self; +} + +- (bool)renewable +{ + return false; +} + +- (void)_serializeParametersWithCoder:(NSCoder *)aCoder +{ + [aCoder encodeInt64:self.peerId forKey:TGBridgeSendTextMessageSubscriptionPeerIdKey]; + [aCoder encodeObject:self.text forKey:TGBridgeSendTextMessageSubscriptionTextKey]; + [aCoder encodeInt32:self.replyToMid forKey:TGBridgeSendTextMessageSubscriptionReplyToMidKey]; +} + +- (void)_unserializeParametersWithCoder:(NSCoder *)aDecoder +{ + _peerId = [aDecoder decodeInt64ForKey:TGBridgeSendTextMessageSubscriptionPeerIdKey]; + _text = [aDecoder decodeObjectForKey:TGBridgeSendTextMessageSubscriptionTextKey]; + _replyToMid = [aDecoder decodeInt32ForKey:TGBridgeSendTextMessageSubscriptionReplyToMidKey]; +} + ++ (NSString *)subscriptionName +{ + return TGBridgeSendTextMessageSubscriptionName; +} + +@end + + +NSString *const TGBridgeSendStickerMessageSubscriptionName = @"sendMessage.sticker"; +NSString *const TGBridgeSendStickerMessageSubscriptionPeerIdKey = @"peerId"; +NSString *const TGBridgeSendStickerMessageSubscriptionDocumentKey = @"document"; +NSString *const TGBridgeSendStickerMessageSubscriptionReplyToMidKey = @"replyToMid"; + +@implementation TGBridgeSendStickerMessageSubscription + +- (instancetype)initWithPeerId:(int64_t)peerId document:(TGBridgeDocumentMediaAttachment *)document replyToMid:(int32_t)replyToMid +{ + self = [super init]; + if (self != nil) + { + _peerId = peerId; + _document = document; + _replyToMid = replyToMid; + } + return self; +} + +- (bool)renewable +{ + return false; +} + +- (void)_serializeParametersWithCoder:(NSCoder *)aCoder +{ + [aCoder encodeInt64:self.peerId forKey:TGBridgeSendStickerMessageSubscriptionPeerIdKey]; + [aCoder encodeObject:self.document forKey:TGBridgeSendStickerMessageSubscriptionDocumentKey]; + [aCoder encodeInt32:self.replyToMid forKey:TGBridgeSendStickerMessageSubscriptionReplyToMidKey]; +} + + +- (void)_unserializeParametersWithCoder:(NSCoder *)aDecoder +{ + _peerId = [aDecoder decodeInt64ForKey:TGBridgeSendStickerMessageSubscriptionPeerIdKey]; + _document = [aDecoder decodeObjectForKey:TGBridgeSendStickerMessageSubscriptionDocumentKey]; + _replyToMid = [aDecoder decodeInt32ForKey:TGBridgeSendStickerMessageSubscriptionReplyToMidKey]; +} + ++ (NSString *)subscriptionName +{ + return TGBridgeSendStickerMessageSubscriptionName; +} + +@end + + +NSString *const TGBridgeSendLocationMessageSubscriptionName = @"sendMessage.location"; +NSString *const TGBridgeSendLocationMessageSubscriptionPeerIdKey = @"peerId"; +NSString *const TGBridgeSendLocationMessageSubscriptionLocationKey = @"location"; +NSString *const TGBridgeSendLocationMessageSubscriptionReplyToMidKey = @"replyToMid"; + +@implementation TGBridgeSendLocationMessageSubscription + +- (instancetype)initWithPeerId:(int64_t)peerId location:(TGBridgeLocationMediaAttachment *)location replyToMid:(int32_t)replyToMid +{ + self = [super init]; + if (self != nil) + { + _peerId = peerId; + _location = location; + _replyToMid = replyToMid; + } + return self; +} + +- (bool)renewable +{ + return false; +} + +- (void)_serializeParametersWithCoder:(NSCoder *)aCoder +{ + [aCoder encodeInt64:self.peerId forKey:TGBridgeSendLocationMessageSubscriptionPeerIdKey]; + [aCoder encodeObject:self.location forKey:TGBridgeSendLocationMessageSubscriptionLocationKey]; + [aCoder encodeInt32:self.replyToMid forKey:TGBridgeSendLocationMessageSubscriptionReplyToMidKey]; +} + +- (void)_unserializeParametersWithCoder:(NSCoder *)aDecoder +{ + _peerId = [aDecoder decodeInt64ForKey:TGBridgeSendLocationMessageSubscriptionPeerIdKey]; + _location = [aDecoder decodeObjectForKey:TGBridgeSendLocationMessageSubscriptionLocationKey]; + _replyToMid = [aDecoder decodeInt32ForKey:TGBridgeSendLocationMessageSubscriptionReplyToMidKey]; +} + ++ (NSString *)subscriptionName +{ + return TGBridgeSendLocationMessageSubscriptionName; +} + +@end + + +NSString *const TGBridgeSendForwardedMessageSubscriptionName = @"sendMessage.forward"; +NSString *const TGBridgeSendForwardedMessageSubscriptionPeerIdKey = @"peerId"; +NSString *const TGBridgeSendForwardedMessageSubscriptionMidKey = @"mid"; +NSString *const TGBridgeSendForwardedMessageSubscriptionTargetPeerIdKey = @"targetPeerId"; + +@implementation TGBridgeSendForwardedMessageSubscription + +- (instancetype)initWithPeerId:(int64_t)peerId messageId:(int32_t)messageId targetPeerId:(int64_t)targetPeerId +{ + self = [super init]; + if (self != nil) + { + _peerId = peerId; + _messageId = messageId; + _targetPeerId = targetPeerId; + } + return self; +} + +- (bool)renewable +{ + return false; +} + +- (void)_serializeParametersWithCoder:(NSCoder *)aCoder +{ + [aCoder encodeInt64:self.peerId forKey:TGBridgeSendForwardedMessageSubscriptionPeerIdKey]; + [aCoder encodeInt32:self.messageId forKey:TGBridgeSendForwardedMessageSubscriptionMidKey]; + [aCoder encodeInt64:self.targetPeerId forKey:TGBridgeSendForwardedMessageSubscriptionTargetPeerIdKey]; +} + +- (void)_unserializeParametersWithCoder:(NSCoder *)aDecoder +{ + _peerId = [aDecoder decodeInt64ForKey:TGBridgeSendForwardedMessageSubscriptionPeerIdKey]; + _messageId = [aDecoder decodeInt32ForKey:TGBridgeSendForwardedMessageSubscriptionMidKey]; + _targetPeerId = [aDecoder decodeInt64ForKey:TGBridgeSendForwardedMessageSubscriptionTargetPeerIdKey]; +} + ++ (NSString *)subscriptionName +{ + return TGBridgeSendForwardedMessageSubscriptionName; +} + +@end + + +NSString *const TGBridgeStateSubscriptionName = @"state.syncState"; + +@implementation TGBridgeStateSubscription + +- (bool)dropPreviouslyQueued +{ + return true; +} + ++ (NSString *)subscriptionName +{ + return TGBridgeStateSubscriptionName; +} + +@end + + +NSString *const TGBridgeStickerPacksSubscriptionName = @"stickers.packs"; + +@implementation TGBridgeStickerPacksSubscription + ++ (NSString *)subscriptionName +{ + return TGBridgeStickerPacksSubscriptionName; +} + +@end + + +NSString *const TGBridgeRecentStickersSubscriptionName = @"stickers.recent"; +NSString *const TGBridgeRecentStickersSubscriptionLimitKey = @"limit"; + +@implementation TGBridgeRecentStickersSubscription + +- (instancetype)initWithLimit:(int32_t)limit +{ + self = [super init]; + if (self != nil) + { + _limit = limit; + } + return self; +} + +- (void)_serializeParametersWithCoder:(NSCoder *)aCoder +{ + [aCoder encodeInt32:self.limit forKey:TGBridgeRecentStickersSubscriptionLimitKey]; +} + +- (void)_unserializeParametersWithCoder:(NSCoder *)aDecoder +{ + _limit = [aDecoder decodeInt32ForKey:TGBridgeRecentStickersSubscriptionLimitKey]; +} + ++ (NSString *)subscriptionName +{ + return TGBridgeRecentStickersSubscriptionName; +} + +@end + + +NSString *const TGBridgeUserInfoSubscriptionName = @"user.userInfo"; +NSString *const TGBridgeUserInfoSubscriptionUserIdsKey = @"uids"; + +@implementation TGBridgeUserInfoSubscription + +- (instancetype)initWithUserIds:(NSArray *)userIds +{ + self = [super init]; + if (self != nil) + { + _userIds = userIds; + } + return self; +} + +- (bool)dropPreviouslyQueued +{ + return true; +} + +- (void)_serializeParametersWithCoder:(NSCoder *)aCoder +{ + [aCoder encodeObject:self.userIds forKey:TGBridgeUserInfoSubscriptionUserIdsKey]; +} + +- (void)_unserializeParametersWithCoder:(NSCoder *)aDecoder +{ + _userIds = [aDecoder decodeObjectForKey:TGBridgeUserInfoSubscriptionUserIdsKey]; +} + ++ (NSString *)subscriptionName +{ + return TGBridgeUserInfoSubscriptionName; +} + +@end + + +NSString *const TGBridgeUserBotInfoSubscriptionName = @"user.botInfo"; +NSString *const TGBridgeUserBotInfoSubscriptionUserIdsKey = @"uids"; + +@implementation TGBridgeUserBotInfoSubscription + +- (instancetype)initWithUserIds:(NSArray *)userIds +{ + self = [super init]; + if (self != nil) + { + _userIds = userIds; + } + return self; +} + +- (bool)dropPreviouslyQueued +{ + return true; +} + +- (void)_serializeParametersWithCoder:(NSCoder *)aCoder +{ + [aCoder encodeObject:self.userIds forKey:TGBridgeUserBotInfoSubscriptionUserIdsKey]; +} + +- (void)_unserializeParametersWithCoder:(NSCoder *)aDecoder +{ + _userIds = [aDecoder decodeObjectForKey:TGBridgeUserBotInfoSubscriptionUserIdsKey]; +} + ++ (NSString *)subscriptionName +{ + return TGBridgeUserBotInfoSubscriptionName; +} + +@end + + +NSString *const TGBridgeBotReplyMarkupSubscriptionName = @"user.botReplyMarkup"; +NSString *const TGBridgeBotReplyMarkupPeerIdKey = @"peerId"; + +@implementation TGBridgeBotReplyMarkupSubscription + +- (instancetype)initWithPeerId:(int64_t)peerId +{ + self = [super init]; + if (self != nil) + { + _peerId = peerId; + } + return self; +} + +- (bool)dropPreviouslyQueued +{ + return true; +} + +- (void)_serializeParametersWithCoder:(NSCoder *)aCoder +{ + [aCoder encodeInt64:self.peerId forKey:TGBridgeBotReplyMarkupPeerIdKey]; +} + +- (void)_unserializeParametersWithCoder:(NSCoder *)aDecoder +{ + _peerId = [aDecoder decodeInt64ForKey:TGBridgeBotReplyMarkupPeerIdKey]; +} + ++ (NSString *)subscriptionName +{ + return TGBridgeBotReplyMarkupSubscriptionName; +} + +@end diff --git a/Watch/Bridge/TGBridgeUnsupportedMediaAttachment.h b/Watch/Bridge/TGBridgeUnsupportedMediaAttachment.h new file mode 100644 index 0000000000..8803a6a8f4 --- /dev/null +++ b/Watch/Bridge/TGBridgeUnsupportedMediaAttachment.h @@ -0,0 +1,9 @@ +#import "TGBridgeMediaAttachment.h" + +@interface TGBridgeUnsupportedMediaAttachment : TGBridgeMediaAttachment + +@property (nonatomic, strong) NSString *compactTitle; +@property (nonatomic, strong) NSString *title; +@property (nonatomic, strong) NSString *subtitle; + +@end diff --git a/Watch/Bridge/TGBridgeUnsupportedMediaAttachment.m b/Watch/Bridge/TGBridgeUnsupportedMediaAttachment.m new file mode 100644 index 0000000000..b51e422fd1 --- /dev/null +++ b/Watch/Bridge/TGBridgeUnsupportedMediaAttachment.m @@ -0,0 +1,35 @@ +#import "TGBridgeUnsupportedMediaAttachment.h" + +const NSInteger TGBridgeUnsupportedMediaAttachmentType = 0x3837BEF7; + +NSString *const TGBridgeUnsupportedMediaCompactTitleKey = @"compactTitle"; +NSString *const TGBridgeUnsupportedMediaTitleKey = @"title"; +NSString *const TGBridgeUnsupportedMediaSubtitleKey = @"subtitle"; + +@implementation TGBridgeUnsupportedMediaAttachment + +- (instancetype)initWithCoder:(NSCoder *)aDecoder +{ + self = [super init]; + if (self != nil) + { + _compactTitle = [aDecoder decodeObjectForKey:TGBridgeUnsupportedMediaCompactTitleKey]; + _title = [aDecoder decodeObjectForKey:TGBridgeUnsupportedMediaTitleKey]; + _subtitle = [aDecoder decodeObjectForKey:TGBridgeUnsupportedMediaSubtitleKey]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder +{ + [aCoder encodeObject:self.compactTitle forKey:TGBridgeUnsupportedMediaCompactTitleKey]; + [aCoder encodeObject:self.title forKey:TGBridgeUnsupportedMediaTitleKey]; + [aCoder encodeObject:self.subtitle forKey:TGBridgeUnsupportedMediaSubtitleKey]; +} + ++ (NSInteger)mediaType +{ + return TGBridgeUnsupportedMediaAttachmentType; +} + +@end diff --git a/Watch/Bridge/TGBridgeUser+TGTableItem.h b/Watch/Bridge/TGBridgeUser+TGTableItem.h new file mode 100644 index 0000000000..4f8c2bdff9 --- /dev/null +++ b/Watch/Bridge/TGBridgeUser+TGTableItem.h @@ -0,0 +1,6 @@ +#import "TGBridgeUser.h" +#import "WKInterfaceTable+TGDataDrivenTable.h" + +@interface TGBridgeUser (TGTableItem) + +@end diff --git a/Watch/Bridge/TGBridgeUser+TGTableItem.m b/Watch/Bridge/TGBridgeUser+TGTableItem.m new file mode 100644 index 0000000000..3b9873fc85 --- /dev/null +++ b/Watch/Bridge/TGBridgeUser+TGTableItem.m @@ -0,0 +1,10 @@ +#import "TGBridgeUser+TGTableItem.h" + +@implementation TGBridgeUser (TGTableItem) + +- (NSString *)uniqueIdentifier +{ + return [NSString stringWithFormat:@"%d", self.identifier]; +} + +@end diff --git a/Watch/Bridge/TGBridgeUser+TGUser.h b/Watch/Bridge/TGBridgeUser+TGUser.h new file mode 100644 index 0000000000..15617d77b3 --- /dev/null +++ b/Watch/Bridge/TGBridgeUser+TGUser.h @@ -0,0 +1,10 @@ +#import "TGBridgeUser.h" + +@class TGUser; +@class TGBotInfo; + +@interface TGBridgeUser (TGUser) + ++ (TGBridgeUser *)userWithTGUser:(TGUser *)user; + +@end diff --git a/Watch/Bridge/TGBridgeUser+TGUser.m b/Watch/Bridge/TGBridgeUser+TGUser.m new file mode 100644 index 0000000000..a57b55c4fc --- /dev/null +++ b/Watch/Bridge/TGBridgeUser+TGUser.m @@ -0,0 +1,34 @@ +#import "TGBridgeUser+TGUser.h" + +#import + +#import "TGBridgeBotInfo+TGBotInfo.h" + +@implementation TGBridgeUser (TGUser) + ++ (TGBridgeUser *)userWithTGUser:(TGUser *)user +{ + if (user == nil) + return nil; + + TGBridgeUser *bridgeUser = [[TGBridgeUser alloc] init]; + bridgeUser->_identifier = user.uid; + bridgeUser->_firstName = user.firstName; + bridgeUser->_lastName = user.lastName; + bridgeUser->_userName = user.userName; + bridgeUser->_phoneNumber = user.phoneNumber; + if (user.phoneNumber != nil) + bridgeUser->_prettyPhoneNumber = [TGPhoneUtils formatPhone:user.phoneNumber forceInternational:false]; + bridgeUser->_online = user.presence.online; + bridgeUser->_lastSeen = user.presence.lastSeen; + bridgeUser->_photoSmall = user.photoUrlSmall; + bridgeUser->_photoBig = user.photoUrlBig; + bridgeUser->_kind = user.kind; + bridgeUser->_botKind = user.botKind; + bridgeUser->_botVersion = user.botInfoVersion; + bridgeUser->_verified = user.isVerified; + + return bridgeUser; +} + +@end diff --git a/Watch/Bridge/TGBridgeUser.h b/Watch/Bridge/TGBridgeUser.h new file mode 100644 index 0000000000..f1ecadd951 --- /dev/null +++ b/Watch/Bridge/TGBridgeUser.h @@ -0,0 +1,59 @@ +#import "TGBridgeCommon.h" + +@class TGBridgeBotInfo; +@class TGBridgeUserChange; + +typedef NS_ENUM(NSUInteger, TGBridgeUserKind) { + TGBridgeUserKindGeneric, + TGBridgeUserKindBot, + TGBridgeUserKindSmartBot +}; + +typedef NS_ENUM(NSUInteger, TGBridgeBotKind) { + TGBridgeBotKindGeneric, + TGBridgeBotKindPrivate +}; + +@interface TGBridgeUser : NSObject + +@property (nonatomic) int64_t identifier; +@property (nonatomic, strong) NSString *firstName; +@property (nonatomic, strong) NSString *lastName; +@property (nonatomic, strong) NSString *userName; +@property (nonatomic, strong) NSString *phoneNumber; +@property (nonatomic, strong) NSString *prettyPhoneNumber; +@property (nonatomic, strong) NSString *about; + +@property (nonatomic) bool online; +@property (nonatomic) NSTimeInterval lastSeen; + +@property (nonatomic, strong) NSString *photoSmall; +@property (nonatomic, strong) NSString *photoBig; + +@property (nonatomic) TGBridgeUserKind kind; +@property (nonatomic) TGBridgeBotKind botKind; +@property (nonatomic) int32_t botVersion; + +@property (nonatomic) bool verified; + +@property (nonatomic) int32_t userVersion; + +- (NSString *)displayName; +- (TGBridgeUserChange *)changeFromUser:(TGBridgeUser *)user; +- (TGBridgeUser *)userByApplyingChange:(TGBridgeUserChange *)change; + +- (bool)isBot; + +@end + + +@interface TGBridgeUserChange : NSObject + +@property (nonatomic, readonly) int32_t userIdentifier; +@property (nonatomic, readonly) NSDictionary *fields; + +- (instancetype)initWithUserIdentifier:(int32_t)userIdentifier fields:(NSDictionary *)fields; + +@end + +extern NSString *const TGBridgeUsersDictionaryKey; diff --git a/Watch/Bridge/TGBridgeUser.m b/Watch/Bridge/TGBridgeUser.m new file mode 100644 index 0000000000..615e6a34a1 --- /dev/null +++ b/Watch/Bridge/TGBridgeUser.m @@ -0,0 +1,289 @@ +#import "TGBridgeUser.h" +#import "TGWatchCommon.h" +#import "TGBridgeBotInfo.h" + +#import "../Extension/TGStringUtils.h" + +NSString *const TGBridgeUserIdentifierKey = @"identifier"; +NSString *const TGBridgeUserFirstNameKey = @"firstName"; +NSString *const TGBridgeUserLastNameKey = @"lastName"; +NSString *const TGBridgeUserUserNameKey = @"userName"; +NSString *const TGBridgeUserPhoneNumberKey = @"phoneNumber"; +NSString *const TGBridgeUserPrettyPhoneNumberKey = @"prettyPhoneNumber"; +NSString *const TGBridgeUserOnlineKey = @"online"; +NSString *const TGBridgeUserLastSeenKey = @"lastSeen"; +NSString *const TGBridgeUserPhotoSmallKey = @"photoSmall"; +NSString *const TGBridgeUserPhotoBigKey = @"photoBig"; +NSString *const TGBridgeUserKindKey = @"kind"; +NSString *const TGBridgeUserBotKindKey = @"botKind"; +NSString *const TGBridgeUserBotVersionKey = @"botVersion"; +NSString *const TGBridgeUserVerifiedKey = @"verified"; +NSString *const TGBridgeUserAboutKey = @"about"; +NSString *const TGBridgeUserVersionKey = @"version"; + +NSString *const TGBridgeUsersDictionaryKey = @"users"; + +@implementation TGBridgeUser + +- (instancetype)initWithCoder:(NSCoder *)aDecoder +{ + self = [super init]; + if (self != nil) + { + _identifier = [aDecoder decodeInt64ForKey:TGBridgeUserIdentifierKey]; + _firstName = [aDecoder decodeObjectForKey:TGBridgeUserFirstNameKey]; + _lastName = [aDecoder decodeObjectForKey:TGBridgeUserLastNameKey]; + _userName = [aDecoder decodeObjectForKey:TGBridgeUserUserNameKey]; + _phoneNumber = [aDecoder decodeObjectForKey:TGBridgeUserPhoneNumberKey]; + _prettyPhoneNumber = [aDecoder decodeObjectForKey:TGBridgeUserPrettyPhoneNumberKey]; + _online = [aDecoder decodeBoolForKey:TGBridgeUserOnlineKey]; + _lastSeen = [aDecoder decodeDoubleForKey:TGBridgeUserLastSeenKey]; + _photoSmall = [aDecoder decodeObjectForKey:TGBridgeUserPhotoSmallKey]; + _photoBig = [aDecoder decodeObjectForKey:TGBridgeUserPhotoBigKey]; + _kind = [aDecoder decodeInt32ForKey:TGBridgeUserKindKey]; + _botKind = [aDecoder decodeInt32ForKey:TGBridgeUserBotKindKey]; + _botVersion = [aDecoder decodeInt32ForKey:TGBridgeUserBotVersionKey]; + _verified = [aDecoder decodeBoolForKey:TGBridgeUserVerifiedKey]; + _about = [aDecoder decodeObjectForKey:TGBridgeUserAboutKey]; + _userVersion = [aDecoder decodeInt32ForKey:TGBridgeUserVersionKey]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder +{ + [aCoder encodeInt64:self.identifier forKey:TGBridgeUserIdentifierKey]; + [aCoder encodeObject:self.firstName forKey:TGBridgeUserFirstNameKey]; + [aCoder encodeObject:self.lastName forKey:TGBridgeUserLastNameKey]; + [aCoder encodeObject:self.userName forKey:TGBridgeUserUserNameKey]; + [aCoder encodeObject:self.phoneNumber forKey:TGBridgeUserPhoneNumberKey]; + [aCoder encodeObject:self.prettyPhoneNumber forKey:TGBridgeUserPrettyPhoneNumberKey]; + [aCoder encodeBool:self.online forKey:TGBridgeUserOnlineKey]; + [aCoder encodeDouble:self.lastSeen forKey:TGBridgeUserLastSeenKey]; + [aCoder encodeObject:self.photoSmall forKey:TGBridgeUserPhotoSmallKey]; + [aCoder encodeObject:self.photoBig forKey:TGBridgeUserPhotoBigKey]; + [aCoder encodeInt32:self.kind forKey:TGBridgeUserKindKey]; + [aCoder encodeInt32:self.botKind forKey:TGBridgeUserBotKindKey]; + [aCoder encodeInt32:self.botVersion forKey:TGBridgeUserBotVersionKey]; + [aCoder encodeBool:self.verified forKey:TGBridgeUserVerifiedKey]; + [aCoder encodeObject:self.about forKey:TGBridgeUserAboutKey]; + [aCoder encodeInt32:self.userVersion forKey:TGBridgeUserVersionKey]; +} + +- (instancetype)copyWithZone:(NSZone *)__unused zone +{ + TGBridgeUser *user = [[TGBridgeUser alloc] init]; + user->_identifier = self.identifier; + user->_firstName = self.firstName; + user->_lastName = self.lastName; + user->_userName = self.userName; + user->_phoneNumber = self.phoneNumber; + user->_prettyPhoneNumber = self.prettyPhoneNumber; + user->_online = self.online; + user->_lastSeen = self.lastSeen; + user->_photoSmall = self.photoSmall; + user->_photoBig = self.photoBig; + user->_kind = self.kind; + user->_botKind = self.botKind; + user->_botVersion = self.botVersion; + user->_verified = self.verified; + user->_about = self.about; + user->_userVersion = self.userVersion; + + return user; +} + +- (NSString *)displayName +{ + NSString *firstName = self.firstName; + NSString *lastName = self.lastName; + + if (firstName != nil && firstName.length != 0 && lastName != nil && lastName.length != 0) + { + if (TGIsKorean()) + return [[NSString alloc] initWithFormat:@"%@ %@", lastName, firstName]; + else + return [[NSString alloc] initWithFormat:@"%@ %@", firstName, lastName]; + } + else if (firstName != nil && firstName.length != 0) + return firstName; + else if (lastName != nil && lastName.length != 0) + return lastName; + + return @""; +} + +- (bool)isBot +{ + return (self.kind == TGBridgeUserKindBot || self.kind ==TGBridgeUserKindSmartBot); +} + +- (TGBridgeUserChange *)changeFromUser:(TGBridgeUser *)user +{ + NSMutableDictionary *fields = [[NSMutableDictionary alloc] init]; + + [self _compareString:self.firstName oldString:user.firstName dict:fields key:TGBridgeUserFirstNameKey]; + [self _compareString:self.lastName oldString:user.lastName dict:fields key:TGBridgeUserLastNameKey]; + [self _compareString:self.userName oldString:user.userName dict:fields key:TGBridgeUserUserNameKey]; + [self _compareString:self.phoneNumber oldString:user.phoneNumber dict:fields key:TGBridgeUserPhoneNumberKey]; + [self _compareString:self.prettyPhoneNumber oldString:user.prettyPhoneNumber dict:fields key:TGBridgeUserPrettyPhoneNumberKey]; + + if (self.online != user.online) + fields[TGBridgeUserOnlineKey] = @(self.online); + + if (fabs(self.lastSeen - user.lastSeen) > DBL_EPSILON) + fields[TGBridgeUserLastSeenKey] = @(self.lastSeen); + + [self _compareString:self.photoSmall oldString:user.photoSmall dict:fields key:TGBridgeUserPhotoSmallKey]; + [self _compareString:self.photoBig oldString:user.photoBig dict:fields key:TGBridgeUserPhotoBigKey]; + + if (self.kind != user.kind) + fields[TGBridgeUserKindKey] = @(self.kind); + + if (self.botKind != user.botKind) + fields[TGBridgeUserBotKindKey] = @(self.botKind); + + if (self.botVersion != user.botVersion) + fields[TGBridgeUserBotVersionKey] = @(self.botVersion); + + if (self.verified != user.verified) + fields[TGBridgeUserVerifiedKey] = @(self.verified); + + if (fields.count == 0) + return nil; + + return [[TGBridgeUserChange alloc] initWithUserIdentifier:user.identifier fields:fields]; +} + +- (void)_compareString:(NSString *)newString oldString:(NSString *)oldString dict:(NSMutableDictionary *)dict key:(NSString *)key +{ + if (newString == nil && oldString == nil) + return; + + if (![newString isEqualToString:oldString]) + { + if (newString == nil) + dict[key] = [NSNull null]; + else + dict[key] = newString; + } +} + +- (TGBridgeUser *)userByApplyingChange:(TGBridgeUserChange *)change +{ + if (change.userIdentifier != self.identifier) + return nil; + + TGBridgeUser *user = [self copy]; + + NSString *firstNameChange = change.fields[TGBridgeUserFirstNameKey]; + if (firstNameChange != nil) + user->_firstName = [self _stringForFieldChange:firstNameChange]; + + NSString *lastNameChange = change.fields[TGBridgeUserLastNameKey]; + if (lastNameChange != nil) + user->_lastName = [self _stringForFieldChange:lastNameChange]; + + NSString *userNameChange = change.fields[TGBridgeUserUserNameKey]; + if (userNameChange != nil) + user->_userName = [self _stringForFieldChange:userNameChange]; + + NSString *phoneNumberChange = change.fields[TGBridgeUserPhoneNumberKey]; + if (phoneNumberChange != nil) + user->_phoneNumber = [self _stringForFieldChange:phoneNumberChange]; + + NSString *prettyPhoneNumberChange = change.fields[TGBridgeUserPrettyPhoneNumberKey]; + if (prettyPhoneNumberChange != nil) + user->_prettyPhoneNumber = [self _stringForFieldChange:prettyPhoneNumberChange]; + + NSNumber *onlineChange = change.fields[TGBridgeUserOnlineKey]; + if (onlineChange != nil) + user->_online = [onlineChange boolValue]; + + NSNumber *lastSeenChange = change.fields[TGBridgeUserLastSeenKey]; + if (lastSeenChange != nil) + user->_lastSeen = [lastSeenChange doubleValue]; + + NSString *photoSmallChange = change.fields[TGBridgeUserPhotoSmallKey]; + if (photoSmallChange != nil) + user->_photoSmall = [self _stringForFieldChange:photoSmallChange]; + + NSString *photoBigChange = change.fields[TGBridgeUserPhotoBigKey]; + if (photoBigChange != nil) + user->_photoBig = [self _stringForFieldChange:photoBigChange]; + + NSNumber *kindChange = change.fields[TGBridgeUserKindKey]; + if (kindChange != nil) + user->_kind = [kindChange int32Value]; + + NSNumber *botKindChange = change.fields[TGBridgeUserBotKindKey]; + if (botKindChange != nil) + user->_botKind = [botKindChange int32Value]; + + NSNumber *botVersionChange = change.fields[TGBridgeUserBotVersionKey]; + if (botVersionChange != nil) + user->_botVersion = [botVersionChange int32Value]; + + NSNumber *verifiedChange = change.fields[TGBridgeUserVerifiedKey]; + if (verifiedChange != nil) + user->_verified = [verifiedChange boolValue]; + + return user; +} + +- (NSString *)_stringForFieldChange:(NSString *)fieldChange +{ + if ([fieldChange isKindOfClass:[NSNull class]]) + return nil; + + return fieldChange; +} + +- (BOOL)isEqual:(id)object +{ + if (object == self) + return YES; + + if (!object || ![object isKindOfClass:[self class]]) + return NO; + + return self.identifier == ((TGBridgeUser *)object).identifier; +} + +@end + + +NSString *const TGBridgeUserChangeIdentifierKey = @"userIdentifier"; +NSString *const TGBridgeUserChangeFieldsKey = @"fields"; + +@implementation TGBridgeUserChange + +- (instancetype)initWithUserIdentifier:(int32_t)userIdentifier fields:(NSDictionary *)fields +{ + self = [super init]; + if (self != nil) + { + _userIdentifier = userIdentifier; + _fields = fields; + } + return self; +} + +- (instancetype)initWithCoder:(NSCoder *)aDecoder +{ + self = [super init]; + if (self != nil) + { + _userIdentifier = [aDecoder decodeInt32ForKey:TGBridgeUserChangeIdentifierKey]; + _fields = [aDecoder decodeObjectForKey:TGBridgeUserChangeFieldsKey]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder +{ + [aCoder encodeInt32:self.userIdentifier forKey:TGBridgeUserChangeIdentifierKey]; + [aCoder encodeObject:self.fields forKey:TGBridgeUserChangeFieldsKey]; +} + +@end diff --git a/Watch/Bridge/TGBridgeUserInfoSignals.h b/Watch/Bridge/TGBridgeUserInfoSignals.h new file mode 100644 index 0000000000..0a4fe21387 --- /dev/null +++ b/Watch/Bridge/TGBridgeUserInfoSignals.h @@ -0,0 +1,8 @@ +#import + +@interface TGBridgeUserInfoSignals : NSObject + ++ (SSignal *)userInfoWithUserId:(int32_t)userId; ++ (SSignal *)usersInfoWithUserIds:(NSArray *)userIds; + +@end diff --git a/Watch/Bridge/TGBridgeUserInfoSignals.m b/Watch/Bridge/TGBridgeUserInfoSignals.m new file mode 100644 index 0000000000..753ee5ecfa --- /dev/null +++ b/Watch/Bridge/TGBridgeUserInfoSignals.m @@ -0,0 +1,24 @@ +#import "TGBridgeUserInfoSignals.h" +#import "TGBridgeSubscriptions.h" +#import "TGBridgeUser.h" +#import "TGBridgeClient.h" + +@implementation TGBridgeUserInfoSignals + ++ (SSignal *)userInfoWithUserId:(int32_t)userId; +{ + return [[self usersInfoWithUserIds:@[ @(userId) ]] map:^TGBridgeUser *(NSDictionary *users) + { + return users[@(userId)]; + }]; +} + ++ (SSignal *)usersInfoWithUserIds:(NSArray *)userIds +{ + return [[[TGBridgeClient instance] requestSignalWithSubscription:[[TGBridgeUserInfoSubscription alloc] initWithUserIds:userIds]] map:^NSDictionary *(id next) + { + return next; + }]; +} + +@end diff --git a/Watch/Bridge/TGBridgeVideoMediaAttachment.h b/Watch/Bridge/TGBridgeVideoMediaAttachment.h new file mode 100644 index 0000000000..fca37e940f --- /dev/null +++ b/Watch/Bridge/TGBridgeVideoMediaAttachment.h @@ -0,0 +1,11 @@ +#import "TGBridgeMediaAttachment.h" +#import + +@interface TGBridgeVideoMediaAttachment : TGBridgeMediaAttachment + +@property (nonatomic, assign) int64_t videoId; +@property (nonatomic, assign) int32_t duration; +@property (nonatomic, assign) CGSize dimensions; +@property (nonatomic, assign) bool round; + +@end diff --git a/Watch/Bridge/TGBridgeVideoMediaAttachment.m b/Watch/Bridge/TGBridgeVideoMediaAttachment.m new file mode 100644 index 0000000000..66fe867996 --- /dev/null +++ b/Watch/Bridge/TGBridgeVideoMediaAttachment.m @@ -0,0 +1,39 @@ +#import "TGBridgeVideoMediaAttachment.h" +#import + +const NSInteger TGBridgeVideoMediaAttachmentType = 0x338EAA20; + +NSString *const TGBridgeVideoMediaVideoIdKey = @"videoId"; +NSString *const TGBridgeVideoMediaDimensionsKey = @"dimensions"; +NSString *const TGBridgeVideoMediaDurationKey = @"duration"; +NSString *const TGBridgeVideoMediaRoundKey = @"round"; + +@implementation TGBridgeVideoMediaAttachment + +- (instancetype)initWithCoder:(NSCoder *)aDecoder +{ + self = [super init]; + if (self != nil) + { + _videoId = [aDecoder decodeInt64ForKey:TGBridgeVideoMediaVideoIdKey]; + _dimensions = [aDecoder decodeCGSizeForKey:TGBridgeVideoMediaDimensionsKey]; + _duration = [aDecoder decodeInt32ForKey:TGBridgeVideoMediaDurationKey]; + _round = [aDecoder decodeBoolForKey:TGBridgeVideoMediaRoundKey]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder +{ + [aCoder encodeInt64:self.videoId forKey:TGBridgeVideoMediaVideoIdKey]; + [aCoder encodeCGSize:self.dimensions forKey:TGBridgeVideoMediaDimensionsKey]; + [aCoder encodeInt32:self.duration forKey:TGBridgeVideoMediaDurationKey]; + [aCoder encodeBool:self.round forKey:TGBridgeVideoMediaRoundKey]; +} + ++ (NSInteger)mediaType +{ + return TGBridgeVideoMediaAttachmentType; +} + +@end diff --git a/Watch/Bridge/TGBridgeWebPageMediaAttachment.h b/Watch/Bridge/TGBridgeWebPageMediaAttachment.h new file mode 100644 index 0000000000..c5e81d329a --- /dev/null +++ b/Watch/Bridge/TGBridgeWebPageMediaAttachment.h @@ -0,0 +1,22 @@ +#import "TGBridgeMediaAttachment.h" +#import + +@class TGBridgeImageMediaAttachment; + +@interface TGBridgeWebPageMediaAttachment : TGBridgeMediaAttachment + +@property (nonatomic, assign) int64_t webPageId; +@property (nonatomic, strong) NSString *url; +@property (nonatomic, strong) NSString *displayUrl; +@property (nonatomic, strong) NSString *pageType; +@property (nonatomic, strong) NSString *siteName; +@property (nonatomic, strong) NSString *title; +@property (nonatomic, strong) NSString *pageDescription; +@property (nonatomic, strong) TGBridgeImageMediaAttachment *photo; +@property (nonatomic, strong) NSString *embedUrl; +@property (nonatomic, strong) NSString *embedType; +@property (nonatomic, assign) CGSize embedSize; +@property (nonatomic, strong) NSNumber *duration; +@property (nonatomic, strong) NSString *author; + +@end diff --git a/Watch/Bridge/TGBridgeWebPageMediaAttachment.m b/Watch/Bridge/TGBridgeWebPageMediaAttachment.m new file mode 100644 index 0000000000..3983075d69 --- /dev/null +++ b/Watch/Bridge/TGBridgeWebPageMediaAttachment.m @@ -0,0 +1,65 @@ +#import "TGBridgeWebPageMediaAttachment.h" +#import "TGBridgeImageMediaAttachment.h" +#import + +const NSInteger TGBridgeWebPageMediaAttachmentType = 0x584197af; + +NSString *const TGBridgeWebPageMediaWebPageIdKey = @"webPageId"; +NSString *const TGBridgeWebPageMediaUrlKey = @"url"; +NSString *const TGBridgeWebPageMediaDisplayUrlKey = @"displayUrl"; +NSString *const TGBridgeWebPageMediaPageTypeKey = @"pageType"; +NSString *const TGBridgeWebPageMediaSiteNameKey = @"siteName"; +NSString *const TGBridgeWebPageMediaTitleKey = @"title"; +NSString *const TGBridgeWebPageMediaPageDescriptionKey = @"pageDescription"; +NSString *const TGBridgeWebPageMediaPhotoKey = @"photo"; +NSString *const TGBridgeWebPageMediaEmbedUrlKey = @"embedUrl"; +NSString *const TGBridgeWebPageMediaEmbedTypeKey = @"embedType"; +NSString *const TGBridgeWebPageMediaEmbedSizeKey = @"embedSize"; +NSString *const TGBridgeWebPageMediaDurationKey = @"duration"; +NSString *const TGBridgeWebPageMediaAuthorKey = @"author"; + +@implementation TGBridgeWebPageMediaAttachment + +- (instancetype)initWithCoder:(NSCoder *)aDecoder +{ + self = [super init]; + if (self != nil) + { + _webPageId = [aDecoder decodeInt64ForKey:TGBridgeWebPageMediaWebPageIdKey]; + _url = [aDecoder decodeObjectForKey:TGBridgeWebPageMediaUrlKey]; + _displayUrl = [aDecoder decodeObjectForKey:TGBridgeWebPageMediaDisplayUrlKey]; + _pageType = [aDecoder decodeObjectForKey:TGBridgeWebPageMediaPageTypeKey]; + _siteName = [aDecoder decodeObjectForKey:TGBridgeWebPageMediaSiteNameKey]; + _title = [aDecoder decodeObjectForKey:TGBridgeWebPageMediaTitleKey]; + _pageDescription = [aDecoder decodeObjectForKey:TGBridgeWebPageMediaPageDescriptionKey]; + _photo = [aDecoder decodeObjectForKey:TGBridgeWebPageMediaPhotoKey]; + _embedUrl = [aDecoder decodeObjectForKey:TGBridgeWebPageMediaEmbedUrlKey]; + _embedSize = [aDecoder decodeCGSizeForKey:TGBridgeWebPageMediaEmbedSizeKey]; + _duration = [aDecoder decodeObjectForKey:TGBridgeWebPageMediaDurationKey]; + _author = [aDecoder decodeObjectForKey:TGBridgeWebPageMediaAuthorKey]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder +{ + [aCoder encodeInt64:self.webPageId forKey:TGBridgeWebPageMediaWebPageIdKey]; + [aCoder encodeObject:self.url forKey:TGBridgeWebPageMediaUrlKey]; + [aCoder encodeObject:self.displayUrl forKey:TGBridgeWebPageMediaDisplayUrlKey]; + [aCoder encodeObject:self.pageType forKey:TGBridgeWebPageMediaPageTypeKey]; + [aCoder encodeObject:self.siteName forKey:TGBridgeWebPageMediaSiteNameKey]; + [aCoder encodeObject:self.title forKey:TGBridgeWebPageMediaTitleKey]; + [aCoder encodeObject:self.pageDescription forKey:TGBridgeWebPageMediaPageDescriptionKey]; + [aCoder encodeObject:self.photo forKey:TGBridgeWebPageMediaPhotoKey]; + [aCoder encodeObject:self.embedUrl forKey:TGBridgeWebPageMediaEmbedUrlKey]; + [aCoder encodeCGSize:self.embedSize forKey:TGBridgeWebPageMediaEmbedSizeKey]; + [aCoder encodeObject:self.duration forKey:TGBridgeWebPageMediaDurationKey]; + [aCoder encodeObject:self.author forKey:TGBridgeWebPageMediaAuthorKey]; +} + ++ (NSInteger)mediaType +{ + return TGBridgeWebPageMediaAttachmentType; +} + +@end diff --git a/Watch/Extension/Info.plist b/Watch/Extension/Info.plist new file mode 100644 index 0000000000..5fd9264401 --- /dev/null +++ b/Watch/Extension/Info.plist @@ -0,0 +1,36 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + $(APP_NAME) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + XPC! + CFBundleShortVersionString + 5.0.17 + CFBundleVersion + 624 + NSExtension + + NSExtensionAttributes + + WKAppBundleIdentifier + $(APP_BUNDLE_ID).watchkitapp + + NSExtensionPointIdentifier + com.apple.watchkit + + WKExtensionDelegateClassName + TGExtensionDelegate + + diff --git a/Watch/Extension/Resources/File@2x.png b/Watch/Extension/Resources/File@2x.png new file mode 100644 index 0000000000..0f4d832b32 Binary files /dev/null and b/Watch/Extension/Resources/File@2x.png differ diff --git a/Watch/Extension/Resources/Home88@2x.png b/Watch/Extension/Resources/Home88@2x.png new file mode 100644 index 0000000000..87d7692cf1 Binary files /dev/null and b/Watch/Extension/Resources/Home88@2x.png differ diff --git a/Watch/Extension/Resources/Location@2x.png b/Watch/Extension/Resources/Location@2x.png new file mode 100644 index 0000000000..2a58dd6d77 Binary files /dev/null and b/Watch/Extension/Resources/Location@2x.png differ diff --git a/Watch/Extension/Resources/MediaAudio@2x.png b/Watch/Extension/Resources/MediaAudio@2x.png new file mode 100644 index 0000000000..f8cf3ee3d5 Binary files /dev/null and b/Watch/Extension/Resources/MediaAudio@2x.png differ diff --git a/Watch/Extension/Resources/MediaDocument@2x.png b/Watch/Extension/Resources/MediaDocument@2x.png new file mode 100644 index 0000000000..95d413c5a9 Binary files /dev/null and b/Watch/Extension/Resources/MediaDocument@2x.png differ diff --git a/Watch/Extension/Resources/MediaLocation@2x.png b/Watch/Extension/Resources/MediaLocation@2x.png new file mode 100644 index 0000000000..52240d4107 Binary files /dev/null and b/Watch/Extension/Resources/MediaLocation@2x.png differ diff --git a/Watch/Extension/Resources/MediaPhoto@2x.png b/Watch/Extension/Resources/MediaPhoto@2x.png new file mode 100644 index 0000000000..3044d9f5b5 Binary files /dev/null and b/Watch/Extension/Resources/MediaPhoto@2x.png differ diff --git a/Watch/Extension/Resources/MediaVideo@2x.png b/Watch/Extension/Resources/MediaVideo@2x.png new file mode 100644 index 0000000000..3b1fabcf1a Binary files /dev/null and b/Watch/Extension/Resources/MediaVideo@2x.png differ diff --git a/Watch/Extension/Resources/SavedMessagesAvatar@2x.png b/Watch/Extension/Resources/SavedMessagesAvatar@2x.png new file mode 100644 index 0000000000..8fa8e689b4 Binary files /dev/null and b/Watch/Extension/Resources/SavedMessagesAvatar@2x.png differ diff --git a/Watch/Extension/Resources/VerifiedList@2x.png b/Watch/Extension/Resources/VerifiedList@2x.png new file mode 100644 index 0000000000..a688a94751 Binary files /dev/null and b/Watch/Extension/Resources/VerifiedList@2x.png differ diff --git a/Watch/Extension/TGAudioMicAlertController.h b/Watch/Extension/TGAudioMicAlertController.h new file mode 100644 index 0000000000..4522a5f736 --- /dev/null +++ b/Watch/Extension/TGAudioMicAlertController.h @@ -0,0 +1,7 @@ +#import "TGInterfaceController.h" + +@interface TGAudioMicAlertController : TGInterfaceController + +@property (nonatomic, weak) IBOutlet WKInterfaceLabel *alertLabel; + +@end diff --git a/Watch/Extension/TGAudioMicAlertController.m b/Watch/Extension/TGAudioMicAlertController.m new file mode 100644 index 0000000000..16133d1b7f --- /dev/null +++ b/Watch/Extension/TGAudioMicAlertController.m @@ -0,0 +1,18 @@ +#import "TGAudioMicAlertController.h" +#import "TGWatchCommon.h" + +NSString *const TGAudioMicAlertControllerIdentifier = @"TGAudioMicAlertController"; + +@implementation TGAudioMicAlertController + +- (void)configureWithContext:(id)context +{ + self.alertLabel.text = TGLocalized(@"Watch.Microphone.Access"); +} + ++ (NSString *)identifier +{ + return TGAudioMicAlertControllerIdentifier; +} + +@end diff --git a/Watch/Extension/TGAvatarViewModel.h b/Watch/Extension/TGAvatarViewModel.h new file mode 100644 index 0000000000..ae0511631a --- /dev/null +++ b/Watch/Extension/TGAvatarViewModel.h @@ -0,0 +1,17 @@ +#import + +@class TGBridgeContext; +@class TGBridgeUser; +@class TGBridgeChat; + +@interface TGAvatarViewModel : NSObject + +@property (nonatomic, weak) WKInterfaceGroup *group; +@property (nonatomic, weak) WKInterfaceLabel *label; + +- (void)updateWithUser:(TGBridgeUser *)user context:(TGBridgeContext *)context isVisible:(bool (^)(void))isVisible; +- (void)updateWithChat:(TGBridgeChat *)chat isVisible:(bool (^)(void))isVisible; + +- (void)updateIfNeeded; + +@end diff --git a/Watch/Extension/TGAvatarViewModel.m b/Watch/Extension/TGAvatarViewModel.m new file mode 100644 index 0000000000..4b4f31f30a --- /dev/null +++ b/Watch/Extension/TGAvatarViewModel.m @@ -0,0 +1,109 @@ +#import "TGAvatarViewModel.h" + +#import "TGBridgeContext.h" +#import "TGBridgeUser.h" +#import "TGBridgeChat.h" + +#import "TGStringUtils.h" +#import "TGWatchColor.h" + +#import "WKInterfaceGroup+Signals.h" +#import "TGBridgeMediaSignals.h" + +@interface TGAvatarViewModel () +{ + TGBridgeUser *_currentUser; + TGBridgeChat *_currentChat; +} +@end + +@implementation TGAvatarViewModel + +- (void)updateWithUser:(TGBridgeUser *)user context:(TGBridgeContext *)context isVisible:(bool (^)(void))isVisible +{ + TGBridgeUser *oldUser = _currentUser; + _currentUser = user; + + if (_currentUser.identifier == context.userId) + { + self.label.hidden = true; + self.group.backgroundColor = [UIColor hexColor:0x222223]; + [self.group setBackgroundImageSignal:[SSignal single:@"SavedMessagesAvatar"] isVisible:isVisible]; + } + else if (_currentUser.photoSmall.length > 0) + { + if (![_currentUser.photoSmall isEqualToString:oldUser.photoSmall]) + { + self.label.hidden = true; + self.group.backgroundColor = [UIColor hexColor:0x222223]; + + __block bool completed = false; + + __weak TGAvatarViewModel *weakSelf = self; + [self.group setBackgroundImageSignal:[[[TGBridgeMediaSignals avatarWithPeerId:_currentUser.identifier url:_currentUser.photoSmall type:TGBridgeMediaAvatarTypeSmall] onNext:^(id next) + { + completed = true; + }] onDispose:^ + { + __strong TGAvatarViewModel *strongSelf = weakSelf; + + if (strongSelf != nil && !completed) + strongSelf->_currentUser = nil; + }] isVisible:isVisible]; + } + } + else + { + if (oldUser.photoSmall.length > 0 || ![[oldUser displayName] isEqualToString:[_currentUser displayName]]) + { + self.label.hidden = false; + self.label.text = [TGStringUtils initialsForFirstName:_currentUser.firstName lastName:_currentUser.lastName single:true]; + self.group.backgroundColor = [TGColor colorForUserId:(int32_t)user.identifier myUserId:context.userId]; + } + } +} + +- (void)updateWithChat:(TGBridgeChat *)chat isVisible:(bool (^)(void))isVisible +{ + TGBridgeChat *oldChat = _currentChat; + _currentChat = chat; + + if (_currentChat.groupPhotoSmall.length > 0) + { + if (![_currentChat.groupPhotoSmall isEqualToString:oldChat.groupPhotoSmall]) + { + self.label.hidden = true; + self.group.backgroundColor = [UIColor hexColor:0x222223]; + + __block bool completed = false; + + __weak TGAvatarViewModel *weakSelf = self; + [self.group setBackgroundImageSignal:[[[TGBridgeMediaSignals avatarWithPeerId:_currentChat.identifier url:_currentChat.groupPhotoSmall type:TGBridgeMediaAvatarTypeSmall] onNext:^(id next) + { + completed = true; + }] onDispose:^ + { + __strong TGAvatarViewModel *strongSelf = weakSelf; + + if (strongSelf != nil && !completed) + strongSelf->_currentChat = nil; + }] isVisible:isVisible]; + } + } + else + { + if (oldChat.groupPhotoSmall.length > 0 || ![[oldChat groupTitle] isEqualToString:[_currentChat groupTitle]]) + { + self.label.hidden = false; + self.label.text = [TGStringUtils initialForGroupName:_currentChat.groupTitle]; + self.group.backgroundColor = [TGColor colorForGroupId:_currentChat.identifier]; + } + } +} + +- (void)updateIfNeeded +{ + [self.group updateIfNeeded]; +} + +@end diff --git a/Watch/Extension/TGBotCommandController.h b/Watch/Extension/TGBotCommandController.h new file mode 100644 index 0000000000..6f02461754 --- /dev/null +++ b/Watch/Extension/TGBotCommandController.h @@ -0,0 +1,22 @@ +#import "TGInterfaceController.h" + +@class SSignal; +@class TGBridgeContext; + +@interface TGBotCommandControllerContext : NSObject + +@property (nonatomic, strong) TGBridgeContext *context; +@property (nonatomic, strong) SSignal *commandListSignal; +@property (nonatomic, copy) void (^completionBlock)(NSString *command); + +@end + +@interface TGBotCommandController : TGInterfaceController + +@property (nonatomic, weak) IBOutlet WKInterfaceTable *table; +@property (nonatomic, weak) IBOutlet WKInterfaceImage *activityIndicator; + +@end + +extern NSString *const TGBotCommandUserKey; +extern NSString *const TGBotCommandListKey; \ No newline at end of file diff --git a/Watch/Extension/TGBotCommandController.m b/Watch/Extension/TGBotCommandController.m new file mode 100644 index 0000000000..7ff6606da7 --- /dev/null +++ b/Watch/Extension/TGBotCommandController.m @@ -0,0 +1,149 @@ +#import "TGBotCommandController.h" +#import "TGWatchCommon.h" +#import + +#import "TGBridgeUser.h" +#import "TGBridgeBotCommandInfo.h" + +#import "WKInterfaceTable+TGDataDrivenTable.h" + +#import "TGUserRowController.h" + +NSString *const TGBotCommandControllerIdentifier = @"TGBotCommandController"; + +NSString *const TGBotCommandKey = @"command"; +NSString *const TGBotCommandUserKey = @"user"; +NSString *const TGBotCommandListKey = @"list"; + +@implementation TGBotCommandControllerContext + +@end + + +@interface TGBotCommandController () +{ + SMetaDisposable *_commandDisposable; + NSArray *_commandList; + + TGBotCommandControllerContext *_context; +} +@end + +@implementation TGBotCommandController + +- (instancetype)init +{ + self = [super init]; + if (self != nil) + { + _commandDisposable = [[SMetaDisposable alloc] init]; + + [self.table _setInitialHidden:true]; + self.table.tableDataSource = self; + } + return self; +} + +- (void)dealloc +{ + [_commandDisposable dispose]; +} + +- (void)configureWithContext:(TGBotCommandControllerContext *)context +{ + _context = context; + + __weak TGBotCommandController *weakSelf = self; + [_commandDisposable setDisposable:[[context.commandListSignal deliverOn:[SQueue mainQueue]] startWithNext:^(NSArray *next) + { + __strong TGBotCommandController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + strongSelf->_commandList = next; + [strongSelf performInterfaceUpdate:^(bool animated) + { + __strong TGBotCommandController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + strongSelf.activityIndicator.hidden = true; + [strongSelf.table reloadData]; + strongSelf.table.hidden = false; + }]; + }]]; +} + +#pragma mark - + +- (Class)table:(WKInterfaceTable *)table rowControllerClassAtIndexPath:(TGIndexPath *)indexPath +{ + return [TGUserRowController class]; +} + +- (NSUInteger)numberOfRowsInTable:(WKInterfaceTable *)table section:(NSUInteger)section +{ + return [self numberOfAvailableCommands]; +} + +- (void)table:(WKInterfaceTable *)table updateRowController:(TGUserRowController *)controller forIndexPath:(TGIndexPath *)indexPath +{ + NSDictionary *dict = [self dictionaryForRow:indexPath.row]; + TGBridgeBotCommandInfo *commandInfo = dict[TGBotCommandKey]; + TGBridgeUser *botUser = dict[TGBotCommandUserKey]; + [controller updateWithBotCommandInfo:commandInfo botUser:botUser context:_context.context]; +} + +- (void)table:(WKInterfaceTable *)table didSelectRowAtIndexPath:(TGIndexPath *)indexPath +{ + [self dismissController]; + + NSDictionary *dict = [self dictionaryForRow:indexPath.row]; + TGBridgeBotCommandInfo *commandInfo = dict[TGBotCommandKey]; + TGBridgeUser *botUser = dict[TGBotCommandUserKey]; + + bool isSingleBot = (_commandList.count == 1); + NSString *mention = isSingleBot ? @"" : [NSString stringWithFormat:@"@%@", botUser.userName]; + NSString *command = [NSString stringWithFormat:@"/%@%@", commandInfo.command, mention]; + + if (_context.completionBlock != nil) + _context.completionBlock(command); +} + +- (NSDictionary *)dictionaryForRow:(NSUInteger)row +{ + NSRange currentRange = NSMakeRange(0, 0); + for (NSDictionary *dict in _commandList) + { + NSArray *commandList = dict[TGBotCommandListKey]; + currentRange = NSMakeRange(currentRange.location + currentRange.length, commandList.count); + + NSInteger transposedRow = row - currentRange.location; + if (transposedRow >= 0 && transposedRow < currentRange.length) + return @{ TGBotCommandUserKey: dict[TGBotCommandUserKey], TGBotCommandKey: commandList[transposedRow]}; + } + + return nil; +} + +- (NSUInteger)numberOfAvailableCommands +{ + NSUInteger count = 0; + for (NSDictionary *dict in _commandList) + { + id commandList = dict[TGBotCommandListKey]; + if ([commandList isKindOfClass:[NSArray class]]) + count += [commandList count]; + } + + return count; +} + +#pragma mark - + ++ (NSString *)identifier +{ + return TGBotCommandControllerIdentifier; +} + +@end diff --git a/Watch/Extension/TGBotKeyboardButtonController.h b/Watch/Extension/TGBotKeyboardButtonController.h new file mode 100644 index 0000000000..e0a7167670 --- /dev/null +++ b/Watch/Extension/TGBotKeyboardButtonController.h @@ -0,0 +1,11 @@ +#import "WKInterfaceTable+TGDataDrivenTable.h" + +@class TGBridgeBotReplyMarkupButton; + +@interface TGBotKeyboardButtonController : TGTableRowController + +@property (nonatomic, weak) IBOutlet WKInterfaceLabel *textLabel; + +- (void)updateWithButton:(TGBridgeBotReplyMarkupButton *)button; + +@end diff --git a/Watch/Extension/TGBotKeyboardButtonController.m b/Watch/Extension/TGBotKeyboardButtonController.m new file mode 100644 index 0000000000..2544be9c94 --- /dev/null +++ b/Watch/Extension/TGBotKeyboardButtonController.m @@ -0,0 +1,18 @@ +#import "TGBotKeyboardButtonController.h" +#import "TGBridgeBotReplyMarkup.h" + +NSString *const TGBotKeyboardButtonRowIdentifier = @"TGBotKeyboardButton"; + +@implementation TGBotKeyboardButtonController + +- (void)updateWithButton:(TGBridgeBotReplyMarkupButton *)button +{ + self.textLabel.text = button.text; +} + ++ (NSString *)identifier +{ + return TGBotKeyboardButtonRowIdentifier; +} + +@end diff --git a/Watch/Extension/TGBotKeyboardController.h b/Watch/Extension/TGBotKeyboardController.h new file mode 100644 index 0000000000..bd8386c582 --- /dev/null +++ b/Watch/Extension/TGBotKeyboardController.h @@ -0,0 +1,17 @@ +#import "TGInterfaceController.h" + +@class TGBridgeBotReplyMarkup; + +@interface TGBotKeyboardControllerContext : NSObject + +@property (nonatomic, strong) TGBridgeBotReplyMarkup *replyMarkup; +@property (nonatomic, copy) void (^completionBlock)(NSString *command); + +@end + + +@interface TGBotKeyboardController : TGInterfaceController + +@property (nonatomic, weak) IBOutlet WKInterfaceTable *table; + +@end diff --git a/Watch/Extension/TGBotKeyboardController.m b/Watch/Extension/TGBotKeyboardController.m new file mode 100644 index 0000000000..b91c155aa9 --- /dev/null +++ b/Watch/Extension/TGBotKeyboardController.m @@ -0,0 +1,106 @@ +#import "TGBotKeyboardController.h" +#import "TGWatchCommon.h" +#import "TGBridgeBotReplyMarkup.h" + +#import "WKInterfaceTable+TGDataDrivenTable.h" + +#import "TGBotKeyboardButtonController.h" + +NSString *const TGBotKeyboardControllerIdentifier = @"TGBotKeyboardController"; + +@implementation TGBotKeyboardControllerContext + +@end + + +@interface TGBotKeyboardController () +{ + TGBotKeyboardControllerContext *_context; + TGBridgeBotReplyMarkup *_replyMarkup; +} + +@end + +@implementation TGBotKeyboardController + +- (instancetype)init +{ + self = [super init]; + if (self != nil) + { + [self.table _setInitialHidden:true]; + self.table.tableDataSource = self; + } + return self; +} + +- (void)configureWithContext:(TGBotKeyboardControllerContext *)context +{ + _context = context; + _replyMarkup = context.replyMarkup; + + [self.table reloadData]; +} + +#pragma mark - + +- (NSUInteger)numberOfRowsInTable:(WKInterfaceTable *)table section:(NSUInteger)section +{ + return [self numberOfAvailableButtons]; +} + +- (Class)table:(WKInterfaceTable *)table rowControllerClassAtIndexPath:(TGIndexPath *)indexPath +{ + return [TGBotKeyboardButtonController class]; +} + +- (void)table:(WKInterfaceTable *)table updateRowController:(TGBotKeyboardButtonController *)controller forIndexPath:(TGIndexPath *)indexPath +{ + TGBridgeBotReplyMarkupButton *button = [self buttonForRow:indexPath.row]; + [controller updateWithButton:button]; +} + +- (void)table:(WKInterfaceTable *)table didSelectRowAtIndexPath:(TGIndexPath *)indexPath +{ + [self dismissController]; + + TGBridgeBotReplyMarkupButton *button = [self buttonForRow:indexPath.row]; + if (_context.completionBlock != nil) + _context.completionBlock(button.text); +} + +#pragma mark - + +- (TGBridgeBotReplyMarkupButton *)buttonForRow:(NSUInteger)row +{ + NSRange currentRange = NSMakeRange(0, 0); + for (TGBridgeBotReplyMarkupRow *markupRow in _replyMarkup.rows) + { + NSArray *buttons = markupRow.buttons; + currentRange = NSMakeRange(currentRange.location + currentRange.length, buttons.count); + + NSInteger transposedRow = row - currentRange.location; + if (transposedRow >= 0 && transposedRow < currentRange.length) + return buttons[transposedRow]; + } + + return nil; +} + +- (NSUInteger)numberOfAvailableButtons +{ + NSUInteger count = 0; + for (TGBridgeBotReplyMarkupRow *row in _replyMarkup.rows) + count += row.buttons.count; + + return count; +} + +#pragma mark - + ++ (NSString *)identifier +{ + return TGBotKeyboardControllerIdentifier; +} + +@end diff --git a/Watch/Extension/TGBridgeUserCache.h b/Watch/Extension/TGBridgeUserCache.h new file mode 100644 index 0000000000..533341e37c --- /dev/null +++ b/Watch/Extension/TGBridgeUserCache.h @@ -0,0 +1,19 @@ +#import + +@class TGBridgeUser; +@class TGBridgeBotInfo; + +@interface TGBridgeUserCache : NSObject + +- (TGBridgeUser *)userWithId:(int32_t)userId; +- (NSDictionary *)usersWithIndexSet:(NSIndexSet *)indexSet; +- (void)storeUser:(TGBridgeUser *)user; +- (void)storeUsers:(NSArray *)users; +- (NSArray *)applyUserChanges:(NSArray *)userChanges; + +- (TGBridgeBotInfo *)botInfoForUserId:(int32_t)userId; +- (void)storeBotInfo:(TGBridgeBotInfo *)botInfo forUserId:(int32_t)userId; + ++ (instancetype)instance; + +@end diff --git a/Watch/Extension/TGBridgeUserCache.m b/Watch/Extension/TGBridgeUserCache.m new file mode 100644 index 0000000000..5477c6bee4 --- /dev/null +++ b/Watch/Extension/TGBridgeUserCache.m @@ -0,0 +1,171 @@ +#import "TGBridgeUserCache.h" +#import "TGFileCache.h" + +#import + +#import "TGBridgeUser.h" +#import "TGBridgeBotInfo.h" + +@interface TGBridgeUserCache () +{ + NSMutableDictionary *_userByUid; + OSSpinLock _userByUidLock; + + NSMutableDictionary *_botInfoByUid; + OSSpinLock _botInfoByUidLock; + + TGFileCache *_fileCache; +} +@end + +@implementation TGBridgeUserCache + +- (instancetype)init +{ + self = [super init]; + if (self != nil) + { + _userByUid = [[NSMutableDictionary alloc] init]; + _botInfoByUid = [[NSMutableDictionary alloc] init]; + + _fileCache = [[TGFileCache alloc] initWithName:@"users" useMemoryCache:false]; + } + return self; +} + +- (TGBridgeUser *)userWithId:(int32_t)userId +{ + __block TGBridgeUser *user = nil; + + OSSpinLockLock(&_userByUidLock); + user = _userByUid[@(userId)]; + OSSpinLockUnlock(&_userByUidLock); + + return user; +} + +- (NSDictionary *)usersWithIndexSet:(NSIndexSet *)indexSet +{ + NSMutableDictionary *users = [[NSMutableDictionary alloc] init]; + NSMutableIndexSet *neededUsers = [indexSet mutableCopy]; + + NSMutableIndexSet *foundUsers = [[NSMutableIndexSet alloc] init]; + + OSSpinLockLock(&_userByUidLock); + [neededUsers enumerateIndexesUsingBlock:^(NSUInteger index, BOOL * _Nonnull stop) + { + TGBridgeUser *user = _userByUid[@(index)]; + if (user != nil) + { + users[@(index)] = user; + [foundUsers addIndex:index]; + } + }]; + OSSpinLockUnlock(&_userByUidLock); + + [neededUsers removeIndexes:foundUsers]; + + return users; +} + +- (void)storeUser:(TGBridgeUser *)user +{ + if (user == nil) + return; + + [self storeUsers:@[ user ]]; +} + +- (void)storeUsers:(NSArray *)users +{ + OSSpinLockLock(&_userByUidLock); + for (id peer in users) + { + if ([peer isKindOfClass:[TGBridgeUser class]]) + _userByUid[@(((TGBridgeUser *)peer).identifier)] = peer; + } + OSSpinLockUnlock(&_userByUidLock); +} + +- (NSArray *)applyUserChanges:(NSArray *)userChanges +{ + NSMutableArray *missedUserIds = [[NSMutableArray alloc] init]; + NSMutableArray *updatedUsers = [[NSMutableArray alloc] init]; + for (TGBridgeUserChange *change in userChanges) + { + TGBridgeUser *user = [self userWithId:change.userIdentifier]; + if (user != nil) + { + TGBridgeUser *updatedUser = [user userByApplyingChange:change]; + [updatedUsers addObject:updatedUser]; + } + else + { + [missedUserIds addObject:@(change.userIdentifier)]; + } + } + + [self storeUsers:updatedUsers]; + + if (missedUserIds.count == 0) + return nil; + + return missedUserIds; +} + +- (TGBridgeBotInfo *)botInfoForUserId:(int32_t)userId +{ + __block TGBridgeBotInfo *botInfo = nil; + + OSSpinLockLock(&_botInfoByUidLock); + botInfo = _botInfoByUid[@(userId)]; + OSSpinLockUnlock(&_botInfoByUidLock); + + if (botInfo == nil) + { + [_fileCache fetchDataForKey:[NSString stringWithFormat:@"botInfo-%d", userId] synchronous:true unserializeBlock:^id(NSData *data) + { + id object = [NSKeyedUnarchiver unarchiveObjectWithData:data]; + if ([object isKindOfClass:[TGBridgeBotInfo class]]) + return object; + + return nil; + } completion:^(TGBridgeBotInfo *result) + { + if (result != nil) + { + botInfo = result; + OSSpinLockLock(&_botInfoByUidLock); + _botInfoByUid[@(userId)] = botInfo; + OSSpinLockUnlock(&_botInfoByUidLock); + } + }]; + } + + return botInfo; +} + +- (void)storeBotInfo:(TGBridgeBotInfo *)botInfo forUserId:(int32_t)userId +{ + OSSpinLockLock(&_botInfoByUidLock); + _botInfoByUid[@(userId)] = botInfo; + + [_fileCache cacheData:botInfo key:[NSString stringWithFormat:@"botInfo-%d", userId] synchronous:true serializeBlock:^NSData *(NSObject *object) + { + return [NSKeyedArchiver archivedDataWithRootObject:object]; + } completion:nil]; + OSSpinLockUnlock(&_botInfoByUidLock); +} + ++ (instancetype)instance +{ + static dispatch_once_t onceToken; + static TGBridgeUserCache *userCache; + dispatch_once(&onceToken, ^ + { + userCache = [[TGBridgeUserCache alloc] init]; + }); + return userCache; +} + +@end diff --git a/Watch/Extension/TGChatInfo.h b/Watch/Extension/TGChatInfo.h new file mode 100644 index 0000000000..f18879c875 --- /dev/null +++ b/Watch/Extension/TGChatInfo.h @@ -0,0 +1,8 @@ +#import "WKInterfaceTable+TGDataDrivenTable.h" + +@interface TGChatInfo : NSObject + +@property (nonatomic, strong) NSString *title; +@property (nonatomic, strong) NSString *text; + +@end diff --git a/Watch/Extension/TGChatInfo.m b/Watch/Extension/TGChatInfo.m new file mode 100644 index 0000000000..0b8e83362a --- /dev/null +++ b/Watch/Extension/TGChatInfo.m @@ -0,0 +1,10 @@ +#import "TGChatInfo.h" + +@implementation TGChatInfo + +- (NSString *)uniqueIdentifier +{ + return @"chatInfo"; +} + +@end diff --git a/Watch/Extension/TGChatTimestamp.h b/Watch/Extension/TGChatTimestamp.h new file mode 100644 index 0000000000..dad1a37c03 --- /dev/null +++ b/Watch/Extension/TGChatTimestamp.h @@ -0,0 +1,10 @@ +#import "WKInterfaceTable+TGDataDrivenTable.h" + +@interface TGChatTimestamp : NSObject + +@property (nonatomic, readonly) NSTimeInterval date; +@property (nonatomic, readonly) NSString *string; + +- (instancetype)initWithDate:(NSTimeInterval)date string:(NSString *)string; + +@end diff --git a/Watch/Extension/TGChatTimestamp.m b/Watch/Extension/TGChatTimestamp.m new file mode 100644 index 0000000000..047d46f472 --- /dev/null +++ b/Watch/Extension/TGChatTimestamp.m @@ -0,0 +1,30 @@ +#import "TGChatTimestamp.h" + +@interface TGChatTimestamp () +{ + NSString *_cachedIdentifier; +} +@end + +@implementation TGChatTimestamp + +- (instancetype)initWithDate:(NSTimeInterval)date string:(NSString *)string +{ + self = [super init]; + if (self != nil) + { + _date = date; + _string = string; + } + return self; +} + +- (NSString *)uniqueIdentifier +{ + if (_cachedIdentifier == nil) + _cachedIdentifier = [NSString stringWithFormat:@"ts-%ld", (long)_date]; + + return _cachedIdentifier; +} + +@end diff --git a/Watch/Extension/TGComplicationController.h b/Watch/Extension/TGComplicationController.h new file mode 100644 index 0000000000..711f2e50f4 --- /dev/null +++ b/Watch/Extension/TGComplicationController.h @@ -0,0 +1,5 @@ +#import + +@interface TGComplicationController : NSObject + +@end diff --git a/Watch/Extension/TGComplicationController.m b/Watch/Extension/TGComplicationController.m new file mode 100644 index 0000000000..88e65cba75 --- /dev/null +++ b/Watch/Extension/TGComplicationController.m @@ -0,0 +1,78 @@ +#import "TGComplicationController.h" +#import "TGWatchCommon.h" +#import "TGStringUtils.h" + +@implementation TGComplicationController + +- (void)getSupportedTimeTravelDirectionsForComplication:(CLKComplication *)complication withHandler:(void (^)(CLKComplicationTimeTravelDirections))handler +{ + handler(CLKComplicationTimeTravelDirectionNone); +} + +- (void)getPrivacyBehaviorForComplication:(CLKComplication *)complication withHandler:(void (^)(CLKComplicationPrivacyBehavior))handler +{ + handler(CLKComplicationPrivacyBehaviorShowOnLockScreen); +} + +- (void)getPlaceholderTemplateForComplication:(CLKComplication *)complication withHandler:(void (^)(CLKComplicationTemplate * _Nullable))handler +{ + CLKComplicationTemplate *result = nil; + + switch (complication.family) + { + case CLKComplicationFamilyModularLarge: + { + + } + break; + + case CLKComplicationFamilyUtilitarianSmall: + { + + } + break; + + case CLKComplicationFamilyUtilitarianLarge: + { + CLKComplicationTemplateUtilitarianLargeFlat *template = [[CLKComplicationTemplateUtilitarianLargeFlat alloc] init]; + + CLKSimpleTextProvider *textProvider = [[CLKSimpleTextProvider alloc] init]; + textProvider.text = TGLocalized(@"Complication.LongNone"); + template.textProvider = textProvider; + result = template; + } + break; + + default: + break; + } + + handler(result); +} + +- (void)getCurrentTimelineEntryForComplication:(CLKComplication *)complication withHandler:(void (^)(CLKComplicationTimelineEntry * _Nullable))handler +{ + CLKComplicationTemplate *result = nil; + + switch (complication.family) + { + case CLKComplicationFamilyUtilitarianLarge: + { + CLKComplicationTemplateUtilitarianLargeFlat *template = [[CLKComplicationTemplateUtilitarianLargeFlat alloc] init]; + + CLKSimpleTextProvider *textProvider = [[CLKSimpleTextProvider alloc] init]; + textProvider.text = TGLocalized(@"Complication.LongNone"); + template.textProvider = textProvider; + result = template; + } + break; + + default: + break; + } + + CLKComplicationTimelineEntry *entry = [CLKComplicationTimelineEntry entryWithDate:[NSDate date] complicationTemplate:result]; + handler(entry); +} + +@end diff --git a/Watch/Extension/TGComposeController.h b/Watch/Extension/TGComposeController.h new file mode 100644 index 0000000000..60117e442d --- /dev/null +++ b/Watch/Extension/TGComposeController.h @@ -0,0 +1,24 @@ +#import "TGInterfaceController.h" + +@interface TGComposeController : TGInterfaceController + +@property (nonatomic, weak) IBOutlet WKInterfaceLabel *recipientLabel; +@property (nonatomic, weak) IBOutlet WKInterfaceLabel *messageLabel; +@property (nonatomic, weak) IBOutlet WKInterfaceButton *stickerButton; +@property (nonatomic, weak) IBOutlet WKInterfaceButton *locationButton; + +@property (nonatomic, weak) IBOutlet WKInterfaceButton *addContactButton; +@property (nonatomic, weak) IBOutlet WKInterfaceButton *createMessageButton; +@property (nonatomic, weak) IBOutlet WKInterfaceButton *sendButton; +@property (nonatomic, weak) IBOutlet WKInterfaceGroup *bottomGroup; + +@property (nonatomic, weak) IBOutlet WKInterfaceGroup *stickerGroup; +@property (nonatomic, weak) IBOutlet WKInterfaceImage *locationIcon; + +- (IBAction)addContactPressedAction; +- (IBAction)createMessagePressedAction; +- (IBAction)stickerPressedAction; +- (IBAction)locationPressedAction; +- (IBAction)sendPressedAction; + +@end diff --git a/Watch/Extension/TGComposeController.m b/Watch/Extension/TGComposeController.m new file mode 100644 index 0000000000..a2ab1c9ad6 --- /dev/null +++ b/Watch/Extension/TGComposeController.m @@ -0,0 +1,290 @@ +#import "TGComposeController.h" +#import "TGWatchCommon.h" +#import "TGBridgeSendMessageSignals.h" +#import "TGBridgeUser.h" + +#import "WKInterfaceGroup+Signals.h" +#import "TGBridgeMediaSignals.h" + +#import "TGContactsController.h" +#import "TGInputController.h" +#import "TGStickersController.h" +#import "TGLocationController.h" + +NSString *const TGComposeControllerIdentifier = @"TGComposeController"; + +@interface TGComposeController () +{ + TGBridgeUser *_recipient; + NSString *_messageText; + TGBridgeDocumentMediaAttachment *_messageSticker; + TGBridgeLocationMediaAttachment *_messageLocation; + + SMetaDisposable *_sendMessageDisposable; +} +@end + +@implementation TGComposeController + +- (instancetype)init +{ + self = [super init]; + if (self != nil) + { + _sendMessageDisposable = [[SMetaDisposable alloc] init]; + [self.locationIcon _setInitialHidden:true]; + [self.stickerGroup _setInitialHidden:true]; + } + return self; +} + +- (void)dealloc +{ + [_sendMessageDisposable dispose]; +} + +- (void)configureWithContext:(id)__unused context +{ + self.recipientLabel.text = TGLocalized(@"Watch.Compose.AddContact"); + self.messageLabel.text = TGLocalized(@"Watch.Compose.CreateMessage"); + [self setSendButtonEnabled:false]; +} + +- (void)willActivate +{ + [super willActivate]; + + [self.stickerGroup updateIfNeeded]; +} + +- (void)didDeactivate +{ + [super didDeactivate]; +} + +- (IBAction)addContactPressedAction +{ + [TGInputController presentPlainInputControllerForInterfaceController:self completion:^(NSString *text) + { + __weak TGComposeController *weakSelf = self; + + TGContactsControllerContext *context = [[TGContactsControllerContext alloc] initWithQuery:text]; + context.completionBlock = ^(TGBridgeUser *contact) + { + __strong TGComposeController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf setRecipient:contact]; + }; + + [self presentControllerWithClass:[TGContactsController class] context:context]; + }]; +} + +- (IBAction)createMessagePressedAction +{ + [TGInputController presentInputControllerForInterfaceController:self suggestionsForText:nil completion:^(NSString *text) + { + [self setMessageText:text]; + }]; +} + +- (IBAction)stickerPressedAction +{ + __weak TGComposeController *weakSelf = self; + TGStickersControllerContext *context = [[TGStickersControllerContext alloc] init]; + context.completionBlock = ^(TGBridgeDocumentMediaAttachment *sticker) + { + __strong TGComposeController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf setMessageSticker:sticker]; + }; + [self presentControllerWithClass:[TGStickersController class] context:context]; +} + +- (IBAction)locationPressedAction +{ + __weak TGComposeController *weakSelf = self; + TGLocationControllerContext *context = [[TGLocationControllerContext alloc] init]; + context.completionBlock = ^(TGBridgeLocationMediaAttachment *location) + { + __strong TGComposeController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf setMessageLocation:location]; + }; + [self presentControllerWithClass:[TGLocationController class] context:context]; +} + +- (IBAction)sendPressedAction +{ + __weak TGComposeController *weakSelf = self; + if (_messageSticker != nil) + { + [_sendMessageDisposable setDisposable:[[TGBridgeSendMessageSignals sendMessageWithPeerId:_recipient.identifier sticker:_messageSticker replyToMid:0] startWithNext:^(id next) + { + __strong TGComposeController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf dismissController]; + } completed:^ + { + + }]]; + } + else if (_messageLocation != nil) + { + [_sendMessageDisposable setDisposable:[[TGBridgeSendMessageSignals sendMessageWithPeerId:_recipient.identifier location:_messageLocation replyToMid:0] startWithNext:^(id next) + { + __strong TGComposeController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf dismissController]; + } completed:^ + { + + }]]; + } + else if (_messageText != nil) + { + [_sendMessageDisposable setDisposable:[[TGBridgeSendMessageSignals sendMessageWithPeerId:_recipient.identifier text:_messageText replyToMid:0] startWithNext:^(id next) + { + __strong TGComposeController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf dismissController]; + } completed:^{ + + }]]; + } +} + +- (void)setRecipient:(TGBridgeUser *)recipient +{ + _recipient = recipient; + + [self performInterfaceUpdate:^(bool animated) + { + if (recipient != nil) + { + self.recipientLabel.text = [recipient displayName]; + self.recipientLabel.textColor = [UIColor whiteColor]; + } + else + { + self.recipientLabel.text = TGLocalized(@"Watch.Compose.AddContact"); + self.recipientLabel.textColor = [UIColor hexColor:0xaeb4bf]; + } + + [self updateSendButtonEnabled]; + }]; +} + +- (void)setMessageText:(NSString *)messageText +{ + _messageSticker = nil; + _messageLocation = nil; + + _messageText = messageText; + + [self performInterfaceUpdate:^(bool animated) + { + self.stickerGroup.hidden = true; + self.locationIcon.hidden = true; + self.messageLabel.hidden = false; + + if (messageText.length > 0) + { + self.messageLabel.text = messageText; + self.messageLabel.textColor = [UIColor whiteColor]; + } + else + { + self.messageLabel.text = TGLocalized(@"Watch.Compose.CreateMessage"); + self.messageLabel.textColor = [UIColor hexColor:0xaeb4bf]; + } + + [self updateSendButtonEnabled]; + }]; +} + +- (void)setMessageSticker:(TGBridgeDocumentMediaAttachment *)messageSticker +{ + _messageText = nil; + _messageLocation = nil; + + _messageSticker = messageSticker; + + [self performInterfaceUpdate:^(bool animated) + { + self.stickerGroup.hidden = false; + self.locationIcon.hidden = true; + self.messageLabel.hidden = true; + self.messageLabel.text = @""; + + __weak TGComposeController *weakSelf = self; + [self.stickerGroup setBackgroundImageSignal:[TGBridgeMediaSignals stickerWithDocumentId:messageSticker.documentId packId:messageSticker.stickerPackId accessHash:messageSticker.stickerPackAccessHash type:TGMediaStickerImageTypeInput] isVisible:^bool + { + __strong TGComposeController *strongSelf = weakSelf; + if (strongSelf == nil) + return false; + + return strongSelf.isVisible; + }]; + + [self updateSendButtonEnabled]; + }]; +} + +- (void)setMessageLocation:(TGBridgeLocationMediaAttachment *)messageLocation +{ + _messageText = nil; + _messageSticker = nil; + + _messageLocation = messageLocation; + + [self performInterfaceUpdate:^(bool animated) + { + self.stickerGroup.hidden = true; + self.locationIcon.hidden = false; + self.messageLabel.hidden = false; + + if (messageLocation.venue != nil) + self.messageLabel.text = messageLocation.venue.title; + else + self.messageLabel.text = TGLocalized(@"Watch.Compose.CurrentLocation"); + self.messageLabel.textColor = [UIColor hexColor:0xaeb4bf]; + + [self updateSendButtonEnabled]; + }]; +} + +- (void)setSendButtonEnabled:(bool)enabled +{ + NSAttributedString *buttonTitle = [[NSAttributedString alloc] initWithString:TGLocalized(@"Watch.Compose.Send") attributes:@{ NSForegroundColorAttributeName:enabled ? [UIColor hexColor:0x2094fa] : [UIColor hexColor:0xaeb4bf], NSFontAttributeName: [UIFont systemFontOfSize:15] }]; + + self.sendButton.enabled = enabled; + self.sendButton.attributedTitle = buttonTitle; +} + +- (void)updateSendButtonEnabled +{ + bool hasRecipient = (_recipient != nil); + bool hasContent = (_messageText.length > 0 || _messageSticker != nil || _messageLocation != nil); + + [self setSendButtonEnabled:hasRecipient && hasContent]; +} + ++ (NSString *)identifier +{ + return TGComposeControllerIdentifier; +} + +@end diff --git a/Watch/Extension/TGContactsController.h b/Watch/Extension/TGContactsController.h new file mode 100644 index 0000000000..e0a701946d --- /dev/null +++ b/Watch/Extension/TGContactsController.h @@ -0,0 +1,23 @@ +#import "TGInterfaceController.h" + +@class TGBridgeContext; +@class TGBridgeUser; + +@interface TGContactsControllerContext : NSObject + +@property (nonatomic, strong) TGBridgeContext *context; +@property (nonatomic, readonly) NSString *query; +@property (nonatomic, copy) void (^completionBlock)(TGBridgeUser *user); + +- (instancetype)initWithQuery:(NSString *)query; + +@end + +@interface TGContactsController : TGInterfaceController + +@property (nonatomic, weak) IBOutlet WKInterfaceTable *table; +@property (nonatomic, weak) IBOutlet WKInterfaceImage *activityIndicator; + +@property (nonatomic, weak) IBOutlet WKInterfaceLabel *alertLabel; + +@end diff --git a/Watch/Extension/TGContactsController.m b/Watch/Extension/TGContactsController.m new file mode 100644 index 0000000000..821f23447a --- /dev/null +++ b/Watch/Extension/TGContactsController.m @@ -0,0 +1,154 @@ +#import "TGContactsController.h" +#import "TGWatchCommon.h" +#import "TGBridgeContactsSignals.h" + +#import "WKInterfaceTable+TGDataDrivenTable.h" + +#import "TGInputController.h" + +#import "TGUserRowController.h" + +NSString *const TGContactsControllerIdentifier = @"TGContactsController"; +const NSUInteger TGContactsControllerBatchCount = 15; + +@implementation TGContactsControllerContext + +- (instancetype)initWithQuery:(NSString *)query +{ + self = [super init]; + if (self != nil) + { + _query = query; + } + return self; +} + +@end + + +@interface TGContactsController () +{ + SMetaDisposable *_contactsDisposable; + + TGContactsControllerContext *_context; + NSArray *_userModels; +} +@end + +@implementation TGContactsController + +- (instancetype)init +{ + self = [super init]; + if (self != nil) + { + _contactsDisposable = [[SMetaDisposable alloc] init]; + + [self.alertLabel _setInitialHidden:true]; + [self.table _setInitialHidden:true]; + self.table.tableDataSource = self; + } + return self; +} + +- (void)dealloc +{ + [_contactsDisposable dispose]; +} + +- (void)configureWithContext:(TGContactsControllerContext *)context +{ + _context = context; + + __weak TGContactsController *weakSelf = self; + [_contactsDisposable setDisposable:[[[TGBridgeContactsSignals searchContactsWithQuery:_context.query] deliverOn:[SQueue mainQueue]] startWithNext:^(NSArray *users) + { + __strong TGContactsController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + if (users.count > TGContactsControllerBatchCount) + strongSelf->_userModels = [users subarrayWithRange:NSMakeRange(0, TGContactsControllerBatchCount)]; + else + strongSelf->_userModels = users; + + [strongSelf performInterfaceUpdate:^(bool animated) + { + __strong TGContactsController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + strongSelf.activityIndicator.hidden = true; + + if (strongSelf->_userModels.count > 0) + { + strongSelf.table.hidden = false; + [strongSelf.table reloadData]; + } + else + { + strongSelf->_alertLabel.hidden = false; + strongSelf->_alertLabel.text = TGLocalized(@"Watch.Contacts.NoResults"); + } + }]; + } error:^(id error) + { + + } completed:^ + { + + }]]; +} + +- (void)willActivate +{ + [super willActivate]; + + [self.table notifyVisiblityChange]; +} + +- (void)didDeactivate +{ + [super didDeactivate]; +} + +#pragma mark - + +- (NSUInteger)numberOfRowsInTable:(WKInterfaceTable *)table section:(NSUInteger)section +{ + return _userModels.count; +} + +- (Class)table:(WKInterfaceTable *)table rowControllerClassAtIndexPath:(NSIndexPath *)indexPath +{ + return [TGUserRowController class]; +} + +- (void)table:(WKInterfaceTable *)table updateRowController:(TGUserRowController *)controller forIndexPath:(TGIndexPath *)indexPath +{ + __weak TGContactsController *weakSelf = self; + controller.isVisible = ^bool + { + __strong TGContactsController *strongSelf = weakSelf; + if (strongSelf == nil) + return false; + + return strongSelf.isVisible; + }; + [controller updateWithUser:_userModels[indexPath.row] context:_context.context]; +} + +- (void)table:(WKInterfaceTable *)table didSelectRowAtIndexPath:(TGIndexPath *)indexPath +{ + [self dismissController]; + + if (_context.completionBlock != nil) + _context.completionBlock(_userModels[indexPath.row]); +} + ++ (NSString *)identifier +{ + return TGContactsControllerIdentifier; +} + +@end diff --git a/Watch/Extension/TGConversationFooterController.h b/Watch/Extension/TGConversationFooterController.h new file mode 100644 index 0000000000..5ede87b04d --- /dev/null +++ b/Watch/Extension/TGConversationFooterController.h @@ -0,0 +1,44 @@ +#import "WKInterfaceTable+TGDataDrivenTable.h" + +typedef NS_OPTIONS(NSUInteger, TGConversationFooterOptions) { + TGConversationFooterOptionsSendMessage = 1 << 0, + TGConversationFooterOptionsUnblock = 1 << 1, + TGConversationFooterOptionsStartBot = 1 << 2, + TGConversationFooterOptionsRestartBot = 1 << 3, + TGConversationFooterOptionsInactive = 1 << 4, + TGConversationFooterOptionsBotCommands = 1 << 5, + TGConversationFooterOptionsBotKeyboard = 1 << 6, + TGConversationFooterOptionsVoice = 1 << 7 +}; + +@interface TGConversationFooterController : TGTableRowController + +@property (nonatomic, weak) IBOutlet WKInterfaceGroup *attachmentsGroup; +@property (nonatomic, weak) IBOutlet WKInterfaceImage *commandsIcon; +@property (nonatomic, weak) IBOutlet WKInterfaceButton *commandsButton; +@property (nonatomic, weak) IBOutlet WKInterfaceButton *stickerButton; +@property (nonatomic, weak) IBOutlet WKInterfaceButton *locationButton; +@property (nonatomic, weak) IBOutlet WKInterfaceButton *voiceButton; +@property (nonatomic, weak) IBOutlet WKInterfaceButton *bottomButton; + +- (IBAction)commandsButtonPressedAction; +- (IBAction)stickerButtonPressedAction; +- (IBAction)locationButtonPressedAction; +- (IBAction)voiceButtonPressedAction; +- (IBAction)bottomButtonPressedAction; + +@property (nonatomic, assign) TGConversationFooterOptions options; +- (void)setOptions:(TGConversationFooterOptions)options animated:(bool)animated; + +@property (nonatomic, copy) void (^commandsPressed)(void); +@property (nonatomic, copy) void (^stickerPressed)(void); +@property (nonatomic, copy) void (^locationPressed)(void); +@property (nonatomic, copy) void (^voicePressed)(void); +@property (nonatomic, copy) void (^replyPressed)(void); +@property (nonatomic, copy) void (^unblockPressed)(void); +@property (nonatomic, copy) void (^startPressed)(void); +@property (nonatomic, copy) void (^restartPressed)(void); + +@property (nonatomic, copy) void (^animate)(void (^)(void)); + +@end diff --git a/Watch/Extension/TGConversationFooterController.m b/Watch/Extension/TGConversationFooterController.m new file mode 100644 index 0000000000..528b765f3a --- /dev/null +++ b/Watch/Extension/TGConversationFooterController.m @@ -0,0 +1,150 @@ +#import "TGConversationFooterController.h" +#import "TGWatchCommon.h" + +NSString *const TGConversationFooterIdentifier = @"TGConversationFooter"; + +@implementation TGConversationFooterController + +- (void)setOptions:(TGConversationFooterOptions)options +{ + [self setOptions:options animated:false]; +} + +- (void)setOptions:(TGConversationFooterOptions)options animated:(bool)animated +{ + void (^changeBlock)() = ^ + { + if (options == _options) + return; + + _options = options; + + bool isSendMessage = options & TGConversationFooterOptionsSendMessage; + bool isStartBot = options & TGConversationFooterOptionsStartBot; + bool isRestartBot = options & TGConversationFooterOptionsRestartBot; + bool isUnblock = options & TGConversationFooterOptionsUnblock; + bool isInactive = options & TGConversationFooterOptionsInactive; + + bool hasCommandsButton = options & TGConversationFooterOptionsBotCommands; + bool hasKeyboardButton = options & TGConversationFooterOptionsBotKeyboard; + bool hasVoiceButton = options & TGConversationFooterOptionsVoice; + + if (isSendMessage) + { + self.attachmentsGroup.hidden = false; + self.bottomButton.hidden = false; + self.bottomButton.title = TGLocalized(@"Watch.Conversation.Reply"); + + NSInteger buttonCount = 2; + CGFloat buttonWidth = 0.5f; + if (hasCommandsButton || hasKeyboardButton) + buttonCount += 1; + if (hasVoiceButton) + buttonCount += 1; + + buttonWidth = 1.0f / buttonCount; + + bool commandButtonHidden = (!hasCommandsButton && !hasKeyboardButton); + [self.commandsButton setHidden:commandButtonHidden]; + [self.voiceButton setHidden:!hasVoiceButton]; + + if (!commandButtonHidden) + [self.commandsIcon setImageNamed:hasCommandsButton ? @"BotCommandIcon": @"BotKeyboardIcon"]; + + [self.commandsButton setRelativeWidth:commandButtonHidden ? 0.0 : buttonWidth withAdjustment:0]; + [self.voiceButton setRelativeWidth:!hasVoiceButton ? 0.0 : buttonWidth withAdjustment:0]; + [self.stickerButton setRelativeWidth:buttonWidth withAdjustment:0]; + [self.locationButton setRelativeWidth:buttonWidth withAdjustment:0]; + } + else if (isStartBot) + { + self.attachmentsGroup.hidden = true; + self.bottomButton.hidden = false; + self.bottomButton.title = TGLocalized(@"Bot.Start"); + } + else if (isRestartBot) + { + self.attachmentsGroup.hidden = true; + self.bottomButton.hidden = false; + self.bottomButton.title = TGLocalized(@"Watch.Bot.Restart"); + } + else if (isUnblock) + { + self.attachmentsGroup.hidden = true; + self.bottomButton.hidden = false; + self.bottomButton.title = TGLocalized(@"Watch.Conversation.Unblock"); + } + else if (isInactive) + { + self.attachmentsGroup.hidden = true; + self.bottomButton.hidden = true; + } + }; + + if (animated) + self.animate(changeBlock); + else + changeBlock(); +} + +- (IBAction)commandsButtonPressedAction +{ + if (self.commandsPressed != nil) + self.commandsPressed(); +} + +- (IBAction)stickerButtonPressedAction +{ + if (self.stickerPressed != nil) + self.stickerPressed(); +} + +- (IBAction)locationButtonPressedAction +{ + if (self.locationPressed != nil) + self.locationPressed(); +} + +- (IBAction)voiceButtonPressedAction +{ + if (self.voicePressed != nil) + self.voicePressed(); +} + +- (IBAction)bottomButtonPressedAction +{ + bool isSendMessage = _options & TGConversationFooterOptionsSendMessage; + bool isStartBot = _options & TGConversationFooterOptionsStartBot; + bool isRestartBot = _options & TGConversationFooterOptionsRestartBot; + bool isUnblock = _options & TGConversationFooterOptionsUnblock; + + if (isSendMessage) + { + if (self.replyPressed != nil) + self.replyPressed(); + } + else if (isStartBot) + { + if (self.startPressed != nil) + self.startPressed(); + } + else if (isRestartBot) + { + if (self.restartPressed != nil) + self.restartPressed(); + } + else if (isUnblock) + { + if (self.unblockPressed != nil) + self.unblockPressed(); + } +} + +#pragma mark - + ++ (NSString *)identifier +{ + return TGConversationFooterIdentifier; +} + +@end diff --git a/Watch/Extension/TGDateUtils.h b/Watch/Extension/TGDateUtils.h new file mode 100644 index 0000000000..ded0b49e1a --- /dev/null +++ b/Watch/Extension/TGDateUtils.h @@ -0,0 +1,29 @@ +#import +#import "TGChatTimestamp.h" + +typedef enum +{ + TGDateRelativeSpanLately = -2, + TGDateRelativeSpanWithinAWeek = -3, + TGDateRelativeSpanWithinAMonth = -4, + TGDateRelativeSpanALongTimeAgo = -5 +} TGDateRelativeSpan; + +@interface TGDateUtils : NSObject + ++ (void)reset; + ++ (NSString *)stringForFullDate:(int)date; ++ (NSString *)stringForShortTime:(int)time; ++ (NSString *)stringForShortTime:(int)time daytimeVariant:(int *)daytimeVariant; ++ (NSString *)stringForDialogTime:(int)time; ++ (NSString *)stringForDayOfWeek:(int)date; ++ (NSString *)stringForMonthOfYear:(int)date; ++ (NSString *)stringForPreciseDate:(int)date; ++ (NSString *)stringForApproximateDate:(int)date; ++ (NSString *)stringForRelativeLastSeen:(int)date; ++ (NSString *)stringForMessageListDate:(int)date; + ++ (TGChatTimestamp *)timestampForDateIfNeeded:(int)date previousDate:(NSNumber *)previousDate; + +@end diff --git a/Watch/Extension/TGDateUtils.m b/Watch/Extension/TGDateUtils.m new file mode 100644 index 0000000000..73c8b09b9e --- /dev/null +++ b/Watch/Extension/TGDateUtils.m @@ -0,0 +1,547 @@ +#import "TGDateUtils.h" +#import "TGStringUtils.h" +#import "TGWatchCommon.h" +#import + +static bool value_dateHas12hFormat = false; +static __strong NSString *value_monthNamesGenShort[] = { + nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil +}; +static __strong NSString *value_monthNamesGenFull[] = { + nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil +}; +static __strong NSString *value_weekdayNamesShort[] = { + nil, nil, nil, nil, nil, nil, nil +}; +static __strong NSString *value_weekdayNamesFull[] = { + nil, nil, nil, nil, nil, nil, nil +}; + +static NSString *value_dialogTimeFormat = nil; + +static NSString *value_date_separator = @"."; +static bool value_monthFirst = false; + +static bool isArabic = false; +static bool isKorean = false; + +static bool TGDateUtilsInitialized = false; +static void initializeTGDateUtils() +{ + NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; + [dateFormatter setLocale:[NSLocale currentLocale]]; + [dateFormatter setDateStyle:NSDateFormatterNoStyle]; + [dateFormatter setTimeStyle:NSDateFormatterMediumStyle]; + NSTimeZone *timeZone = [NSTimeZone localTimeZone]; + [dateFormatter setTimeZone:timeZone]; + NSString *dateString = [dateFormatter stringFromDate:[NSDate date]]; + NSRange amRange = [dateString rangeOfString:[dateFormatter AMSymbol]]; + NSRange pmRange = [dateString rangeOfString:[dateFormatter PMSymbol]]; + value_dateHas12hFormat = !(amRange.location == NSNotFound && pmRange.location == NSNotFound); + + dateString = [NSDateFormatter dateFormatFromTemplate:@"MdY" options:0 locale:[NSLocale currentLocale]]; + if ([dateString rangeOfString:@"."].location != NSNotFound) + { + value_date_separator = @"."; + } + else if ([dateString rangeOfString:@"/"].location != NSNotFound) + { + value_date_separator = @"/"; + } + else if ([dateString rangeOfString:@"-"].location != NSNotFound) + { + value_date_separator = @"-"; + } + + if ([dateString rangeOfString:[NSString stringWithFormat:@"M%@d", value_date_separator]].location != NSNotFound) + { + value_monthFirst = true; + } + + NSString *identifier = [[NSLocale currentLocale] localeIdentifier]; + if ([identifier isEqualToString:@"ar"] || [identifier hasPrefix:@"ar_"]) + { + isArabic = true; + value_date_separator = @"\u060d"; + } + else if ([identifier isEqualToString:@"ko"] || [identifier hasPrefix:@"ko-"]) + { + isKorean = true; + } + + value_monthNamesGenShort[0] = TGLocalized(@"Month.ShortJanuary"); + value_monthNamesGenShort[1] = TGLocalized(@"Month.ShortFebruary"); + value_monthNamesGenShort[2] = TGLocalized(@"Month.ShortMarch"); + value_monthNamesGenShort[3] = TGLocalized(@"Month.ShortApril"); + value_monthNamesGenShort[4] = TGLocalized(@"Month.ShortMay"); + value_monthNamesGenShort[5] = TGLocalized(@"Month.ShortJune"); + value_monthNamesGenShort[6] = TGLocalized(@"Month.ShortJuly"); + value_monthNamesGenShort[7] = TGLocalized(@"Month.ShortAugust"); + value_monthNamesGenShort[8] = TGLocalized(@"Month.ShortSeptember"); + value_monthNamesGenShort[9] = TGLocalized(@"Month.ShortOctober"); + value_monthNamesGenShort[10] = TGLocalized(@"Month.ShortNovember"); + value_monthNamesGenShort[11] = TGLocalized(@"Month.ShortDecember"); + + value_monthNamesGenFull[0] = TGLocalized(@"Month.GenJanuary"); + value_monthNamesGenFull[1] = TGLocalized(@"Month.GenFebruary"); + value_monthNamesGenFull[2] = TGLocalized(@"Month.GenMarch"); + value_monthNamesGenFull[3] = TGLocalized(@"Month.GenApril"); + value_monthNamesGenFull[4] = TGLocalized(@"Month.GenMay"); + value_monthNamesGenFull[5] = TGLocalized(@"Month.GenJune"); + value_monthNamesGenFull[6] = TGLocalized(@"Month.GenJuly"); + value_monthNamesGenFull[7] = TGLocalized(@"Month.GenAugust"); + value_monthNamesGenFull[8] = TGLocalized(@"Month.GenSeptember"); + value_monthNamesGenFull[9] = TGLocalized(@"Month.GenOctober"); + value_monthNamesGenFull[10] = TGLocalized(@"Month.GenNovember"); + value_monthNamesGenFull[11] = TGLocalized(@"Month.GenDecember"); + + value_weekdayNamesShort[0] = TGLocalized(@"Weekday.ShortMonday"); + value_weekdayNamesShort[1] = TGLocalized(@"Weekday.ShortTuesday"); + value_weekdayNamesShort[2] = TGLocalized(@"Weekday.ShortWednesday"); + value_weekdayNamesShort[3] = TGLocalized(@"Weekday.ShortThursday"); + value_weekdayNamesShort[4] = TGLocalized(@"Weekday.ShortFriday"); + value_weekdayNamesShort[5] = TGLocalized(@"Weekday.ShortSaturday"); + value_weekdayNamesShort[6] = TGLocalized(@"Weekday.ShortSunday"); + + value_weekdayNamesFull[0] = TGLocalized(@"Weekday.Monday"); + value_weekdayNamesFull[1] = TGLocalized(@"Weekday.Tuesday"); + value_weekdayNamesFull[2] = TGLocalized(@"Weekday.Wednesday"); + value_weekdayNamesFull[3] = TGLocalized(@"Weekday.Thursday"); + value_weekdayNamesFull[4] = TGLocalized(@"Weekday.Friday"); + value_weekdayNamesFull[5] = TGLocalized(@"Weekday.Saturday"); + value_weekdayNamesFull[6] = TGLocalized(@"Weekday.Sunday"); + + value_dialogTimeFormat = [[TGLocalized(@"Date.DialogDateFormat") stringByReplacingOccurrencesOfString:@"{month}" withString:@"%1$@"] stringByReplacingOccurrencesOfString:@"{day}" withString:@"%2$@"]; + + TGDateUtilsInitialized = true; +} + +static inline bool dateHas12hFormat() +{ + if (!TGDateUtilsInitialized) + initializeTGDateUtils(); + + return value_dateHas12hFormat; +} + +bool TGUse12hDateFormat() +{ + if (!TGDateUtilsInitialized) + initializeTGDateUtils(); + + return value_dateHas12hFormat; +} + +static inline NSString *weekdayNameShort(int number) +{ + if (!TGDateUtilsInitialized) + initializeTGDateUtils(); + + if (number < 0) + number = 0; + if (number > 6) + number = 6; + + if (number == 0) + number = 6; + else + number--; + + return value_weekdayNamesShort[number]; +} + +static inline NSString *weekdayNameFull(int number) +{ + if (!TGDateUtilsInitialized) + initializeTGDateUtils(); + + if (number < 0) + number = 0; + if (number > 6) + number = 6; + + if (number == 0) + number = 6; + else + number--; + + return value_weekdayNamesFull[number]; +} + +static inline NSString *monthNameGenFull(int number) +{ + if (!TGDateUtilsInitialized) + initializeTGDateUtils(); + + if (number < 0) + number = 0; + if (number > 11) + number = 11; + + return value_monthNamesGenFull[number]; +} + +static inline NSString *dialogTimeFormat() +{ + if (!TGDateUtilsInitialized) + initializeTGDateUtils(); + + return value_dialogTimeFormat; +} + +@implementation TGDateUtils + ++ (void)reset +{ + TGDateUtilsInitialized = false; +} + ++ (NSString *)stringForShortTime:(int)time +{ + time_t t = time; + struct tm timeinfo; + localtime_r(&t, &timeinfo); + + return [self stringForShortTimeWithHours:timeinfo.tm_hour minutes:timeinfo.tm_min]; +} + ++ (NSString *)stringForShortTimeWithHours:(int)hours minutes:(int)minutes +{ + if (!TGDateUtilsInitialized) + initializeTGDateUtils(); + + if (isArabic) + { + if (dateHas12hFormat()) + { + if (hours < 12) + return [TGStringUtils stringWithLocalizedNumberCharacters:[[NSString alloc] initWithFormat:@"%d:%02d ص", hours == 0 ? 12 : hours, minutes]]; + else + return [TGStringUtils stringWithLocalizedNumberCharacters:[[NSString alloc] initWithFormat:@"%d:%02d م", (hours - 12 == 0) ? 12 : (hours - 12), minutes]]; + } + else + return [TGStringUtils stringWithLocalizedNumberCharacters:[[NSString alloc] initWithFormat:@"%02d:%02d", hours, minutes]]; + } + else if (isKorean) + { + return [[NSString alloc] initWithFormat:@"%02d:%02d", hours, minutes]; + } + else + { + if (dateHas12hFormat()) + { + if (hours < 12) + return [[NSString alloc] initWithFormat:@"%d:%02d AM", hours == 0 ? 12 : hours, minutes]; + else + return [[NSString alloc] initWithFormat:@"%d:%02d PM", (hours - 12 == 0) ? 12 : (hours - 12), minutes]; + } + else + return [[NSString alloc] initWithFormat:@"%02d:%02d", hours, minutes]; + } +} + ++ (NSString *)stringForShortTime:(int)time daytimeVariant:(int *)__unused daytimeVariant +{ + return [self stringForShortTime:time]; +} + ++ (NSString *)stringForDialogTime:(int)time +{ + time_t t = time; + struct tm timeinfo; + gmtime_r(&t, &timeinfo); + + return [[NSString alloc] initWithFormat:dialogTimeFormat(), monthNameGenFull(timeinfo.tm_mon), [TGStringUtils stringWithLocalizedNumber:timeinfo.tm_mday]]; +} + ++ (NSString *)stringForDayOfWeek:(int)date +{ + time_t t = date; + struct tm timeinfo; + localtime_r(&t, &timeinfo); + + return weekdayNameFull(timeinfo.tm_wday); +} + ++ (NSString *)stringForMonthOfYear:(int)date +{ + time_t t = date; + struct tm timeinfo; + localtime_r(&t, &timeinfo); + + NSString *format = TGLocalized(([[NSString alloc] initWithFormat:@"Time.MonthOfYear_m%d", (int)timeinfo.tm_mon + 1])); + + return [[NSString alloc] initWithFormat:format, [[NSString alloc] initWithFormat:@"%d", 2000 + timeinfo.tm_year - 100]]; +} + ++ (NSString *)stringForFullDateWithDay:(int)day month:(int)month year:(int)year +{ + if (isArabic) + { + return [TGStringUtils stringWithLocalizedNumberCharacters:[[NSString alloc] initWithFormat:@"%d%@%d%@%02d", day, value_date_separator, month, value_date_separator, year - 100]]; + } + else if (isKorean) + { + return [TGStringUtils stringWithLocalizedNumberCharacters:[[NSString alloc] initWithFormat:@"%04d년 %d월 %d일", year - 100 + 2000, month, day]]; + } + else + { + if (value_monthFirst) + { + return [[NSString alloc] initWithFormat:@"%d%@%d%@%02d", month, value_date_separator, day, value_date_separator, year - 100]; + } + else + { + return [[NSString alloc] initWithFormat:@"%d%@%02d%@%02d", day, value_date_separator, month, value_date_separator, year - 100]; + } + } +} + ++ (NSString *)stringForPreciseDate:(int)date +{ + time_t t = date; + struct tm timeinfo; + localtime_r(&t, &timeinfo); + + NSString *format = TGLocalized(([[NSString alloc] initWithFormat:@"Time.PreciseDate_m%d", (int)timeinfo.tm_mon + 1])); + return [[NSString alloc] initWithFormat:format, [[NSString alloc] initWithFormat:@"%d", timeinfo.tm_mday], [[NSString alloc] initWithFormat:@"%d", (int)(2000 + timeinfo.tm_year - 100)], [self stringForShortTimeWithHours:timeinfo.tm_hour minutes:timeinfo.tm_min]]; +} + ++ (NSString *)stringForMessageListDate:(int)date +{ + time_t t = date; + struct tm timeinfo; + localtime_r(&t, &timeinfo); + + time_t t_now; + time(&t_now); + struct tm timeinfo_now; + localtime_r(&t_now, &timeinfo_now); + + if (timeinfo.tm_year != timeinfo_now.tm_year) + { + return [self stringForFullDateWithDay:timeinfo.tm_mday month:timeinfo.tm_mon + 1 year:timeinfo.tm_year]; + } + else + { + int dayDiff = timeinfo.tm_yday - timeinfo_now.tm_yday; + + if(dayDiff == 0) + return [self stringForShortTime:date]; + else if(dayDiff == -1) + return weekdayNameFull(timeinfo.tm_wday); + else if(dayDiff == -2) + return weekdayNameFull(timeinfo.tm_wday); + else if(dayDiff > -7 && dayDiff <= -2) + return weekdayNameFull(timeinfo.tm_wday); + else + return [self stringForFullDateWithDay:timeinfo.tm_mday month:timeinfo.tm_mon + 1 year:timeinfo.tm_year]; + } + + return nil; +} + ++ (NSString *)stringForApproximateDate:(int)date +{ + time_t t = date; + struct tm timeinfo; + localtime_r(&t, &timeinfo); + + time_t t_now; + time(&t_now); + struct tm timeinfo_now; + localtime_r(&t_now, &timeinfo_now); + + if (timeinfo.tm_year != timeinfo_now.tm_year) + return [self stringForFullDateWithDay:timeinfo.tm_mday month:timeinfo.tm_mon + 1 year:timeinfo.tm_year]; + else + { + int dayDiff = timeinfo.tm_yday - timeinfo_now.tm_yday; + + if(dayDiff == 0 || dayDiff == -1) + return [self stringForTodayOrYesterday:dayDiff == 0 hours:timeinfo.tm_hour minutes:timeinfo.tm_min]; + else if (false && dayDiff > -7 && dayDiff <= -2) + return weekdayNameShort(timeinfo.tm_wday); + else + return [self stringForFullDateWithDay:timeinfo.tm_mday month:timeinfo.tm_mon + 1 year:timeinfo.tm_year]; + } + + return nil; +} + ++ (NSString *)stringForFullDate:(int)date +{ + time_t t = date; + struct tm timeinfo; + localtime_r(&t, &timeinfo); + + time_t t_now; + time(&t_now); + struct tm timeinfo_now; + localtime_r(&t_now, &timeinfo_now); + + return [self stringForFullDateWithDay:timeinfo.tm_mday month:timeinfo.tm_mon + 1 year:timeinfo.tm_year]; +} + ++ (NSString *)stringForTodayOrYesterday:(bool)today hours:(int)hours minutes:(int)minutes +{ + NSString *timeString = [self stringForShortTimeWithHours:hours minutes:minutes]; + + return [[NSString alloc] initWithFormat:today ? TGLocalized(@"Time.TodayAt") : TGLocalized(@"Time.YesterdayAt"), timeString]; +} + ++ (NSString *)stringForRelativeLastSeen:(int)date +{ + if (date == -1) + return TGLocalized(@"Presence.invisible"); + else if (date == TGDateRelativeSpanLately) + return TGLocalized(@"Watch.LastSeen.Lately"); + else if (date == TGDateRelativeSpanWithinAWeek) + return TGLocalized(@"Watch.LastSeen.WithinAWeek"); + else if (date == TGDateRelativeSpanWithinAMonth) + return TGLocalized(@"Watch.LastSeen.WithinAMonth"); + else if (date == TGDateRelativeSpanALongTimeAgo) + return TGLocalized(@"Watch.LastSeen.ALongTimeAgo"); + else if (date <= 0) + return @" "; + + time_t t = date; + struct tm timeinfo; + localtime_r(&t, &timeinfo); + + time_t t_now; + time(&t_now); + struct tm timeinfo_now; + localtime_r(&t_now, &timeinfo_now); + + if (timeinfo.tm_year != timeinfo_now.tm_year) + return [[NSString alloc] initWithFormat:TGLocalized(@"Watch.LastSeen.AtDate"), [self stringForFullDateWithDay:timeinfo.tm_mday month:timeinfo.tm_mon + 1 year:timeinfo.tm_year]]; + else + { + int dayDiff = timeinfo.tm_yday - timeinfo_now.tm_yday; + + int minutesDiff = (int)((t_now - date) / 60); + int hoursDiff = (int)((t_now - date) / (60 * 60)); + + if (dayDiff == 0 && hoursDiff <= 23) + { + if (minutesDiff < 1) + return TGLocalized(@"Watch.LastSeen.JustNow"); + else if (minutesDiff < 60) + { + if (minutesDiff == 1) + return TGLocalized(@"Watch.LastSeen.MinutesAgo_1"); + else if (minutesDiff == 2) + return TGLocalized(@"Watch.LastSeen.MinutesAgo_2"); + else if (minutesDiff >= 3 && minutesDiff <= 10) + return [[NSString alloc] initWithFormat:TGLocalized(@"Watch.LastSeen.MinutesAgo_3_10"), [TGStringUtils stringWithLocalizedNumber:minutesDiff]]; + else + return [[NSString alloc] initWithFormat:TGLocalized(@"Watch.LastSeen.MinutesAgo_any"), [TGStringUtils stringWithLocalizedNumber:minutesDiff]]; + } + else + { + if (hoursDiff == 1) + return TGLocalized(@"Watch.LastSeen.HoursAgo_1"); + else if (hoursDiff == 2) + return TGLocalized(@"Watch.LastSeen.HoursAgo_2"); + else if (hoursDiff >= 3 && hoursDiff <= 10) + return [[NSString alloc] initWithFormat:TGLocalized(@"Watch.LastSeen.HoursAgo_3_10"), [TGStringUtils stringWithLocalizedNumber:hoursDiff]]; + else + return [[NSString alloc] initWithFormat:TGLocalized(@"Watch.LastSeen.HoursAgo_any"), [TGStringUtils stringWithLocalizedNumber:hoursDiff]]; + } + } + return [[NSString alloc] initWithFormat:TGLocalized(@"Watch.LastSeen.AtDate"), [self stringForFullDateWithDay:timeinfo.tm_mday month:timeinfo.tm_mon + 1 year:timeinfo.tm_year]]; + } + + return nil; +} + ++ (TGChatTimestamp *)timestampForDateIfNeeded:(int)date previousDate:(NSNumber *)previousDate +{ + if (previousDate == nil) + return [self timestampForDate:date]; + + TGChatTimestamp *timestamp = nil; + + time_t t = date; + struct tm timeinfo; + localtime_r(&t, &timeinfo); + + time_t t_prev = previousDate.integerValue; + struct tm timeinfo_prev; + localtime_r(&t_prev, &timeinfo_prev); + + time_t t_now; + time(&t_now); + struct tm timeinfo_now; + localtime_r(&t_now, &timeinfo_now); + + bool timestampNeeded = false; + + if (timeinfo.tm_yday != timeinfo_prev.tm_yday) + { + timestampNeeded = true; + } + else + { + if (timeinfo.tm_year != timeinfo_prev.tm_year) + { + timestampNeeded = true; + } + else + { + if (timeinfo.tm_year == timeinfo_now.tm_year) + { + if (timeinfo_now.tm_yday - timeinfo.tm_yday < 7) + { + if (abs(t - t_prev) > 3600) + timestampNeeded = true; + } + } + } + } + + if (timestampNeeded) + timestamp = [self timestampForDate:date]; + + return timestamp; +} + ++ (TGChatTimestamp *)timestampForDate:(int)date +{ + time_t t = date; + struct tm timeinfo; + localtime_r(&t, &timeinfo); + + time_t t_now; + time(&t_now); + struct tm timeinfo_now; + localtime_r(&t_now, &timeinfo_now); + + NSString *string = nil; + if (timeinfo.tm_year != timeinfo_now.tm_year) + { + string = [self stringForFullDateWithDay:timeinfo.tm_mday month:timeinfo.tm_mon + 1 year:timeinfo.tm_year]; + } + else + { + int dayDiff = timeinfo.tm_yday - timeinfo_now.tm_yday; + + if (dayDiff == 0 || dayDiff == -1) + { + string = [NSString stringWithFormat:dayDiff == 0 ? TGLocalized(@"Watch.Time.ShortTodayAt") : TGLocalized(@"Watch.Time.ShortYesterdayAt"), [self stringForShortTimeWithHours:timeinfo.tm_hour minutes:timeinfo.tm_min]]; + } + else if (dayDiff > -7) + { + string = [NSString stringWithFormat:TGLocalized(@"Watch.Time.ShortWeekdayAt"), [self stringForDayOfWeek:timeinfo.tm_wday], [self stringForShortTimeWithHours:timeinfo.tm_hour minutes:timeinfo.tm_min]]; + } + else + { + string = [NSString stringWithFormat:TGLocalized(@"Watch.Time.ShortFullAt"), [self stringForFullDateWithDay:timeinfo.tm_mday month:timeinfo.tm_mon + 1 year:timeinfo.tm_year], [self stringForShortTimeWithHours:timeinfo.tm_hour minutes:timeinfo.tm_min]]; + } + } + + return [[TGChatTimestamp alloc] initWithDate:date string:string]; +} + +@end diff --git a/Watch/Extension/TGExtensionDelegate.h b/Watch/Extension/TGExtensionDelegate.h new file mode 100644 index 0000000000..41c6258b64 --- /dev/null +++ b/Watch/Extension/TGExtensionDelegate.h @@ -0,0 +1,32 @@ +#import + +@class TGNeoChatsController; +@class TGFileCache; + +typedef enum +{ + TGContentSizeCategoryXS, + TGContentSizeCategoryS, + TGContentSizeCategoryM, + TGContentSizeCategoryL, + TGContentSizeCategoryXL, + TGContentSizeCategoryXXL, + TGContentSizeCategoryXXXL +} TGContentSizeCategory; + +@interface TGExtensionDelegate : NSObject + +@property (nonatomic, readonly) TGFileCache *audioCache; +@property (nonatomic, readonly) TGFileCache *imageCache; + +@property (nonatomic, readonly) TGNeoChatsController *chatsController; + +@property (nonatomic, readonly) TGContentSizeCategory contentSizeCategory; + +- (void)setCustomLocalizationFile:(NSURL *)fileUrl; + ++ (NSString *)documentsPath; + ++ (instancetype)instance; + +@end diff --git a/Watch/Extension/TGExtensionDelegate.m b/Watch/Extension/TGExtensionDelegate.m new file mode 100644 index 0000000000..60c912675c --- /dev/null +++ b/Watch/Extension/TGExtensionDelegate.m @@ -0,0 +1,117 @@ +#import "TGExtensionDelegate.h" +#import "TGWatchCommon.h" +#import "TGFileCache.h" +#import "TGBridgeClient.h" +#import "TGDateUtils.h" +#import "TGNeoChatsController.h" + +@interface TGExtensionDelegate () +{ + NSString *_cachedContentSize; + TGContentSizeCategory _sizeCategory; +} +@end + +@implementation TGExtensionDelegate + +- (instancetype)init +{ + self = [super init]; + if (self != nil) + { + TGLog(@"Extension initialization start"); + [TGBridgeClient instance]; + + _audioCache = [[TGFileCache alloc] initWithName:@"audio" useMemoryCache:false]; + _audioCache.defaultFileExtension = @"m4a"; + + _imageCache = [[TGFileCache alloc] initWithName:@"images" useMemoryCache:true]; + } + return self; +} + +- (TGNeoChatsController *)chatsController +{ + return (TGNeoChatsController *)[WKExtension sharedExtension].rootInterfaceController; +} + +- (void)applicationDidBecomeActive +{ + [[TGBridgeClient instance] handleDidBecomeActive]; +} + +- (void)applicationWillResignActive +{ + [[TGBridgeClient instance] handleWillResignActive]; +} + +- (void)didReceiveRemoteNotification:(NSDictionary *)userInfo +{ + +} + +- (void)didReceiveLocalNotification:(UILocalNotification *)notification +{ + +} + +- (void)setCustomLocalizationFile:(NSURL *)fileUrl +{ + if (fileUrl == nil) + TGResetLocalization(); + else + TGSetLocalizationFromFile(fileUrl); + + [TGDateUtils reset]; + [[self chatsController] resetLocalization]; +} + +- (TGContentSizeCategory)contentSizeCategory +{ + NSString *contentSize = [WKInterfaceDevice currentDevice].preferredContentSizeCategory; + if (![_cachedContentSize isEqualToString:contentSize]) + { + _cachedContentSize = contentSize; + _sizeCategory = [TGExtensionDelegate contentSizeCategoryForString:contentSize]; + } + + return _sizeCategory; +} + ++ (TGContentSizeCategory)contentSizeCategoryForString:(NSString *)string +{ + if ([string isEqualToString:@"UICTContentSizeCategoryXS"]) + return TGContentSizeCategoryXS; + else if ([string isEqualToString:@"UICTContentSizeCategoryS"]) + return TGContentSizeCategoryS; + else if ([string isEqualToString:@"UICTContentSizeCategoryM"]) + return TGContentSizeCategoryM; + else if ([string isEqualToString:@"UICTContentSizeCategoryL"]) + return TGContentSizeCategoryL; + else if ([string isEqualToString:@"UICTContentSizeCategoryXL"]) + return TGContentSizeCategoryXL; + else if ([string isEqualToString:@"UICTContentSizeCategoryXXL"]) + return TGContentSizeCategoryXXL; + else if ([string isEqualToString:@"UICTContentSizeCategoryXXXL"]) + return TGContentSizeCategoryXXXL; + + return TGContentSizeCategoryL; +} + ++ (NSString *)documentsPath +{ + static dispatch_once_t onceToken; + static NSString *path; + dispatch_once(&onceToken, ^ + { + path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, true)[0]; + }); + return path; +} + ++ (instancetype)instance +{ + return (TGExtensionDelegate *)[[WKExtension sharedExtension] delegate]; +} + +@end diff --git a/Watch/Extension/TGFileCache.h b/Watch/Extension/TGFileCache.h new file mode 100644 index 0000000000..ebb93c5f62 --- /dev/null +++ b/Watch/Extension/TGFileCache.h @@ -0,0 +1,22 @@ +#import + +@interface TGFileCache : NSObject + +@property (nonatomic, strong) NSString *defaultFileExtension; + +- (instancetype)initWithName:(NSString *)name useMemoryCache:(bool)useMemoryCache; + +- (void)fetchDataForKey:(NSString *)key synchronous:(bool)synchronous unserializeBlock:(id (^)(NSData *))unserializeBlock completion:(void (^)(id))completion; +- (void)fetchDataForKey:(NSString *)key memoryOnly:(bool)memoryOnly synchronous:(bool)synchronous unserializeBlock:(id (^)(NSData *))unserializeBlock completion:(void (^)(id))completion; +- (void)cacheData:(NSData *)data key:(NSString *)key synchronous:(bool)synchronous completion:(void (^)(NSURL *))completion; +- (void)cacheData:(NSObject *)data key:(NSString *)key synchronous:(bool)synchronous serializeBlock:(NSData *(^)(NSObject *))serializeBlock completion:(void (^)(NSURL *))completion; +- (void)cacheFileAtURL:(NSURL *)url key:(NSString *)key synchronous:(bool)synchronous completion:(void (^)(NSURL *))completion; +- (void)cacheFileAtURL:(NSURL *)url key:(NSString *)key synchronous:(bool)synchronous unserializeBlock:(id (^)(NSData *))unserializeBlock completion:(void (^)(NSURL *))completion; +- (void)cacheData:(NSData *)data key:(NSString *)key synchronous:(bool)synchronous unserializeBlock:(id (^)(NSData *))unserializeBlock completion:(void (^)(NSURL *))completion; +- (void)clearCacheSynchronous:(bool)synchronous; + +- (bool)hasDataForKey:(NSString *)key; +- (NSURL *)urlForKey:(NSString *)key; + + +@end diff --git a/Watch/Extension/TGFileCache.m b/Watch/Extension/TGFileCache.m new file mode 100644 index 0000000000..4108b2549f --- /dev/null +++ b/Watch/Extension/TGFileCache.m @@ -0,0 +1,208 @@ +#import "TGFileCache.h" +#import "TGStringUtils.h" + +NSString *const TGFileCacheDomain = @"com.telegram.FileCache"; + +@interface TGFileCache () +{ + NSCache *_memoryCache; + dispatch_queue_t _queue; + NSURL *_url; +} +@end + +@implementation TGFileCache + +- (instancetype)init +{ + return [self initWithName:nil useMemoryCache:true]; +} + +- (instancetype)initWithName:(NSString *)name useMemoryCache:(bool)useMemoryCache +{ + self = [super init]; + if (self != nil) + { + if (useMemoryCache) + _memoryCache = [[NSCache alloc] init]; + _queue = dispatch_queue_create(TGFileCacheDomain.UTF8String, nil); + _url = [NSURL fileURLWithPath:name relativeToURL:[TGFileCache baseURL]]; + + dispatch_async(_queue, ^ + { + if (![[NSFileManager defaultManager] fileExistsAtPath:_url.path]) + { + NSError *error; + [[NSFileManager defaultManager] createDirectoryAtURL:_url withIntermediateDirectories:true attributes:nil error:&error]; + } + }); + } + return self; +} + +- (void)fetchDataForKey:(NSString *)key synchronous:(bool)synchronous unserializeBlock:(id (^)(NSData *))unserializeBlock completion:(void (^)(id))completion +{ + [self fetchDataForKey:key memoryOnly:false synchronous:synchronous unserializeBlock:unserializeBlock completion:completion]; +} + +- (void)fetchDataForKey:(NSString *)key memoryOnly:(bool)memoryOnly synchronous:(bool)synchronous unserializeBlock:(id (^)(NSData *))unserializeBlock completion:(void (^)(id))completion +{ + if (completion == nil) + return; + + void (^block)(void) = ^ + { + id cachedObject = [_memoryCache objectForKey:key]; + if (cachedObject != nil) + { + completion(cachedObject); + return; + } + + if (!memoryOnly) + { + NSData *data = [[NSData alloc] initWithContentsOfURL:[self urlForKey:key] options:kNilOptions error:nil]; + if (data.length > 0) + { + id result = data; + if (unserializeBlock != nil) + { + result = unserializeBlock(data); + [_memoryCache setObject:result forKey:key]; + } + + completion(result); + return; + } + } + + completion(nil); + }; + + if (synchronous) + dispatch_sync(_queue, block); + else + dispatch_async(_queue, block); +} + +- (void)cacheData:(NSData *)data key:(NSString *)key synchronous:(bool)synchronous completion:(void (^)(NSURL *))completion +{ + [self cacheData:data key:key synchronous:synchronous serializeBlock:nil completion:completion]; +} + +- (void)cacheData:(NSObject *)data key:(NSString *)key synchronous:(bool)synchronous serializeBlock:(NSData *(^)(NSObject *))serializeBlock completion:(void (^)(NSURL *))completion +{ + void (^block)(void) = ^ + { + NSURL *url = [self urlForKey:key]; + NSData *serializedData = nil; + if (serializeBlock != nil) + serializedData = serializeBlock(data); + else if ([data isKindOfClass:[NSData class]]) + serializedData = (NSData *)data; + + [[NSFileManager defaultManager] removeItemAtURL:url error:nil]; + + [serializedData writeToURL:url atomically:true]; + if (completion != nil) + completion(url); + }; + + if (synchronous) + dispatch_sync(_queue, block); + else + dispatch_async(_queue, block); +} + +- (void)cacheFileAtURL:(NSURL *)url key:(NSString *)key synchronous:(bool)synchronous completion:(void (^)(NSURL *))completion +{ + [self cacheFileAtURL:url key:key synchronous:synchronous unserializeBlock:nil completion:completion]; +} + +- (void)cacheFileAtURL:(NSURL *)url key:(NSString *)key synchronous:(bool)synchronous unserializeBlock:(id (^)(NSData *))unserializeBlock completion:(void (^)(NSURL *))completion +{ + void (^block)(void) = ^ + { + NSURL *newUrl = [self urlForKey:key]; + [[NSFileManager defaultManager] copyItemAtURL:url toURL:newUrl error:NULL]; + if (completion != nil) + completion(newUrl); + + if (unserializeBlock != nil && _memoryCache != nil) + { + NSData *data = [NSData dataWithContentsOfURL:url]; + id result = unserializeBlock(data); + [_memoryCache setObject:result forKey:key]; + } + }; + + if (synchronous) + dispatch_sync(_queue, block); + else + dispatch_async(_queue, block); +} + +- (void)cacheData:(NSData *)data key:(NSString *)key synchronous:(bool)synchronous unserializeBlock:(id (^)(NSData *))unserializeBlock completion:(void (^)(NSURL *))completion +{ + void (^block)(void) = ^ + { + NSURL *newUrl = [self urlForKey:key]; + [data writeToURL:newUrl atomically:true]; + if (completion != nil) + completion(newUrl); + + if (unserializeBlock != nil && _memoryCache != nil) + { + id result = unserializeBlock(data); + [_memoryCache setObject:result forKey:key]; + } + }; + + if (synchronous) + dispatch_sync(_queue, block); + else + dispatch_async(_queue, block); +} + +- (void)clearCacheSynchronous:(bool)synchronous +{ + void (^block)(void) = ^ + { + NSArray *contents = [[NSFileManager defaultManager] contentsOfDirectoryAtURL:_url includingPropertiesForKeys:nil options:kNilOptions error:NULL]; + for (NSURL *url in contents) + [[NSFileManager defaultManager] removeItemAtURL:url error:NULL]; + }; + + if (synchronous) + dispatch_sync(_queue, block); + else + dispatch_async(_queue, block); +} + +- (bool)hasDataForKey:(NSString *)key +{ + return [[NSFileManager defaultManager] fileExistsAtPath:[self urlForKey:key].path]; +} + +- (NSURL *)urlForKey:(NSString *)key +{ + NSString *fileName = [TGStringUtils md5WithString:key]; + if (self.defaultFileExtension != nil) + fileName = [fileName stringByAppendingPathExtension:self.defaultFileExtension]; + + return [NSURL fileURLWithPath:[_url.path stringByAppendingPathComponent:fileName]]; +} + ++ (NSURL *)baseURL +{ + static dispatch_once_t onceToken; + static NSURL *baseURL; + dispatch_once(&onceToken, ^ + { + NSString *cachesPath = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, true)[0]; + baseURL = [[NSURL alloc] initFileURLWithPath:[cachesPath stringByAppendingPathComponent:TGFileCacheDomain]]; + }); + return baseURL; +} + +@end diff --git a/Watch/Extension/TGGeometry.h b/Watch/Extension/TGGeometry.h new file mode 100644 index 0000000000..83b7fb8db3 --- /dev/null +++ b/Watch/Extension/TGGeometry.h @@ -0,0 +1,10 @@ +#import +#import + +@interface TGGeometry : NSObject + +CGSize TGFitSize(CGSize size, CGSize maxSize); +CGSize TGFillSize(CGSize size, CGSize maxSize); +CGSize TGScaleToFill(CGSize size, CGSize boundsSize); + +@end diff --git a/Watch/Extension/TGGeometry.m b/Watch/Extension/TGGeometry.m new file mode 100644 index 0000000000..2a11ba9422 --- /dev/null +++ b/Watch/Extension/TGGeometry.m @@ -0,0 +1,56 @@ +#import "TGGeometry.h" + +@implementation TGGeometry + +CGSize TGFitSize(CGSize size, CGSize maxSize) +{ + if (size.width < 1.0f) + return CGSizeZero; + if (size.height < 1.0f) + return CGSizeZero; + + if (size.width > maxSize.width) + { + size.height = (CGFloat)floor((size.height * maxSize.width / size.width)); + size.width = maxSize.width; + } + if (size.height > maxSize.height) + { + size.width = (CGFloat)floor((size.width * maxSize.height / size.height)); + size.height = maxSize.height; + } + return size; +} + +CGSize TGFillSize(CGSize size, CGSize maxSize) +{ + if (size.width < 1) + size.width = 1; + if (size.height < 1) + size.height = 1; + + if (/*size.width >= size.height && */size.width < maxSize.width) + { + size.height = floor(maxSize.width * size.height / MAX(1.0f, size.width)); + size.width = maxSize.width; + } + + if (/*size.width <= size.height &&*/ size.height < maxSize.height) + { + size.width = floor(maxSize.height * size.width / MAX(1.0f, size.height)); + size.height = maxSize.height; + } + + return size; +} + +CGSize TGScaleToFill(CGSize size, CGSize boundsSize) +{ + if (size.width < 1.0f || size.height < 1.0f) + return CGSizeMake(1.0f, 1.0f); + + CGFloat scale = MAX(boundsSize.width / size.width, boundsSize.height / size.height); + return CGSizeMake(floor(size.width * scale), floor(size.height * scale)); +} + +@end diff --git a/Watch/Extension/TGGroupInfoController.h b/Watch/Extension/TGGroupInfoController.h new file mode 100644 index 0000000000..f171e55fbf --- /dev/null +++ b/Watch/Extension/TGGroupInfoController.h @@ -0,0 +1,20 @@ +#import "TGInterfaceController.h" + +@class TGBridgeContext; +@class TGBridgeChat; + +@interface TGGroupInfoControllerContext : NSObject + +@property (nonatomic, strong) TGBridgeContext *context; +@property (nonatomic, readonly) TGBridgeChat *groupChat; + +- (instancetype)initWithGroupChat:(TGBridgeChat *)groupChat; + +@end + +@interface TGGroupInfoController : TGInterfaceController + +@property (nonatomic, weak) IBOutlet WKInterfaceTable *table; +@property (nonatomic, weak) IBOutlet WKInterfaceImage *activityIndicator; + +@end diff --git a/Watch/Extension/TGGroupInfoController.m b/Watch/Extension/TGGroupInfoController.m new file mode 100644 index 0000000000..1d56836583 --- /dev/null +++ b/Watch/Extension/TGGroupInfoController.m @@ -0,0 +1,335 @@ +#import "TGGroupInfoController.h" +#import "TGWatchCommon.h" + +#import "TGStringUtils.h" + +#import "TGBridgeContext.h" +#import "TGBridgeConversationSignals.h" +#import "TGBridgePeerSettingsSignals.h" +#import "TGBridgeChat.h" +#import "TGBridgeUser.h" + +#import "WKInterfaceTable+TGDataDrivenTable.h" +#import "TGTableDeltaUpdater.h" +#import "TGInterfaceMenu.h" + +#import "TGGroupInfoHeaderController.h" +#import "TGGroupInfoFooterController.h" +#import "TGUserRowController.h" + +#import "TGInputController.h" +#import "TGUserInfoController.h" +#import "TGContactsController.h" +#import "TGProfilePhotoController.h" + +NSString *const TGGroupInfoControllerIdentifier = @"TGGroupInfoController"; + +@implementation TGGroupInfoControllerContext + +- (instancetype)initWithGroupChat:(TGBridgeChat *)groupChat +{ + self = [super init]; + if (self != nil) + { + _groupChat = groupChat; + } + return self; +} + +@end + +@interface TGGroupInfoController () +{ + SMetaDisposable *_chatDisposable; + SMetaDisposable *_peerSettingsDisposable; + SMetaDisposable *_updateSettingsDisposable; + + TGInterfaceMenu *_menu; + + TGGroupInfoControllerContext *_context; + TGBridgeChat *_chatModel; + NSDictionary *_userModels; + NSArray *_participantsModels; + NSArray *_currentParticipantsModels; + bool _muted; + + NSDictionary *_preferredParticipantsOrder; +} +@end + +@implementation TGGroupInfoController + +- (instancetype)init +{ + self = [super init]; + if (self != nil) + { + _chatDisposable = [[SMetaDisposable alloc] init]; + _peerSettingsDisposable = [[SMetaDisposable alloc] init]; + _updateSettingsDisposable = [[SMetaDisposable alloc] init]; + + [self.table _setInitialHidden:true]; + self.table.tableDataSource = self; + } + return self; +} + +- (void)dealloc +{ + [_chatDisposable dispose]; + [_peerSettingsDisposable dispose]; + [_updateSettingsDisposable dispose]; +} + +- (void)configureWithContext:(TGGroupInfoControllerContext *)context +{ + _context = context; + + self.title = TGLocalized(@"Watch.GroupInfo.Title"); + + __weak TGGroupInfoController *weakSelf = self; + [_chatDisposable setDisposable:[[[TGBridgeConversationSignals conversationWithPeerId:_context.groupChat.identifier] deliverOn:[SQueue mainQueue]] startWithNext:^(NSDictionary *models) + { + __strong TGGroupInfoController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + strongSelf->_chatModel = models[TGBridgeChatKey]; + strongSelf->_userModels = models[TGBridgeUsersDictionaryKey]; + + NSMutableArray *participantsModels = [[NSMutableArray alloc] init]; + for (NSNumber *uid in strongSelf->_chatModel.participants) + { + TGBridgeUser *user = strongSelf->_userModels[uid]; + if (user != nil) + [participantsModels addObject:user]; + } + + participantsModels = [TGGroupInfoController sortedParticipantsList:participantsModels preferredOrder:strongSelf->_preferredParticipantsOrder ownUid:strongSelf->_context.context.userId]; + strongSelf->_preferredParticipantsOrder = [TGGroupInfoController participantsOrderForList:participantsModels]; + + strongSelf->_participantsModels = participantsModels; + + [strongSelf performInterfaceUpdate:^(bool animated) + { + strongSelf.activityIndicator.hidden = true; + strongSelf.table.hidden = false; + + NSArray *currentParticipantsModels = strongSelf->_currentParticipantsModels; + bool initial = (currentParticipantsModels == 0); + + strongSelf->_currentParticipantsModels = strongSelf->_participantsModels; + + if (animated && !initial) + { + [TGTableDeltaUpdater updateTable:strongSelf.table oldData:currentParticipantsModels newData:strongSelf->_currentParticipantsModels controllerClassForIndexPath:^Class(TGIndexPath *indexPath) + { + return [strongSelf table:strongSelf->_table rowControllerClassAtIndexPath:indexPath]; + }]; + + [strongSelf.table reloadHeader]; + [strongSelf.table reloadFooter]; + } + else + { + [strongSelf.table reloadData]; + } + }]; + + } error:^(id error) + { + + } completed:^ + { + + }]]; + + [_peerSettingsDisposable setDisposable:[[[TGBridgePeerSettingsSignals peerSettingsWithPeerId:_context.groupChat.identifier] deliverOn:[SQueue mainQueue]] startWithNext:^(NSDictionary *next) + { + __strong TGGroupInfoController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + bool muted = [next[@"muted"] boolValue]; + + if (strongSelf->_menu == nil || muted != strongSelf->_muted) + { + strongSelf->_muted = muted; + + [strongSelf performInterfaceUpdate:^(bool animated) + { + [strongSelf updateMenuItemsMuted:strongSelf->_muted]; + }]; + } + }]]; +} + +- (void)willActivate +{ + [super willActivate]; + + [self.table notifyVisiblityChange]; +} + +- (void)didDeactivate +{ + [super didDeactivate]; +} + +#pragma mark - + +- (void)updateMenuItemsMuted:(bool)muted +{ + [_menu clearItems]; + + if (_menu == nil) + _menu = [[TGInterfaceMenu alloc] initForInterfaceController:self]; + + __weak TGGroupInfoController *weakSelf = self; + + NSMutableArray *menuItems = [[NSMutableArray alloc] init]; + + bool muteForever = true; + int32_t muteFor = muteForever ? INT_MAX : 1; + NSString *muteTitle = muteForever ? TGLocalized(@"Watch.UserInfo.MuteTitle") : [NSString stringWithFormat:TGLocalized([TGStringUtils integerValueFormat:@"Watch.UserInfo.Mute_" value:muteFor]), muteFor]; + + TGInterfaceMenuItem *muteItem = [[TGInterfaceMenuItem alloc] initWithItemIcon:muted ? WKMenuItemIconSpeaker : WKMenuItemIconMute title:muted ? TGLocalized(@"Watch.UserInfo.Unmute") : muteTitle actionBlock:^(TGInterfaceController *controller, TGInterfaceMenuItem *sender) + { + __strong TGGroupInfoController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf->_updateSettingsDisposable setDisposable:[[TGBridgePeerSettingsSignals toggleMutedWithPeerId:strongSelf->_context.groupChat.identifier] startWithNext:nil completed:^ + { + __strong TGGroupInfoController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + strongSelf->_muted = !muted; + [strongSelf updateMenuItemsMuted:strongSelf->_muted]; + }]]; + }]; + [menuItems addObject:muteItem]; + + [_menu addItems:menuItems]; +} + +#pragma mark - + +- (Class)headerControllerClassForTable:(WKInterfaceTable *)table +{ + return [TGGroupInfoHeaderController class]; +} + +- (void)table:(WKInterfaceTable *)table updateHeaderController:(TGGroupInfoHeaderController *)controller +{ + __weak TGGroupInfoController *weakSelf = self; + controller.isVisible = ^bool + { + __strong TGGroupInfoController *strongSelf = weakSelf; + if (strongSelf == nil) + return false; + + return strongSelf.isVisible; + }; + controller.avatarPressed = ^ + { + __strong TGGroupInfoController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + TGProfilePhotoControllerContext *context = [[TGProfilePhotoControllerContext alloc] initWithIdentifier:strongSelf->_chatModel.identifier imageUrl:strongSelf->_chatModel.groupPhotoSmall]; + [strongSelf pushControllerWithClass:[TGProfilePhotoController class] context:context]; + }; + + [controller updateWithGroupChat:_chatModel users:_userModels context:_context.context]; +} + +- (void)table:(WKInterfaceTable *)table updateFooterController:(TGGroupInfoFooterController *)controller +{ + +} + +- (Class)table:(WKInterfaceTable *)table rowControllerClassAtIndexPath:(NSIndexPath *)indexPath +{ + return [TGUserRowController class]; +} + +- (NSUInteger)numberOfRowsInTable:(WKInterfaceTable *)table section:(NSUInteger)section +{ + return _currentParticipantsModels.count; +} + +- (void)table:(WKInterfaceTable *)table updateRowController:(TGUserRowController *)controller forIndexPath:(TGIndexPath *)indexPath +{ + __weak TGGroupInfoController *weakSelf = self; + controller.isVisible = ^bool + { + __strong TGGroupInfoController *strongSelf = weakSelf; + if (strongSelf == nil) + return false; + + return strongSelf.isVisible; + }; + [controller updateWithUser:_currentParticipantsModels[indexPath.row] context:_context.context]; +} + +- (id)contextForSegueWithIdentifer:(NSString *)segueIdentifier table:(WKInterfaceTable *)table indexPath:(TGIndexPath *)indexPath +{ + return [[TGUserInfoControllerContext alloc] initWithUser:_currentParticipantsModels[indexPath.row]]; +} + ++ (NSMutableArray *)sortedParticipantsList:(NSMutableArray *)list preferredOrder:(NSDictionary *)preferredOrder ownUid:(int32_t)ownUid +{ + NSMutableArray *resultList = [list mutableCopy]; + + [resultList sortUsingComparator:^NSComparisonResult(TGBridgeUser *user1, TGBridgeUser *user2) + { + if (user1.identifier == ownUid) + return NSOrderedAscending; + else if (user2.identifier == ownUid) + return NSOrderedDescending; + + NSNumber *order1 = preferredOrder[@(user1.identifier)]; + NSNumber *order2 = preferredOrder[@(user2.identifier)]; + + if (order1 != nil && order2 != nil) + return order1.integerValue < order2.integerValue ? NSOrderedAscending : NSOrderedDescending; + + if (user1.online != user2.online) + return user1.online ? NSOrderedAscending : NSOrderedDescending; + + if ((user1.lastSeen < 0) != (user2.lastSeen < 0)) + return user1.lastSeen >= 0 ? NSOrderedAscending : NSOrderedDescending; + + if (user1.online || user1.lastSeen < 0) + return user1.identifier < user2.identifier ? NSOrderedAscending : NSOrderedDescending; + + return user1.lastSeen > user2.lastSeen ? NSOrderedAscending : NSOrderedDescending; + }]; + + return resultList; +} + ++ (NSDictionary *)participantsOrderForList:(NSArray *)list +{ + NSMutableDictionary *dictionary = [[NSMutableDictionary alloc] init]; + + NSInteger i = 0; + for (TGBridgeUser *user in list) + { + dictionary[@(user.identifier)] = @(i); + i++; + } + + return dictionary; +} + +#pragma mark - + ++ (NSString *)identifier +{ + return TGGroupInfoControllerIdentifier; +} + +@end diff --git a/Watch/Extension/TGGroupInfoFooterController.h b/Watch/Extension/TGGroupInfoFooterController.h new file mode 100644 index 0000000000..ebafe1c77f --- /dev/null +++ b/Watch/Extension/TGGroupInfoFooterController.h @@ -0,0 +1,10 @@ +#import + +@interface TGGroupInfoFooterController : NSObject + +@property (nonatomic, weak) IBOutlet WKInterfaceButton *button; +- (IBAction)buttonPressedAction; + +@property (nonatomic, copy) void (^buttonPressed)(void); + +@end diff --git a/Watch/Extension/TGGroupInfoFooterController.m b/Watch/Extension/TGGroupInfoFooterController.m new file mode 100644 index 0000000000..e8358ef3aa --- /dev/null +++ b/Watch/Extension/TGGroupInfoFooterController.m @@ -0,0 +1,20 @@ +#import "TGGroupInfoFooterController.h" + +NSString *const TGGroupInfoFooterIdentifier = @"TGGroupInfoFooter"; + +@implementation TGGroupInfoFooterController + +- (IBAction)buttonPressedAction +{ + if (self.buttonPressed != nil) + self.buttonPressed(); +} + +#pragma mark - + ++ (NSString *)identifier +{ + return TGGroupInfoFooterIdentifier; +} + +@end diff --git a/Watch/Extension/TGGroupInfoHeaderController.h b/Watch/Extension/TGGroupInfoHeaderController.h new file mode 100644 index 0000000000..60f215f298 --- /dev/null +++ b/Watch/Extension/TGGroupInfoHeaderController.h @@ -0,0 +1,20 @@ +#import "WKInterfaceTable+TGDataDrivenTable.h" + +@class TGBridgeChat; +@class TGBridgeContext; + +@interface TGGroupInfoHeaderController : TGTableRowController + +@property (nonatomic, weak) IBOutlet WKInterfaceButton *avatarButton; +@property (nonatomic, weak) IBOutlet WKInterfaceGroup *avatarGroup; +@property (nonatomic, weak) IBOutlet WKInterfaceLabel *avatarInitialsLabel; +@property (nonatomic, weak) IBOutlet WKInterfaceLabel *nameLabel; +@property (nonatomic, weak) IBOutlet WKInterfaceLabel *participantsLabel; +@property (nonatomic, weak) IBOutlet WKInterfaceLabel *onlineLabel; +- (IBAction)avatarPressedAction; + +@property (nonatomic, copy) void (^avatarPressed)(void); + +- (void)updateWithGroupChat:(TGBridgeChat *)chat users:(NSDictionary *)users context:(TGBridgeContext *)context; + +@end diff --git a/Watch/Extension/TGGroupInfoHeaderController.m b/Watch/Extension/TGGroupInfoHeaderController.m new file mode 100644 index 0000000000..4432e7599b --- /dev/null +++ b/Watch/Extension/TGGroupInfoHeaderController.m @@ -0,0 +1,98 @@ +#import "TGGroupInfoHeaderController.h" +#import "TGWatchCommon.h" +#import "TGStringUtils.h" + +#import "WKInterfaceGroup+Signals.h" +#import "TGBridgeMediaSignals.h" + +#import "TGBridgeChat.h" +#import "TGBridgeUser.h" + +#import "TGBridgeContext.h" + +NSString *const TGGroupInfoHeaderIdentifier = @"TGGroupInfoHeader"; + +@interface TGGroupInfoHeaderController () +{ + NSString *_currentAvatarPhoto; +} +@end + +@implementation TGGroupInfoHeaderController + + + +- (void)updateWithGroupChat:(TGBridgeChat *)chat users:(NSDictionary *)users context:(TGBridgeContext *)context +{ + self.nameLabel.text = chat.groupTitle; + + NSInteger onlineCount = 1; + for (NSNumber *uid in chat.participants) + { + TGBridgeUser *user = users[uid]; + if (user != nil && user.online && user.identifier != context.userId) + onlineCount++; + } + + NSString *membersText = [NSString stringWithFormat:TGLocalized([TGStringUtils integerValueFormat:@"Conversation.StatusMembers_" value:chat.participantsCount]), [NSString stringWithFormat:@"%d", (int32_t)chat.participantsCount]]; + + if (onlineCount > 1) + membersText = [NSString stringWithFormat:@"%@,", membersText]; + + self.participantsLabel.text = membersText; + + self.onlineLabel.hidden = (onlineCount <= 1); + if (!self.onlineLabel.hidden) + { + self.onlineLabel.text = [NSString stringWithFormat:TGLocalized([TGStringUtils integerValueFormat:@"Conversation.StatusOnline_" value:onlineCount]), [NSString stringWithFormat:@"%d", (int32_t)onlineCount]]; + } + + if (chat.groupPhotoSmall.length > 0) + { + self.avatarButton.enabled = true; + self.avatarInitialsLabel.hidden = true; + self.avatarGroup.backgroundColor = [UIColor hexColor:0x1a1a1a]; + if (![_currentAvatarPhoto isEqualToString:chat.groupPhotoSmall]) + { + _currentAvatarPhoto = chat.groupPhotoSmall; + + __weak TGGroupInfoHeaderController *weakSelf = self; + [self.avatarGroup setBackgroundImageSignal:[[TGBridgeMediaSignals avatarWithPeerId:chat.identifier url:_currentAvatarPhoto type:TGBridgeMediaAvatarTypeProfile] onError:^(id error) + { + __strong TGGroupInfoHeaderController *strongSelf = weakSelf; + if (strongSelf != nil) + strongSelf->_currentAvatarPhoto = nil; + }] isVisible:self.isVisible]; + } + } + else + { + self.avatarButton.enabled = false; + self.avatarInitialsLabel.hidden = false; + self.avatarGroup.backgroundColor = [TGColor colorForGroupId:chat.identifier]; + self.avatarInitialsLabel.text = [TGStringUtils initialForGroupName:chat.groupTitle]; + + _currentAvatarPhoto = nil; + [self.avatarGroup setBackgroundImageSignal:nil isVisible:self.isVisible]; + } +} + +- (void)avatarPressedAction +{ + if (self.avatarPressed != nil) + self.avatarPressed(); +} + +- (void)notifyVisiblityChange +{ + [self.avatarGroup updateIfNeeded]; +} + +#pragma mark - + ++ (NSString *)identifier +{ + return TGGroupInfoHeaderIdentifier; +} + +@end diff --git a/Watch/Extension/TGIndexPath.h b/Watch/Extension/TGIndexPath.h new file mode 100644 index 0000000000..d6c6e0dafe --- /dev/null +++ b/Watch/Extension/TGIndexPath.h @@ -0,0 +1,10 @@ +#import + +@interface TGIndexPath : NSObject + +@property (nonatomic, assign) NSUInteger section; +@property (nonatomic, assign) NSUInteger row; + ++ (instancetype)indexPathForRow:(NSUInteger)row inSection:(NSUInteger)section; + +@end diff --git a/Watch/Extension/TGIndexPath.m b/Watch/Extension/TGIndexPath.m new file mode 100644 index 0000000000..eb463337c7 --- /dev/null +++ b/Watch/Extension/TGIndexPath.m @@ -0,0 +1,37 @@ +#import "TGIndexPath.h" + +@implementation TGIndexPath + ++ (instancetype)indexPathForRow:(NSUInteger)row inSection:(NSUInteger)section +{ + TGIndexPath *indexPath = [[TGIndexPath alloc] init]; + indexPath.section = section; + indexPath.row = row; + return indexPath; +} + +- (id)copyWithZone:(NSZone *)zone +{ + TGIndexPath *copy = [[[self class] alloc] init]; + if (copy != nil) + { + copy.section = self.section; + copy.row = self.row; + } + return copy; +} + +- (BOOL)isEqual:(id)object +{ + if (object == self) + return YES; + + if (!object || ![object isKindOfClass:[self class]]) + return NO; + + TGIndexPath *indexPath = (TGIndexPath *)object; + + return self.section == indexPath.section && self.row == indexPath.row; +} + +@end diff --git a/Watch/Extension/TGInputController.h b/Watch/Extension/TGInputController.h new file mode 100644 index 0000000000..93008f96b5 --- /dev/null +++ b/Watch/Extension/TGInputController.h @@ -0,0 +1,13 @@ +#import + +@class TGInterfaceController; + +@interface TGInputController : NSObject + ++ (void)presentPlainInputControllerForInterfaceController:(TGInterfaceController *)interfaceController completion:(void (^)(NSString *))completion; ++ (void)presentInputControllerForInterfaceController:(TGInterfaceController *)interfaceController suggestionsForText:(NSString *)text completion:(void (^)(NSString *))completion; ++ (void)presentAudioControllerForInterfaceController:(TGInterfaceController *)interfaceController completion:(void (^)(int64_t uniqueId, int32_t duration, NSURL *url))completion; + ++ (NSArray *)suggestionsForText:(NSString *)text; + +@end diff --git a/Watch/Extension/TGInputController.m b/Watch/Extension/TGInputController.m new file mode 100644 index 0000000000..e9e3f32367 --- /dev/null +++ b/Watch/Extension/TGInputController.m @@ -0,0 +1,176 @@ +#import "TGInputController.h" +#import "TGWatchCommon.h" +#import "TGBridgeCommon.h" +#import "TGInterfaceController.h" + +#import "TGFileCache.h" +#import "TGExtensionDelegate.h" +#import "TGBridgePresetsSignals.h" + +@implementation TGInputController + ++ (void)presentPlainInputControllerForInterfaceController:(TGInterfaceController *)interfaceController completion:(void (^)(NSString *))completion; +{ + [interfaceController presentTextInputControllerWithSuggestions:nil allowedInputMode:WKTextInputModePlain completion:^(NSArray *results) + { + if (completion != nil && results.count > 0 && [results.firstObject isKindOfClass:[NSString class]]) + completion(results.firstObject); + }]; +} + ++ (void)presentInputControllerForInterfaceController:(TGInterfaceController *)interfaceController suggestionsForText:(NSString *)text completion:(void (^)(NSString *))completion +{ + [interfaceController presentTextInputControllerWithSuggestions:[self suggestionsForText:text] allowedInputMode:WKTextInputModeAllowEmoji completion:^(NSArray *results) + { + if (completion != nil && results.count > 0 && [results.firstObject isKindOfClass:[NSString class]]) + completion(results.firstObject); + }]; +} + ++ (void)presentAudioControllerForInterfaceController:(TGInterfaceController *)interfaceController completion:(void (^)(int64_t, int32_t, NSURL *))completion +{ + NSDictionary *options = @ + { + WKAudioRecorderControllerOptionsActionTitleKey: TGLocalized(@"Watch.Compose.Send"), + }; + + int64_t randomId = 0; + arc4random_buf(&randomId, sizeof(int64_t)); + + NSURL *url = [[TGExtensionDelegate instance].audioCache urlForKey:[NSString stringWithFormat:@"%lld", randomId]]; + [interfaceController presentAudioRecorderControllerWithOutputURL:url preset:WKAudioRecorderPresetWideBandSpeech options:options completion:^(BOOL didSave, NSError * _Nullable error) + { + WKAudioFileAsset *asset = [WKAudioFileAsset assetWithURL:url]; + + if (didSave && !error) + completion(randomId, (int32_t)asset.duration, url); + }]; +} + ++ (NSArray *)suggestionsForText:(NSString *)text +{ + return [self customSuggestions]; +} + ++ (NSArray *)customSuggestions +{ + NSArray *presetIdentifiers = [self presetIdentifiers]; + + NSMutableArray *suggestions = [[NSMutableArray alloc] init]; + NSDictionary *customPresets = [self customPresets]; + for (NSString *identifier in presetIdentifiers) + { + NSString *preset = customPresets[identifier]; + if (preset == nil) + preset = TGLocalized([NSString stringWithFormat:@"Watch.Suggestion.%@", identifier]); + + [suggestions addObject:preset]; + } + + return suggestions; +} + ++ (NSDictionary *)customPresets +{ + NSData *data = [NSData dataWithContentsOfURL:[TGBridgePresetsSignals presetsURL]]; + + @try + { + id presets = [NSKeyedUnarchiver unarchiveObjectWithData:data]; + if ([presets isKindOfClass:[NSDictionary class]]) + return presets; + + return nil; + } + @catch (NSException *exception) + { + return nil; + } +} + ++ (NSArray *)presetIdentifiers +{ + return @ + [ + @"OK", + @"Thanks", + @"WhatsUp", + @"TalkLater", + @"CantTalk", + @"HoldOn", + @"BRB", + @"OnMyWay" + ]; +} + ++ (NSArray *)composeSuggestions +{ + static NSArray *suggestions; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^ + { + suggestions = @ + [ + TGLocalized(@"Watch.Suggestion.WhatsUp"), + TGLocalized(@"Watch.Suggestion.OnMyWay"), + TGLocalized(@"Watch.Suggestion.OK"), + TGLocalized(@"Watch.Suggestion.CantTalk"), + TGLocalized(@"Watch.Suggestion.CallSoon"), + TGLocalized(@"Watch.Suggestion.Thanks") + ]; + }); + return suggestions; +} + ++ (NSArray *)generalSuggestions +{ + static NSArray *suggestions; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^ + { + suggestions = @ + [ + TGLocalized(@"Watch.Suggestion.OK"), + TGLocalized(@"Watch.Suggestion.Thanks"), + TGLocalized(@"Watch.Suggestion.WhatsUp") + ]; + }); + return suggestions; +} + ++ (NSArray *)yesNoSuggestions +{ + static NSArray *suggestions; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^ + { + suggestions = @ + [ + TGLocalized(@"Watch.Suggestion.Yes"), + TGLocalized(@"Watch.Suggestion.No"), + TGLocalized(@"Watch.Suggestion.Absolutely"), + TGLocalized(@"Watch.Suggestion.Nope") + ]; + }); + return suggestions; +} + ++ (NSArray *)laterSuggestions +{ + static NSArray *suggestions; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^ + { + suggestions = @ + [ + TGLocalized(@"Watch.Suggestion.TalkLater"), + TGLocalized(@"Watch.Suggestion.CantTalk"), + TGLocalized(@"Watch.Suggestion.HoldOn"), + TGLocalized(@"Watch.Suggestion.BRB"), + TGLocalized(@"Watch.Suggestion.OnMyWay") + ]; + }); + return suggestions; +} + +@end diff --git a/Watch/Extension/TGInterfaceController.h b/Watch/Extension/TGInterfaceController.h new file mode 100644 index 0000000000..b50700e4aa --- /dev/null +++ b/Watch/Extension/TGInterfaceController.h @@ -0,0 +1,29 @@ +#import + +@class TGIndexPath; + +@protocol TGInterfaceContext + +@end + +@interface TGInterfaceController : WKInterfaceController + +@property (nonatomic, strong) NSString *title; +@property (nonatomic, readonly, getter=isVisible) bool visible; +@property (nonatomic, readonly, getter=isPresenting) bool presenting; + +@property (nonatomic, weak, readonly) TGInterfaceController *presentingController; +@property (nonatomic, readonly) NSArray *presentedControllers; + +- (void)configureWithContext:(id)context; + +- (void)pushControllerWithClass:(Class)controllerClass context:(id)context; +- (void)presentControllerWithClass:(Class)controllerClass context:(id)context; + +- (void)performInterfaceUpdate:(void (^)(bool animated))update; + +- (id)contextForSegueWithIdentifer:(NSString *)segueIdentifier table:(WKInterfaceTable *)table indexPath:(TGIndexPath *)indexPath; + ++ (NSString *)identifier; + +@end diff --git a/Watch/Extension/TGInterfaceController.m b/Watch/Extension/TGInterfaceController.m new file mode 100644 index 0000000000..0d42eaada7 --- /dev/null +++ b/Watch/Extension/TGInterfaceController.m @@ -0,0 +1,176 @@ +#import "TGInterfaceController.h" +#import "WKInterfaceTable+TGDataDrivenTable.h" + +@interface TGInterfaceControllerContext : NSObject + +@property (nonatomic, weak) TGInterfaceController *presentingController; +@property (nonatomic, strong) id context; + +@end + +@implementation TGInterfaceControllerContext + ++ (TGInterfaceControllerContext *)contextWithPresentingController:(TGInterfaceController *)presentingController + context:(id)context +{ + NSParameterAssert(presentingController); + + TGInterfaceControllerContext *controllerContext = [[TGInterfaceControllerContext alloc] init]; + controllerContext.presentingController = presentingController; + controllerContext.context = context; + return controllerContext; +} + +@end + + +@interface TGInterfaceController () +{ + NSString *_title; + void (^_pendingInterfaceUpdate)(bool animated); +} +@end + +@implementation TGInterfaceController + +@dynamic title; + +- (void)configureWithContext:(id)context +{ + +} + +- (void)awakeWithContext:(id)context +{ + [super awakeWithContext:context]; + + _visible = true; + + id unwrappedContext = nil; + if ([context isKindOfClass:[TGInterfaceControllerContext class]]) + { + TGInterfaceControllerContext *controllerContext = (TGInterfaceControllerContext *)context; + _presentingController = controllerContext.presentingController; + unwrappedContext = controllerContext.context; + } + + [self configureWithContext:unwrappedContext]; +} + +- (void)pushControllerWithClass:(Class)controllerClass context:(id)context +{ + NSParameterAssert([controllerClass isSubclassOfClass:[TGInterfaceController class]]); + + TGInterfaceControllerContext *controllerContext = [TGInterfaceControllerContext contextWithPresentingController:self context:context]; + + [self pushControllerWithName:[controllerClass identifier] context:controllerContext]; +} + +- (void)presentControllerWithClass:(Class)controllerClass context:(id)context +{ + NSParameterAssert([controllerClass isSubclassOfClass:[TGInterfaceController class]]); + + TGInterfaceControllerContext *controllerContext = [TGInterfaceControllerContext contextWithPresentingController:self context:context]; + + [self presentControllerWithName:[controllerClass identifier] context:controllerContext]; +} + +- (void)willActivate +{ + [super willActivate]; + + _visible = true; + + if (_pendingInterfaceUpdate != nil) + { + _pendingInterfaceUpdate(false); + _pendingInterfaceUpdate = nil; + } +} + +- (void)didDeactivate +{ + [super didDeactivate]; + + _visible = false; +} + +- (bool)isPresenting +{ + return !_visible; +} + +- (void)_willPresentController +{ + _visible = false; +} + +- (void)presentControllerWithName:(NSString *)name context:(id)context +{ + [self _willPresentController]; + + [super presentControllerWithName:name context:context]; +} + +- (void)presentControllerWithNames:(NSArray *)names contexts:(NSArray *)contexts +{ + [self _willPresentController]; + + [super presentControllerWithNames:names contexts:contexts]; +} + +- (void)presentTextInputControllerWithSuggestions:(NSArray *)suggestions allowedInputMode:(WKTextInputMode)inputMode completion:(void (^)(NSArray *))completion +{ + [self _willPresentController]; + + [super presentTextInputControllerWithSuggestions:suggestions allowedInputMode:inputMode completion:completion]; +} + +- (void)performInterfaceUpdate:(void (^)(bool))updates +{ + if (updates == nil) + return; + + if (self.isVisible) + updates(true); + else + _pendingInterfaceUpdate = [updates copy]; +} + +- (TGInterfaceControllerContext *)contextForSegueWithIdentifier:(NSString *)segueIdentifier inTable:(WKInterfaceTable *)table rowIndex:(NSInteger)rowIndex +{ + TGIndexPath *indexPath = [table indexPathForRowIndex:rowIndex]; + return [TGInterfaceControllerContext contextWithPresentingController:self context:[self contextForSegueWithIdentifer:segueIdentifier table:table indexPath:indexPath]]; +} + +- (id)contextForSegueWithIdentifer:(NSString *)segueIdentifier table:(WKInterfaceTable *)table indexPath:(TGIndexPath *)indexPath +{ + return nil; +} + +#pragma mark - Properties + +- (NSString *)title +{ + return _title; +} + +- (void)setTitle:(NSString *)title +{ + if ([title isEqualToString:_title]) + return; + + _title = title; + + [super setTitle:title]; +} + +#pragma mark - + ++ (NSString *)identifier +{ + NSAssert(true, @"Do not use TGInterfaceController directly"); + return nil; +} + +@end diff --git a/Watch/Extension/TGInterfaceMenu.h b/Watch/Extension/TGInterfaceMenu.h new file mode 100644 index 0000000000..5c5392aa42 --- /dev/null +++ b/Watch/Extension/TGInterfaceMenu.h @@ -0,0 +1,25 @@ +#import + +@class TGInterfaceMenuItem; +@class TGInterfaceController; + +typedef void (^TGInterfaceMenuItemActionBlock)(TGInterfaceController *controller, TGInterfaceMenuItem *sender); + +@interface TGInterfaceMenuItem : NSObject + +- (instancetype)initWithImage:(UIImage *)image title:(NSString *)title actionBlock:(TGInterfaceMenuItemActionBlock)actionBlock; +- (instancetype)initWithImageNamed:(NSString *)imageName title:(NSString *)title actionBlock:(TGInterfaceMenuItemActionBlock)actionBlock; +- (instancetype)initWithItemIcon:(WKMenuItemIcon)itemIcon title:(NSString *)title actionBlock:(TGInterfaceMenuItemActionBlock)actionBlock; + +@end + +@interface TGInterfaceMenu : NSObject + +- (instancetype)initForInterfaceController:(TGInterfaceController *)interfaceController; +- (instancetype)initForInterfaceController:(TGInterfaceController *)interfaceController items:(NSArray *)items; + +- (void)addItem:(TGInterfaceMenuItem *)item; +- (void)addItems:(NSArray *)items; +- (void)clearItems; + +@end diff --git a/Watch/Extension/TGInterfaceMenu.m b/Watch/Extension/TGInterfaceMenu.m new file mode 100644 index 0000000000..1f0af1b478 --- /dev/null +++ b/Watch/Extension/TGInterfaceMenu.m @@ -0,0 +1,190 @@ +#import "TGInterfaceMenu.h" + +#import "TGInterfaceController.h" + +#import + +@interface TGInterfaceMenuItem () + +@property (nonatomic, readonly) NSString *uniqueIdentifier; +@property (nonatomic, copy) TGInterfaceMenuItemActionBlock actionBlock; +@property (nonatomic, strong) NSString *title; +@property (nonatomic, strong) UIImage *image; +@property (nonatomic, strong) NSString *imageName; +@property (nonatomic, assign) WKMenuItemIcon itemIcon; + +@end + +@implementation TGInterfaceMenuItem + +- (instancetype)init +{ + self = [super init]; + if (self != nil) + { + _uniqueIdentifier = [[NSProcessInfo processInfo] globallyUniqueString]; + } + return self; +} + +- (instancetype)initWithImage:(UIImage *)image title:(NSString *)title actionBlock:(TGInterfaceMenuItemActionBlock)actionBlock +{ + self = [self init]; + if (self != nil) + { + self.title = title; + self.image = image;; + self.actionBlock = actionBlock; + } + return self; +} + +- (instancetype)initWithImageNamed:(NSString *)imageName title:(NSString *)title actionBlock:(TGInterfaceMenuItemActionBlock)actionBlock +{ + self = [self init]; + if (self != nil) + { + self.title = title; + self.imageName = imageName; + self.actionBlock = actionBlock; + } + return self; +} + +- (instancetype)initWithItemIcon:(WKMenuItemIcon)itemIcon title:(NSString *)title actionBlock:(TGInterfaceMenuItemActionBlock)actionBlock +{ + self = [self init]; + if (self != nil) + { + self.title = title; + self.itemIcon = itemIcon; + self.actionBlock = actionBlock; + } + return self; +} + +@end + +#pragma mark - + +@interface TGInterfaceMenu () + +@property (nonatomic, readonly) NSString *uniqueIdentifier; +@property (nonatomic, weak) TGInterfaceController *interfaceController; +@property (nonatomic, strong) NSArray *items; + +@end + +@implementation TGInterfaceMenu + +- (instancetype)initForInterfaceController:(TGInterfaceController *)interfaceController +{ + return [self initForInterfaceController:interfaceController items:nil]; +} + +- (instancetype)initForInterfaceController:(TGInterfaceController *)interfaceController items:(NSArray *)items +{ + NSParameterAssert(interfaceController); + + self = [super init]; + if (self != nil) + { + _uniqueIdentifier = [[NSProcessInfo processInfo] globallyUniqueString]; + + self.interfaceController = interfaceController; + self.items = items; + + for (TGInterfaceMenuItem *item in self.items) + { + if (![item isKindOfClass:[TGInterfaceMenuItem class]]) + continue; + + [self _appendItem:item]; + } + } + return self; +} + +- (void)addItem:(TGInterfaceMenuItem *)item +{ + NSParameterAssert(item); + + [self _appendItem:item]; + + if (self.items != nil) + self.items = [self.items arrayByAddingObject:item]; + else + self.items = @[ item ]; +} + +- (void)addItems:(NSArray *)items +{ + NSParameterAssert(items); + + NSMutableArray *addedItems = [NSMutableArray array]; + + for (TGInterfaceMenuItem *item in items) + { + if (![item isKindOfClass:[TGInterfaceMenuItem class]]) + continue; + + [self _appendItem:item]; + [addedItems addObject:item]; + } + + if (self.items != nil) + self.items = [self.items arrayByAddingObjectsFromArray:addedItems]; + else + self.items = addedItems; +} + +- (void)_appendItem:(TGInterfaceMenuItem *)item +{ + NSParameterAssert(item); + + SEL actionSelector = [self _actionSelectorForItem:item]; + + if (self.interfaceController != nil && ![self.interfaceController respondsToSelector:actionSelector]) + { + bool succeed = class_addMethod([self.interfaceController class], actionSelector, imp_implementationWithBlock(^(id receiver) + { + if (item.actionBlock != nil) + item.actionBlock(receiver, item); + }), [[NSString stringWithFormat: @"%s%s%s", @encode(id), @encode(id), @encode(SEL)] UTF8String]); + + if (succeed) + { + if (item.image != nil) + [self.interfaceController addMenuItemWithImage:item.image title:item.title action:actionSelector]; + else if (item.imageName != nil) + [self.interfaceController addMenuItemWithImageNamed:item.imageName title:item.title action:actionSelector]; + else + [self.interfaceController addMenuItemWithItemIcon:item.itemIcon title:item.title action:actionSelector]; + } + } +} + +- (void)clearItems +{ + for (TGInterfaceMenuItem *item in self.items) + { + SEL actionSelector = [self _actionSelectorForItem:item]; + Method method = class_getInstanceMethod([self.interfaceController class], actionSelector); + imp_removeBlock(method_getImplementation(method)); + method_setImplementation(method, NULL); + } + + [self.interfaceController clearAllMenuItems]; + self.items = nil; +} + +#pragma mark - + +NSString *const TGInterfaceMenuActionSelectorPrefix = @"tg_interfaceMenuAction_"; + +- (SEL)_actionSelectorForItem:(TGInterfaceMenuItem *)item +{ + return NSSelectorFromString([TGInterfaceMenuActionSelectorPrefix stringByAppendingFormat:@"%lx_%lx", (unsigned long)self.uniqueIdentifier.hash, (unsigned long)item.uniqueIdentifier.hash]); +} + +@end diff --git a/Watch/Extension/TGLocationController.h b/Watch/Extension/TGLocationController.h new file mode 100644 index 0000000000..52b43a954b --- /dev/null +++ b/Watch/Extension/TGLocationController.h @@ -0,0 +1,20 @@ +#import "TGInterfaceController.h" + +@class TGBridgeLocationMediaAttachment; + +@interface TGLocationControllerContext : NSObject + +@property (nonatomic, copy) void (^completionBlock)(TGBridgeLocationMediaAttachment *location); + +@end + +@interface TGLocationController : TGInterfaceController + +@property (nonatomic, weak) IBOutlet WKInterfaceTable *table; +@property (nonatomic, weak) IBOutlet WKInterfaceGroup *activityGroup; +@property (nonatomic, weak) IBOutlet WKInterfaceImage *activityIndicator; + +@property (nonatomic, weak) IBOutlet WKInterfaceGroup *alertGroup; +@property (nonatomic, weak) IBOutlet WKInterfaceLabel *alertLabel; + +@end diff --git a/Watch/Extension/TGLocationController.m b/Watch/Extension/TGLocationController.m new file mode 100644 index 0000000000..a92e9b1c1d --- /dev/null +++ b/Watch/Extension/TGLocationController.m @@ -0,0 +1,184 @@ +#import "TGLocationController.h" +#import "TGWatchCommon.h" + +#import "TGBridgeLocationSignals.h" + +#import "TGBridgeLocationVenue+TGTableItem.h" +#import "TGBridgeLocationMediaAttachment.h" + +#import "WKInterfaceTable+TGDataDrivenTable.h" +#import "TGTableDeltaUpdater.h" + +#import "TGLocationMapHeaderController.h" +#import "TGLocationVenueRowController.h" + +NSString *const TGLocationControllerIdentifier = @"TGLocationController"; +const NSUInteger TGLocationControllerBatchLimit = 14; + +@implementation TGLocationControllerContext + +@end + +@interface TGLocationController () +{ + TGLocationControllerContext *_context; + + SMetaDisposable *_locationDisposable; + NSArray *_venueModels; + NSArray *_currentVenueModels; + CLLocation *_currentLocation; +} +@end + +@implementation TGLocationController + +- (instancetype)init +{ + self = [super init]; + if (self != nil) + { + _locationDisposable = [[SMetaDisposable alloc] init]; + + [self.alertGroup _setInitialHidden:true]; + [self.table _setInitialHidden:true]; + self.table.tableDataSource = self; + } + return self; +} + +- (void)dealloc +{ + [_locationDisposable dispose]; +} + +- (void)configureWithContext:(TGLocationControllerContext *)context +{ + _context = context; + + __weak TGLocationController *weakSelf = self; + [_locationDisposable setDisposable:[[[TGBridgeLocationSignals nearbyVenuesWithLimit:TGLocationControllerBatchLimit] deliverOn:[SQueue mainQueue]] startWithNext:^(id next) + { + __strong TGLocationController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + if ([next isKindOfClass:[NSString class]]) + { + [strongSelf performInterfaceUpdate:^(bool animated) + { + __strong TGLocationController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + if ([next isEqualToString:TGBridgeLocationAccessRequiredKey]) + { + strongSelf.alertGroup.hidden = false; + strongSelf.alertLabel.text = TGLocalized(@"Watch.Location.Access"); + strongSelf.activityGroup.hidden = true; + } + else if ([next isEqualToString:TGBridgeLocationLoadingKey]) + { + strongSelf.alertGroup.hidden = true; + strongSelf.activityGroup.hidden = false; + } + }]; + } + else if ([next isKindOfClass:[CLLocation class]]) + { + strongSelf->_currentLocation = (CLLocation *)next; + [strongSelf performInterfaceUpdate:^(bool animated) + { + __strong TGLocationController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + strongSelf.table.hidden = false; + [strongSelf.table reloadData]; + }]; + } + else if ([next isKindOfClass:[NSArray class]]) + { + strongSelf->_venueModels = (NSArray *)next; + + [strongSelf performInterfaceUpdate:^(bool animated) + { + __strong TGLocationController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + NSArray *currentVenueModels = strongSelf->_currentVenueModels; + strongSelf->_currentVenueModels = strongSelf->_venueModels; + + strongSelf.alertGroup.hidden = true; + strongSelf.table.hidden = false; + strongSelf.activityGroup.hidden = true; + [TGTableDeltaUpdater updateTable:strongSelf.table oldData:currentVenueModels newData:strongSelf->_currentVenueModels controllerClassForIndexPath:^Class(TGIndexPath *indexPath) + { + return [strongSelf table:strongSelf.table rowControllerClassAtIndexPath:indexPath]; + }]; + }]; + } + } error:^(id error) + { + + } completed:^ + { + + }]]; +} + +- (Class)headerControllerClassForTable:(WKInterfaceTable *)table +{ + return [TGLocationMapHeaderController class]; +} + +- (void)table:(WKInterfaceTable *)table updateHeaderController:(TGLocationMapHeaderController *)controller +{ + __weak TGLocationController *weakSelf = self; + controller.currentLocationPressed = ^ + { + __strong TGLocationController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + TGBridgeLocationMediaAttachment *location = [[TGBridgeLocationMediaAttachment alloc] init]; + location.latitude = strongSelf->_currentLocation.coordinate.latitude; + location.longitude = strongSelf->_currentLocation.coordinate.longitude; + + [strongSelf dismissController]; + + if (strongSelf->_context.completionBlock != nil) + strongSelf->_context.completionBlock(location); + }; + [controller updateWithLocation:_currentLocation]; +} + +- (NSUInteger)numberOfRowsInTable:(WKInterfaceTable *)table section:(NSUInteger)section +{ + return _venueModels.count; +} + +- (Class)table:(WKInterfaceTable *)table rowControllerClassAtIndexPath:(NSIndexPath *)indexPath +{ + return [TGLocationVenueRowController class]; +} + +- (void)table:(WKInterfaceTable *)table updateRowController:(TGLocationVenueRowController *)controller forIndexPath:(TGIndexPath *)indexPath +{ + [controller updateWithLocationVenue:_venueModels[indexPath.row]]; +} + +- (void)table:(WKInterfaceTable *)table didSelectRowAtIndexPath:(TGIndexPath *)indexPath +{ + [self dismissController]; + + if (_context.completionBlock != nil) + _context.completionBlock([_venueModels[indexPath.row] locationAttachment]); +} + ++ (NSString *)identifier +{ + return TGLocationControllerIdentifier; +} + +@end diff --git a/Watch/Extension/TGLocationMapHeaderController.h b/Watch/Extension/TGLocationMapHeaderController.h new file mode 100644 index 0000000000..6ca1e311a3 --- /dev/null +++ b/Watch/Extension/TGLocationMapHeaderController.h @@ -0,0 +1,14 @@ +#import "WKInterfaceTable+TGDataDrivenTable.h" + +@interface TGLocationMapHeaderController : TGTableRowController + +@property (nonatomic, weak) IBOutlet WKInterfaceMap *map; +@property (nonatomic, weak) IBOutlet WKInterfaceButton *currentLocationButton; +@property (nonatomic, weak) IBOutlet WKInterfaceLabel *currentLocationLabel; +- (IBAction)currentLocationPressedAction; + +@property (nonatomic, copy) void (^currentLocationPressed)(void); + +- (void)updateWithLocation:(CLLocation *)location; + +@end diff --git a/Watch/Extension/TGLocationMapHeaderController.m b/Watch/Extension/TGLocationMapHeaderController.m new file mode 100644 index 0000000000..a178792310 --- /dev/null +++ b/Watch/Extension/TGLocationMapHeaderController.m @@ -0,0 +1,44 @@ +#import "TGLocationMapHeaderController.h" +#import "TGWatchCommon.h" +#import "TGLocationUtils.h" + +NSString *const TGLocationMapHeaderIdentifier = @"TGLocationMapHeader"; + +@interface TGLocationMapHeaderController () +{ + CLLocation *_location; +} +@end + +@implementation TGLocationMapHeaderController + +- (void)updateWithLocation:(CLLocation *)location +{ + self.currentLocationLabel.text = TGLocalized(@"Watch.Location.Current"); + + if (_location == nil || [_location distanceFromLocation:location] > 50) + { + CLLocationDegrees latitude = [TGLocationUtils adjustGMapLatitude:location.coordinate.latitude withPixelOffset:-20 zoom:15]; + [self.map setRegion:MKCoordinateRegionMake(CLLocationCoordinate2DMake(latitude, location.coordinate.longitude), MKCoordinateSpanMake(0.003, 0.003))]; + + if (_location != nil) + [self.map removeAllAnnotations]; + + [self.map addAnnotation:location.coordinate withPinColor:WKInterfaceMapPinColorRed]; + + _location = location; + } +} + +- (void)currentLocationPressedAction +{ + if (self.currentLocationPressed != nil) + self.currentLocationPressed(); +} + ++ (NSString *)identifier +{ + return TGLocationMapHeaderIdentifier; +} + +@end diff --git a/Watch/Extension/TGLocationUtils.h b/Watch/Extension/TGLocationUtils.h new file mode 100644 index 0000000000..f299115545 --- /dev/null +++ b/Watch/Extension/TGLocationUtils.h @@ -0,0 +1,10 @@ +#import +#import + +@interface TGLocationUtils : NSObject + ++ (CLLocationDegrees)adjustGMapLatitude:(CLLocationDegrees)latitude withPixelOffset:(NSInteger)offset zoom:(NSInteger)zoom; ++ (CLLocationDegrees)adjustGMapLongitude:(CLLocationDegrees)longitude withPixelOffset:(NSInteger)offset zoom:(NSInteger)zoom; ++ (CLLocationCoordinate2D)adjustGMapCoordinate:(CLLocationCoordinate2D)coordinate withPixelOffset:(CGPoint)offset zoom:(NSInteger)zoom; + +@end diff --git a/Watch/Extension/TGLocationUtils.m b/Watch/Extension/TGLocationUtils.m new file mode 100644 index 0000000000..2df3600f0f --- /dev/null +++ b/Watch/Extension/TGLocationUtils.m @@ -0,0 +1,43 @@ +#import "TGLocationUtils.h" + +const NSInteger TGGoogleMapsOffset = 268435456; +const CGFloat TGGoogleMapsRadius = TGGoogleMapsOffset / (CGFloat)M_PI; + +@implementation TGLocationUtils + ++ (CLLocationCoordinate2D)adjustGMapCoordinate:(CLLocationCoordinate2D)coordinate withPixelOffset:(CGPoint)offset zoom:(NSInteger)zoom +{ + return CLLocationCoordinate2DMake([self adjustGMapLatitude:coordinate.latitude withPixelOffset:(NSInteger)offset.y zoom:zoom], [self adjustGMapLongitude:coordinate.longitude withPixelOffset:(NSInteger)offset.x zoom:zoom]); +} + ++ (CLLocationDegrees)adjustGMapLatitude:(CLLocationDegrees)latitude withPixelOffset:(NSInteger)offset zoom:(NSInteger)zoom +{ + return [self _yToLatitude:([self _latitudeToY:latitude] + (offset << (21 - zoom)))]; +} + ++ (CLLocationDegrees)adjustGMapLongitude:(CLLocationDegrees)longitude withPixelOffset:(NSInteger)offset zoom:(NSInteger)zoom +{ + return [self _xToLongitude:([self _longitudeToX:longitude] + (offset << (21 - zoom)))]; +} + ++ (NSInteger)_latitudeToY:(CLLocationDegrees)latitude +{ + return (NSInteger)round(TGGoogleMapsOffset - TGGoogleMapsRadius * log((1 + sin(latitude * M_PI / 180.0)) / (1 - sin(latitude * M_PI / 180.0))) / 2); +} + ++ (CLLocationDegrees)_yToLatitude:(NSInteger)y +{ + return (M_PI_2 - 2 * atan(exp((y - TGGoogleMapsOffset) / TGGoogleMapsRadius))) * 180.0 / M_PI; +} + ++ (NSInteger)_longitudeToX:(CLLocationDegrees)longitude +{ + return (NSInteger)round(TGGoogleMapsOffset + TGGoogleMapsRadius * longitude * M_PI / 180); +} + ++ (CLLocationDegrees)_xToLongitude:(NSInteger)x +{ + return (x - TGGoogleMapsOffset) / TGGoogleMapsRadius * 180.0 / M_PI; +} + +@end diff --git a/Watch/Extension/TGLocationVenueRowController.h b/Watch/Extension/TGLocationVenueRowController.h new file mode 100644 index 0000000000..1f335ac9cc --- /dev/null +++ b/Watch/Extension/TGLocationVenueRowController.h @@ -0,0 +1,12 @@ +#import "WKInterfaceTable+TGDataDrivenTable.h" + +@class TGBridgeLocationVenue; + +@interface TGLocationVenueRowController : TGTableRowController + +@property (nonatomic, weak) IBOutlet WKInterfaceLabel *nameLabel; +@property (nonatomic, weak) IBOutlet WKInterfaceLabel *addressLabel; + +- (void)updateWithLocationVenue:(TGBridgeLocationVenue *)locationVenue; + +@end diff --git a/Watch/Extension/TGLocationVenueRowController.m b/Watch/Extension/TGLocationVenueRowController.m new file mode 100644 index 0000000000..c9607d1ab0 --- /dev/null +++ b/Watch/Extension/TGLocationVenueRowController.m @@ -0,0 +1,20 @@ +#import "TGLocationVenueRowController.h" + +#import "TGBridgeLocationVenue.h" + +NSString *const TGLocationVenueRowIdentifier = @"TGLocationVenueRow"; + +@implementation TGLocationVenueRowController + +- (void)updateWithLocationVenue:(TGBridgeLocationVenue *)locationVenue +{ + self.nameLabel.text = locationVenue.name; + self.addressLabel.text = locationVenue.address.length > 0 ? locationVenue.address : @" "; +} + ++ (NSString *)identifier +{ + return TGLocationVenueRowIdentifier; +} + +@end diff --git a/Watch/Extension/TGMessageViewController.h b/Watch/Extension/TGMessageViewController.h new file mode 100644 index 0000000000..78720917ea --- /dev/null +++ b/Watch/Extension/TGMessageViewController.h @@ -0,0 +1,25 @@ +#import "TGInterfaceController.h" + +@class TGBridgeContext; +@class TGBridgeChat; +@class TGBridgeMessage; + +@interface TGMessageViewControllerContext : NSObject + +@property (nonatomic, strong) TGBridgeContext *context; +@property (nonatomic, readonly) TGBridgeMessage *message; +@property (nonatomic, readonly) int64_t peerId; +@property (nonatomic, readonly) TGBridgeChat *channel; +@property (nonatomic, strong) NSDictionary *additionalPeers; + +- (instancetype)initWithMessage:(TGBridgeMessage *)message peerId:(int64_t)peerId; +- (instancetype)initWithMessage:(TGBridgeMessage *)message channel:(TGBridgeChat *)channel; + +@end + +@interface TGMessageViewController : TGInterfaceController + +@property (nonatomic, weak) IBOutlet WKInterfaceTable *table; +@property (nonatomic, weak) IBOutlet WKInterfaceImage *activityIndicator; + +@end diff --git a/Watch/Extension/TGMessageViewController.m b/Watch/Extension/TGMessageViewController.m new file mode 100644 index 0000000000..eafa9cfc0d --- /dev/null +++ b/Watch/Extension/TGMessageViewController.m @@ -0,0 +1,436 @@ +#import "TGMessageViewController.h" +#import "TGWatchCommon.h" +#import "TGBridgeSendMessageSignals.h" +#import "TGBridgeRemoteSignals.h" +#import "TGBridgeAudioSignals.h" + +#import "TGBridgePeerIdAdapter.h" + +#import "TGBridgeChat.h" +#import "TGBridgeUser.h" +#import "TGBridgeMessage.h" +#import "TGBridgeUserCache.h" + +#import "WKInterfaceTable+TGDataDrivenTable.h" + +#import "TGInputController.h" + +#import "TGUserRowController.h" +#import "TGMessageViewMessageRowController.h" +#import "TGMessageViewWebPageRowController.h" +#import "TGMessageViewFooterController.h" + +#import "TGUserInfoController.h" +#import "TGNeoChatsController.h" + +#import "TGExtensionDelegate.h" + +NSString *const TGMessageViewControllerIdentifier = @"TGMessageViewController"; + +@implementation TGMessageViewControllerContext + +- (instancetype)initWithMessage:(TGBridgeMessage *)message peerId:(int64_t)peerId +{ + self = [super init]; + if (self != nil) + { + _message = message; + _peerId = peerId; + } + return self; +} + +- (instancetype)initWithMessage:(TGBridgeMessage *)message channel:(TGBridgeChat *)channel +{ + self = [super init]; + if (self != nil) + { + _message = message; + _peerId = channel.identifier; + _channel = channel; + } + return self; +} + +@end + +@interface TGMessageViewController () +{ + TGMessageViewControllerContext *_context; + + SMetaDisposable *_sendMessageDisposable; + SMetaDisposable *_remoteActionDisposable; + SMetaDisposable *_playAudioDisposable; + + TGBridgeMediaAttachment *_pendingAudioAttachment; +} +@end + +@implementation TGMessageViewController + +- (instancetype)init +{ + self = [super init]; + if (self != nil) + { + _sendMessageDisposable = [[SMetaDisposable alloc] init]; + _remoteActionDisposable = [[SMetaDisposable alloc] init]; + _playAudioDisposable = [[SMetaDisposable alloc] init]; + + [self.table _setInitialHidden:true]; + self.table.tableDataSource = self; + } + return self; +} + +- (void)dealloc +{ + [_sendMessageDisposable dispose]; + [_remoteActionDisposable dispose]; + [_playAudioDisposable dispose]; +} + +- (void)configureWithContext:(TGMessageViewControllerContext *)context +{ + _context = context; + + [self configureHandoff]; + + self.title = TGLocalized(@"Watch.MessageView.Title"); + + __weak TGMessageViewController *weakSelf = self; + [self performInterfaceUpdate:^(bool animated) + { + __strong TGMessageViewController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + strongSelf.table.hidden = false; + strongSelf.activityIndicator.hidden = true; + [strongSelf.table reloadData]; + }]; +} + +- (void)configureHandoff +{ +// int64_t peerId = _context.peerId; +// bool isGroup = _context.peerId < 0; +// +// if (isGroup) +// peerId = -peerId; +// +// NSMutableDictionary *peerDict = [[NSMutableDictionary alloc] init]; +// peerDict[@"type"] = isGroup ? @"group" : @"user"; +// peerDict[@"id"] = @(peerId); +// +// NSMutableDictionary *messageDict = [[NSMutableDictionary alloc] init]; +// messageDict[@"autoplay"] = @false; +// messageDict[@"id"] = @(_context.message.identifier); +// +// NSDictionary *userInfo = @{@"user_id": @(_context.authorizedContext.userId), @"peer": peerDict, @"message": messageDict}; +// [self updateUserActivity:@"org.telegram.message" userInfo:userInfo webpageURL:[NSURL URLWithString:@"https://telegram.org/dl"]]; +} + +- (void)willActivate +{ + [super willActivate]; + + [self configureHandoff]; + + [self.table notifyVisiblityChange]; +} + +- (void)didDeactivate +{ + [super didDeactivate]; +} + +#pragma mark - + +- (Class)headerControllerClassForTable:(WKInterfaceTable *)table +{ + return [TGUserRowController class]; +} + +- (void)table:(WKInterfaceTable *)table updateHeaderController:(TGUserRowController *)controller +{ + if (_context.channel != nil) + { + [controller updateWithChannel:_context.channel context:_context.context]; + } + else + { + TGBridgeUser *user = [[TGBridgeUserCache instance] userWithId:(int32_t)_context.message.fromUid]; + [controller updateWithUser:user context:_context.context]; + } +} + +- (void)tableDidSelectHeader:(WKInterfaceTable *)table +{ + if (_context.channel != nil) + { + TGUserInfoControllerContext *context = [[TGUserInfoControllerContext alloc] initWithChannel:_context.channel]; + context.disallowCompose = true; + [self pushControllerWithClass:[TGUserInfoController class] context:context]; + } + else + { + TGUserInfoControllerContext *context = [[TGUserInfoControllerContext alloc] initWithUserId:(int32_t)_context.message.fromUid]; + [self pushControllerWithClass:[TGUserInfoController class] context:context]; + } +} + +- (Class)footerControllerClassForTable:(WKInterfaceTable *)table +{ + return [TGMessageViewFooterController class]; +} + +- (void)table:(WKInterfaceTable *)table updateFooterController:(TGMessageViewFooterController *)controller +{ + __weak TGMessageViewController *weakSelf = self; + controller.forwardPressed = ^ + { + __strong TGMessageViewController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + TGNeoChatsControllerContext *context = [[TGNeoChatsControllerContext alloc] init]; + context.context = strongSelf->_context.context; + context.initialChats = [[TGExtensionDelegate instance] chatsController].chats; + context.completionBlock = ^(TGBridgeChat *peer) + { + __strong TGMessageViewController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf->_sendMessageDisposable setDisposable:[[TGBridgeSendMessageSignals forwardMessageWithPeerId:strongSelf->_context.message.cid mid:strongSelf->_context.message.identifier targetPeerId:peer.identifier] startWithNext:^(TGBridgeMessage *message) + { + + }]]; + }; + [strongSelf presentControllerWithClass:[TGNeoChatsController class] context:context]; + }; + controller.replyPressed = ^ + { + __strong TGMessageViewController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [TGInputController presentInputControllerForInterfaceController:strongSelf suggestionsForText:strongSelf->_context.message.text completion:^(NSString *text) + { + [strongSelf->_sendMessageDisposable setDisposable:[[TGBridgeSendMessageSignals sendMessageWithPeerId:strongSelf->_context.peerId text:text replyToMid:strongSelf->_context.message.identifier] startWithNext:^(TGBridgeMessage *message) + { + + }]]; + }]; + }; + controller.viewPressed = ^ + { + __strong TGMessageViewController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf->_remoteActionDisposable setDisposable:[[TGBridgeRemoteSignals openRemoteMessageWithPeerId:strongSelf->_context.peerId messageId:strongSelf->_context.message.identifier type:0 autoPlay:false] startWithNext:^(id next) + { + + }]]; + }; + + [controller updateWithMessage:_context.message channel:(_context.channel != nil)]; +} + +- (NSUInteger)numberOfRowsInTable:(WKInterfaceTable *)table section:(NSUInteger)section +{ + return 1 + [self _messageHasWebPage]; +} + +- (Class)table:(WKInterfaceTable *)table rowControllerClassAtIndexPath:(TGIndexPath *)indexPath +{ + if (indexPath.row == 1) + return [TGMessageViewWebPageRowController class]; + + return [TGMessageViewMessageRowController class]; +} + +- (void)table:(WKInterfaceTable *)table updateRowController:(TGTableRowController *)rowController forIndexPath:(NSIndexPath *)indexPath +{ + __weak TGMessageViewController *weakSelf = self; + rowController.isVisible = ^bool + { + __strong TGMessageViewController *strongSelf = weakSelf; + if (strongSelf == nil) + return false; + + return strongSelf.isVisible; + }; + + TGBridgeMessage *message = _context.message; + if ([rowController isKindOfClass:[TGMessageViewMessageRowController class]]) + { + TGMessageViewMessageRowController *controller = (TGMessageViewMessageRowController *)rowController; + [controller updateWithMessage:message context:_context.context additionalPeers:_context.additionalPeers]; + + void (^openUserInfo)(int64_t) = ^(int64_t peerId) + { + __strong TGMessageViewController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + if (peerId != 0) + { + TGUserInfoControllerContext *context = nil; + if (TGPeerIdIsChannel(peerId)) + context = [[TGUserInfoControllerContext alloc] initWithChannel:_context.additionalPeers[@(peerId)]]; + else + context = [[TGUserInfoControllerContext alloc] initWithUserId:(int32_t)peerId]; + + [strongSelf pushControllerWithClass:[TGUserInfoController class] context:context]; + } + }; + + void (^openRemote)(void) = ^ + { + __strong TGMessageViewController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf->_remoteActionDisposable setDisposable:[[TGBridgeRemoteSignals openRemoteMessageWithPeerId:strongSelf->_context.peerId messageId:message.identifier type:0 autoPlay:true] startWithNext:^(id next) + { + + }]]; + }; + + for (TGBridgeMediaAttachment *attachment in message.media) + { + if ([attachment isKindOfClass:[TGBridgeForwardedMessageMediaAttachment class]]) + { + TGBridgeForwardedMessageMediaAttachment *forwardAttachment = (TGBridgeForwardedMessageMediaAttachment *)attachment; + + controller.forwardPressed = ^ + { + openUserInfo(forwardAttachment.peerId); + }; + } + else if ([attachment isKindOfClass:[TGBridgeContactMediaAttachment class]]) + { + TGBridgeContactMediaAttachment *contactAttachment = (TGBridgeContactMediaAttachment *)attachment; + + controller.contactPressed = ^ + { + openUserInfo(contactAttachment.uid); + }; + } + else if ([attachment isKindOfClass:[TGBridgeVideoMediaAttachment class]]) + { + controller.playPressed = ^ + { + openRemote(); + }; + } + else if ([attachment isKindOfClass:[TGBridgeAudioMediaAttachment class]]) + { + __weak TGMessageViewMessageRowController *weakMessageRow = controller; + controller.playPressed = ^ + { + __strong TGMessageViewController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + TGBridgeMediaAttachment *audioAttachment = nil; + for (TGBridgeMediaAttachment *attachment in message.media) + { + if ([attachment isKindOfClass:[TGBridgeAudioMediaAttachment class]]) + { + audioAttachment = (TGBridgeAudioMediaAttachment *)attachment; + } + else if ([attachment isKindOfClass:[TGBridgeDocumentMediaAttachment class]]) + { + TGBridgeDocumentMediaAttachment *documentAttachment = (TGBridgeDocumentMediaAttachment *)attachment; + if (documentAttachment.isVoice) + audioAttachment = documentAttachment; + } + } + + if (audioAttachment != nil) + { + __strong TGMessageViewMessageRowController *strongConversationRow = weakMessageRow; + if ([strongSelf->_pendingAudioAttachment isEqual:audioAttachment]) + { + if (strongConversationRow != nil) + [strongConversationRow setProcessingState:false]; + + strongSelf->_pendingAudioAttachment = nil; + + [strongSelf->_playAudioDisposable setDisposable:nil]; + } + else + { + if (strongConversationRow != nil) + [strongConversationRow setProcessingState:true]; + + strongSelf->_pendingAudioAttachment = audioAttachment; + + [strongSelf->_playAudioDisposable setDisposable:[[[TGBridgeAudioSignals audioForAttachment:audioAttachment conversationId:message.cid messageId:message.identifier] deliverOn:[SQueue mainQueue]] startWithNext:^(NSURL *url) + { + if (url == nil) + return; + + __strong TGMessageViewController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + __strong TGMessageViewMessageRowController *strongConversationRow = weakMessageRow; + if (strongConversationRow != nil) + [strongConversationRow setProcessingState:false]; + + strongSelf->_pendingAudioAttachment = nil; + + [strongSelf presentMediaPlayerControllerWithURL:url options:@{ WKMediaPlayerControllerOptionsAutoplayKey: @true } completion:^(BOOL didPlayToEnd, NSTimeInterval endTime, NSError *error) {}]; + }]]; + } + } + }; + } + } + } + else if ([rowController isKindOfClass:[TGMessageViewWebPageRowController class]]) + { + TGMessageViewWebPageRowController *controller = (TGMessageViewWebPageRowController *)rowController; + + TGBridgeWebPageMediaAttachment *pageAttachment = nil; + for (TGBridgeMediaAttachment *attachment in _context.message.media) + { + if ([attachment isKindOfClass:[TGBridgeWebPageMediaAttachment class]]) + { + pageAttachment = (TGBridgeWebPageMediaAttachment *)attachment; + break; + } + } + + [controller updateWithAttachment:pageAttachment message:message]; + } +} + +- (bool)_messageHasWebPage +{ + for (TGBridgeMediaAttachment *attachment in _context.message.media) + { + if ([attachment isKindOfClass:[TGBridgeWebPageMediaAttachment class]]) + { + TGBridgeWebPageMediaAttachment *webAttachment = (TGBridgeWebPageMediaAttachment *)attachment; + if (webAttachment.title.length == 0 && webAttachment.pageDescription.length == 0) + return false; + + return true; + } + } + + return false; +} + ++ (NSString *)identifier +{ + return TGMessageViewControllerIdentifier; +} + +@end diff --git a/Watch/Extension/TGMessageViewFooterController.h b/Watch/Extension/TGMessageViewFooterController.h new file mode 100644 index 0000000000..26a865db7a --- /dev/null +++ b/Watch/Extension/TGMessageViewFooterController.h @@ -0,0 +1,29 @@ +#import "WKInterfaceTable+TGDataDrivenTable.h" + +@class TGBridgeMessage; + +@interface TGMessageViewFooterController : TGTableRowController + +@property (nonatomic, weak) IBOutlet WKInterfaceLabel *dateLabel; +@property (nonatomic, weak) IBOutlet WKInterfaceLabel *timeLabel; +@property (nonatomic, weak) IBOutlet WKInterfaceGroup *statusGroup; + +@property (nonatomic, weak) IBOutlet WKInterfaceButton *forwardButton; +@property (nonatomic, weak) IBOutlet WKInterfaceButton *replyButton; +@property (nonatomic, weak) IBOutlet WKInterfaceButton *viewButton; + +@property (nonatomic, weak) IBOutlet WKInterfaceLabel *forwardLabel; +@property (nonatomic, weak) IBOutlet WKInterfaceLabel *replyLabel; +@property (nonatomic, weak) IBOutlet WKInterfaceLabel *viewLabel; + +- (IBAction)forwardButtonPressedAction; +- (IBAction)replyButtonPressedAction; +- (IBAction)viewButtonPressedAction; + +@property (nonatomic, copy) void (^forwardPressed)(void); +@property (nonatomic, copy) void (^replyPressed)(void); +@property (nonatomic, copy) void (^viewPressed)(void); + +- (void)updateWithMessage:(TGBridgeMessage *)message channel:(bool)channel; + +@end diff --git a/Watch/Extension/TGMessageViewFooterController.m b/Watch/Extension/TGMessageViewFooterController.m new file mode 100644 index 0000000000..d0aaaa7caa --- /dev/null +++ b/Watch/Extension/TGMessageViewFooterController.m @@ -0,0 +1,50 @@ +#import "TGMessageViewFooterController.h" +#import "TGWatchCommon.h" +#import "TGDateUtils.h" + +#import "TGBridgeMessage.h" + +NSString *const TGMessageViewFooterIdentifier = @"TGMessageViewFooter"; + +@implementation TGMessageViewFooterController + +- (void)updateWithMessage:(TGBridgeMessage *)message channel:(bool)channel +{ + self.dateLabel.text = [TGDateUtils stringForFullDate:message.date]; + self.timeLabel.text = [TGDateUtils stringForShortTime:message.date]; + + self.forwardLabel.text = TGLocalized(@"Watch.MessageView.Forward"); + self.replyLabel.text = TGLocalized(@"Watch.MessageView.Reply"); + self.viewLabel.text = TGLocalized(@"Watch.MessageView.ViewOnPhone"); + + if (channel) + { + self.forwardButton.hidden = true; + self.replyButton.hidden = true; + } +} + +- (IBAction)forwardButtonPressedAction +{ + if (self.forwardPressed != nil) + self.forwardPressed(); +} + +- (IBAction)replyButtonPressedAction +{ + if (self.replyPressed != nil) + self.replyPressed(); +} + +- (IBAction)viewButtonPressedAction +{ + if (self.viewPressed != nil) + self.viewPressed(); +} + ++ (NSString *)identifier +{ + return TGMessageViewFooterIdentifier; +} + +@end diff --git a/Watch/Extension/TGMessageViewMessageRowController.h b/Watch/Extension/TGMessageViewMessageRowController.h new file mode 100644 index 0000000000..46d785ab66 --- /dev/null +++ b/Watch/Extension/TGMessageViewMessageRowController.h @@ -0,0 +1,59 @@ +#import "WKInterfaceTable+TGDataDrivenTable.h" + +@class TGBridgeMessage; +@class TGBridgeContext; + +@interface TGMessageViewMessageRowController : TGTableRowController + +@property (nonatomic, weak) IBOutlet WKInterfaceButton *forwardHeaderButton; +@property (nonatomic, weak) IBOutlet WKInterfaceLabel *forwardTitleLabel; +@property (nonatomic, weak) IBOutlet WKInterfaceLabel *forwardFromLabel; + +@property (nonatomic, weak) IBOutlet WKInterfaceGroup *replyHeaderGroup; +@property (nonatomic, weak) IBOutlet WKInterfaceGroup *replyHeaderImageGroup; +@property (nonatomic, weak) IBOutlet WKInterfaceLabel *replyAuthorNameLabel; +@property (nonatomic, weak) IBOutlet WKInterfaceLabel *replyMessageTextLabel; + +@property (nonatomic, weak) IBOutlet WKInterfaceGroup *mediaGroup; +@property (nonatomic, weak) IBOutlet WKInterfaceImage *activityIndicator; + +@property (nonatomic, weak) IBOutlet WKInterfaceGroup *mapGroup; +@property (nonatomic, weak) IBOutlet WKInterfaceMap *map; + +@property (nonatomic, weak) IBOutlet WKInterfaceButton *playButton; + +@property (nonatomic, weak) IBOutlet WKInterfaceGroup *durationGroup; +@property (nonatomic, weak) IBOutlet WKInterfaceLabel *durationLabel; + +@property (nonatomic, weak) IBOutlet WKInterfaceLabel *titleLabel; +@property (nonatomic, weak) IBOutlet WKInterfaceLabel *subtitleLabel; + +@property (nonatomic, weak) IBOutlet WKInterfaceGroup *fileGroup; +@property (nonatomic, weak) IBOutlet WKInterfaceGroup *fileIconGroup; +@property (nonatomic, weak) IBOutlet WKInterfaceButton *audioButton; +@property (nonatomic, weak) IBOutlet WKInterfaceImage *audioIcon; +@property (nonatomic, weak) IBOutlet WKInterfaceImage *venueIcon; + +@property (nonatomic, weak) IBOutlet WKInterfaceButton *contactButton; +@property (nonatomic, weak) IBOutlet WKInterfaceGroup *avatarGroup; +@property (nonatomic, weak) IBOutlet WKInterfaceLabel *avatarInitialsLabel; +@property (nonatomic, weak) IBOutlet WKInterfaceLabel *nameLabel; +@property (nonatomic, weak) IBOutlet WKInterfaceLabel *phoneLabel; + +@property (nonatomic, weak) IBOutlet WKInterfaceGroup *stickerGroup; + +@property (nonatomic, weak) IBOutlet WKInterfaceLabel *messageTextLabel; + +@property (nonatomic, copy) void (^forwardPressed)(void); +@property (nonatomic, copy) void (^playPressed)(void); +@property (nonatomic, copy) void (^contactPressed)(void); + +- (IBAction)forwardButtonPressedAction; +- (IBAction)playButtonPressedAction; +- (IBAction)contactButtonPressedAction; + +- (void)setProcessingState:(bool)processing; + +- (void)updateWithMessage:(TGBridgeMessage *)message context:(TGBridgeContext *)context additionalPeers:(NSDictionary *)additionalPeers; + +@end diff --git a/Watch/Extension/TGMessageViewMessageRowController.m b/Watch/Extension/TGMessageViewMessageRowController.m new file mode 100644 index 0000000000..ce10c48756 --- /dev/null +++ b/Watch/Extension/TGMessageViewMessageRowController.m @@ -0,0 +1,379 @@ +#import "TGMessageViewMessageRowController.h" +#import "TGWatchCommon.h" +#import "TGExtensionDelegate.h" + +#import "TGDateUtils.h" +#import "TGStringUtils.h" +#import "TGLocationUtils.h" + +#import "WKInterfaceGroup+Signals.h" +#import "TGMessageViewModel.h" + +#import "TGBridgeMediaSignals.h" + +#import "TGBridgeUser.h" +#import "TGBridgeMessage.h" +#import "TGBridgeUserCache.h" + +#import "TGBridgeContext.h" + +#import "TGBridgePeerIdAdapter.h" + +NSString *const TGMessageViewMessageRowIdentifier = @"TGMessageViewMessageRow"; + +@interface TGMessageViewMessageRowController () +{ + NSString *_currentAvatarPhoto; + int64_t _currentDocumentId; + int64_t _currentPhotoId; + int64_t _currentReplyPhotoId; + + bool _processing; +} +@end + +@implementation TGMessageViewMessageRowController + +- (IBAction)forwardButtonPressedAction +{ + if (self.forwardPressed != nil) + self.forwardPressed(); +} + +- (IBAction)playButtonPressedAction +{ + if (self.playPressed != nil) + self.playPressed(); +} + +- (IBAction)contactButtonPressedAction +{ + if (self.contactPressed != nil) + self.contactPressed(); +} + +- (void)updateWithMessage:(TGBridgeMessage *)message context:(TGBridgeContext *)context additionalPeers:(NSDictionary *)additionalPeers +{ + bool mediaGroupHidden = true; + bool mapGroupHidden = true; + bool fileGroupHidden = true; + bool stickerGroupHidden = true; + bool contactButtonHidden = true; + + TGBridgeForwardedMessageMediaAttachment *forwardAttachment = nil; + TGBridgeReplyMessageMediaAttachment *replyAttachment = nil; + id messageText = nil; + CGFloat fontSize = [TGMessageViewMessageRowController textFontSize]; + + bool inhibitForwardHeader = false; + + for (TGBridgeMediaAttachment *attachment in message.media) + { + if ([attachment isKindOfClass:[TGBridgeForwardedMessageMediaAttachment class]]) + { + forwardAttachment = (TGBridgeForwardedMessageMediaAttachment *)attachment; + } + else if ([attachment isKindOfClass:[TGBridgeReplyMessageMediaAttachment class]]) + { + replyAttachment = (TGBridgeReplyMessageMediaAttachment *)attachment; + } + else if ([attachment isKindOfClass:[TGBridgeImageMediaAttachment class]]) + { + mediaGroupHidden = false; + + TGBridgeImageMediaAttachment *imageAttachment = (TGBridgeImageMediaAttachment *)attachment; + + if (message.text.length > 0) + messageText = message.text; + + CGSize imageSize = CGSizeZero; + + [TGMessageViewModel updateMediaGroup:self.mediaGroup activityIndicator:self.activityIndicator attachment:imageAttachment message:message notification:false currentPhoto:&_currentPhotoId standalone:true margin:0 imageSize:&imageSize isVisible:self.isVisible completion:nil]; + + self.mediaGroup.width = imageSize.width; + self.mediaGroup.height = imageSize.height; + + self.playButton.hidden = true; + self.durationGroup.hidden = true; + } + else if ([attachment isKindOfClass:[TGBridgeVideoMediaAttachment class]]) + { + mediaGroupHidden = false; + + TGBridgeVideoMediaAttachment *videoAttachment = (TGBridgeVideoMediaAttachment *)attachment; + + if (message.text.length > 0) + messageText = message.text; + + CGSize imageSize = CGSizeZero; + + [TGMessageViewModel updateMediaGroup:self.mediaGroup activityIndicator:self.activityIndicator attachment:videoAttachment message:message notification:false currentPhoto:NULL standalone:true margin:0 imageSize:&imageSize isVisible:self.isVisible completion:nil]; + + self.mediaGroup.width = imageSize.width; + self.mediaGroup.height = imageSize.height; + + self.playButton.hidden = false; + self.durationGroup.hidden = false; + + NSInteger durationMinutes = floor(videoAttachment.duration / 60.0); + NSInteger durationSeconds = videoAttachment.duration % 60; + self.durationLabel.text = [NSString stringWithFormat:@"%ld:%02ld", (long)durationMinutes, (long)durationSeconds]; + } + else if ([attachment isKindOfClass:[TGBridgeDocumentMediaAttachment class]]) + { + TGBridgeDocumentMediaAttachment *documentAttachment = (TGBridgeDocumentMediaAttachment *)attachment; + + if (documentAttachment.isSticker) + { + stickerGroupHidden = false; + + [TGStickerViewModel updateWithMessage:message notification:false isGroup:false context:context currentDocumentId:&_currentDocumentId authorLabel:nil imageGroup:self.stickerGroup isVisible:self.isVisible completion:nil]; + } + else if (documentAttachment.isAudio && documentAttachment.isVoice) + { + fileGroupHidden = false; + + if (documentAttachment.isAudio && message.text.length > 0) + messageText = message.text; + + self.titleLabel.text = TGLocalized(@"Message.Audio"); + + NSInteger durationMinutes = floor(documentAttachment.duration / 60.0); + NSInteger durationSeconds = documentAttachment.duration % 60; + self.subtitleLabel.text = [NSString stringWithFormat:@"%ld:%02ld", (long)durationMinutes, (long)durationSeconds]; + + self.audioButton.hidden = false; + self.fileIconGroup.hidden = true; + self.venueIcon.hidden = true; + + inhibitForwardHeader = true; + } + else + { + fileGroupHidden = false; + + if (message.text.length > 0) + messageText = message.text; + + self.titleLabel.text = documentAttachment.fileName; + self.subtitleLabel.text = [TGStringUtils stringForFileSize:documentAttachment.fileSize precision:2]; + + self.fileIconGroup.hidden = false; + self.audioButton.hidden = true; + self.venueIcon.hidden = true; + } + } + else if ([attachment isKindOfClass:[TGBridgeAudioMediaAttachment class]]) + { + fileGroupHidden = false; + + TGBridgeAudioMediaAttachment *audioAttachment = (TGBridgeAudioMediaAttachment *)attachment; + + self.titleLabel.text = TGLocalized(@"Message.Audio"); + + NSInteger durationMinutes = floor(audioAttachment.duration / 60.0); + NSInteger durationSeconds = audioAttachment.duration % 60; + self.subtitleLabel.text = [NSString stringWithFormat:@"%ld:%02ld", (long)durationMinutes, (long)durationSeconds]; + + self.audioButton.hidden = false; + self.fileIconGroup.hidden = true; + self.venueIcon.hidden = true; + + inhibitForwardHeader = true; + } + else if ([attachment isKindOfClass:[TGBridgeLocationMediaAttachment class]]) + { + mapGroupHidden = false; + + TGBridgeLocationMediaAttachment *locationAttachment = (TGBridgeLocationMediaAttachment *)attachment; + + CLLocationCoordinate2D coordinate = CLLocationCoordinate2DMake([TGLocationUtils adjustGMapLatitude:locationAttachment.latitude withPixelOffset:-10 zoom:15], locationAttachment.longitude); + self.map.region = MKCoordinateRegionMake(coordinate, MKCoordinateSpanMake(0.003, 0.003)); + self.map.centerPinCoordinate = CLLocationCoordinate2DMake(locationAttachment.latitude, locationAttachment.longitude); + + if (locationAttachment.venue != nil) + { + fileGroupHidden = false; + + self.titleLabel.text = locationAttachment.venue.title; + self.subtitleLabel.text = locationAttachment.venue.address; + } + + self.audioButton.hidden = true; + self.fileIconGroup.hidden = true; + self.venueIcon.hidden = false; + } + else if ([attachment isKindOfClass:[TGBridgeContactMediaAttachment class]]) + { + contactButtonHidden = false; + + TGBridgeContactMediaAttachment *contactAttachment = (TGBridgeContactMediaAttachment *)attachment; + + TGBridgeUser *user = [[TGBridgeUserCache instance] userWithId:contactAttachment.uid]; + + self.avatarGroup.hidden = false; + + if (user != nil) + { + self.contactButton.enabled = true; + + if (user.photoSmall.length > 0) + { + self.avatarInitialsLabel.hidden = true; + self.avatarGroup.backgroundColor = [UIColor hexColor:0x222223]; + if (![_currentAvatarPhoto isEqualToString:user.photoSmall]) + { + _currentAvatarPhoto = user.photoSmall; + + __weak TGMessageViewMessageRowController *weakSelf = self; + [self.avatarGroup setBackgroundImageSignal:[[TGBridgeMediaSignals avatarWithPeerId:user.identifier url:_currentAvatarPhoto type:TGBridgeMediaAvatarTypeSmall] onError:^(id next) + { + __strong TGMessageViewMessageRowController *strongSelf = weakSelf; + if (strongSelf != nil) + strongSelf->_currentAvatarPhoto = nil; + }] isVisible:self.isVisible]; + } + } + else + { + self.avatarInitialsLabel.hidden = false; + self.avatarGroup.backgroundColor = [TGColor colorForUserId:(int32_t)user.identifier myUserId:context.userId]; + self.avatarInitialsLabel.text = [TGStringUtils initialsForFirstName:user.firstName lastName:user.lastName single:true]; + + [self.avatarGroup setBackgroundImageSignal:nil isVisible:self.isVisible]; + _currentAvatarPhoto = nil; + } + } + else + { + self.contactButton.enabled = false; + + self.avatarInitialsLabel.hidden = false; + self.avatarGroup.backgroundColor = [UIColor grayColor]; + self.avatarInitialsLabel.text = [TGStringUtils initialsForFirstName:contactAttachment.firstName lastName:contactAttachment.lastName single:true]; + } + + self.nameLabel.text = [contactAttachment displayName]; + self.phoneLabel.text = contactAttachment.prettyPhoneNumber; + } + else if ([attachment isKindOfClass:[TGBridgeUnsupportedMediaAttachment class]]) + { + fileGroupHidden = false; + + TGBridgeUnsupportedMediaAttachment *unsupportedAttachment = (TGBridgeUnsupportedMediaAttachment *)attachment; + + self.titleLabel.text = unsupportedAttachment.title; + self.subtitleLabel.text = unsupportedAttachment.subtitle; + + self.fileIconGroup.hidden = true; + self.audioButton.hidden = true; + self.venueIcon.hidden = true; + } + } + + if (messageText == nil) + messageText = [TGMessageViewModel attributedTextForMessage:message fontSize:fontSize textColor:[UIColor whiteColor]]; + + if (inhibitForwardHeader) + forwardAttachment = nil; + + id forwardPeer = nil; + if (forwardAttachment != nil) + { + if (TGPeerIdIsChannel(forwardAttachment.peerId)) + forwardPeer = additionalPeers[@(forwardAttachment.peerId)]; + else + forwardPeer = [[TGBridgeUserCache instance] userWithId:(int32_t)forwardAttachment.peerId]; + } + + [TGMessageViewModel updateForwardHeaderGroup:self.forwardHeaderButton titleLabel:self.forwardTitleLabel fromLabel:self.forwardFromLabel forwardAttachment:forwardAttachment forwardPeer:forwardPeer textColor:[UIColor whiteColor]]; + + [TGMessageViewModel updateReplyHeaderGroup:self.replyHeaderGroup authorLabel:self.replyAuthorNameLabel imageGroup:self.replyHeaderImageGroup textLabel:self.replyMessageTextLabel titleColor:[UIColor whiteColor] subtitleColor:[UIColor hexColor:0x7e7e81] replyAttachment:replyAttachment currentReplyPhoto:&_currentReplyPhotoId isVisible:self.isVisible completion:nil]; + + self.mediaGroup.hidden = mediaGroupHidden; + self.mapGroup.hidden = mapGroupHidden; + self.fileGroup.hidden = fileGroupHidden; + self.contactButton.hidden = contactButtonHidden; + self.stickerGroup.hidden = stickerGroupHidden; + + self.messageTextLabel.hidden = (((NSString *)messageText).length == 0); + if (!self.messageTextLabel.hidden) + { + if ([messageText isKindOfClass:[NSString class]]) + { + if (fontSize == 16.0f) + self.messageTextLabel.text = messageText; + else + self.messageTextLabel.attributedText = [TGMessageViewModel attributedTextForMessage:message fontSize:fontSize textColor:[UIColor whiteColor]]; + } + else if ([messageText isKindOfClass:[NSAttributedString class]]) + { + self.messageTextLabel.attributedText = messageText; + } + } +} + +- (void)setProcessingState:(bool)processing +{ + if (processing == _processing) + return; + + _processing = processing; + + if (processing) + { + [self.audioIcon setImageNamed:@"BubbleSpinner"]; + [self.audioIcon startAnimatingWithImagesInRange:NSMakeRange(0, 39) duration:0.65 repeatCount:0]; + } + else + { + [self.audioIcon stopAnimating]; + [self.audioIcon setImageNamed:@"MediaAudioPlay"]; + } +} + +- (void)notifyVisiblityChange +{ + [self.replyHeaderImageGroup updateIfNeeded]; + [self.mediaGroup updateIfNeeded]; + [self.avatarGroup updateIfNeeded]; + [self.stickerGroup updateIfNeeded]; +} + ++ (CGFloat)textFontSize +{ + TGContentSizeCategory category = [TGExtensionDelegate instance].contentSizeCategory; + + switch (category) + { + case TGContentSizeCategoryXS: + return 14.0f; + + case TGContentSizeCategoryS: + return 15.0f; + + case TGContentSizeCategoryL: + return 16.0f; + + case TGContentSizeCategoryXL: + return 17.0f; + + case TGContentSizeCategoryXXL: + return 18.0f; + + case TGContentSizeCategoryXXXL: + return 19.0f; + + default: + break; + } + + return 16.0f; +} + ++ (NSString *)identifier +{ + return TGMessageViewMessageRowIdentifier; +} + +@end diff --git a/Watch/Extension/TGMessageViewModel.h b/Watch/Extension/TGMessageViewModel.h new file mode 100644 index 0000000000..7feef050c4 --- /dev/null +++ b/Watch/Extension/TGMessageViewModel.h @@ -0,0 +1,32 @@ +#import + +@class TGBridgeContext; +@class TGBridgeUser; +@class TGBridgeMessage; +@class TGBridgeMediaAttachment; +@class TGBridgeActionMediaAttachment; +@class TGBridgeForwardedMessageMediaAttachment; +@class TGBridgeReplyMessageMediaAttachment; + +@interface TGMessageViewModel : NSObject + ++ (void)updateAuthorLabel:(WKInterfaceLabel *)authorLabel isOutgoing:(bool)isOutgoing isGroup:(bool)isGroup user:(TGBridgeUser *)user ownUserId:(int32_t)ownUserId; + ++ (void)updateMediaGroup:(WKInterfaceGroup *)mediaGroup activityIndicator:(WKInterfaceImage *)activityIndicator attachment:(TGBridgeMediaAttachment *)mediaAttachment message:(TGBridgeMessage *)message notification:(bool)notification currentPhoto:(int64_t *)currentPhoto standalone:(bool)standalone margin:(CGFloat)margin imageSize:(CGSize *)imageSize isVisible:(bool (^)(void))isVisible completion:(void (^)(void))completion; + ++ (void)updateForwardHeaderGroup:(WKInterfaceObject *)forwardHeaderGroup titleLabel:(WKInterfaceLabel *)titleLabel fromLabel:(WKInterfaceLabel *)fromLabel forwardAttachment:(TGBridgeForwardedMessageMediaAttachment *)forwardAttachment forwardPeer:(id)forwardPeer textColor:(UIColor *)textColor; + ++ (void)updateReplyHeaderGroup:(WKInterfaceGroup *)replyHeaderGroup authorLabel:(WKInterfaceLabel *)authorLabel imageGroup:(WKInterfaceGroup *)imageGroup textLabel:(WKInterfaceLabel *)textLabel titleColor:(UIColor *)titleColor subtitleColor:(UIColor *)subtitleColor replyAttachment:(TGBridgeReplyMessageMediaAttachment *)replyAttachment currentReplyPhoto:(int64_t *)currentReplyPhoto isVisible:(bool (^)(void))isVisible completion:(void (^)(void))completion; + ++ (void)imageBubbleSizeForImageSize:(CGSize)imageSize minSize:(CGSize)minSize maxSize:(CGSize)maxSize thumbnailSize:(out CGSize *)thumbnailSize renderSize:(out CGSize *)renderSize; + ++ (NSAttributedString *)attributedTextForMessage:(TGBridgeMessage *)message fontSize:(CGFloat)fontSize textColor:(UIColor *)textColor; ++ (NSString *)stringForActionAttachment:(TGBridgeActionMediaAttachment *)actionAttachment message:(TGBridgeMessage *)message users:(NSDictionary *)users forChannel:(bool)forChannel; + +@end + +@interface TGStickerViewModel : NSObject + ++ (void)updateWithMessage:(TGBridgeMessage *)message notification:(bool)notification isGroup:(bool)isGroup context:(TGBridgeContext *)context currentDocumentId:(int64_t *)currentDocumentId authorLabel:(WKInterfaceLabel *)authorLabel imageGroup:(WKInterfaceGroup *)imageGroup isVisible:(bool (^)(void))isVisible completion:(void (^)(void))completion; + +@end diff --git a/Watch/Extension/TGMessageViewModel.m b/Watch/Extension/TGMessageViewModel.m new file mode 100644 index 0000000000..ff4f47e1f8 --- /dev/null +++ b/Watch/Extension/TGMessageViewModel.m @@ -0,0 +1,522 @@ +#import "TGMessageViewModel.h" +#import "TGWatchCommon.h" +#import "TGStringUtils.h" +#import "TGGeometry.h" + +#import "WKInterfaceImage+Signals.h" +#import "WKInterfaceGroup+Signals.h" + +#import "TGBridgeUser.h" +#import "TGBridgeChat.h" +#import "TGBridgeMessage.h" +#import "TGBridgeUserCache.h" + +#import "TGBridgeContext.h" + +#import "TGBridgeMediaSignals.h" + +@implementation TGMessageViewModel + ++ (void)imageBubbleSizeForImageSize:(CGSize)imageSize minSize:(CGSize)minSize maxSize:(CGSize)maxSize thumbnailSize:(out CGSize *)thumbnailSize renderSize:(out CGSize *)renderSize +{ + CGSize imageTargetMaxSize = maxSize; + CGSize imageScalingMaxSize = CGSizeMake(imageTargetMaxSize.width - 18.0f, imageTargetMaxSize.height - 18.0f); + CGSize imageTargetMinSize = minSize; + + CGFloat imageAspect = 1.0f; + if (imageSize.width > 1.0f - FLT_EPSILON && imageSize.height > 1.0f - FLT_EPSILON) + imageAspect = imageSize.width / imageSize.height; + + if (imageSize.width < imageScalingMaxSize.width || imageSize.height < imageScalingMaxSize.height) + { + if (imageSize.width <= FLT_EPSILON || imageSize.height <= FLT_EPSILON) + imageSize = imageTargetMinSize; + } + else + { + if (imageSize.width > imageTargetMaxSize.width) + { + imageSize.width = imageTargetMaxSize.width; + imageSize.height = floorf(imageTargetMaxSize.width / imageAspect); + } + + if (imageSize.height > imageTargetMaxSize.height) + { + imageSize.width = floorf(imageTargetMaxSize.height * imageAspect); + imageSize.height = imageTargetMaxSize.height; + } + } + + if (renderSize != NULL) + *renderSize = imageSize; + + imageSize.width = MIN(imageTargetMaxSize.width, imageSize.width); + imageSize.height = MIN(imageTargetMaxSize.height, imageSize.height); + + imageSize.width = MAX(imageTargetMinSize.width, imageSize.width); + imageSize.height = MAX(imageTargetMinSize.height, imageSize.height); + + if (thumbnailSize != NULL) + *thumbnailSize = imageSize; +} + ++ (void)updateAuthorLabel:(WKInterfaceLabel *)authorLabel isOutgoing:(bool)isOutgoing isGroup:(bool)isGroup user:(TGBridgeUser *)user ownUserId:(int32_t)ownUserId +{ + if (isGroup && !isOutgoing) + { + authorLabel.hidden = false; + authorLabel.text = user.displayName; + authorLabel.textColor = [TGColor colorForUserId:(int32_t)user.identifier myUserId:ownUserId]; + } + else + { + authorLabel.hidden = true; + } +} + ++ (void)updateMediaGroup:(WKInterfaceGroup *)mediaGroup activityIndicator:(WKInterfaceImage *)activityIndicator attachment:(TGBridgeMediaAttachment *)mediaAttachment message:(TGBridgeMessage *)message notification:(bool)notification currentPhoto:(int64_t *)currentPhoto standalone:(bool)standalone margin:(CGFloat)margin imageSize:(CGSize *)imageSize isVisible:(bool (^)(void))isVisible completion:(void (^)(void))completion +{ + CGSize targetImageSize = CGSizeZero; + + if ([mediaAttachment isKindOfClass:[TGBridgeImageMediaAttachment class]]) + targetImageSize = ((TGBridgeImageMediaAttachment *)mediaAttachment).dimensions; + else if ([mediaAttachment isKindOfClass:[TGBridgeVideoMediaAttachment class]]) + targetImageSize = ((TGBridgeVideoMediaAttachment *)mediaAttachment).dimensions; + + CGSize screenSize = TGWatchScreenSize(); + + CGSize mediaGroupSize = CGSizeZero; + if (standalone) + { + mediaGroupSize = TGFitSize(targetImageSize, CGSizeMake(screenSize.width - margin * 2, FLT_MAX)); + } + else + { + CGSize maxSize = CGSizeMake(screenSize.width - 15, screenSize.width); + CGSize minSize = CGSizeMake(screenSize.width / 1.25f, screenSize.width / 2); + [self imageBubbleSizeForImageSize:targetImageSize minSize:minSize maxSize:maxSize thumbnailSize:&mediaGroupSize renderSize:NULL]; + } + + mediaGroupSize = CGSizeMake(ceilf(mediaGroupSize.width), ceilf(mediaGroupSize.height)); + + if (imageSize != NULL) + *imageSize = CGSizeMake(mediaGroupSize.width, mediaGroupSize.height); + + if ([mediaAttachment isKindOfClass:[TGBridgeImageMediaAttachment class]]) + { + TGBridgeImageMediaAttachment *imageAttachment = (TGBridgeImageMediaAttachment *)mediaAttachment; + + if (currentPhoto == NULL || imageAttachment.imageId != *currentPhoto) + { + if (currentPhoto != NULL) + *currentPhoto = imageAttachment.imageId; + + [mediaGroup setBackgroundImageSignal:[[[TGBridgeMediaSignals thumbnailWithPeerId:message.cid messageId:message.identifier size:mediaGroupSize notification:notification] onNext:^(id next) + { + if (next != nil) + activityIndicator.hidden = true; + + if (completion != nil) + completion(); + }] onError:^(id error) + { + if (currentPhoto != NULL) + *currentPhoto = 0; + + if (completion != nil) + completion(); + }] isVisible:isVisible]; + } + } + else if ([mediaAttachment isKindOfClass:[TGBridgeVideoMediaAttachment class]]) + { + activityIndicator.hidden = true; + + TGBridgeVideoMediaAttachment *videoAttachment = (TGBridgeVideoMediaAttachment *)mediaAttachment; + + if (currentPhoto == NULL || videoAttachment.videoId != *currentPhoto) + { + if (currentPhoto != NULL) + *currentPhoto = videoAttachment.videoId; + + [mediaGroup setBackgroundImageSignal:[[[TGBridgeMediaSignals thumbnailWithPeerId:message.cid messageId:message.identifier size:mediaGroupSize notification:false] onNext:^(id next) + { + if (next != nil) + activityIndicator.hidden = true; + + if (completion != nil) + completion(); + }] onError:^(id error) + { + if (currentPhoto != NULL) + *currentPhoto = 0; + + if (completion != nil) + completion(); + }] isVisible:isVisible]; + } + } +} + ++ (void)updateForwardHeaderGroup:(WKInterfaceGroup *)forwardHeaderGroup titleLabel:(WKInterfaceLabel *)titleLabel fromLabel:(WKInterfaceLabel *)fromLabel forwardAttachment:(TGBridgeForwardedMessageMediaAttachment *)forwardAttachment forwardPeer:(id)forwardPeer textColor:(UIColor *)textColor +{ + forwardHeaderGroup.hidden = (forwardAttachment == nil); + if (forwardHeaderGroup.hidden) + return; + + titleLabel.text = TGLocalized(@"Watch.Message.ForwardedFrom"); + + NSString *authorName = nil; + if ([forwardPeer isKindOfClass:[TGBridgeUser class]]) + authorName = ((TGBridgeUser *)forwardPeer).displayName; + else if ([forwardPeer isKindOfClass:[TGBridgeChat class]]) + authorName = ((TGBridgeChat *)forwardPeer).groupTitle; + + if (authorName == nil) + authorName = @""; + + NSMutableAttributedString *forwardAttributedText = [[NSMutableAttributedString alloc] initWithString:authorName attributes:@{ NSFontAttributeName: [UIFont systemFontOfSize:12], NSForegroundColorAttributeName:textColor }]; + + NSRange formatNameRange = NSMakeRange(0, authorName.length); + if (formatNameRange.location != NSNotFound) + { + [forwardAttributedText addAttribute:NSFontAttributeName value:[UIFont systemFontOfSize:12 weight:UIFontWeightMedium] range:NSMakeRange(formatNameRange.location, authorName.length)]; + } + + fromLabel.attributedText = forwardAttributedText; +} + ++ (void)updateReplyHeaderGroup:(WKInterfaceGroup *)replyHeaderGroup authorLabel:(WKInterfaceLabel *)authorLabel imageGroup:(WKInterfaceGroup *)imageGroup textLabel:(WKInterfaceLabel *)textLabel titleColor:(UIColor *)titleColor subtitleColor:(UIColor *)subtitleColor replyAttachment:(TGBridgeReplyMessageMediaAttachment *)replyAttachment currentReplyPhoto:(int64_t *)currentReplyPhoto isVisible:(bool (^)(void))isVisible completion:(void (^)(void))completion +{ + TGBridgeMessage *message = replyAttachment.message; + replyHeaderGroup.hidden = (message == nil); + if (replyHeaderGroup.hidden) + return; + + bool hasAttachment = false; + bool hasImagePreview = false; + NSString *messageText = nil; + UIColor *textColor = nil; + TGBridgeImageMediaAttachment *imageAttachment = nil; + TGBridgeVideoMediaAttachment *videoAttachment = nil; + + for (TGBridgeMediaAttachment *attachment in message.media) + { + if ([attachment isKindOfClass:[TGBridgeImageMediaAttachment class]]) + { + hasAttachment = true; + hasImagePreview = true; + messageText = TGLocalized(@"Message.Photo"); + imageAttachment = (TGBridgeImageMediaAttachment *)attachment; + } + else if ([attachment isKindOfClass:[TGBridgeVideoMediaAttachment class]]) + { + hasAttachment = true; + hasImagePreview = true; + messageText = TGLocalized(@"Message.Video"); + videoAttachment = (TGBridgeVideoMediaAttachment *)attachment; + } + else if ([attachment isKindOfClass:[TGBridgeAudioMediaAttachment class]]) + { + hasAttachment = true; + messageText = TGLocalized(@"Message.Audio"); + } + else if ([attachment isKindOfClass:[TGBridgeDocumentMediaAttachment class]]) + { + hasAttachment = true; + TGBridgeDocumentMediaAttachment *documentAttachment = (TGBridgeDocumentMediaAttachment *)attachment; + + if (documentAttachment.isSticker) + { + if (documentAttachment.stickerAlt.length > 0) + messageText = [NSString stringWithFormat:@"%@ %@", documentAttachment.stickerAlt, TGLocalized(@"Message.Sticker")]; + else + messageText = TGLocalized(@"Message.Sticker"); + } + else + { + if (documentAttachment.fileName.length > 0) + messageText = documentAttachment.fileName; + else + messageText = TGLocalized(@"Message.File"); + } + } + else if ([attachment isKindOfClass:[TGBridgeLocationMediaAttachment class]]) + { + hasAttachment = true; + messageText = TGLocalized(@"Message.Location"); + } + else if ([attachment isKindOfClass:[TGBridgeContactMediaAttachment class]]) + { + hasAttachment = true; + messageText = TGLocalized(@"Message.Contact"); + } + else if ([attachment isKindOfClass:[TGBridgeActionMediaAttachment class]]) + { + hasAttachment = true; + + TGBridgeActionMediaAttachment *actionAttachment = (TGBridgeActionMediaAttachment *)attachment; + [self stringForActionAttachment:actionAttachment message:message users:nil forChannel:false]; + } + } + + if (!hasAttachment) + { + messageText = message.text; + textColor = titleColor; + } + else + { + textColor = subtitleColor; + } + + authorLabel.text = [[[TGBridgeUserCache instance] userWithId:(int32_t)message.fromUid] displayName]; + imageGroup.hidden = !hasImagePreview; + textLabel.text = messageText; + textLabel.textColor = textColor; + + if (imageGroup != nil && imageAttachment != nil) + { + if (currentReplyPhoto == NULL || imageAttachment.imageId != *currentReplyPhoto) + { + if (currentReplyPhoto != NULL) + *currentReplyPhoto = imageAttachment.imageId; + + [imageGroup setBackgroundImageSignal:[[[TGBridgeMediaSignals thumbnailWithPeerId:message.cid messageId:message.identifier size:CGSizeMake(26, 26) notification:false] onNext:^(id next) + { + if (completion != nil) + completion(); + }] onError:^(id error) + { + if (currentReplyPhoto != NULL) + *currentReplyPhoto = 0; + + if (completion != nil) + completion(); + }] isVisible:isVisible]; + } + } + else if (imageGroup != nil && videoAttachment != nil) + { + if (currentReplyPhoto == NULL || videoAttachment.videoId != *currentReplyPhoto) + { + if (currentReplyPhoto != NULL) + *currentReplyPhoto = videoAttachment.videoId; + + [imageGroup setBackgroundImageSignal:[[[TGBridgeMediaSignals thumbnailWithPeerId:message.cid messageId:message.identifier size:CGSizeMake(26, 26) notification:false] onNext:^(id next) + { + if (completion != nil) + completion(); + }] onError:^(id error) + { + if (currentReplyPhoto != NULL) + *currentReplyPhoto = 0; + + if (completion != nil) + completion(); + }] isVisible:isVisible]; + } + } + else + { + if (completion != nil) + completion(); + } +} + ++ (NSString *)stringForActionAttachment:(TGBridgeActionMediaAttachment *)actionAttachment message:(TGBridgeMessage *)message users:(NSDictionary *)users forChannel:(bool)forChannel +{ + NSString *messageText = nil; + TGBridgeUser *author = (users != nil) ? users[@(message.fromUid)] : [[TGBridgeUserCache instance] userWithId:(int32_t)message.fromUid]; + + switch (actionAttachment.actionType) + { + case TGBridgeMessageActionChatEditTitle: + { + if (forChannel) + { + messageText = TGLocalized(@"Notification.RenamedChannel"); + } + else + { + NSString *authorName = [TGStringUtils initialsForFirstName:author.firstName lastName:author.lastName single:false]; + NSString *formatString = TGLocalized(@"Notification.RenamedChat"); + + messageText = [NSString stringWithFormat:formatString, authorName]; + } + } + break; + + case TGBridgeMessageActionChatEditPhoto: + { + NSString *authorName = [TGStringUtils initialsForFirstName:author.firstName lastName:author.lastName single:false]; + bool changed = actionAttachment.actionData[@"photo"]; + + if (forChannel) + { + messageText = changed ? TGLocalized(@"Channel.MessagePhotoUpdated") : TGLocalized(@"Channel.MessagePhotoRemoved"); + } + else + { + NSString *formatString = changed ? TGLocalized(@"Notification.ChangedGroupPhoto") : TGLocalized(@"Notification.RemovedGroupPhoto"); + + messageText = [NSString stringWithFormat:formatString, authorName]; + } + } + break; + + case TGBridgeMessageActionUserChangedPhoto: + { + + } + break; + + case TGBridgeMessageActionChatAddMember: + case TGBridgeMessageActionChatDeleteMember: + { + NSString *authorName = [TGStringUtils initialsForFirstName:author.firstName lastName:author.lastName single:false]; + TGBridgeUser *user = (users != nil) ? users[actionAttachment.actionData[@"uid"]] : [[TGBridgeUserCache instance] userWithId:[actionAttachment.actionData[@"uid"] int32Value]]; + + if (user.identifier == author.identifier) + { + NSString *formatString = (actionAttachment.actionType == TGBridgeMessageActionChatAddMember) ? TGLocalized(@"Notification.JoinedChat") : TGLocalized(@"Notification.LeftChat"); + messageText = [[NSString alloc] initWithFormat:formatString, authorName]; + } + else + { + NSString *userName = [TGStringUtils initialsForFirstName:user.firstName lastName:user.lastName single:false]; + NSString *formatString = (actionAttachment.actionType == TGBridgeMessageActionChatAddMember) ? TGLocalized(@"Notification.Invited") : TGLocalized(@"Notification.Kicked"); + messageText = [[NSString alloc] initWithFormat:formatString, authorName, userName]; + } + } + break; + + case TGBridgeMessageActionJoinedByLink: + { + NSString *authorName = [TGStringUtils initialsForFirstName:author.firstName lastName:author.lastName single:false]; + NSString *formatString = TGLocalized(@"Notification.JoinedGroupByLink"); + messageText = [[NSString alloc] initWithFormat:formatString, authorName, actionAttachment.actionData[@"title"]]; + } + break; + + case TGBridgeMessageActionCreateChat: + { + NSString *authorName = [TGStringUtils initialsForFirstName:author.firstName lastName:author.lastName single:false]; + NSString *formatString = TGLocalized(@"Notification.CreatedChatWithTitle"); + messageText = [[NSString alloc] initWithFormat:formatString, authorName, actionAttachment.actionData[@"title"]]; + } + break; + + case TGBridgeMessageActionContactRegistered: + { + messageText = TGLocalized(@"Watch.Notification.Joined"); + } + break; + + case TGBridgeMessageActionChannelCreated: + { + messageText = TGLocalized(@"Notification.CreatedChannel"); + } + break; + + case TGBridgeMessageActionChannelInviter: + { + TGBridgeUser *user = (users != nil) ? users[actionAttachment.actionData[@"uid"]] : [[TGBridgeUserCache instance] userWithId:[actionAttachment.actionData[@"uid"] int32Value]]; + NSString *authorName = [TGStringUtils initialsForFirstName:user.firstName lastName:user.lastName single:false]; + NSString *formatString = TGLocalized(@"Notification.ChannelInviter"); + + messageText = [[NSString alloc] initWithFormat:formatString, authorName]; + } + break; + + case TGBridgeMessageActionGroupMigratedTo: + { + messageText = TGLocalized(@"Notification.ChannelMigratedFrom"); + } + break; + + case TGBridgeMessageActionGroupActivated: + { + messageText = TGLocalized(@"Notification.GroupActivated"); + } + break; + + case TGBridgeMessageActionGroupDeactivated: + { + messageText = TGLocalized(@"Notification.GroupDeactivated"); + } + break; + + case TGBridgeMessageActionChannelMigratedFrom: + { + messageText = TGLocalized(@"Notification.ChannelMigratedFrom"); + } + break; + + default: + break; + } + + return messageText; +} + ++ (NSAttributedString *)attributedTextForMessage:(TGBridgeMessage *)message fontSize:(CGFloat)fontSize textColor:(UIColor *)textColor +{ + NSArray *textCheckingResults = [message textCheckingResults]; + + NSString *messageText = message.text ?: @""; + + NSMutableAttributedString *string = [[NSMutableAttributedString alloc] initWithString:messageText attributes:@{ NSFontAttributeName: [UIFont systemFontOfSize:fontSize], NSForegroundColorAttributeName: textColor }]; + + for (TGBridgeTextCheckingResult *result in textCheckingResults) + { + if (result.type == TGBridgeTextCheckingResultTypeBold) + [string addAttributes:@{ NSFontAttributeName: [UIFont systemFontOfSize:fontSize weight:UIFontWeightMedium] } range:result.range]; + else if (result.type == TGBridgeTextCheckingResultTypeItalic) + [string addAttributes:@{ NSFontAttributeName: [UIFont italicSystemFontOfSize:fontSize] } range:result.range]; + else if (result.type == TGBridgeTextCheckingResultTypeCode || result.type == TGBridgeTextCheckingResultTypePre) + [string addAttributes:@{ NSFontAttributeName: [UIFont fontWithName:@"Courier" size:fontSize] } range:result.range]; + } + + return string; +} + +@end + + +@implementation TGStickerViewModel + ++ (void)updateWithMessage:(TGBridgeMessage *)message notification:(bool)notification isGroup:(bool)isGroup context:(TGBridgeContext *)context currentDocumentId:(int64_t *)currentDocumentId authorLabel:(WKInterfaceLabel *)authorLabel imageGroup:(WKInterfaceGroup *)imageGroup isVisible:(bool (^)(void))isVisible completion:(void (^)(void))completion +{ + [TGMessageViewModel updateAuthorLabel:authorLabel isOutgoing:message.outgoing isGroup:isGroup user:[[TGBridgeUserCache instance] userWithId:(int32_t)message.fromUid] ownUserId:context.userId]; + + for (TGBridgeMediaAttachment *attachment in message.media) + { + if ([attachment isKindOfClass:[TGBridgeDocumentMediaAttachment class]]) + { + TGBridgeDocumentMediaAttachment *documentAttachment = (TGBridgeDocumentMediaAttachment *)attachment; + + if (currentDocumentId == NULL || *currentDocumentId != documentAttachment.documentId) + { + if (currentDocumentId != NULL) + *currentDocumentId = documentAttachment.documentId; + + [imageGroup setBackgroundImageSignal:[[[TGBridgeMediaSignals stickerWithDocumentId:documentAttachment.documentId peerId:message.cid messageId:message.identifier type:TGMediaStickerImageTypeNormal notification:notification] onNext:^(id next) + { + if (completion != nil) + completion(); + }] onError:^(id error) + { + if (currentDocumentId != NULL) + *currentDocumentId = 0; + + if (completion != nil) + completion(); + }] isVisible:isVisible]; + } + break; + } + } +} + +@end diff --git a/Watch/Extension/TGMessageViewWebPageRowController.h b/Watch/Extension/TGMessageViewWebPageRowController.h new file mode 100644 index 0000000000..952ecf5c93 --- /dev/null +++ b/Watch/Extension/TGMessageViewWebPageRowController.h @@ -0,0 +1,19 @@ +#import "WKInterfaceTable+TGDataDrivenTable.h" + +@class TGBridgeWebPageMediaAttachment; +@class TGBridgeMessage; + +@interface TGMessageViewWebPageRowController : TGTableRowController + +@property (nonatomic, weak) IBOutlet WKInterfaceLabel *siteNameLabel; +@property (nonatomic, weak) IBOutlet WKInterfaceLabel *titleLabel; +@property (nonatomic, weak) IBOutlet WKInterfaceGroup *titleImageGroup; +@property (nonatomic, weak) IBOutlet WKInterfaceLabel *textLabel; +@property (nonatomic, weak) IBOutlet WKInterfaceGroup *imageGroup; +@property (nonatomic, weak) IBOutlet WKInterfaceImage *activityIndicator; +@property (nonatomic, weak) IBOutlet WKInterfaceGroup *durationGroup; +@property (nonatomic, weak) IBOutlet WKInterfaceLabel *durationLabel; + +- (void)updateWithAttachment:(TGBridgeWebPageMediaAttachment *)attachment message:(TGBridgeMessage *)message; + +@end diff --git a/Watch/Extension/TGMessageViewWebPageRowController.m b/Watch/Extension/TGMessageViewWebPageRowController.m new file mode 100644 index 0000000000..ce3db6fea6 --- /dev/null +++ b/Watch/Extension/TGMessageViewWebPageRowController.m @@ -0,0 +1,89 @@ +#import "TGMessageViewWebPageRowController.h" + +#import "WKInterfaceGroup+Signals.h" +#import "TGBridgeMediaSignals.h" +#import "TGBridgeMessage.h" + +#import "TGMessageViewModel.h" +#import "TGBridgeImageMediaAttachment.h" +#import "TGBridgeWebPageMediaAttachment.h" + +NSString *const TGMessageViewWebPageRowIdentifier = @"TGMessageViewWebPageRow"; + +@interface TGMessageViewWebPageRowController () +{ + int64_t _photoId; +} +@end + +@implementation TGMessageViewWebPageRowController + +- (void)updateWithAttachment:(TGBridgeWebPageMediaAttachment *)attachment message:(TGBridgeMessage *)message +{ + if (attachment.siteName.length > 0) + self.siteNameLabel.text = attachment.siteName; + else + self.siteNameLabel.hidden = true; + + bool inTextImage = !([attachment.pageType isEqualToString:@"photo"] || [attachment.pageType isEqualToString:@"video"]); + if (attachment.pageDescription.length == 0) + inTextImage = false; + + NSString *title = attachment.title; + if (title.length == 0) + title = attachment.author; + + if (title.length > 0) + self.titleLabel.text = title; + else + self.titleLabel.hidden = true; + + if (attachment.pageDescription.length > 0) + self.textLabel.text = attachment.pageDescription; + else + self.textLabel.hidden = true; + + if (attachment.photo != nil) + { + if (inTextImage) + { + self.imageGroup.hidden = true; + + [self.titleImageGroup setBackgroundImageSignal:[TGBridgeMediaSignals thumbnailWithPeerId:message.cid messageId:message.identifier size:CGSizeMake(26, 26) notification:false] isVisible:self.isVisible]; + } + else + { + self.titleImageGroup.hidden = true; + self.imageGroup.hidden = false; + + CGSize imageSize = CGSizeZero; + + [TGMessageViewModel updateMediaGroup:self.imageGroup activityIndicator:self.activityIndicator attachment:attachment.photo message:message notification:false currentPhoto:&_photoId standalone:true margin:0 imageSize:&imageSize isVisible:self.isVisible completion:nil]; + + self.imageGroup.width = imageSize.width; + self.imageGroup.height = imageSize.height; + } + } + else + { + self.titleImageGroup.hidden = true; + self.imageGroup.hidden = true; + } + + if (attachment.duration != nil) + { + self.durationGroup.hidden = false; + + NSInteger duration = [attachment.duration doubleValue]; + NSInteger durationMinutes = floor(duration / 60.0); + NSInteger durationSeconds = duration % 60; + self.durationLabel.text = [NSString stringWithFormat:@"%ld:%02ld", (long)durationMinutes, (long)durationSeconds]; + } +} + ++ (NSString *)identifier +{ + return TGMessageViewWebPageRowIdentifier; +} + +@end diff --git a/Watch/Extension/TGNeoAttachmentViewModel.h b/Watch/Extension/TGNeoAttachmentViewModel.h new file mode 100644 index 0000000000..8fb5ba6013 --- /dev/null +++ b/Watch/Extension/TGNeoAttachmentViewModel.h @@ -0,0 +1,13 @@ +#import "TGNeoViewModel.h" +#import + +@class TGBridgeUser; + +@interface TGNeoAttachmentViewModel : TGNeoViewModel + +@property (nonatomic, readonly) bool inhibitsInitials; +@property (nonatomic, readonly) bool hasCaption; + +- (instancetype)initWithAttachments:(NSArray *)attachments author:(TGBridgeUser *)author forChannel:(bool)forChannel users:(NSDictionary *)users font:(UIFont *)font subTitleColor:(UIColor *)subTitleColor normalColor:(UIColor *)normalColor compact:(bool)compact caption:(NSString *)caption; + +@end diff --git a/Watch/Extension/TGNeoAttachmentViewModel.m b/Watch/Extension/TGNeoAttachmentViewModel.m new file mode 100644 index 0000000000..9af87799d1 --- /dev/null +++ b/Watch/Extension/TGNeoAttachmentViewModel.m @@ -0,0 +1,424 @@ +#import "TGNeoAttachmentViewModel.h" +#import "TGWatchCommon.h" +#import "TGNeoImageViewModel.h" +#import "TGNeoLabelViewModel.h" + +#import "TGStringUtils.h" + +#import "TGBridgeMessage.h" +#import "TGBridgeUser.h" + +@interface TGNeoAttachmentViewModel () +{ + TGNeoImageViewModel *_iconModel; + TGNeoLabelViewModel *_textModel; +} +@end + +@implementation TGNeoAttachmentViewModel + +- (instancetype)initWithAttachments:(NSArray *)attachments author:(TGBridgeUser *)author forChannel:(bool)forChannel users:(NSDictionary *)users font:(UIFont *)font subTitleColor:(UIColor *)subTitleColor normalColor:(UIColor *)normalColor compact:(bool)compact caption:(NSString *)caption +{ + bool hasAttachment = false; + NSString *messageText = nil; + NSMutableAttributedString *attributedText = nil; + NSString *messageIcon = nil; + bool useNormalColor = false; + bool inhibitsInitials = false; + bool hasCaption = false; + + CGFloat fontSize = font.pointSize; + + for (TGBridgeMediaAttachment *attachment in attachments) + { + if ([attachment isKindOfClass:[TGBridgeImageMediaAttachment class]]) + { + hasAttachment = true; + if (caption.length > 0) + { + hasCaption = true; + messageText = caption; + if (compact) + useNormalColor = true; + } + else + { + messageText = TGLocalized(@"Message.Photo"); + } + + if (!(useNormalColor && compact)) + messageIcon = @"MediaPhoto"; + } + else if ([attachment isKindOfClass:[TGBridgeVideoMediaAttachment class]]) + { + TGBridgeVideoMediaAttachment *videoAttachment = (TGBridgeVideoMediaAttachment *)attachment; + hasAttachment = true; + if (caption.length > 0) + { + hasCaption = true; + messageText = caption; + if (compact) + useNormalColor = true; + } + else + { + if (videoAttachment.round) + messageText = TGLocalized(@"Message.VideoMessage"); + else + messageText = TGLocalized(@"Message.Video"); + } + + if (!(useNormalColor && compact)) + messageIcon = @"MediaVideo"; + } + else if ([attachment isKindOfClass:[TGBridgeAudioMediaAttachment class]]) + { + hasAttachment = true; + messageText = TGLocalized(@"Message.Audio"); + + messageIcon = @"MediaAudio"; + } + else if ([attachment isKindOfClass:[TGBridgeDocumentMediaAttachment class]]) + { + hasAttachment = true; + TGBridgeDocumentMediaAttachment *documentAttachment = (TGBridgeDocumentMediaAttachment *)attachment; + + if (documentAttachment.isSticker) + { + if (documentAttachment.stickerAlt.length > 0) + messageText = [NSString stringWithFormat:@"%@ %@", documentAttachment.stickerAlt, TGLocalized(@"Message.Sticker")]; + else + messageText = TGLocalized(@"Message.Sticker"); + } + else if (documentAttachment.isAnimated) + { + messageText = TGLocalized(@"Message.Animation"); + messageIcon = @"MediaVideo"; + } + else if (documentAttachment.isAudio && documentAttachment.isVoice) + { + messageText = TGLocalized(@"Message.Audio"); + messageIcon = @"MediaAudio"; + } + else + { + if (caption.length > 0) + { + hasCaption = true; + messageText = caption; + if (compact) + useNormalColor = true; + } + else if (documentAttachment.fileName.length > 0) + { + messageText = documentAttachment.fileName; + } + else + { + messageText = TGLocalized(@"Message.File"); + } + messageIcon = @"MediaDocument"; + } + } + else if ([attachment isKindOfClass:[TGBridgeLocationMediaAttachment class]]) + { + hasAttachment = true; + messageText = TGLocalized(@"Message.Location"); + + messageIcon = @"MediaLocation"; + } + else if ([attachment isKindOfClass:[TGBridgeContactMediaAttachment class]]) + { + hasAttachment = true; + messageText = TGLocalized(@"Message.Contact"); + } + else if ([attachment isKindOfClass:[TGBridgeActionMediaAttachment class]]) + { + hasAttachment = true; + + TGBridgeActionMediaAttachment *actionAttachment = (TGBridgeActionMediaAttachment *)attachment; + NSString *actionText = nil; + NSArray *additionalAttributes = nil; + + switch (actionAttachment.actionType) + { + case TGBridgeMessageActionChatEditTitle: + { + if (forChannel) + { + messageText = TGLocalized(@"Notification.RenamedChannel"); + } + else + { + NSString *authorName = [TGStringUtils initialsForFirstName:author.firstName lastName:author.lastName single:false]; + NSString *formatString = TGLocalized(@"Notification.RenamedChat"); + + actionText = [NSString stringWithFormat:formatString, authorName]; + + NSRange formatNameRange = [formatString rangeOfString:@"%@"]; + if (formatNameRange.location != NSNotFound) + { + additionalAttributes = [TGNeoAttachmentViewModel _mediumFontAttributeForRange:NSMakeRange(formatNameRange.location, authorName.length) fontSize:fontSize]; + } + } + } + break; + + case TGBridgeMessageActionChatEditPhoto: + { + NSString *authorName = [TGStringUtils initialsForFirstName:author.firstName lastName:author.lastName single:false]; + bool changed = actionAttachment.actionData[@"photo"]; + + if (forChannel) + { + messageText = changed ? TGLocalized(@"Channel.MessagePhotoUpdated") : TGLocalized(@"Channel.MessagePhotoRemoved"); + } + else + { + NSString *formatString = changed ? TGLocalized(@"Notification.ChangedGroupPhoto") : TGLocalized(@"Notification.RemovedGroupPhoto"); + + actionText = [NSString stringWithFormat:formatString, authorName]; + + NSRange formatNameRange = [formatString rangeOfString:@"%@"]; + if (formatNameRange.location != NSNotFound) + { + additionalAttributes = [TGNeoAttachmentViewModel _mediumFontAttributeForRange:NSMakeRange(formatNameRange.location, authorName.length) fontSize:fontSize]; + } + } + } + break; + + case TGBridgeMessageActionUserChangedPhoto: + { + + } + break; + + case TGBridgeMessageActionChatAddMember: + case TGBridgeMessageActionChatDeleteMember: + { + NSString *authorName = [TGStringUtils initialsForFirstName:author.firstName lastName:author.lastName single:false]; + TGBridgeUser *user = users[@([actionAttachment.actionData[@"uid"] int32Value])]; + + if (user.identifier == author.identifier) + { + NSString *formatString = (actionAttachment.actionType == TGBridgeMessageActionChatAddMember) ? TGLocalized(@"Notification.JoinedChat") : TGLocalized(@"Notification.LeftChat"); + actionText = [[NSString alloc] initWithFormat:formatString, authorName]; + + NSRange formatNameRange = [formatString rangeOfString:@"%@"]; + if (formatNameRange.location != NSNotFound) + { + additionalAttributes = [TGNeoAttachmentViewModel _mediumFontAttributeForRange:NSMakeRange(formatNameRange.location, authorName.length) fontSize:fontSize]; + } + } + else + { + NSString *userName = [TGStringUtils initialsForFirstName:user.firstName lastName:user.lastName single:false]; + NSString *formatString = (actionAttachment.actionType == TGBridgeMessageActionChatAddMember) ? TGLocalized(@"Notification.Invited") : TGLocalized(@"Notification.Kicked"); + actionText = [[NSString alloc] initWithFormat:formatString, authorName, userName]; + + NSRange formatNameRangeFirst = [formatString rangeOfString:@"%@"]; + NSRange formatNameRangeSecond = formatNameRangeFirst.location != NSNotFound ? [formatString rangeOfString:@"%@" options:0 range:NSMakeRange(formatNameRangeFirst.location + formatNameRangeFirst.length, formatString.length - (formatNameRangeFirst.location + formatNameRangeFirst.length))] : NSMakeRange(NSNotFound, 0); + + if (formatNameRangeFirst.location != NSNotFound && formatNameRangeSecond.location != NSNotFound) + { + NSMutableArray *array = [[NSMutableArray alloc] init]; + + NSRange rangeFirst = NSMakeRange(formatNameRangeFirst.location, authorName.length); + [array addObjectsFromArray:[TGNeoAttachmentViewModel _mediumFontAttributeForRange:rangeFirst fontSize:fontSize]]; + [array addObjectsFromArray:[TGNeoAttachmentViewModel _mediumFontAttributeForRange:NSMakeRange(rangeFirst.length - formatNameRangeFirst.length + formatNameRangeSecond.location, userName.length) fontSize:fontSize]]; + + additionalAttributes = array; + } + } + } + break; + + case TGBridgeMessageActionJoinedByLink: + { + NSString *authorName = [TGStringUtils initialsForFirstName:author.firstName lastName:author.lastName single:false]; + NSString *formatString = TGLocalized(@"Notification.JoinedGroupByLink"); + actionText = [[NSString alloc] initWithFormat:formatString, authorName, actionAttachment.actionData[@"title"]]; + + NSRange formatNameRange = [formatString rangeOfString:@"%@"]; + if (formatNameRange.location != NSNotFound) + { + additionalAttributes = [TGNeoAttachmentViewModel _mediumFontAttributeForRange:NSMakeRange(formatNameRange.location, authorName.length) fontSize:fontSize]; + } + } + break; + + case TGBridgeMessageActionCreateChat: + { + NSString *authorName = [TGStringUtils initialsForFirstName:author.firstName lastName:author.lastName single:false]; + NSString *formatString = TGLocalized(@"Notification.CreatedChatWithTitle"); + actionText = [[NSString alloc] initWithFormat:formatString, authorName, actionAttachment.actionData[@"title"]]; + + NSRange formatNameRange = [formatString rangeOfString:@"%@"]; + if (formatNameRange.location != NSNotFound) + { + additionalAttributes = [TGNeoAttachmentViewModel _mediumFontAttributeForRange:NSMakeRange(formatNameRange.location, authorName.length) fontSize:fontSize]; + } + } + break; + + case TGBridgeMessageActionContactRegistered: + { + messageText = TGLocalized(@"Watch.Notification.Joined"); + } + break; + + case TGBridgeMessageActionChannelCreated: + { + messageText = TGLocalized(@"Notification.CreatedChannel"); + } + break; + + case TGBridgeMessageActionChannelInviter: + { + TGBridgeUser *user = users[@([actionAttachment.actionData[@"uid"] int32Value])]; + NSString *authorName = [TGStringUtils initialsForFirstName:user.firstName lastName:user.lastName single:false]; + NSString *formatString = TGLocalized(@"Notification.ChannelInviter"); + + actionText = [[NSString alloc] initWithFormat:formatString, authorName]; + + NSRange formatNameRange = [formatString rangeOfString:@"%@"]; + if (formatNameRange.location != NSNotFound) + { + additionalAttributes = [TGNeoAttachmentViewModel _mediumFontAttributeForRange:NSMakeRange(formatNameRange.location, authorName.length) fontSize:fontSize]; + } + } + break; + + case TGBridgeMessageActionGroupMigratedTo: + { + messageText = TGLocalized(@"Notification.ChannelMigratedFrom"); + } + break; + + case TGBridgeMessageActionGroupActivated: + { + messageText = TGLocalized(@"Notification.GroupActivated"); + } + break; + + case TGBridgeMessageActionGroupDeactivated: + { + messageText = TGLocalized(@"Notification.GroupDeactivated"); + } + break; + + case TGBridgeMessageActionChannelMigratedFrom: + { + messageText = TGLocalized(@"Notification.ChannelMigratedFrom"); + } + break; + + default: + break; + } + + if (actionText != nil) + { + attributedText = [[NSMutableAttributedString alloc] initWithString:actionText attributes:@{ NSFontAttributeName: [UIFont systemFontOfSize:fontSize weight:UIFontWeightRegular], NSForegroundColorAttributeName: subTitleColor }]; + + if (additionalAttributes != nil) + { + NSUInteger count = additionalAttributes.count; + for (NSUInteger i = 0; i < count; i += 2) + { + NSRange range = NSMakeRange(0, 0); + [(NSValue *)[additionalAttributes objectAtIndex:i] getValue:&range]; + NSDictionary *attributes = [additionalAttributes objectAtIndex:i + 1]; + + if (range.location + range.length <= attributedText.length) + [attributedText addAttributes:attributes range:range]; + } + } + + inhibitsInitials = true; + } + } + else if ([attachment isKindOfClass:[TGBridgeUnsupportedMediaAttachment class]]) + { + TGBridgeUnsupportedMediaAttachment *unsupportedAttachment = (TGBridgeUnsupportedMediaAttachment *)attachment; + hasAttachment = true; + messageText = unsupportedAttachment.compactTitle; + if (caption.length > 0) + { + hasCaption = true; + if (compact) + useNormalColor = true; + } + } + } + + if (!hasAttachment) + return nil; + + self = [super init]; + if (self != nil) + { + _inhibitsInitials = inhibitsInitials; + _hasCaption = hasCaption; + if (attributedText != nil) + { + _textModel = [[TGNeoLabelViewModel alloc] initWithAttributedText:attributedText]; + _textModel.multiline = false; + [self addSubmodel:_textModel]; + } + else + { + if (messageIcon != nil && !compact) + { + _iconModel = [[TGNeoImageViewModel alloc] initWithImage:[UIImage imageNamed:messageIcon] tintColor:subTitleColor]; + if (!compact) + _iconModel.frame = CGRectMake(0, 0.5f, 17, 18); + else + _iconModel.frame = CGRectMake(0, -2, 17, 18); + [self addSubmodel:_iconModel]; + } + + UIColor *color = useNormalColor ? normalColor : subTitleColor; + + _textModel = [[TGNeoLabelViewModel alloc] initWithText:messageText font:font color:color attributes:nil]; + _textModel.multiline = false; + [self addSubmodel:_textModel]; + } + } + return self; +} + +- (void)setFrame:(CGRect)frame +{ + [super setFrame:frame]; + + CGFloat textOffset = 0; + if (_iconModel != nil) + textOffset = CGRectGetMaxX(_iconModel.frame) + 2; + + _textModel.frame = CGRectMake(textOffset, 0, frame.size.width - textOffset, 20); +} + +- (CGSize)contentSizeWithContainerSize:(CGSize)containerSize +{ + CGFloat textOffset = 0; + if (_iconModel != nil) + textOffset = CGRectGetMaxX(_iconModel.frame) + 2; + + CGSize textSize = [_textModel contentSizeWithContainerSize:CGSizeMake(self.frame.size.width - textOffset, FLT_MAX)]; + + CGSize contentSize = CGSizeZero; + contentSize.width = CGRectGetMaxX(self.frame); + contentSize.height = textSize.height; + + return contentSize; +} + ++ (NSArray *)_mediumFontAttributeForRange:(NSRange)range fontSize:(CGFloat)fontSize +{ + NSDictionary *fontAttributes = @{ NSFontAttributeName: [UIFont systemFontOfSize:fontSize weight:UIFontWeightMedium], NSForegroundColorAttributeName: [UIColor whiteColor] }; + return [[NSArray alloc] initWithObjects:[[NSValue alloc] initWithBytes:&range objCType:@encode(NSRange)], fontAttributes, nil]; +} + +@end diff --git a/Watch/Extension/TGNeoAudioMessageViewModel.h b/Watch/Extension/TGNeoAudioMessageViewModel.h new file mode 100644 index 0000000000..a7d36f541d --- /dev/null +++ b/Watch/Extension/TGNeoAudioMessageViewModel.h @@ -0,0 +1,5 @@ +#import "TGNeoBubbleMessageViewModel.h" + +@interface TGNeoAudioMessageViewModel : TGNeoBubbleMessageViewModel + +@end diff --git a/Watch/Extension/TGNeoAudioMessageViewModel.m b/Watch/Extension/TGNeoAudioMessageViewModel.m new file mode 100644 index 0000000000..b42693ad06 --- /dev/null +++ b/Watch/Extension/TGNeoAudioMessageViewModel.m @@ -0,0 +1,128 @@ +#import "TGNeoAudioMessageViewModel.h" +#import "TGWatchCommon.h" +#import "TGBridgeMessage.h" + +@interface TGNeoAudioMessageViewModel () +{ + TGNeoLabelViewModel *_nameModel; + TGNeoLabelViewModel *_durationModel; + + bool _isVoiceMessage; + int32_t _duration; + + UIColor *_buttonTint; + UIColor *_iconTint; + NSString *_spinnerName; +} +@end + +@implementation TGNeoAudioMessageViewModel + +- (instancetype)initWithMessage:(TGBridgeMessage *)message type:(TGNeoMessageType)type users:(NSDictionary *)users context:(TGBridgeContext *)context +{ + self = [super initWithMessage:message type:type users:users context:context]; + if (self != nil) + { + TGBridgeAudioMediaAttachment *audioAttachment = nil; + TGBridgeDocumentMediaAttachment *documentAttachment = nil; + + for (TGBridgeMediaAttachment *attachment in message.media) + { + if ([attachment isKindOfClass:[TGBridgeAudioMediaAttachment class]]) + { + audioAttachment = (TGBridgeAudioMediaAttachment *)attachment; + _isVoiceMessage = true; + _duration = audioAttachment.duration; + break; + } + else if ([attachment isKindOfClass:[TGBridgeDocumentMediaAttachment class]]) + { + documentAttachment = (TGBridgeDocumentMediaAttachment *)attachment; + _isVoiceMessage = documentAttachment.isVoice; + _duration = documentAttachment.duration; + break; + } + } + + NSString *title = TGLocalized(@"Message.Audio"); + NSString *subtitle = @""; + + if (!_isVoiceMessage) + { + [self removeSubmodel:self.forwardHeaderModel]; + self.forwardHeaderModel = nil; + + if (documentAttachment.title.length > 0) + title = documentAttachment.title; + else + title = documentAttachment.fileName; + + subtitle = documentAttachment.performer.length > 0 ? documentAttachment.performer : @""; + } + else + { + NSInteger durationMinutes = floor(_duration / 60.0); + NSInteger durationSeconds = _duration % 60; + subtitle = [NSString stringWithFormat:@"%ld:%02ld", (long)durationMinutes, (long)durationSeconds]; + } + + _nameModel = [[TGNeoLabelViewModel alloc] initWithText:title font:[UIFont systemFontOfSize:12 weight:UIFontWeightMedium] color:[self normalColorForMessage:message type:type] attributes:nil]; + _nameModel.multiline = false; + [self addSubmodel:_nameModel]; + + _durationModel = [[TGNeoLabelViewModel alloc] initWithText:subtitle font:[UIFont systemFontOfSize:12] color:[self subtitleColorForMessage:message type:type] attributes:nil]; + _durationModel.multiline = false; + [self addSubmodel:_durationModel]; + + _buttonTint = [self accentColorForMessage:message type:type]; + _iconTint = [self contrastAccentColorForMessage:message type:type]; + if (message.outgoing && type != TGNeoMessageTypeChannel) + _spinnerName = @"BubbleSpinnerIncoming"; + else + _spinnerName = @"BubbleSpinner"; + } + return self; +} + +- (CGSize)layoutWithContainerSize:(CGSize)containerSize +{ + CGSize contentContainerSize = [self contentContainerSizeWithContainerSize:containerSize]; + + CGSize headerSize = [self layoutHeaderModelsWithContainerSize:contentContainerSize]; + CGFloat maxContentWidth = headerSize.width; + CGFloat textTopOffset = headerSize.height; + + CGFloat leftOffset = 26 + TGNeoBubbleMessageMetaSpacing; + + UIEdgeInsets inset = UIEdgeInsetsMake(textTopOffset + 1.5f, TGNeoBubbleMessageViewModelInsets.left, 0, 0); + NSDictionary *audioButtonDictionary = @{}; + if (_isVoiceMessage) + { + audioButtonDictionary = @{ TGNeoMessageAudioIcon: @"MediaAudioPlay", + TGNeoMessageAudioIconTint: _iconTint, + TGNeoMessageAudioBackgroundColor: _buttonTint, + TGNeoMessageAudioAnimatedIcon: _spinnerName, + TGNeoMessageAudioButtonHasBackground: @true }; + inset.left -= 4; + leftOffset -= 5; + } + + contentContainerSize = CGSizeMake(containerSize.width - TGNeoBubbleMessageViewModelInsets.left - TGNeoBubbleMessageViewModelInsets.right - leftOffset, FLT_MAX); + + CGSize nameSize = [_nameModel contentSizeWithContainerSize:contentContainerSize]; + CGSize durationSize = [_durationModel contentSizeWithContainerSize:contentContainerSize]; + maxContentWidth = MAX(maxContentWidth, MAX(nameSize.width, durationSize.width) + leftOffset); + + _nameModel.frame = CGRectMake(TGNeoBubbleMessageViewModelInsets.left + leftOffset, textTopOffset, nameSize.width, 14); + _durationModel.frame = CGRectMake(TGNeoBubbleMessageViewModelInsets.left + leftOffset, CGRectGetMaxY(_nameModel.frame), durationSize.width, 14); + + [self addAdditionalLayout:@{ TGNeoContentInset: [NSValue valueWithUIEdgeInsets:inset], TGNeoMessageAudioButton: audioButtonDictionary } withKey:TGNeoMessageMetaGroup]; + + CGSize contentSize = CGSizeMake(inset.left + TGNeoBubbleMessageViewModelInsets.right + maxContentWidth, CGRectGetMaxY(_durationModel.frame) + TGNeoBubbleMessageViewModelInsets.bottom); + + [super layoutWithContainerSize:contentSize]; + + return contentSize; +} + +@end diff --git a/Watch/Extension/TGNeoBackgroundViewModel.h b/Watch/Extension/TGNeoBackgroundViewModel.h new file mode 100644 index 0000000000..e3f8398d59 --- /dev/null +++ b/Watch/Extension/TGNeoBackgroundViewModel.h @@ -0,0 +1,7 @@ +#import "TGNeoViewModel.h" + +@interface TGNeoBackgroundViewModel : TGNeoViewModel + +- (instancetype)initWithOutgoing:(bool)outgoing; + +@end diff --git a/Watch/Extension/TGNeoBackgroundViewModel.m b/Watch/Extension/TGNeoBackgroundViewModel.m new file mode 100644 index 0000000000..74473c3469 --- /dev/null +++ b/Watch/Extension/TGNeoBackgroundViewModel.m @@ -0,0 +1,50 @@ +#import "TGNeoBackgroundViewModel.h" +#import + +@interface TGNeoBackgroundViewModel () +{ + bool _outgoing; +} +@end + +@implementation TGNeoBackgroundViewModel + +- (instancetype)initWithOutgoing:(bool)outgoing +{ + self = [super init]; + if (self != nil) + { + _outgoing = outgoing; + } + return self; +} + +- (void)drawInContext:(CGContextRef)context +{ + UIImage *backgroundImage = _outgoing ? [TGNeoBackgroundViewModel outgoingBubbleImage] : [TGNeoBackgroundViewModel incomingBubbleImage]; + [backgroundImage drawInRect:CGRectMake(0, 0, self.frame.size.width, self.frame.size.height) blendMode:kCGBlendModeCopy alpha:1.0f]; +} + ++ (UIImage *)incomingBubbleImage +{ + static dispatch_once_t onceToken; + static UIImage *image; + dispatch_once(&onceToken, ^ + { + image = [[UIImage imageNamed:@"ChatBubbleIncoming"] resizableImageWithCapInsets:UIEdgeInsetsMake(13, 13, 16, 13) resizingMode:UIImageResizingModeStretch]; + }); + return image; +} + ++ (UIImage *)outgoingBubbleImage +{ + static dispatch_once_t onceToken; + static UIImage *image; + dispatch_once(&onceToken, ^ + { + image = [[UIImage imageNamed:@"ChatBubbleOutgoing"] resizableImageWithCapInsets:UIEdgeInsetsMake(13, 13, 16, 13) resizingMode:UIImageResizingModeStretch]; + }); + return image; +} + +@end diff --git a/Watch/Extension/TGNeoBubbleMessageViewModel.h b/Watch/Extension/TGNeoBubbleMessageViewModel.h new file mode 100644 index 0000000000..32d15ac70e --- /dev/null +++ b/Watch/Extension/TGNeoBubbleMessageViewModel.h @@ -0,0 +1,30 @@ +#import "TGNeoMessageViewModel.h" +#import "TGNeoLabelViewModel.h" +#import "TGNeoForwardHeaderViewModel.h" +#import "TGNeoReplyHeaderViewModel.h" + +@class TGBridgeMessage; +@class TGBridgeUser; + +@interface TGNeoBubbleMessageViewModel : TGNeoMessageViewModel + +@property (nonatomic, readonly) TGNeoLabelViewModel *authorNameModel; + +@property (nonatomic, strong) TGNeoForwardHeaderViewModel *forwardHeaderModel; +@property (nonatomic, readonly) TGNeoReplyHeaderViewModel *replyHeaderModel; + +- (CGSize)contentContainerSizeWithContainerSize:(CGSize)containerSize; +- (CGSize)layoutHeaderModelsWithContainerSize:(CGSize)containerSize; + +- (UIColor *)normalColorForMessage:(TGBridgeMessage *)message type:(TGNeoMessageType)type; +- (UIColor *)subtitleColorForMessage:(TGBridgeMessage *)message type:(TGNeoMessageType)type; +- (UIColor *)accentColorForMessage:(TGBridgeMessage *)message type:(TGNeoMessageType)type; +- (UIColor *)contrastAccentColorForMessage:(TGBridgeMessage *)message type:(TGNeoMessageType)type; + ++ (CGFloat)bodyTextFontSize; + +@end + +extern const UIEdgeInsets TGNeoBubbleMessageViewModelInsets; +extern const CGFloat TGNeoBubbleMessageMetaSpacing; +extern const CGFloat TGNeoBubbleHeaderSpacing; diff --git a/Watch/Extension/TGNeoBubbleMessageViewModel.m b/Watch/Extension/TGNeoBubbleMessageViewModel.m new file mode 100644 index 0000000000..660c890fa2 --- /dev/null +++ b/Watch/Extension/TGNeoBubbleMessageViewModel.m @@ -0,0 +1,193 @@ +#import "TGNeoBubbleMessageViewModel.h" +#import "TGNeoBackgroundViewModel.h" + +#import "TGExtensionDelegate.h" +#import "TGWatchColor.h" + +#import "TGBridgeContext.h" +#import "TGBridgeMessage.h" +#import "TGBridgeUser.h" + +#import "TGBridgePeerIdAdapter.h" + +const UIEdgeInsets TGNeoBubbleMessageViewModelInsets = { 4.5, 11, 9, 11 }; +const CGFloat TGNeoBubbleMessageMetaSpacing = 5.0f; +const CGFloat TGNeoBubbleHeaderSpacing = 2.0f; + +@interface TGNeoBubbleMessageViewModel () +{ + TGNeoBackgroundViewModel *_backgroundModel; +} +@end + +@implementation TGNeoBubbleMessageViewModel + +- (instancetype)initWithMessage:(TGBridgeMessage *)message type:(TGNeoMessageType)type users:(NSDictionary *)users context:(TGBridgeContext *)context +{ + self = [super initWithMessage:message type:type users:users context:context]; + if (self != nil) + { + self.showBubble = true; + + if (!message.outgoing && type == TGNeoMessageTypeGroup) + { + _authorNameModel = [[TGNeoLabelViewModel alloc] initWithText:[users[@(message.fromUid)] displayName] font:[UIFont systemFontOfSize:14] color:[TGColor colorForUserId:(int32_t)message.fromUid myUserId:context.userId] attributes:nil]; + [self addSubmodel:_authorNameModel]; + } + + TGBridgeForwardedMessageMediaAttachment *forwardAttachment = nil; + TGBridgeReplyMessageMediaAttachment *replyAttachment = nil; + for (TGBridgeMediaAttachment *attachment in message.media) + { + if ([attachment isKindOfClass:[TGBridgeForwardedMessageMediaAttachment class]]) + forwardAttachment = (TGBridgeForwardedMessageMediaAttachment *)attachment; + else if ([attachment isKindOfClass:[TGBridgeReplyMessageMediaAttachment class]]) + replyAttachment = (TGBridgeReplyMessageMediaAttachment *)attachment; + } + + if (forwardAttachment != nil) + { + if (TGPeerIdIsChannel(forwardAttachment.peerId)) + { + _forwardHeaderModel = [[TGNeoForwardHeaderViewModel alloc] initWithForwardAttachment:forwardAttachment chat:users[@(forwardAttachment.peerId)] outgoing:message.outgoing]; + } + else + { + _forwardHeaderModel = [[TGNeoForwardHeaderViewModel alloc] initWithForwardAttachment:forwardAttachment user:users[@(forwardAttachment.peerId)] outgoing:message.outgoing]; + } + [self addSubmodel:_forwardHeaderModel]; + } + + if (replyAttachment != nil) + { + _replyHeaderModel = [[TGNeoReplyHeaderViewModel alloc] initWithReplyAttachment:replyAttachment users:users outgoing:message.outgoing]; + [self addSubmodel:_replyHeaderModel]; + } + } + return self; +} + +- (CGSize)contentContainerSizeWithContainerSize:(CGSize)containerSize +{ + return CGSizeMake(containerSize.width - TGNeoBubbleMessageViewModelInsets.left - TGNeoBubbleMessageViewModelInsets.right, FLT_MAX); +} + +- (CGSize)layoutHeaderModelsWithContainerSize:(CGSize)containerSize +{ + CGFloat textTopOffset = self.showBubble ? TGNeoBubbleMessageViewModelInsets.top : 0; + CGFloat maxContentWidth = 0; + if (self.authorNameModel != nil) + { + CGSize textSize = [self.authorNameModel contentSizeWithContainerSize:containerSize]; + self.authorNameModel.frame = CGRectMake(TGNeoBubbleMessageViewModelInsets.left, textTopOffset, textSize.width, 16.5f); + textTopOffset += self.authorNameModel.frame.size.height; + + if (textSize.width > maxContentWidth) + maxContentWidth = textSize.width; + } + + if (self.replyHeaderModel != nil) + { + textTopOffset += TGNeoBubbleHeaderSpacing; + + CGSize headerSize = [self.replyHeaderModel contentSizeWithContainerSize:containerSize]; + self.replyHeaderModel.frame = CGRectMake(TGNeoBubbleMessageViewModelInsets.left, textTopOffset, headerSize.width, headerSize.height); + if (headerSize.width > maxContentWidth) + maxContentWidth = headerSize.width; + + textTopOffset += self.replyHeaderModel.frame.size.height + TGNeoBubbleHeaderSpacing; + + if (_replyHeaderModel.mediaAttachment != nil) + { + UIEdgeInsets inset = UIEdgeInsetsMake(self.replyHeaderModel.frame.origin.y + 1.5f, self.replyHeaderModel.frame.origin.x + TGNeoReplyHeaderLineWidth + TGNeoReplyHeaderSpacing, 0, 0); + NSDictionary *imageDictionary = @{ TGNeoMessageMediaPeerId: @(_replyHeaderModel.replyMessage.cid), TGNeoMessageMediaMessageId: @(_replyHeaderModel.replyMessage.identifier), TGNeoMessageReplyMediaAttachment: _replyHeaderModel.mediaAttachment }; + [self addAdditionalLayout:@{ TGNeoContentInset: [NSValue valueWithUIEdgeInsets:inset], TGNeoMessageReplyImageGroup: imageDictionary } withKey:TGNeoMessageHeaderGroup]; + } + } + + if (self.forwardHeaderModel != nil) + { + textTopOffset += TGNeoBubbleHeaderSpacing; + + CGSize headerSize = [self.forwardHeaderModel contentSizeWithContainerSize:containerSize]; + self.forwardHeaderModel.frame = CGRectMake(TGNeoBubbleMessageViewModelInsets.left, textTopOffset, headerSize.width, headerSize.height); + if (headerSize.width > maxContentWidth) + maxContentWidth = headerSize.width; + + textTopOffset += self.forwardHeaderModel.frame.size.height + TGNeoBubbleHeaderSpacing; + } + + return CGSizeMake(maxContentWidth, textTopOffset); +} + +- (UIColor *)normalColorForMessage:(TGBridgeMessage *)message type:(TGNeoMessageType)type +{ + if (message.outgoing && type != TGNeoMessageTypeChannel) + return [UIColor whiteColor]; + else + return [UIColor blackColor]; +} + +- (UIColor *)subtitleColorForMessage:(TGBridgeMessage *)message type:(TGNeoMessageType)type +{ + if (message.outgoing && type != TGNeoMessageTypeChannel) + return [UIColor hexColor:0xbeddf6]; + else + return [UIColor hexColor:0x7e7e81]; +} + +- (UIColor *)accentColorForMessage:(TGBridgeMessage *)message type:(TGNeoMessageType)type +{ + if (message.outgoing && type != TGNeoMessageTypeChannel) + return [UIColor whiteColor]; + else + return [UIColor hexColor:0x1f97f8]; +} + +- (UIColor *)contrastAccentColorForMessage:(TGBridgeMessage *)message type:(TGNeoMessageType)type +{ + if (message.outgoing && type != TGNeoMessageTypeChannel) + return [UIColor hexColor:0x1f97f8]; + else + return [UIColor whiteColor]; +} + +- (CGSize)layoutWithContainerSize:(CGSize)containerSize +{ + self.contentSize = containerSize; + + return CGSizeZero; +} + ++ (CGFloat)bodyTextFontSize +{ + TGContentSizeCategory category = [TGExtensionDelegate instance].contentSizeCategory; + + switch (category) + { + case TGContentSizeCategoryXS: + return 14.0f; + + case TGContentSizeCategoryS: + return 15.0f; + + case TGContentSizeCategoryL: + return 16.0f; + + case TGContentSizeCategoryXL: + return 17.0f; + + case TGContentSizeCategoryXXL: + return 18.0f; + + case TGContentSizeCategoryXXXL: + return 19.0f; + + default: + break; + } + + return 16.0f; +} + +@end diff --git a/Watch/Extension/TGNeoChatRowController.h b/Watch/Extension/TGNeoChatRowController.h new file mode 100644 index 0000000000..d1b0743052 --- /dev/null +++ b/Watch/Extension/TGNeoChatRowController.h @@ -0,0 +1,18 @@ +#import "WKInterfaceTable+TGDataDrivenTable.h" + +@class TGBridgeContext; +@class TGBridgeChat; + +@interface TGNeoChatRowController : TGTableRowController + +@property (nonatomic, copy) bool (^shouldRenderContent)(void); + +@property (nonatomic, weak) IBOutlet WKInterfaceGroup *contentGroup; +@property (nonatomic, weak) IBOutlet WKInterfaceGroup *avatarGroup; +@property (nonatomic, weak) IBOutlet WKInterfaceLabel *avatarLabel; +@property (nonatomic, weak) IBOutlet WKInterfaceGroup *statusGroup; +@property (nonatomic, weak) IBOutlet WKInterfaceLabel *statusLabel; + +- (void)updateWithChat:(TGBridgeChat *)chat forForward:(bool)forForward context:(TGBridgeContext *)context; + +@end diff --git a/Watch/Extension/TGNeoChatRowController.m b/Watch/Extension/TGNeoChatRowController.m new file mode 100644 index 0000000000..540c9d21b8 --- /dev/null +++ b/Watch/Extension/TGNeoChatRowController.m @@ -0,0 +1,209 @@ +#import "TGNeoChatRowController.h" +#import "TGWatchCommon.h" +#import "TGBridgeContext.h" +#import "TGBridgeChat.h" +#import "TGBridgeUser.h" +#import "TGBridgeUserCache.h" + +#import "TGNeoChatViewModel.h" +#import "TGAvatarViewModel.h" + +NSString *const TGNeoChatRowIdentifier = @"TGNeoChatRow"; + +@interface TGNeoChatRowController () +{ + TGBridgeChat *_currentChat; + NSDictionary *_currentUsers; + + TGNeoChatViewModel *_viewModel; + SMetaDisposable *_renderDisposable; + + TGAvatarViewModel *_avatarViewModel; + + bool _pendingRendering; +} +@end + +@implementation TGNeoChatRowController + +- (instancetype)init +{ + self = [super init]; + if (self != nil) + { + _renderDisposable = [[SMetaDisposable alloc] init]; + } + return self; +} + +- (void)dealloc +{ + [_renderDisposable dispose]; +} + +- (void)setupInterface +{ + [super setupInterface]; + + _avatarViewModel = [[TGAvatarViewModel alloc] init]; + _avatarViewModel.group = self.avatarGroup; + _avatarViewModel.label = self.avatarLabel; + + [self.avatarLabel _setInitialHidden:true]; + [self.statusGroup _setInitialHidden:true]; +} + +- (void)updateWithChat:(TGBridgeChat *)chat forForward:(bool)forForward context:(TGBridgeContext *)context +{ + TGBridgeChat *oldChat = _currentChat; + _currentChat = chat; + + NSDictionary *oldUsers = _currentUsers; + _currentUsers = [[TGBridgeUserCache instance] usersWithIndexSet:[chat involvedUserIds]]; + + bool shouldUpdate = [self shouldUpdateContentFrom:oldChat oldUsers:oldUsers to:_currentChat newUsers:_currentUsers]; + if (shouldUpdate) + { + _viewModel = [[TGNeoChatViewModel alloc] initWithChat:chat users:_currentUsers context:context]; + CGSize containerSize = [[WKInterfaceDevice currentDevice] screenBounds].size; + CGSize contentSize = [_viewModel layoutWithContainerSize:containerSize]; + + self.contentGroup.width = contentSize.width; + self.contentGroup.height = contentSize.height; + + SSignal *signal = [TGNeoRenderableViewModel renderSignalForViewModel:_viewModel]; + + __weak TGNeoChatRowController *weakSelf = self; + [_renderDisposable setDisposable:[signal startWithNext:^(UIImage *image) + { + __strong TGNeoChatRowController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf->_contentGroup setBackgroundImage:image]; + }]]; + } + + if (chat.isGroup || chat.isChannel) + [_avatarViewModel updateWithChat:chat isVisible:self.isVisible]; + else + [_avatarViewModel updateWithUser:_currentUsers[@(chat.identifier)] context:context isVisible:self.isVisible]; + + if (forForward) + return; + + bool shouldUpdateStatus = [self shouldUpdateStatusFrom:oldChat to:_currentChat]; + if (shouldUpdateStatus) + { + if ((chat.outgoing && chat.unread) || chat.deliveryError) + { + self.statusGroup.hidden = false; + + if (chat.deliveryError) + { + self.statusGroup.width = 15; + self.statusLabel.text = @"!"; + self.statusLabel.hidden = false; + self.statusGroup.backgroundColor = [UIColor hexColor:0xff4a5c]; + [self.statusGroup setBackgroundImageNamed:nil]; + } + else if (!(_currentChat.identifier > 0 && [_currentUsers[@(_currentChat.identifier)] isBot]) && chat.unread) + { + self.statusGroup.width = 15; + self.statusLabel.hidden = true; + self.statusGroup.backgroundColor = [UIColor clearColor]; + [self.statusGroup setBackgroundImageNamed:@"StatusDot"]; + } + } + else if (!chat.outgoing && chat.unreadCount > 0) + { + self.statusGroup.hidden = false; + self.statusLabel.text = [NSString stringWithFormat:@"%ld", (long)chat.unreadCount]; + self.statusLabel.hidden = false; + if (chat.unreadCount < 10) + self.statusGroup.width = 15; + else + [self.statusGroup sizeToFitWidth]; + self.statusGroup.backgroundColor = [UIColor hexColor:0x2ea4e5]; + [self.statusGroup setBackgroundImageNamed:nil]; + } + else + { + self.statusGroup.hidden = true; + } + } +} + +- (bool)_nameHasChangedFrom:(NSString *)oldName newName:(NSString *)newName +{ + return (![oldName isEqualToString:newName] && !(oldName == nil && newName == nil)); +} + +- (bool)shouldUpdateContentFrom:(TGBridgeChat *)oldChat oldUsers:(NSDictionary *)oldUsers to:(TGBridgeChat *)newChat newUsers:(NSDictionary *)newUsers +{ + if (oldChat == nil) + return true; + + if (oldChat.identifier != newChat.identifier) + return true; + + if (newChat.isGroup || newChat.isChannelGroup) + { + if (![oldChat.groupTitle isEqualToString:newChat.groupTitle]) + return true; + + if (oldChat.fromUid != newChat.fromUid) + return true; + + TGBridgeUser *oldUser = oldUsers[@(oldChat.fromUid)]; + TGBridgeUser *newUser = newUsers[@(newChat.fromUid)]; + + if ([self _nameHasChangedFrom:oldUser.firstName newName:newUser.firstName] || [self _nameHasChangedFrom:oldUser.lastName newName:newUser.firstName]) + return true; + } + else + { + TGBridgeUser *oldUser = oldUsers[@(oldChat.identifier)]; + TGBridgeUser *newUser = newUsers[@(newChat.identifier)]; + + if ([self _nameHasChangedFrom:oldUser.firstName newName:newUser.firstName] || [self _nameHasChangedFrom:oldUser.lastName newName:newUser.firstName]) + return true; + } + + if (![oldChat.text isEqualToString:newChat.text]) + return true; + + if (fabs(oldChat.date - newChat.date) > FLT_EPSILON) + return true; + + return false; +} + +- (void)notifyVisiblityChange +{ + [_avatarViewModel updateIfNeeded]; +} + +- (bool)shouldUpdateStatusFrom:(TGBridgeChat *)oldChat to:(TGBridgeChat *)newChat +{ + if (oldChat.outgoing != newChat.outgoing) + return true; + + if (oldChat.deliveryError != newChat.deliveryError) + return true; + + if (newChat.outgoing && oldChat.unread != newChat.unread) + return true; + + if (!newChat.deliveryError && oldChat.unreadCount != newChat.unreadCount) + return true; + + return false; +} + ++ (NSString *)identifier +{ + return TGNeoChatRowIdentifier; +} + +@end diff --git a/Watch/Extension/TGNeoChatViewModel.h b/Watch/Extension/TGNeoChatViewModel.h new file mode 100644 index 0000000000..a151959e49 --- /dev/null +++ b/Watch/Extension/TGNeoChatViewModel.h @@ -0,0 +1,10 @@ +#import "TGNeoRenderableViewModel.h" + +@class TGBridgeContext; +@class TGBridgeChat; + +@interface TGNeoChatViewModel : TGNeoRenderableViewModel + +- (instancetype)initWithChat:(TGBridgeChat *)chat users:(NSDictionary *)users context:(TGBridgeContext *)context; + +@end diff --git a/Watch/Extension/TGNeoChatViewModel.m b/Watch/Extension/TGNeoChatViewModel.m new file mode 100644 index 0000000000..266f7e9014 --- /dev/null +++ b/Watch/Extension/TGNeoChatViewModel.m @@ -0,0 +1,230 @@ +#import "TGNeoChatViewModel.h" +#import "TGWatchCommon.h" +#import "TGNeoLabelViewModel.h" +#import "TGNeoImageViewModel.h" +#import "TGNeoAttachmentViewModel.h" + +#import "TGBridgeContext.h" +#import "TGBridgeChat.h" +#import "TGBridgeUser.h" + +#import "TGExtensionDelegate.h" +#import "TGStringUtils.h" +#import "TGDateUtils.h" + +@interface TGNeoChatViewModel () +{ + TGNeoLabelViewModel *_nameModel; + TGNeoLabelViewModel *_authorNameModel; + TGNeoImageViewModel *_verifiedModel; + TGNeoLabelViewModel *_authorInitialsModel; + TGNeoLabelViewModel *_textModel; + TGNeoAttachmentViewModel *_attachmentModel; + TGNeoLabelViewModel *_timeModel; +} +@end + +@implementation TGNeoChatViewModel + +- (instancetype)initWithChat:(TGBridgeChat *)chat users:(NSDictionary *)users context:(TGBridgeContext *)context +{ + self = [super init]; + if (self != nil) + { + TGBridgeUser *author = nil; + NSString *name = nil; + + if (chat.isGroup || chat.isChannelGroup) + { + author = users[@(chat.fromUid)]; + name = chat.groupTitle; + } + else if (chat.isChannel) + { + name = chat.groupTitle; + } + else if (chat.identifier == context.userId) + { + name = TGLocalized(@"DialogList.SavedMessages"); + } + else + { + author = users[@(chat.identifier)]; + name = [author displayName]; + } + + _nameModel = [[TGNeoLabelViewModel alloc] initWithText:name font:[UIFont systemFontOfSize:[TGNeoChatViewModel titleFontSize] weight:UIFontWeightMedium] color:[UIColor whiteColor] attributes:nil]; + _nameModel.multiline = false; + [self addSubmodel:_nameModel]; + + if (chat.verified || author.verified) + { + _verifiedModel = [[TGNeoImageViewModel alloc] initWithImage:[UIImage imageNamed:@"VerifiedList"]]; + [self addSubmodel:_verifiedModel]; + } + + _attachmentModel = [[TGNeoAttachmentViewModel alloc] initWithAttachments:chat.media author:author forChannel:(chat.isChannel && !chat.isChannelGroup) users:users font:[UIFont systemFontOfSize:[TGNeoChatViewModel textFontSize]] subTitleColor:[UIColor hexColor:0x8f8f8f] normalColor:[UIColor whiteColor] compact:false caption:chat.text]; + if (_attachmentModel != nil) + [self addSubmodel:_attachmentModel]; + + if ((chat.isGroup || chat.isChannelGroup) && !_attachmentModel.inhibitsInitials) + { + NSString *initials = (chat.fromUid == context.userId) ? TGLocalized(@"DialogList.You") : [TGStringUtils initialsForFirstName:author.firstName lastName:author.lastName single:false]; + + if (initials.length > 0) + { + _authorInitialsModel = [[TGNeoLabelViewModel alloc] initWithText:[NSString stringWithFormat:@"%@:", initials] font:[UIFont systemFontOfSize:[TGNeoChatViewModel textFontSize] weight:UIFontWeightMedium] color:[UIColor whiteColor] attributes:nil]; + _authorInitialsModel.multiline = false; + [self addSubmodel:_authorInitialsModel]; + } + } + + if (chat.text.length > 0 && !_attachmentModel.hasCaption) + { + _textModel = [[TGNeoLabelViewModel alloc] initWithText:chat.text font:[UIFont systemFontOfSize:[TGNeoChatViewModel textFontSize]] color:[UIColor hexColor:0x8f8f8f] attributes:nil]; + _textModel.multiline = false; + [self addSubmodel:_textModel]; + } + + NSString *time = @""; + if (chat.date > 0) + time = [TGDateUtils stringForMessageListDate:chat.date]; + + _timeModel = [[TGNeoLabelViewModel alloc] initWithText:time font:[UIFont systemFontOfSize:[TGNeoChatViewModel timeFontSize]] color:[UIColor hexColor:0x8f8f8f] attributes:nil]; + _timeModel.multiline = false; + [self addSubmodel:_timeModel]; + } + return self; +} + +- (CGSize)layoutWithContainerSize:(CGSize)containerSize +{ + CGSize nameSize = [_nameModel contentSizeWithContainerSize:CGSizeMake(containerSize.width - 31 - 7, FLT_MAX)]; + + if (_verifiedModel != nil) + { + CGFloat margin = 4; + _verifiedModel.frame = CGRectMake(MIN(31.5f + nameSize.width + margin, containerSize.width - 20), 6, 12, 12); + nameSize.width = MIN(nameSize.width, _verifiedModel.frame.origin.x - 31.5f - margin); + + _nameModel.frame = CGRectMake(31.5f, 1.5f, nameSize.width, nameSize.height); + } + else + { + _nameModel.frame = CGRectMake(31.5f, 1.5f, containerSize.width - 31 - 7, nameSize.height); + } + + CGFloat textX = 0; + CGFloat textY = CGRectGetMaxY(_nameModel.frame) - 2.5f; + if (_authorInitialsModel != nil) + { + CGFloat width = [_authorInitialsModel contentSizeWithContainerSize:CGSizeMake(40, 20)].width + 4; + _authorInitialsModel.frame = CGRectMake(31.5f, textY, width, 20); + textX += width; + } + + TGNeoViewModel *contentViewModel = (_attachmentModel != nil) ? _attachmentModel : _textModel; + CGSize textSize = [contentViewModel contentSizeWithContainerSize:CGSizeMake(containerSize.width - 31 - 7, FLT_MAX)]; + contentViewModel.frame = CGRectMake(31.5f + textX, textY, containerSize.width - 31 - 7 - textX, textSize.height); + + CGSize timeSize = [_timeModel contentSizeWithContainerSize:CGSizeMake(containerSize.width - 31 - 7, FLT_MAX)]; + _timeModel.frame = CGRectMake(31.5f, CGRectGetMaxY(contentViewModel.frame) - 1, containerSize.width - 31 - 36, timeSize.height); + + self.contentSize = CGSizeMake(containerSize.width, CGRectGetMaxY(_timeModel.frame) + 3); + return self.contentSize; +} + ++ (CGFloat)titleFontSize +{ + TGContentSizeCategory category = [TGExtensionDelegate instance].contentSizeCategory; + + switch (category) + { + case TGContentSizeCategoryXS: + return 14.0f; + + case TGContentSizeCategoryS: + return 15.0f; + + case TGContentSizeCategoryL: + return 16.0f; + + case TGContentSizeCategoryXL: + return 17.0f; + + case TGContentSizeCategoryXXL: + return 18.0f; + + case TGContentSizeCategoryXXXL: + return 19.0f; + + default: + break; + } + + return 16.0f; +} + ++ (CGFloat)textFontSize +{ + TGContentSizeCategory category = [TGExtensionDelegate instance].contentSizeCategory; + + switch (category) + { + case TGContentSizeCategoryXS: + return 14.0f; + + case TGContentSizeCategoryS: + return 15.0f; + + case TGContentSizeCategoryL: + return 16.0f; + + case TGContentSizeCategoryXL: + return 17.0f; + + case TGContentSizeCategoryXXL: + return 18.0f; + + case TGContentSizeCategoryXXXL: + return 19.0f; + + default: + break; + } + + return 16.0f; +} + ++ (CGFloat)timeFontSize +{ + TGContentSizeCategory category = [TGExtensionDelegate instance].contentSizeCategory; + + switch (category) + { + case TGContentSizeCategoryXS: + return 11.0f; + + case TGContentSizeCategoryS: + return 12.0f; + + case TGContentSizeCategoryL: + return 13.0f; + + case TGContentSizeCategoryXL: + return 14.0f; + + case TGContentSizeCategoryXXL: + return 15.0f; + + case TGContentSizeCategoryXXXL: + return 16.0f; + + default: + break; + } + + return 13.0f; +} + +@end diff --git a/Watch/Extension/TGNeoChatsController.h b/Watch/Extension/TGNeoChatsController.h new file mode 100644 index 0000000000..914648a8c2 --- /dev/null +++ b/Watch/Extension/TGNeoChatsController.h @@ -0,0 +1,40 @@ +#import "TGInterfaceController.h" +#import "TGBridgeStateSignal.h" + +@class TGBridgeContext; +@class TGBridgeChat; + +@interface TGNeoChatsControllerContext : NSObject + +@property (nonatomic, strong) NSArray *initialChats; +@property (nonatomic, strong) TGBridgeContext *context; +@property (nonatomic, copy) void (^completionBlock)(TGBridgeChat *peer); + +@end + + +@interface TGNeoChatsController : TGInterfaceController + +@property (nonatomic, weak) IBOutlet WKInterfaceTable *table; +@property (nonatomic, weak) IBOutlet WKInterfaceImage *activityIndicator; + +@property (nonatomic, weak) IBOutlet WKInterfaceGroup *authAlertGroup; +@property (nonatomic, weak) IBOutlet WKInterfaceImage *authAlertImage; +@property (nonatomic, weak) IBOutlet WKInterfaceGroup *authAlertImageGroup; +@property (nonatomic, weak) IBOutlet WKInterfaceLabel *authAlertLabel; +@property (nonatomic, weak) IBOutlet WKInterfaceLabel *authAlertDescLabel; + +@property (nonatomic, readonly) NSArray *chats; + +- (void)popAllControllers; +- (void)resetLocalization; + ++ (NSString *)stringForSyncState:(TGBridgeSynchronizationStateValue)value; + +@end + +extern NSString *const TGSynchronizationStateNotification; +extern NSString *const TGSynchronizationStateKey; + +extern NSString *const TGContextNotification; +extern NSString *const TGContextNotificationKey; diff --git a/Watch/Extension/TGNeoChatsController.m b/Watch/Extension/TGNeoChatsController.m new file mode 100644 index 0000000000..333c97ac41 --- /dev/null +++ b/Watch/Extension/TGNeoChatsController.m @@ -0,0 +1,486 @@ +#import "TGNeoChatsController.h" +#import "TGWatchCommon.h" +#import "WKInterfaceTable+TGDataDrivenTable.h" +#import "TGTableDeltaUpdater.h" +#import "TGInterfaceMenu.h" + +#import "TGBridgeContext.h" +#import "TGBridgeUser.h" +#import "TGBridgeChat.h" +#import "TGBridgeUserCache.h" + +#import "TGBridgeClient.h" +#import "TGBridgeChatListSignals.h" + +#import "TGNeoChatRowController.h" + +#import "TGNeoConversationController.h" +#import "TGComposeController.h" + +#import "TGExtensionDelegate.h" +#import "TGFileCache.h" + +NSString *const TGNeoChatsControllerIdentifier = @"TGNeoChatsController"; + +NSString *const TGContextNotification = @"TGContextNotification"; +NSString *const TGContextNotificationKey = @"context"; + +NSString *const TGSynchronizationStateNotification = @"TGSynchronizationStateNotification"; +NSString *const TGSynchronizationStateKey = @"state"; + +const NSUInteger TGNeoChatsControllerInitialCount = 3; +const NSUInteger TGNeoChatsControllerLimit = 12; +const NSUInteger TGNeoChatsControllerForwardLimit = 20; + +@implementation TGNeoChatsControllerContext + +@end + + +@interface TGNeoChatsController () +{ + TGBridgeContext *_context; + bool _forForward; + + bool _initialized; + bool _loadedStartup; + + bool _reachable; + TGNeoChatsControllerContext *_forwardContext; + + SMetaDisposable *_reachabilityDisposable; + SMetaDisposable *_contextDisposable; + SMetaDisposable *_chatsDisposable; + SMetaDisposable *_stateDisposable; + + SSignal *_signal; + bool _loading; + + NSArray *_rowModels; + NSArray *_pendingRowModels; + + TGBridgeSynchronizationStateValue _syncState; + + TGInterfaceMenu *_menu; +} +@end + +@implementation TGNeoChatsController + +- (instancetype)init +{ + self = [super init]; + if (self != nil) + { + _reachabilityDisposable = [[SMetaDisposable alloc] init]; + _contextDisposable = [[SMetaDisposable alloc] init]; + _chatsDisposable = [[SMetaDisposable alloc] init]; + _stateDisposable = [[SMetaDisposable alloc] init]; + + self.table.tableDataSource = self; + [self.table _setInitialHidden:true]; + } + return self; +} + +- (void)dealloc +{ + [_reachabilityDisposable dispose]; + [_contextDisposable dispose]; + [_chatsDisposable dispose]; + [_stateDisposable dispose]; +} + +- (void)configureWithContext:(id)context +{ + if (context == nil) + [self configureWithRootContext]; + else + [self configureWithForwardContext:context]; +} + +- (void)configureWithRootContext +{ + __weak TGNeoChatsController *weakSelf = self; + + [_reachabilityDisposable setDisposable:[[[TGBridgeClient instance] reachabilitySignal] startWithNext:^(NSNumber *next) + { + __strong TGNeoChatsController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + bool reachable = next.boolValue; + strongSelf->_reachable = reachable; + + if (strongSelf->_initialized) + { + [strongSelf performInterfaceUpdate:^(bool animated) + { + [strongSelf reloadData]; + }]; + } + }]]; + + [_contextDisposable setDisposable:[[[[TGBridgeClient instance] contextSignal] deliverOn:[SQueue mainQueue]] startWithNext:^(TGBridgeContext *next) + { + __strong TGNeoChatsController *strongSelf = weakSelf; + if (strongSelf == nil || [strongSelf->_context isEqual:next]) + return; + + if (strongSelf->_context.micAccessAllowed != next.micAccessAllowed) + { + [[NSNotificationCenter defaultCenter] postNotificationName:TGContextNotification object:nil userInfo:@{ TGContextNotificationKey: next }]; + } + + strongSelf->_initialized = true; + strongSelf->_context = next; + + if (next.authorized && next.userId != 0) + { + void (^updateBlock)(NSDictionary *) = ^(NSDictionary *models) + { + __strong TGNeoChatsController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + strongSelf->_pendingRowModels = models[TGBridgeChatsArrayKey]; + [[TGBridgeUserCache instance] storeUsers:[models[TGBridgeUsersDictionaryKey] allValues]]; + + [strongSelf performInterfaceUpdate:^(bool animated) + { + [strongSelf reloadData]; + }]; + }; + + if (!strongSelf->_loadedStartup) + { + NSDictionary *contextStartupData = next.preheatData; + if (contextStartupData != nil) + updateBlock(contextStartupData); + + strongSelf->_loadedStartup = true; + } + + strongSelf->_signal = [TGBridgeChatListSignals chatListWithLimit:TGNeoChatsControllerLimit]; + [strongSelf->_chatsDisposable setDisposable:[[strongSelf->_signal deliverOn:[SQueue mainQueue]] startWithNext:^(NSDictionary *models) + { + updateBlock(models); + }]]; + } + else + { + [strongSelf performInterfaceUpdate:^(bool animated) + { + [strongSelf reloadData]; + }]; + } + }]]; +} + +- (void)configureWithForwardContext:(TGNeoChatsControllerContext *)context +{ + self.title = nil; + + _forForward = true; + _forwardContext = context; + _context = _forwardContext.context; + + SSignal *signal = [[SSignal single:context.initialChats] then:[[TGBridgeChatListSignals chatListWithLimit:TGNeoChatsControllerForwardLimit] deliverOn:[SQueue mainQueue]]]; + + __weak TGNeoChatsController *weakSelf = self; + [_chatsDisposable setDisposable:[signal startWithNext:^(id models) + { + __strong TGNeoChatsController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + NSArray *chats = nil; + if ([models isKindOfClass:[NSDictionary class]]) + { + chats = models[TGBridgeChatsArrayKey]; + [[TGBridgeUserCache instance] storeUsers:[models[TGBridgeUsersDictionaryKey] allValues]]; + } + else if ([models isKindOfClass:[NSArray class]]) + { + chats = models; + } + + strongSelf->_pendingRowModels = chats; + + [strongSelf performInterfaceUpdate:^(bool animated) + { + [strongSelf reloadData]; + }]; + }]]; +} + +- (void)willActivate +{ + [super willActivate]; + + [self.table notifyVisiblityChange]; +} + +- (void)popAllControllers +{ + [self popToRootController]; + [self dismissAudioRecorderController]; + [self dismissTextInputController]; +} + +- (void)resetLocalization +{ + [self popAllControllers]; + + [self performInterfaceUpdate:^(bool animated) + { + [self reloadData]; + }]; +} + +- (void)reloadData +{ + [[TGBridgeClient instance] updateReachability]; + _reachable = [[TGBridgeClient instance] isServerReachable]; + + [self updateTitle]; + + if (!_reachable && !_forForward) + { + [self popAllControllers]; + + self.activityIndicator.hidden = true; + self.table.hidden = true; + self.authAlertGroup.hidden = false; + self.authAlertImageGroup.hidden = true; + self.authAlertDescLabel.hidden = false; + self.authAlertLabel.text = TGLocalized(@"Watch.NoConnection"); + self.authAlertDescLabel.text = TGLocalized(@"Watch.ConnectionDescription"); + + return; + } + + if ((_context.authorized && _context.userId != 0) || _forForward) + { + NSArray *currentRowModels = _rowModels; + bool initial = (currentRowModels.count == 0); + + bool partialLoad = false; + + if (initial && _pendingRowModels.count > TGNeoChatsControllerInitialCount) + { + partialLoad = true; + _rowModels = [_pendingRowModels subarrayWithRange:NSMakeRange(0, TGNeoChatsControllerInitialCount)]; + } + else + { + _rowModels = _pendingRowModels; + } + + if (_rowModels.count == 0) + { + self.activityIndicator.hidden = true; + self.table.hidden = true; + self.authAlertGroup.hidden = false; + self.authAlertImageGroup.hidden = true; + self.authAlertDescLabel.hidden = false; + self.authAlertLabel.text = TGLocalized(@"Watch.ChatList.NoConversationsTitle"); + self.authAlertDescLabel.text = TGLocalized(@"Watch.ChatList.NoConversationsText"); + return; + } + + bool tableHidden = false; + bool spinnerHidden = false; + if (currentRowModels == nil && _rowModels == nil) + { + tableHidden = true; + spinnerHidden = false; + } + else if (_rowModels.count == 0) + { + tableHidden = true; + spinnerHidden = true; + } + else + { + tableHidden = false; + spinnerHidden = true; + } + + if (!initial) + { + [TGTableDeltaUpdater updateTable:self.table oldData:currentRowModels newData:_rowModels controllerClassForIndexPath:^Class(TGIndexPath *indexPath) + { + return [self table:self.table rowControllerClassAtIndexPath:indexPath]; + }]; + } + else + { + [self.table reloadData]; + + if (partialLoad) + { + TGDispatchAfter(1.0, dispatch_get_main_queue(), ^ + { + [self reloadData]; + }); + } + } + + self.authAlertGroup.hidden = true; + self.activityIndicator.hidden = spinnerHidden; + self.table.hidden = tableHidden; + + [self updateMenuItems]; + } + else + { + [self popAllControllers]; + + _rowModels = nil; + _pendingRowModels = nil; + [self.table reloadData]; + + self.activityIndicator.hidden = true; + self.table.hidden = true; + self.authAlertGroup.hidden = false; + self.authAlertLabel.text = TGLocalized(@"Watch.AuthRequired"); + self.authAlertImageGroup.hidden = false; + [self.authAlertImage setImageNamed:@"LoginIcon"]; + self.authAlertDescLabel.hidden = true; + } +} + +- (void)updateTitle +{ + if (_forForward) + self.title = nil; + else + self.title = TGLocalized(@"Watch.AppName"); +} + ++ (NSString *)stringForSyncState:(TGBridgeSynchronizationStateValue)value +{ + switch (value) + { + case TGBridgeSynchronizationStateSynchronized: + return nil; + + case TGBridgeSynchronizationStateConnecting: + return TGLocalized(@"Watch.State.Connecting"); + + case TGBridgeSynchronizationStateUpdating: + return TGLocalized(@"Watch.State.Updating"); + + case TGBridgeSynchronizationStateWaitingForNetwork: + return TGLocalized(@"Watch.State.WaitingForNetwork"); + + default: + break; + } + + return nil; +} + +#pragma mark - + +- (void)updateMenuItems +{ + [_menu clearItems]; + + if (!_context.authorized || !_reachable) + return; + + if (_menu == nil) + _menu = [[TGInterfaceMenu alloc] initForInterfaceController:self]; + + NSMutableArray *menuItems = [[NSMutableArray alloc] init]; + + __weak TGNeoChatsController *weakSelf = self; +// TGInterfaceMenuItem *composeItem = [[TGInterfaceMenuItem alloc] initWithImageNamed:@"Compose" title:TGLocalized(@"Watch.ChatList.Compose") actionBlock:^(TGInterfaceController *controller, TGInterfaceMenuItem *sender) +// { +// __strong TGNeoChatsController *strongSelf = weakSelf; +// if (strongSelf != nil) +// [strongSelf presentControllerWithClass:[TGComposeController class] context:nil]; +// }]; + //[menuItems addObject:composeItem]; + + TGInterfaceMenuItem *savedMessagesItem = [[TGInterfaceMenuItem alloc] initWithImageNamed:@"SavedMessages" title:TGLocalized(@"DialogList.SavedMessages") actionBlock:^(TGInterfaceController *controller, TGInterfaceMenuItem *sender) + { + __strong TGNeoChatsController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + TGNeoConversationControllerContext *context = [[TGNeoConversationControllerContext alloc] initWithPeerId:strongSelf->_context.userId]; + context.context = strongSelf->_context; + [strongSelf pushControllerWithClass:[TGNeoConversationController class] context:context]; + }]; + [menuItems addObject:savedMessagesItem]; + + + [_menu addItems:menuItems]; +} + +#pragma mark - + +- (Class)table:(WKInterfaceTable *)table rowControllerClassAtIndexPath:(TGIndexPath *)indexPath +{ + return [TGNeoChatRowController class]; +} + +- (NSUInteger)numberOfRowsInTable:(WKInterfaceTable *)table section:(NSUInteger)section +{ + return _rowModels.count; +} + +- (void)table:(WKInterfaceTable *)table updateRowController:(TGNeoChatRowController *)controller forIndexPath:(TGIndexPath *)indexPath +{ + __weak TGNeoChatsController *weakSelf = self; + controller.isVisible = ^bool + { + __strong TGNeoChatsController *strongSelf = weakSelf; + if (strongSelf == nil) + return false; + + return strongSelf.isVisible; + }; + + TGBridgeChat *chat = _rowModels[indexPath.row]; + [controller updateWithChat:chat forForward:_forForward context:_context]; +} + +- (void)table:(WKInterfaceTable *)table didSelectRowAtIndexPath:(TGIndexPath *)indexPath +{ + if (indexPath.row >= _rowModels.count) + return; + + if (_forForward) + { + [self dismissController]; + + if (_forwardContext.completionBlock != nil) + _forwardContext.completionBlock(_rowModels[indexPath.row]); + } + else + { + TGNeoConversationControllerContext *context = [[TGNeoConversationControllerContext alloc] initWithChat:_rowModels[indexPath.row]]; + context.context = _context; + [self pushControllerWithClass:[TGNeoConversationController class] context:context]; + } +} + +#pragma mark - + +- (NSArray *)chats +{ + return _pendingRowModels; +} + +#pragma mark - + ++ (NSString *)identifier +{ + return TGNeoChatsControllerIdentifier; +} + +@end diff --git a/Watch/Extension/TGNeoContactMessageViewModel.h b/Watch/Extension/TGNeoContactMessageViewModel.h new file mode 100644 index 0000000000..7ab6777c21 --- /dev/null +++ b/Watch/Extension/TGNeoContactMessageViewModel.h @@ -0,0 +1,5 @@ +#import "TGNeoBubbleMessageViewModel.h" + +@interface TGNeoContactMessageViewModel : TGNeoBubbleMessageViewModel + +@end diff --git a/Watch/Extension/TGNeoContactMessageViewModel.m b/Watch/Extension/TGNeoContactMessageViewModel.m new file mode 100644 index 0000000000..396573a1aa --- /dev/null +++ b/Watch/Extension/TGNeoContactMessageViewModel.m @@ -0,0 +1,113 @@ +#import "TGNeoContactMessageViewModel.h" +#import "TGWatchCommon.h" +#import "TGNeoLabelViewModel.h" + +#import "TGBridgeContext.h" +#import "TGBridgeMessage.h" +#import "TGBridgeUser.h" + +#import "TGStringUtils.h" + +@interface TGNeoContactMessageViewModel () +{ + TGNeoLabelViewModel *_nameModel; + TGNeoLabelViewModel *_phoneModel; + + int32_t _userId; + int32_t _ownUserId; + NSString *_avatarUrl; + NSString *_firstName; + NSString *_lastName; +} +@end + +@implementation TGNeoContactMessageViewModel + +- (instancetype)initWithMessage:(TGBridgeMessage *)message type:(TGNeoMessageType)type users:(NSDictionary *)users context:(TGBridgeContext *)context +{ + self = [super initWithMessage:message type:type users:users context:context]; + if (self != nil) + { + TGBridgeContactMediaAttachment *contactAttachment = nil; + for (TGBridgeMediaAttachment *attachment in message.media) + { + if ([attachment isKindOfClass:[TGBridgeContactMediaAttachment class]]) + { + contactAttachment = (TGBridgeContactMediaAttachment *)attachment; + break; + } + } + + _nameModel = [[TGNeoLabelViewModel alloc] initWithText:[contactAttachment displayName] font:[UIFont systemFontOfSize:12 weight:UIFontWeightMedium] color:[self normalColorForMessage:message type:type] attributes:nil]; + _nameModel.multiline = false; + [self addSubmodel:_nameModel]; + + _phoneModel = [[TGNeoLabelViewModel alloc] initWithText:[contactAttachment prettyPhoneNumber] font:[UIFont systemFontOfSize:12] color:[self subtitleColorForMessage:message type:type] attributes:nil]; + _phoneModel.multiline = false; + [self addSubmodel:_phoneModel]; + + TGBridgeUser *user = users[@(contactAttachment.uid)]; + if (user != nil) + { + _userId = user.identifier; + _ownUserId = context.userId; + _firstName = user.firstName; + _lastName = user.lastName; + _avatarUrl = user.photoSmall; + } + else + { + _firstName = contactAttachment.firstName; + _lastName = contactAttachment.lastName; + } + } + return self; +} + +- (CGSize)layoutWithContainerSize:(CGSize)containerSize +{ + CGSize contentContainerSize = [self contentContainerSizeWithContainerSize:containerSize]; + + CGSize headerSize = [self layoutHeaderModelsWithContainerSize:contentContainerSize]; + CGFloat maxContentWidth = headerSize.width; + CGFloat textTopOffset = headerSize.height; + + CGFloat leftOffset = 19 + TGNeoBubbleMessageMetaSpacing; + contentContainerSize = CGSizeMake(containerSize.width - TGNeoBubbleMessageViewModelInsets.left - TGNeoBubbleMessageViewModelInsets.right - leftOffset, FLT_MAX); + + CGSize nameSize = [_nameModel contentSizeWithContainerSize:contentContainerSize]; + CGSize phoneSize = [_phoneModel contentSizeWithContainerSize:contentContainerSize]; + maxContentWidth = MAX(maxContentWidth, MAX(nameSize.width, phoneSize.width) + leftOffset); + + _nameModel.frame = CGRectMake(TGNeoBubbleMessageViewModelInsets.left + leftOffset, textTopOffset, nameSize.width, 14); + _phoneModel.frame = CGRectMake(TGNeoBubbleMessageViewModelInsets.left + leftOffset, CGRectGetMaxY(_nameModel.frame), phoneSize.width, 14); + + UIEdgeInsets inset = UIEdgeInsetsMake(textTopOffset + 5, TGNeoBubbleMessageViewModelInsets.left, 0, 0); + NSDictionary *avatarDictionary; + NSString *initials = [TGStringUtils initialsForFirstName:_firstName lastName:_lastName single:true]; + if (_userId != 0) + { + if (_avatarUrl.length > 0) + { + avatarDictionary = @{ TGNeoMessageAvatarIdentifier: @(_userId), TGNeoMessageAvatarUrl: _avatarUrl }; + } + else + { + avatarDictionary = @{ TGNeoMessageAvatarColor: [TGColor colorForUserId:_userId myUserId:_ownUserId], TGNeoMessageAvatarInitials: initials }; + } + } + else + { + avatarDictionary = @{ TGNeoMessageAvatarColor: [UIColor grayColor], TGNeoMessageAvatarInitials: initials }; + } + + [self addAdditionalLayout:@{ TGNeoContentInset: [NSValue valueWithUIEdgeInsets:inset], TGNeoMessageAvatarGroup: avatarDictionary } withKey:TGNeoMessageMetaGroup]; + + CGSize contentSize = CGSizeMake(TGNeoBubbleMessageViewModelInsets.left + TGNeoBubbleMessageViewModelInsets.right + maxContentWidth, CGRectGetMaxY(_phoneModel.frame) + TGNeoBubbleMessageViewModelInsets.bottom); + + [super layoutWithContainerSize:contentSize]; + + return contentSize; +} + +@end diff --git a/Watch/Extension/TGNeoConversationController.h b/Watch/Extension/TGNeoConversationController.h new file mode 100644 index 0000000000..2b7b251c62 --- /dev/null +++ b/Watch/Extension/TGNeoConversationController.h @@ -0,0 +1,23 @@ +#import "TGInterfaceController.h" + +@class TGBridgeContext; +@class TGBridgeChat; + +@interface TGNeoConversationControllerContext : NSObject + +@property (nonatomic, strong) TGBridgeContext *context; +@property (nonatomic, readonly) TGBridgeChat *chat; +@property (nonatomic, readonly) int64_t peerId; +@property (nonatomic, copy) void(^finished)(void); + +- (instancetype)initWithChat:(TGBridgeChat *)chat; +- (instancetype)initWithPeerId:(int64_t)peerId; + +@end + +@interface TGNeoConversationController : TGInterfaceController + +@property (nonatomic, weak) IBOutlet WKInterfaceTable *table; +@property (nonatomic, weak) IBOutlet WKInterfaceImage *activityIndicator; + +@end diff --git a/Watch/Extension/TGNeoConversationController.m b/Watch/Extension/TGNeoConversationController.m new file mode 100644 index 0000000000..32ce4d0585 --- /dev/null +++ b/Watch/Extension/TGNeoConversationController.m @@ -0,0 +1,1311 @@ +#import "TGNeoConversationController.h" +#import "TGWatchCommon.h" +#import "TGNeoChatsController.h" + +#import "TGStringUtils.h" +#import "TGDateUtils.h" + +#import "WKInterfaceTable+TGDataDrivenTable.h" +#import "TGTableDeltaUpdater.h" +#import "TGInterfaceMenu.h" + +#import "TGBridgeClient.h" +#import "TGBridgeContext.h" +#import "TGBridgeUser.h" +#import "TGBridgeChat.h" +#import "TGBridgeChatMessages.h" +#import "TGBridgeMessage+TGTableItem.h" +#import "TGBridgeBotInfo.h" +#import "TGBridgeBotReplyMarkup.h" +#import "TGBridgeUserCache.h" + +#import "TGChatInfo.h" + +#import "TGBridgeChatMessageListSignals.h" +#import "TGBridgeConversationSignals.h" +#import "TGBridgePeerSettingsSignals.h" +#import "TGBridgeSendMessageSignals.h" +#import "TGBridgeBotSignals.h" +#import "TGBridgeStateSignal.h" +#import "TGBridgeRemoteSignals.h" +#import "TGBridgeAudioSignals.h" + +#import "TGNeoConversationRowController.h" +#import "TGNeoConversationStaticRowController.h" +#import "TGNeoConversationTimeRowController.h" +#import "TGConversationFooterController.h" + +#import "TGUserInfoController.h" +#import "TGGroupInfoController.h" +#import "TGBotCommandController.h" +#import "TGBotKeyboardController.h" +#import "TGStickersController.h" +#import "TGLocationController.h" +#import "TGInputController.h" +#import "TGMessageViewController.h" +#import "TGAudioMicAlertController.h" + +NSString *const TGNeoConversationControllerIdentifier = @"TGNeoConversationController"; +const NSInteger TGNeoConversationControllerDefaultBatchLimit = 8; +const NSInteger TGNeoConversationControllerPerformantBatchLimit = 10; +const NSInteger TGNeoConversationControllerMaximumBatchLimit = 20; + +const NSInteger TGNeoConversationControllerInitialRenderCount = 4; + +@interface TGNeoConversationControllerContext () +{ + int64_t _peerId; + SVariable *_messages; +} + +@property (nonatomic, readonly) SSignal *signal; +@property (nonatomic, readonly) bool shouldReadMessages; + +@end + +@implementation TGNeoConversationControllerContext + +- (instancetype)initWithChat:(TGBridgeChat *)chat +{ + self = [super init]; + if (self != nil) + { + _chat = chat; + + [self initialize]; + } + return self; +} + +- (instancetype)initWithPeerId:(int64_t)peerId +{ + self = [super init]; + if (self != nil) + { + _peerId = peerId; + + [self initialize]; + } + return self; +} + +- (void)initialize +{ + _shouldReadMessages = true; + + NSInteger rangeCount = TGNeoConversationControllerDefaultBatchLimit; + switch (TGWatchScreenType()) { + case TGScreenType40mm: + case TGScreenType44mm: + rangeCount = TGNeoConversationControllerPerformantBatchLimit; + break; + + default: + break; + } + NSInteger initialUnreadCount = _chat.unreadCount; + if (initialUnreadCount > 0) + { + rangeCount = MAX(TGNeoConversationControllerDefaultBatchLimit, MIN(TGNeoConversationControllerMaximumBatchLimit, initialUnreadCount)); + + if (initialUnreadCount > TGNeoConversationControllerMaximumBatchLimit) + _shouldReadMessages = false; + } + + _messages = [[SVariable alloc] init]; + SSignal *loadSignal = [TGBridgeChatMessageListSignals chatMessageListViewWithPeerId:self.peerId atMessageId:0 rangeMessageCount:rangeCount]; + loadSignal = [loadSignal timeout:7.5 onQueue:[SQueue mainQueue] orSignal:loadSignal]; + + [_messages set:[loadSignal deliverOn:[SQueue mainQueue]]]; +} + +- (int64_t)peerId +{ + if (_peerId == 0) + return _chat.identifier; + + return _peerId; +} + +- (SSignal *)signal +{ + return _messages.signal; +} + +@end + +@interface TGNeoConversationController () +{ + TGNeoConversationControllerContext *_context; + + SMetaDisposable *_messagesListDisposable; + SMetaDisposable *_chatGroupDisposable; + SMetaDisposable *_sendMessageDisposable; + SMetaDisposable *_readMessagesDisposable; + SMetaDisposable *_peerSettingsDisposable; + SMetaDisposable *_updateSettingsDisposable; + SMetaDisposable *_botInfoDisposable; + SMetaDisposable *_botReplyMarkupDisposable; + SMetaDisposable *_remoteActionDisposable; + + SMetaDisposable *_sentMediaDisposable; + SMetaDisposable *_playAudioDisposable; + TGBridgeMediaAttachment *_pendingAudioAttachment; + + TGBridgeChat *_chatModel; + NSArray *_messageModels; + TGBridgeBotInfo *_botInfo; + TGBridgeBotReplyMarkup *_botReplyMarkup; + NSDictionary *_peerModels; + + NSMutableArray *_pendingSentMessages; + bool _shouldReadMessages; + + bool _muted; + bool _blocked; + bool _hasBots; + + NSArray *_rowModels; + + bool _initialized; + bool _initialRendering; + bool _shouldScrollToBottom; + TGInterfaceMenu *_menu; + TGConversationFooterOptions _footerOptions; + bool _dontAnimateFooterTransition; +} +@end + +@implementation TGNeoConversationController + +- (instancetype)init +{ + self = [super init]; + if (self != nil) + { + _messagesListDisposable = [[SMetaDisposable alloc] init]; + _chatGroupDisposable = [[SMetaDisposable alloc] init]; + _sendMessageDisposable = [[SMetaDisposable alloc] init]; + _readMessagesDisposable = [[SMetaDisposable alloc] init]; + _peerSettingsDisposable = [[SMetaDisposable alloc] init]; + _updateSettingsDisposable = [[SMetaDisposable alloc] init]; + _botInfoDisposable = [[SMetaDisposable alloc] init]; + _botReplyMarkupDisposable = [[SMetaDisposable alloc] init]; + _remoteActionDisposable = [[SMetaDisposable alloc] init]; + _sentMediaDisposable = [[SMetaDisposable alloc] init]; + _playAudioDisposable = [[SMetaDisposable alloc] init]; + + _pendingSentMessages = [[NSMutableArray alloc] init]; + + _dontAnimateFooterTransition = true; + + self.table.reloadDataReversed = true; + self.table.tableDataSource = self; + [self.table _setInitialHidden:true]; + + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(contextUpdated:) name:TGContextNotification object:nil]; + } + return self; +} + +- (void)dealloc +{ + [_messagesListDisposable dispose]; + [_chatGroupDisposable dispose]; + [_sendMessageDisposable dispose]; + [_readMessagesDisposable dispose]; + [_peerSettingsDisposable dispose]; + [_updateSettingsDisposable dispose]; + [_botInfoDisposable dispose]; + [_botReplyMarkupDisposable dispose]; + [_remoteActionDisposable dispose]; + [_sentMediaDisposable dispose]; + [_playAudioDisposable dispose]; + + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +- (void)configureWithContext:(TGNeoConversationControllerContext *)context +{ + _context = context; + + if (context.finished != nil) + context.finished(); + + if (_context.chat.identifier < 0) + _chatModel = _context.chat; + + self.title = [self conversationTitle]; + + _shouldReadMessages = context.shouldReadMessages; + + __weak TGNeoConversationController *weakSelf = self; + [_messagesListDisposable setDisposable:[context.signal startWithNext:^(NSDictionary *models) + { + __strong TGNeoConversationController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + strongSelf->_shouldScrollToBottom = (strongSelf->_messageModels == nil); + + strongSelf->_messageModels = models[TGBridgeMessagesArrayKey]; + + [[TGBridgeUserCache instance] storeUsers:[models[TGBridgeUsersDictionaryKey] allValues]]; + strongSelf->_peerModels = models[TGBridgeUsersDictionaryKey]; + + [strongSelf _readMessagesIfNeeded]; + + [strongSelf performInterfaceUpdate:^(bool animated) + { + __strong TGNeoConversationController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf reloadData]; + }]; + }]]; + + if ([self peerIsAnyGroup]) + { + [_chatGroupDisposable setDisposable:[[[TGBridgeConversationSignals conversationWithPeerId:[self peerId]] deliverOn:[SQueue mainQueue]] startWithNext:^(NSDictionary *next) + { + __strong TGNeoConversationController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + strongSelf->_chatModel = next[TGBridgeChatKey]; + [[TGBridgeUserCache instance] storeUsers:[next[TGBridgeUsersDictionaryKey] allValues]]; + + [strongSelf _updateBots]; + + [strongSelf performInterfaceUpdate:^(bool animated) + { + __strong TGNeoConversationController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf reloadData]; + }]; + }]]; + } +// else +// { +// [self _updateBots]; +// if (_hasBots) +// { +// [_botInfoDisposable setDisposable:[[TGBridgeBotSignals botInfoForUserId:(int32_t)[self peerId]] startWithNext:^(TGBridgeBotInfo *next) +// { +// __strong TGNeoConversationController *strongSelf = weakSelf; +// if (strongSelf == nil) +// return; +// +// strongSelf->_botInfo = next; +// +// [strongSelf performInterfaceUpdate:^(bool animated) +// { +// __strong TGNeoConversationController *strongSelf = weakSelf; +// if (strongSelf == nil) +// return; +// +// [strongSelf reloadData]; +// }]; +// }]]; +// } +// } + +// if ([self peerIsAnyGroup] || _hasBots) +// { +// [_botReplyMarkupDisposable setDisposable:[[TGBridgeBotSignals botReplyMarkupForPeerId:[self peerId]] startWithNext:^(TGBridgeBotReplyMarkup *next) +// { +// __strong TGNeoConversationController *strongSelf = weakSelf; +// if (strongSelf == nil) +// return; +// +// strongSelf->_botReplyMarkup = next; +// +// [strongSelf performInterfaceUpdate:^(bool animated) +// { +// __strong TGNeoConversationController *strongSelf = weakSelf; +// if (strongSelf == nil) +// return; +// +// [strongSelf reloadData]; +// }]; +// }]]; +// } + + [_peerSettingsDisposable setDisposable:[[[TGBridgePeerSettingsSignals peerSettingsWithPeerId:[self peerId]] deliverOn:[SQueue mainQueue]] startWithNext:^(NSDictionary *next) + { + __strong TGNeoConversationController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + bool blocked = [next[@"blocked"] boolValue]; + bool muted = [next[@"muted"] boolValue]; + + strongSelf->_blocked = blocked; + strongSelf->_muted = muted; + + [strongSelf performInterfaceUpdate:^(bool animated) + { + __strong TGNeoConversationController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf reloadData]; + }]; + }]]; + + [_sentMediaDisposable setDisposable:[[TGBridgeAudioSignals sentAudioForConversationId:[self peerId]] startWithNext:nil]]; + + [self configureHandoff]; +} + +- (void)reloadData +{ + NSArray *currentRowModels = _rowModels; + NSMutableArray *rowModels = [TGNeoConversationController reversedMessagesArray:_messageModels]; + if (rowModels == nil) + return; + + TGConversationFooterOptions oldFooterOptions = _footerOptions; + + _footerOptions = TGConversationFooterOptionsSendMessage; + if (_chatModel.isKickedFromGroup || _chatModel.hasLeftGroup) + _footerOptions = TGConversationFooterOptionsInactive; + else if (_blocked) + _footerOptions = [self _userIsBot] ? TGConversationFooterOptionsRestartBot : TGConversationFooterOptionsUnblock; + else if (![self peerIsGroup] && _hasBots && _messageModels != nil && _messageModels.count == 0) + _footerOptions = TGConversationFooterOptionsStartBot; + + if (_footerOptions == TGConversationFooterOptionsSendMessage && _hasBots) + { + if (_botReplyMarkup.rows.count > 0) + _footerOptions |= TGConversationFooterOptionsBotKeyboard; + else + _footerOptions |= TGConversationFooterOptionsBotCommands; + } + if ([self peerIsAnyGroup] || !_hasBots) + _footerOptions |= TGConversationFooterOptionsVoice; + + NSMutableArray *pendingSentMessages = [[NSMutableArray alloc] init]; + for (TGBridgeMessage *message in _pendingSentMessages) + { + TGBridgeDocumentMediaAttachment *documentAttachment = nil; + TGBridgeLocationMediaAttachment *locationAttachment = nil; + TGBridgeAudioMediaAttachment *audioAttachment = nil; + + for (TGBridgeMediaAttachment *attachment in message.media) + { + if ([attachment isKindOfClass:[TGBridgeDocumentMediaAttachment class]]) + documentAttachment = (TGBridgeDocumentMediaAttachment *)attachment; + else if ([attachment isKindOfClass:[TGBridgeLocationMediaAttachment class]]) + locationAttachment = (TGBridgeLocationMediaAttachment *)attachment; + else if ([attachment isKindOfClass:[TGBridgeAudioMediaAttachment class]]) + audioAttachment = (TGBridgeAudioMediaAttachment *)attachment; + } + + bool skip = false; + + for (TGBridgeMessage *realMessage in rowModels) + { + if (!realMessage.outgoing) + continue; + + if (fabs(realMessage.date - message.date) > 4.0) + continue; + + if ([realMessage.text isEqualToString:message.text]) + { + skip = true; + } + else + { + TGBridgeDocumentMediaAttachment *realDocumentAttachment = nil; + TGBridgeLocationMediaAttachment *realLocationAttachment = nil; + TGBridgeAudioMediaAttachment *realAudioAttachment = nil; + + for (TGBridgeMediaAttachment *attachment in message.media) + { + if ([attachment isKindOfClass:[TGBridgeDocumentMediaAttachment class]]) + realDocumentAttachment = (TGBridgeDocumentMediaAttachment *)attachment; + else if ([attachment isKindOfClass:[TGBridgeLocationMediaAttachment class]]) + realLocationAttachment = (TGBridgeLocationMediaAttachment *)attachment; + else if ([attachment isKindOfClass:[TGBridgeAudioMediaAttachment class]]) + realAudioAttachment = (TGBridgeAudioMediaAttachment *)attachment; + } + + if ([realDocumentAttachment isEqual:documentAttachment] || [realLocationAttachment isEqual:locationAttachment] || [realAudioAttachment isEqual:audioAttachment]) + { + skip = true; + } + } + } + + if (!skip) + [pendingSentMessages addObject:message]; + } + _pendingSentMessages = pendingSentMessages; + + for (TGBridgeMessage *message in pendingSentMessages) + [rowModels addObject:message]; + +// if (_botInfo.botDescription.length > 0) +// { +// TGChatInfo *chatInfo = [[TGChatInfo alloc] init]; +// chatInfo.title = TGLocalized(@"Bot.DescriptionTitle"); +// chatInfo.text = _botInfo.botDescription; +// [rowModels insertObject:chatInfo atIndex:0]; +// } + + _rowModels = [TGNeoConversationController timestampedModelsArray:rowModels]; + + bool initial = (currentRowModels == nil); + if (!initial) + { + [TGTableDeltaUpdater updateTable:self.table oldData:currentRowModels newData:_rowModels controllerClassForIndexPath:^Class(TGIndexPath *indexPath) + { + return [self table:self.table rowControllerClassAtIndexPath:indexPath]; + }]; + + if (oldFooterOptions != _footerOptions) + [self.table reloadFooter]; + } + else + { + _initialRendering = true; + + [self.table reloadData]; + self.activityIndicator.hidden = true; + self.table.hidden = false; + } + + if (_shouldScrollToBottom) + { + _shouldScrollToBottom = false; + + if (!_initialized) + { + [self animateWithDuration:0.4 animations:^ + { + self.table.alpha = 1.0f; + }]; + + _initialized = true; + } + [self.table scrollToRowAtIndexPath:[TGIndexPath indexPathForRow:_rowModels.count - 1 inSection:0]]; + + if (_initialRendering) + { + TGDispatchAfter(1.2, dispatch_get_main_queue(), ^ + { + _initialRendering = false; + [self.table reloadAllRows]; + }); + } + } + + [self updateMenuItems]; +} + +- (NSString *)conversationTitle +{ + if ([self peerIsGroup] || [self peerIsChannel]) + return _chatModel.groupTitle; + else if (_context.context.userId == _context.peerId) + return TGLocalized(@"Conversation.SavedMessages"); + else + return [[[TGBridgeUserCache instance] userWithId:(int32_t)[self peerId]] displayName]; +} + +- (void)configureHandoff +{ + int64_t peerId = [self peerId]; + bool isGroup = [self peerIsGroup] || [self peerIsChannel]; + + if (isGroup) + peerId = -peerId; + + NSMutableDictionary *peerDict = [[NSMutableDictionary alloc] init]; + peerDict[@"type"] = isGroup ? @"group" : @"user"; + peerDict[@"id"] = @(peerId); + + NSDictionary *userInfo = @{@"user_id": @(_context.context.userId), @"peer": peerDict}; + [self updateUserActivity:@"org.telegram.conversation" userInfo:userInfo webpageURL:[NSURL URLWithString:@"https://telegram.org/dl"]]; +} + +#pragma mark - + +- (void)contextUpdated:(NSNotification *)notification +{ + TGBridgeContext *context = notification.userInfo[TGContextNotificationKey]; + if (context != nil) + _context.context = context; +} + +- (void)updateTitleWithState:(TGBridgeSynchronizationStateValue)value +{ + NSString *state = [TGNeoChatsController stringForSyncState:value]; + if (_context.context == nil || state == nil) + self.title = [self conversationTitle]; + else + self.title = state; +} + +- (void)updateMenuItems +{ + [_menu clearItems]; + + if (_context.chat.isKickedFromGroup || _context.chat.hasLeftGroup) + return; + + if (_menu == nil) + _menu = [[TGInterfaceMenu alloc] initForInterfaceController:self]; + + NSMutableArray *menuItems = [[NSMutableArray alloc] init]; + + __weak TGNeoConversationController *weakSelf = self; + TGInterfaceMenuItem *infoItem = [[TGInterfaceMenuItem alloc] initWithItemIcon:WKMenuItemIconInfo title:[self peerIsAnyGroup] ? TGLocalized(@"Watch.Conversation.GroupInfo") : TGLocalized(@"Watch.Conversation.UserInfo") actionBlock:^(TGInterfaceController *controller, TGInterfaceMenuItem *sender) + { + __strong TGNeoConversationController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + if ([strongSelf peerIsGroup]) + { + TGGroupInfoControllerContext *context = [[TGGroupInfoControllerContext alloc] initWithGroupChat:strongSelf->_context.chat]; + [controller pushControllerWithClass:[TGGroupInfoController class] context:context]; + } + else if ([strongSelf peerIsChannel]) + { + TGUserInfoControllerContext *context = [[TGUserInfoControllerContext alloc] initWithChannel:strongSelf->_chatModel]; + context.disallowCompose = true; + [controller pushControllerWithClass:[TGUserInfoController class] context:context]; + } + else + { + TGUserInfoControllerContext *context = [[TGUserInfoControllerContext alloc] initWithUserId:(int32_t)[strongSelf peerId]]; + context.disallowCompose = true; + [controller pushControllerWithClass:[TGUserInfoController class] context:context]; + } + }]; + [menuItems addObject:infoItem]; + + bool muted = _muted; + bool blocked = _blocked; + + bool muteForever = [self peerIsAnyGroup]; + int32_t muteFor = muteForever ? INT_MAX : 1; + NSString *muteTitle = muteForever ? TGLocalized(@"Watch.UserInfo.MuteTitle") : [NSString stringWithFormat:TGLocalized([TGStringUtils integerValueFormat:@"Watch.UserInfo.Mute_" value:muteFor]), muteFor]; + + TGInterfaceMenuItem *muteItem = [[TGInterfaceMenuItem alloc] initWithItemIcon:muted ? WKMenuItemIconSpeaker : WKMenuItemIconMute title:muted ? TGLocalized(@"Watch.UserInfo.Unmute") : muteTitle actionBlock:^(TGInterfaceController *controller, TGInterfaceMenuItem *sender) + { + __strong TGNeoConversationController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf->_updateSettingsDisposable setDisposable:[[[TGBridgePeerSettingsSignals toggleMutedWithPeerId:[strongSelf peerId]] deliverOn:[SQueue mainQueue]] startWithNext:nil completed:^ + { + __strong TGNeoConversationController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + strongSelf->_muted = !muted; + + [strongSelf performInterfaceUpdate:^(bool animated) + { + __strong TGNeoConversationController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf reloadData]; + }]; + }]]; + }]; + [menuItems addObject:muteItem]; + + if (![self peerIsGroup] && ![self peerIsChannel]) + { + TGInterfaceMenuItem *blockItem = [[TGInterfaceMenuItem alloc] initWithItemIcon:WKMenuItemIconBlock title:blocked ? TGLocalized(@"Watch.UserInfo.Unblock") : TGLocalized(@"Watch.UserInfo.Block") actionBlock:^(TGInterfaceController *controller, TGInterfaceMenuItem *sender) + { + __strong TGNeoConversationController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf->_updateSettingsDisposable setDisposable:[[[TGBridgePeerSettingsSignals updateBlockStatusWithPeerId:[strongSelf peerId] blocked:!blocked] deliverOn:[SQueue mainQueue]] startWithNext:nil completed:^ + { + __strong TGNeoConversationController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + strongSelf->_blocked = !blocked; + + [strongSelf performInterfaceUpdate:^(bool animated) + { + __strong TGNeoConversationController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf reloadData]; + }]; + }]]; + }]; + [menuItems addObject:blockItem]; + } + + [_menu addItems:menuItems]; +} + + +#pragma mark - Peer + +- (int64_t)peerId +{ + return _context.peerId; +} + +- (bool)peerIsGroup +{ + if (_chatModel != nil) + return _chatModel.isGroup; + else + return _context.peerId < 0; +} + +- (bool)peerIsChannel +{ + if (_chatModel != nil) + return _chatModel.isChannel; + else + return false; +} + +- (bool)peerIsChannelGroup +{ + if (_chatModel != nil) + return _chatModel.isChannelGroup; + else + return false; +} + +- (bool)peerIsAnyGroup +{ + return [self peerIsGroup] || [self peerIsChannelGroup]; +} + +#pragma mark - Bots + +- (SSignal *)botCommandListSignal +{ + if (!_hasBots) + return nil; + + if ([self peerIsAnyGroup]) + { + NSMutableArray *botInfoSignals = [[NSMutableArray alloc] init]; + NSMutableArray *botUsers = [[NSMutableArray alloc] init]; + NSMutableArray *initialStates = [[NSMutableArray alloc] init]; + [_chatModel.participantsUserIds enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL * _Nonnull stop) + { + TGBridgeUser *user = [[TGBridgeUserCache instance] userWithId:(int32_t)idx]; + if ([user isBot]) + { + [botUsers addObject:user]; + [initialStates addObject:@[]]; + [botInfoSignals addObject:[[TGBridgeBotSignals botInfoForUserId:user.identifier] map:^NSArray *(TGBridgeBotInfo *botInfo) + { + if (botInfo.commandList == nil) + return @[]; + + return botInfo.commandList; + }]]; + } + }]; + + return [[SSignal combineSignals:botInfoSignals withInitialStates:initialStates] map:^id(NSArray *commandLists) + { + NSMutableArray *commands = [[NSMutableArray alloc] init]; + NSInteger index = 0; + for (NSArray *commandList in commandLists) + { + [commands addObject:@{ TGBotCommandUserKey: botUsers[index], TGBotCommandListKey: commandList } ]; + index++; + } + + return commands; + }]; + } + else if ([self _userIsBot]) + { + int32_t userId = (int32_t)[self peerId]; + return [[TGBridgeBotSignals botInfoForUserId:userId] map:^NSArray *(TGBridgeBotInfo *botInfo) + { + if (botInfo != nil) + { + TGBridgeUser *user = [[TGBridgeUserCache instance] userWithId:userId]; + return @[ @{ TGBotCommandUserKey: user, TGBotCommandListKey: botInfo.commandList } ]; + } + + return nil; + }]; + } + + return nil; +} + +- (bool)_userIsBot +{ + if ([self peerId] < 0) + return false; + + TGBridgeUser *user = [[TGBridgeUserCache instance] userWithId:(int32_t)[self peerId]]; + return [user isBot]; +} + +- (void)_updateBots +{ + _hasBots = false; + return; + + if ([self peerIsAnyGroup]) + { + [_chatModel.participantsUserIds enumerateIndexesUsingBlock:^(NSUInteger userId, BOOL * _Nonnull stop) + { + TGBridgeUser *user = [[TGBridgeUserCache instance] userWithId:(int32_t)userId]; + if ([user isBot]) + { + _hasBots = true; + *stop = true; + } + }]; + } + else + { + TGBridgeUser *user = [[TGBridgeUserCache instance] userWithId:(int32_t)[self peerId]]; + _hasBots = [user isBot]; + } +} + +#pragma mark - + +- (void)sendMessageWithText:(NSString *)text +{ + [self sendMessageWithText:text replyToMessage:nil]; +} + +- (void)sendMessageWithText:(NSString *)text replyToMessage:(TGBridgeMessage *)replyToMessage +{ + _shouldReadMessages = true; + _shouldScrollToBottom = true; + + [_pendingSentMessages addObject:[TGBridgeMessage temporaryNewMessageForText:text userId:_context.context.userId replyToMessage:replyToMessage]]; + + __weak TGNeoConversationController *weakSelf = self; + [self performInterfaceUpdate:^(bool animated) + { + __strong TGNeoConversationController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf reloadData]; + }]; + + [_sendMessageDisposable setDisposable:[[[TGBridgeSendMessageSignals sendMessageWithPeerId:[self peerId] text:text replyToMid:0] deliverOn:[SQueue mainQueue]] startWithNext:nil]]; +} + +- (void)sendMessageWithStickerAttachment:(TGBridgeDocumentMediaAttachment *)sticker +{ + _shouldReadMessages = true; + _shouldScrollToBottom = true; + + [_pendingSentMessages addObject:[TGBridgeMessage temporaryNewMessageForSticker:sticker userId:_context.context.userId]]; + + __weak TGNeoConversationController *weakSelf = self; + [self performInterfaceUpdate:^(bool animated) + { + __strong TGNeoConversationController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf reloadData]; + }]; + + [_sendMessageDisposable setDisposable:[[[TGBridgeSendMessageSignals sendMessageWithPeerId:[self peerId] sticker:sticker replyToMid:0] deliverOn:[SQueue mainQueue]] startWithNext:nil]]; +} + +- (void)sendMessageWithLocationAttachment:(TGBridgeLocationMediaAttachment *)location +{ + _shouldReadMessages = true; + _shouldScrollToBottom = true; + + [_pendingSentMessages addObject:[TGBridgeMessage temporaryNewMessageForLocation:location userId:_context.context.userId]]; + + __weak TGNeoConversationController *weakSelf = self; + [self performInterfaceUpdate:^(bool animated) + { + __strong TGNeoConversationController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf reloadData]; + }]; + + [_sendMessageDisposable setDisposable:[[[TGBridgeSendMessageSignals sendMessageWithPeerId:[self peerId] location:location replyToMid:0] deliverOn:[SQueue mainQueue]] startWithNext:nil]]; +} + +- (void)sendAudioWithUniqueId:(int64_t)uniqueId duration:(int32_t)duration url:(NSURL *)url +{ + _shouldReadMessages = true; + _shouldScrollToBottom = true; + + [_pendingSentMessages addObject:[TGBridgeMessage temporaryNewMessageForAudioWithDuration:duration userId:_context.context.userId localAudioId:uniqueId]]; + + __weak TGNeoConversationController *weakSelf = self; + [self performInterfaceUpdate:^(bool animated) + { + __strong TGNeoConversationController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf reloadData]; + }]; + + NSDictionary *metadata = @ + { + TGBridgeIncomingFileTypeKey: TGBridgeIncomingFileTypeAudio, + TGBridgeIncomingFileRandomIdKey: @(uniqueId), + TGBridgeIncomingFilePeerIdKey: @([self peerId]), + TGBridgeIncomingFileReplyToMidKey: @(0) + }; + + [[TGBridgeClient instance] sendFileWithURL:url metadata:metadata]; +} + +- (TGBridgeMessage *)_latestIncomingMessage +{ + __block TGBridgeMessage *incomingMessage = nil; + + for (TGBridgeMessage *message in _messageModels) + { + if (!message.outgoing) + { + incomingMessage = message; + break; + } + } + return incomingMessage; +} + +- (void)_readMessagesIfNeeded +{ + bool hasUnreadMessages = false; + for (TGBridgeMessage *message in _messageModels) + { + if (!message.outgoing && message.unread) + { + hasUnreadMessages = true; + break; + } + } + + if (hasUnreadMessages && _shouldReadMessages) + { + TGBridgeMessage *lastMessage = [self _latestIncomingMessage]; + [_readMessagesDisposable setDisposable:[[TGBridgeChatMessageListSignals readChatMessageListWithPeerId:[self peerId] messageId:lastMessage.identifier] startWithNext:nil completed:nil]]; + } +} + +#pragma mark - Table Data Source & Delegate + +- (Class)table:(WKInterfaceTable *)table rowControllerClassAtIndexPath:(TGIndexPath *)indexPath +{ + id model = _rowModels[indexPath.row]; + + if ([model isKindOfClass:[TGBridgeMessage class]]) + { + return [TGNeoRowController rowControllerClassForMessage:(TGBridgeMessage *)model]; + } + else if ([model isKindOfClass:[TGChatInfo class]]) + { + return [TGNeoConversationStaticRowController class]; + } + else if ([model isKindOfClass:[TGChatTimestamp class]]) + { + return [TGNeoConversationTimeRowController class]; + } + + return nil; +} + +- (NSUInteger)numberOfRowsInTable:(WKInterfaceTable *)table section:(NSUInteger)section +{ + return _rowModels.count; +} + +- (void)table:(WKInterfaceTable *)table updateRowController:(TGTableRowController *)controller forIndexPath:(TGIndexPath *)indexPath +{ + if (![controller isKindOfClass:[TGTableRowController class]]) + return; + + __weak TGNeoConversationController *weakSelf = self; + + id model = _rowModels[indexPath.row]; + NSUInteger index = [self numberOfRowsInTable:self.table section:0] - indexPath.row - 1; + + if ([model isKindOfClass:[TGChatTimestamp class]] && [controller isKindOfClass:[TGNeoConversationTimeRowController class]]) + { + TGNeoConversationTimeRowController *timeController = (TGNeoConversationTimeRowController *)controller; + [timeController updateWithTimestamp:model]; + return; + } + + TGNeoRowController *rowController = (TGNeoRowController *)controller; + rowController.shouldRenderContent = ^bool + { + __strong TGNeoConversationController *strongSelf = weakSelf; + if (strongSelf != nil && strongSelf->_initialRendering && index >= TGNeoConversationControllerInitialRenderCount) + return false; + + return true; + }; + + if ([model isKindOfClass:[TGBridgeMessage class]]) + { + TGBridgeMessage *message = (TGBridgeMessage *)model; + + TGNeoConversationRowController *conversationRow = (TGNeoConversationRowController *)controller; + __weak TGNeoConversationRowController *weakConversationRow = conversationRow; + + conversationRow.animate = ^(void (^animations)(void)) + { + __strong TGNeoConversationController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf animateWithDuration:0.25 animations:animations]; + }; + + conversationRow.buttonPressed = ^ + { + __strong TGNeoConversationController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + TGBridgeMediaAttachment *audioAttachment = nil; + for (TGBridgeMediaAttachment *attachment in message.media) + { + if ([attachment isKindOfClass:[TGBridgeAudioMediaAttachment class]]) + { + audioAttachment = (TGBridgeAudioMediaAttachment *)attachment; + } + else if ([attachment isKindOfClass:[TGBridgeDocumentMediaAttachment class]]) + { + TGBridgeDocumentMediaAttachment *documentAttachment = (TGBridgeDocumentMediaAttachment *)attachment; + if (documentAttachment.isVoice) + audioAttachment = documentAttachment; + } + } + + if (audioAttachment != nil) + { + __strong TGNeoConversationRowController *strongConversationRow = weakConversationRow; + if ([strongSelf->_pendingAudioAttachment isEqual:audioAttachment]) + { + if (strongConversationRow != nil) + [strongConversationRow setProcessingState:false]; + + strongSelf->_pendingAudioAttachment = nil; + + [strongSelf->_playAudioDisposable setDisposable:nil]; + } + else + { + if (strongConversationRow != nil) + [strongConversationRow setProcessingState:true]; + + strongSelf->_pendingAudioAttachment = audioAttachment; + + [strongSelf->_playAudioDisposable setDisposable:[[[TGBridgeAudioSignals audioForAttachment:audioAttachment conversationId:[strongSelf peerId] messageId:message.identifier] deliverOn:[SQueue mainQueue]] startWithNext:^(NSURL *url) + { + if (url == nil) + return; + + __strong TGNeoConversationController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + __strong TGNeoConversationRowController *strongConversationRow = weakConversationRow; + if (strongConversationRow != nil) + [strongConversationRow setProcessingState:false]; + + strongSelf->_pendingAudioAttachment = nil; + + [strongSelf presentMediaPlayerControllerWithURL:url options:@{ WKMediaPlayerControllerOptionsAutoplayKey: @true } completion:^(BOOL didPlayToEnd, NSTimeInterval endTime, NSError *error) {}]; + }]]; + } + } + else + { + [strongSelf->_remoteActionDisposable setDisposable:[[TGBridgeRemoteSignals openRemoteMessageWithPeerId:[strongSelf peerId] messageId:message.identifier type:0 autoPlay:true] startWithNext:nil]]; + } + }; + + TGNeoMessageType type = TGNeoMessageTypeGeneric; + if ([self peerIsAnyGroup]) + type = TGNeoMessageTypeGroup; + else if ([self peerIsChannel]) + type = TGNeoMessageTypeChannel; + + conversationRow.additionalPeers = _peerModels; + [conversationRow updateWithMessage:message context:_context.context index:index type:type]; + } + else if ([model isKindOfClass:[TGChatInfo class]]) + { + TGChatInfo *chatInfo = (TGChatInfo *)model; + + TGNeoConversationStaticRowController *conversationRow = (TGNeoConversationStaticRowController *)controller; + [conversationRow updateWithChatInfo:chatInfo]; + } +} + +- (void)table:(WKInterfaceTable *)table didSelectRowAtIndexPath:(TGIndexPath *)indexPath +{ + TGBridgeMessage *message = _rowModels[indexPath.row]; + + TGMessageViewControllerContext *context = nil; + if ([self peerIsChannel] && ![self peerIsChannelGroup]) + context = [[TGMessageViewControllerContext alloc] initWithMessage:message channel:_chatModel]; + else + context = [[TGMessageViewControllerContext alloc] initWithMessage:message peerId:[self peerId]]; + + context.additionalPeers = _peerModels; + + [self pushControllerWithClass:[TGMessageViewController class] context:context]; +} + +- (Class)footerControllerClassForTable:(WKInterfaceTable *)table +{ + if ([self peerIsChannel] && ![self peerIsChannelGroup]) + return nil; + + return [TGConversationFooterController class]; +} + +- (void)table:(WKInterfaceTable *)table updateFooterController:(TGConversationFooterController *)controller +{ + __weak TGNeoConversationController *weakSelf = self; + controller.animate = ^(void (^animations)(void)) + { + __strong TGNeoConversationController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf animateWithDuration:0.3 animations:animations]; + }; + + [controller setOptions:_footerOptions animated:!_dontAnimateFooterTransition]; + _dontAnimateFooterTransition = false; + + controller.stickerPressed = ^ + { + __strong TGNeoConversationController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + TGStickersControllerContext *context = [[TGStickersControllerContext alloc] init]; + context.completionBlock = ^(TGBridgeDocumentMediaAttachment *sticker) + { + __strong TGNeoConversationController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf sendMessageWithStickerAttachment:sticker]; + }; + [strongSelf presentControllerWithClass:[TGStickersController class] context:context]; + }; + controller.locationPressed = ^ + { + __strong TGNeoConversationController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + TGLocationControllerContext *context = [[TGLocationControllerContext alloc] init]; + context.completionBlock = ^(TGBridgeLocationMediaAttachment *location) + { + __strong TGNeoConversationController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf sendMessageWithLocationAttachment:location]; + }; + [strongSelf presentControllerWithClass:[TGLocationController class] context:context]; + }; + controller.voicePressed = ^ + { + __strong TGNeoConversationController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + bool override = false; +#if TARGET_OS_SIMULATOR + override = true; +#endif + + if (override || strongSelf->_context.context.micAccessAllowed) + { + [TGInputController presentAudioControllerForInterfaceController:strongSelf completion:^(int64_t uniqueId, int32_t duration, NSURL *url) + { + __strong TGNeoConversationController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf sendAudioWithUniqueId:uniqueId duration:duration url:url]; + }]; + } + else + { + [strongSelf presentControllerWithClass:[TGAudioMicAlertController class] context:nil]; + } + }; + controller.commandsPressed = ^ + { + __strong TGNeoConversationController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + if (strongSelf->_botReplyMarkup != nil) + { + TGBridgeBotReplyMarkup *replyMarkup = strongSelf->_botReplyMarkup; + TGBotKeyboardControllerContext *context = [[TGBotKeyboardControllerContext alloc] init]; + context.replyMarkup = replyMarkup; + context.completionBlock = ^(NSString *command) + { + __strong TGNeoConversationController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf sendMessageWithText:command replyToMessage:replyMarkup.message]; + }; + [strongSelf presentControllerWithClass:[TGBotKeyboardController class] context:context]; + } + else + { + TGBotCommandControllerContext *context = [[TGBotCommandControllerContext alloc] init]; + context.commandListSignal = [strongSelf botCommandListSignal]; + context.context = strongSelf->_context.context; + context.completionBlock = ^(NSString *command) + { + __strong TGNeoConversationController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf sendMessageWithText:command]; + }; + [strongSelf presentControllerWithClass:[TGBotCommandController class] context:context]; + } + }; + controller.unblockPressed = ^ + { + __strong TGNeoConversationController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf->_updateSettingsDisposable setDisposable:[[[TGBridgePeerSettingsSignals updateBlockStatusWithPeerId:[strongSelf peerId] blocked:false] deliverOn:[SQueue mainQueue]] startWithNext:nil completed:^ + { + __strong TGNeoConversationController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + strongSelf->_blocked = false; + [strongSelf reloadData]; + }]]; + }; + controller.replyPressed = ^ + { + __strong TGNeoConversationController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [TGInputController presentInputControllerForInterfaceController:strongSelf suggestionsForText:[strongSelf _latestIncomingMessage].text completion:^(NSString *text) + { + [strongSelf sendMessageWithText:text]; + }]; + }; + controller.startPressed = ^ + { + __strong TGNeoConversationController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf sendMessageWithText:@"/start"]; + }; + controller.restartPressed = ^ + { + __strong TGNeoConversationController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf->_updateSettingsDisposable setDisposable:[[[TGBridgePeerSettingsSignals updateBlockStatusWithPeerId:[strongSelf peerId] blocked:false] deliverOn:[SQueue mainQueue]] startWithNext:nil completed:^ + { + __strong TGNeoConversationController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + strongSelf->_blocked = false; + [strongSelf sendMessageWithText:@"/start"]; + }]]; + }; +} + +#pragma mark - + ++ (NSMutableArray *)reversedMessagesArray:(NSArray *)array +{ + if (array == nil) + return nil; + + NSMutableArray *reversedArray = [[NSMutableArray alloc] init]; + for (id object in array) + [reversedArray insertObject:object atIndex:0]; + + return reversedArray; +} + ++ (NSArray *)timestampedModelsArray:(NSArray *)models +{ + NSMutableArray *newModels = [[NSMutableArray alloc] init]; + TGChatTimestamp *lastTimestamp = nil; + + for (id model in models) + { + if ([model isKindOfClass:[TGChatTimestamp class]]) + { + continue; + } + else if ([model isKindOfClass:[TGBridgeMessage class]]) + { + TGBridgeMessage *message = (TGBridgeMessage *)model; + + TGChatTimestamp *timestamp = [TGDateUtils timestampForDateIfNeeded:message.date previousDate:lastTimestamp ? @(lastTimestamp.date) : nil]; + + if (timestamp != nil) + { + lastTimestamp = timestamp; + [newModels addObject:timestamp]; + } + } + + [newModels addObject:model]; + } + + return newModels; +} + +#pragma mark - + ++ (NSString *)identifier +{ + return TGNeoConversationControllerIdentifier; +} + +@end diff --git a/Watch/Extension/TGNeoConversationMediaRowController.h b/Watch/Extension/TGNeoConversationMediaRowController.h new file mode 100644 index 0000000000..56cc0ab2e4 --- /dev/null +++ b/Watch/Extension/TGNeoConversationMediaRowController.h @@ -0,0 +1,5 @@ +#import "WKInterfaceTable+TGDataDrivenTable.h" + +@interface TGNeoConversationMediaRowController : TGTableRowController + +@end diff --git a/Watch/Extension/TGNeoConversationMediaRowController.m b/Watch/Extension/TGNeoConversationMediaRowController.m new file mode 100644 index 0000000000..b08fb9c25d --- /dev/null +++ b/Watch/Extension/TGNeoConversationMediaRowController.m @@ -0,0 +1,12 @@ +#import "TGNeoConversationMediaRowController.h" + +NSString *const TGNeoConversationMediaRowIdentifier = @"TGNeoConversationMediaRow"; + +@implementation TGNeoConversationMediaRowController + ++ (NSString *)identifier +{ + return TGNeoConversationMediaRowIdentifier; +} + +@end diff --git a/Watch/Extension/TGNeoConversationRowController.h b/Watch/Extension/TGNeoConversationRowController.h new file mode 100644 index 0000000000..029c99b576 --- /dev/null +++ b/Watch/Extension/TGNeoConversationRowController.h @@ -0,0 +1,5 @@ +#import "TGNeoRowController.h" + +@interface TGNeoConversationRowController : TGNeoRowController + +@end diff --git a/Watch/Extension/TGNeoConversationRowController.m b/Watch/Extension/TGNeoConversationRowController.m new file mode 100644 index 0000000000..4ee17994bd --- /dev/null +++ b/Watch/Extension/TGNeoConversationRowController.m @@ -0,0 +1,12 @@ +#import "TGNeoConversationRowController.h" + +NSString *const TGNeoConversationRowIdentifier = @"TGNeoConversationRow"; + +@implementation TGNeoConversationRowController + ++ (NSString *)identifier +{ + return TGNeoConversationRowIdentifier; +} + +@end diff --git a/Watch/Extension/TGNeoConversationSimpleRowController.h b/Watch/Extension/TGNeoConversationSimpleRowController.h new file mode 100644 index 0000000000..d0826f6871 --- /dev/null +++ b/Watch/Extension/TGNeoConversationSimpleRowController.h @@ -0,0 +1,5 @@ +#import "TGNeoRowController.h" + +@interface TGNeoConversationSimpleRowController : TGNeoRowController + +@end diff --git a/Watch/Extension/TGNeoConversationSimpleRowController.m b/Watch/Extension/TGNeoConversationSimpleRowController.m new file mode 100644 index 0000000000..e26451f1fa --- /dev/null +++ b/Watch/Extension/TGNeoConversationSimpleRowController.m @@ -0,0 +1,12 @@ +#import "TGNeoConversationSimpleRowController.h" + +NSString *const TGNeoConversationSimpleRowIdentifier = @"TGNeoConversationSimpleRow"; + +@implementation TGNeoConversationSimpleRowController + ++ (NSString *)identifier +{ + return TGNeoConversationSimpleRowIdentifier; +} + +@end diff --git a/Watch/Extension/TGNeoConversationStaticRowController.h b/Watch/Extension/TGNeoConversationStaticRowController.h new file mode 100644 index 0000000000..4f0ed0b6d9 --- /dev/null +++ b/Watch/Extension/TGNeoConversationStaticRowController.h @@ -0,0 +1,11 @@ +#import "TGNeoRowController.h" + +@class TGBridgeContext; +@class TGBridgeMessage; +@class TGChatInfo; + +@interface TGNeoConversationStaticRowController : TGNeoRowController + +- (void)updateWithChatInfo:(TGChatInfo *)chatInfo; + +@end diff --git a/Watch/Extension/TGNeoConversationStaticRowController.m b/Watch/Extension/TGNeoConversationStaticRowController.m new file mode 100644 index 0000000000..04803dc660 --- /dev/null +++ b/Watch/Extension/TGNeoConversationStaticRowController.m @@ -0,0 +1,84 @@ +#import "TGNeoConversationStaticRowController.h" +#import "TGBridgeMessage.h" +#import "TGBridgeUserCache.h" + +#import "TGChatInfo.h" + +#import "TGNeoServiceMessageViewModel.h" + +#import + +NSString *const TGNeoConversationStaticRowIdentifier = @"TGNeoConversationStaticRow"; + +@interface TGNeoConversationStaticRowController () +{ + TGNeoMessageViewModel *_viewModel; + SMetaDisposable *_renderDisposable; + + TGChatInfo *_currentChatInfo; +} +@end + +@implementation TGNeoConversationStaticRowController + +- (instancetype)init +{ + self = [super init]; + if (self != nil) + { + _renderDisposable = [[SMetaDisposable alloc] init]; + } + return self; +} + +- (void)dealloc +{ + [_renderDisposable dispose]; +} + +- (void)updateWithChatInfo:(TGChatInfo *)chatInfo +{ + if (_viewModel != nil) + return; + + _viewModel = [TGNeoConversationStaticRowController viewModelForChatInfo:chatInfo]; + + CGSize containerSize = [[WKInterfaceDevice currentDevice] screenBounds].size; + CGSize contentSize = [_viewModel layoutWithContainerSize:containerSize]; + + self.contentGroup.width = contentSize.width; + self.contentGroup.height = contentSize.height; + + __weak TGNeoConversationStaticRowController *weakSelf = self; + [_renderDisposable setDisposable:[[[[TGNeoRenderableViewModel renderSignalForViewModel:_viewModel] startOn:[SQueue concurrentDefaultQueue]] deliverOn:[SQueue mainQueue]] startWithNext:^(UIImage *image) + { + __strong TGNeoConversationStaticRowController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf.contentGroup setBackgroundImage:image]; + }]]; +} + +- (bool)shouldUpdateChatInfoFrom:(TGChatInfo *)oldChatInfo to:(TGChatInfo *)newChatInfo +{ + if (oldChatInfo == nil) + return true; + + if (![oldChatInfo.text isEqualToString:newChatInfo.text]) + return true; + + return false; +} + ++ (TGNeoMessageViewModel *)viewModelForChatInfo:(TGChatInfo *)chatInfo +{ + return [[TGNeoServiceMessageViewModel alloc] initWithChatInfo:chatInfo]; +} + ++ (NSString *)identifier +{ + return TGNeoConversationStaticRowIdentifier; +} + +@end diff --git a/Watch/Extension/TGNeoConversationTimeRowController.h b/Watch/Extension/TGNeoConversationTimeRowController.h new file mode 100644 index 0000000000..f8ab8c945d --- /dev/null +++ b/Watch/Extension/TGNeoConversationTimeRowController.h @@ -0,0 +1,11 @@ +#import "WKInterfaceTable+TGDataDrivenTable.h" + +@class TGChatTimestamp; + +@interface TGNeoConversationTimeRowController : TGTableRowController + +@property (nonatomic, weak) IBOutlet WKInterfaceLabel *label; + +- (void)updateWithTimestamp:(TGChatTimestamp *)timestamp; + +@end diff --git a/Watch/Extension/TGNeoConversationTimeRowController.m b/Watch/Extension/TGNeoConversationTimeRowController.m new file mode 100644 index 0000000000..18ff6f4f84 --- /dev/null +++ b/Watch/Extension/TGNeoConversationTimeRowController.m @@ -0,0 +1,52 @@ +#import "TGNeoConversationTimeRowController.h" +#import "TGChatTimestamp.h" + +#import "TGExtensionDelegate.h" + +NSString *const TGNeoConversationTimeRowIdentifier = @"TGNeoConversationTimeRow"; + +@implementation TGNeoConversationTimeRowController + +- (void)updateWithTimestamp:(TGChatTimestamp *)timestamp +{ + NSAttributedString *attributedText = [[NSAttributedString alloc] initWithString:timestamp.string attributes:@{ NSFontAttributeName: [UIFont systemFontOfSize:[TGNeoConversationTimeRowController textFontSize]], NSForegroundColorAttributeName: [UIColor whiteColor] }]; + self.label.attributedText = attributedText; +} + ++ (CGFloat)textFontSize +{ + TGContentSizeCategory category = [TGExtensionDelegate instance].contentSizeCategory; + + switch (category) + { + case TGContentSizeCategoryXS: + return 10.0f; + + case TGContentSizeCategoryS: + return 11.0f; + + case TGContentSizeCategoryL: + return 12.0f; + + case TGContentSizeCategoryXL: + return 13.0f; + + case TGContentSizeCategoryXXL: + return 14.0f; + + case TGContentSizeCategoryXXXL: + return 15.0f; + + default: + break; + } + + return 16.0f; +} + ++ (NSString *)identifier +{ + return TGNeoConversationTimeRowIdentifier; +} + +@end diff --git a/Watch/Extension/TGNeoFileMessageViewModel.h b/Watch/Extension/TGNeoFileMessageViewModel.h new file mode 100644 index 0000000000..e376cf2945 --- /dev/null +++ b/Watch/Extension/TGNeoFileMessageViewModel.h @@ -0,0 +1,5 @@ +#import "TGNeoBubbleMessageViewModel.h" + +@interface TGNeoFileMessageViewModel : TGNeoBubbleMessageViewModel + +@end diff --git a/Watch/Extension/TGNeoFileMessageViewModel.m b/Watch/Extension/TGNeoFileMessageViewModel.m new file mode 100644 index 0000000000..8da4a3f15e --- /dev/null +++ b/Watch/Extension/TGNeoFileMessageViewModel.m @@ -0,0 +1,72 @@ +#import "TGNeoFileMessageViewModel.h" +#import "TGNeoImageViewModel.h" +#import "TGBridgeMessage.h" + +#import "TGStringUtils.h" + +@interface TGNeoFileMessageViewModel () +{ + TGNeoImageViewModel *_iconModel; + TGNeoLabelViewModel *_nameModel; + TGNeoLabelViewModel *_sizeModel; +} +@end + +@implementation TGNeoFileMessageViewModel + +- (instancetype)initWithMessage:(TGBridgeMessage *)message type:(TGNeoMessageType)type users:(NSDictionary *)users context:(TGBridgeContext *)context +{ + self = [super initWithMessage:message type:type users:users context:context]; + if (self != nil) + { + TGBridgeDocumentMediaAttachment *documentAttachment = nil; + for (TGBridgeMediaAttachment *attachment in message.media) + { + if ([attachment isKindOfClass:[TGBridgeDocumentMediaAttachment class]]) + { + documentAttachment = (TGBridgeDocumentMediaAttachment *)attachment; + break; + } + } + + _iconModel = [[TGNeoImageViewModel alloc] initWithImage:[UIImage imageNamed:@"File"] tintColor:[self accentColorForMessage:message type:type]]; + [self addSubmodel:_iconModel]; + + _nameModel = [[TGNeoLabelViewModel alloc] initWithText:documentAttachment.fileName font:[UIFont systemFontOfSize:12 weight:UIFontWeightMedium] color:[self normalColorForMessage:message type:type] attributes:nil]; + _nameModel.multiline = false; + [self addSubmodel:_nameModel]; + + _sizeModel = [[TGNeoLabelViewModel alloc] initWithText:[TGStringUtils stringForFileSize:documentAttachment.fileSize precision:2] font:[UIFont systemFontOfSize:12] color:[self subtitleColorForMessage:message type:type] attributes:nil]; + _sizeModel.multiline = false; + [self addSubmodel:_sizeModel]; + } + return self; +} + +- (CGSize)layoutWithContainerSize:(CGSize)containerSize +{ + CGSize contentContainerSize = [self contentContainerSizeWithContainerSize:containerSize]; + + CGSize headerSize = [self layoutHeaderModelsWithContainerSize:contentContainerSize]; + CGFloat maxContentWidth = headerSize.width; + CGFloat textTopOffset = headerSize.height; + + CGFloat leftOffset = 20 + TGNeoBubbleMessageMetaSpacing; + contentContainerSize = CGSizeMake(containerSize.width - TGNeoBubbleMessageViewModelInsets.left - TGNeoBubbleMessageViewModelInsets.right - leftOffset, FLT_MAX); + + CGSize nameSize = [_nameModel contentSizeWithContainerSize:contentContainerSize]; + CGSize metaSize = [_sizeModel contentSizeWithContainerSize:contentContainerSize]; + maxContentWidth = MAX(maxContentWidth, MAX(nameSize.width, metaSize.width) + leftOffset); + + _iconModel.frame = CGRectMake(TGNeoBubbleMessageViewModelInsets.left - 3, textTopOffset + 1.5f, 26, 26); + _nameModel.frame = CGRectMake(TGNeoBubbleMessageViewModelInsets.left + leftOffset, textTopOffset, nameSize.width, 14); + _sizeModel.frame = CGRectMake(TGNeoBubbleMessageViewModelInsets.left + leftOffset, CGRectGetMaxY(_nameModel.frame), metaSize.width, 14); + + CGSize contentSize = CGSizeMake(TGNeoBubbleMessageViewModelInsets.left + TGNeoBubbleMessageViewModelInsets.right + maxContentWidth, CGRectGetMaxY(_sizeModel.frame) + TGNeoBubbleMessageViewModelInsets.bottom); + + [super layoutWithContainerSize:contentSize]; + + return contentSize; +} + +@end diff --git a/Watch/Extension/TGNeoForwardHeaderViewModel.h b/Watch/Extension/TGNeoForwardHeaderViewModel.h new file mode 100644 index 0000000000..2539c314e6 --- /dev/null +++ b/Watch/Extension/TGNeoForwardHeaderViewModel.h @@ -0,0 +1,14 @@ +#import "TGNeoViewModel.h" + +@class TGBridgeForwardedMessageMediaAttachment; +@class TGBridgeUser; +@class TGBridgeChat; + +@interface TGNeoForwardHeaderViewModel : TGNeoViewModel + +- (instancetype)initWithForwardAttachment:(TGBridgeForwardedMessageMediaAttachment *)attachment user:(TGBridgeUser *)user outgoing:(bool)outgoing; +- (instancetype)initWithForwardAttachment:(TGBridgeForwardedMessageMediaAttachment *)attachment chat:(TGBridgeChat *)chat outgoing:(bool)outgoing; + +@end + +extern const CGFloat TGNeoForwardHeaderHeight; diff --git a/Watch/Extension/TGNeoForwardHeaderViewModel.m b/Watch/Extension/TGNeoForwardHeaderViewModel.m new file mode 100644 index 0000000000..9d0f03167e --- /dev/null +++ b/Watch/Extension/TGNeoForwardHeaderViewModel.m @@ -0,0 +1,87 @@ +#import "TGNeoForwardHeaderViewModel.h" +#import "TGWatchCommon.h" +#import "TGNeoLabelViewModel.h" + +#import "TGBridgeUser.h" +#import "TGBridgeChat.h" + +const CGFloat TGNeoForwardHeaderHeight = 29; + +@interface TGNeoForwardHeaderViewModel () +{ + TGNeoLabelViewModel *_forwardedModel; + TGNeoLabelViewModel *_authorNameModel; +} +@end + +@implementation TGNeoForwardHeaderViewModel + +- (instancetype)initWithOutgoing:(bool)outgoing +{ + self = [super init]; + if (self != nil) + { + _forwardedModel = [[TGNeoLabelViewModel alloc] initWithText:TGLocalized(@"Watch.Message.ForwardedFrom") font:[UIFont systemFontOfSize:12] color:[self subtitleColorForOutgoing:outgoing] attributes:nil]; + _forwardedModel.multiline = false; + [self addSubmodel:_forwardedModel]; + } + return self; +} + +- (instancetype)initWithForwardAttachment:(TGBridgeForwardedMessageMediaAttachment *)attachment user:(TGBridgeUser *)user outgoing:(bool)outgoing +{ + self = [self initWithOutgoing:outgoing]; + if (self != nil) + { + _authorNameModel = [[TGNeoLabelViewModel alloc] initWithText:[user displayName] font:[UIFont systemFontOfSize:12 weight:UIFontWeightMedium] color:[self normalColorForOutgoing:outgoing] attributes:nil]; + _authorNameModel.multiline = false; + [self addSubmodel:_authorNameModel]; + } + return self; +} + +- (instancetype)initWithForwardAttachment:(TGBridgeForwardedMessageMediaAttachment *)attachment chat:(TGBridgeChat *)chat outgoing:(bool)outgoing +{ + self = [self initWithOutgoing:outgoing]; + if (self != nil) + { + _authorNameModel = [[TGNeoLabelViewModel alloc] initWithText:chat.groupTitle font:[UIFont systemFontOfSize:12 weight:UIFontWeightMedium] color:[self normalColorForOutgoing:outgoing] attributes:nil]; + _authorNameModel.multiline = false; + [self addSubmodel:_authorNameModel]; + } + return self; +} + +- (UIColor *)normalColorForOutgoing:(bool)outgoing +{ + if (outgoing) + return [UIColor whiteColor]; + else + return [UIColor hexColor:0x1f97f8]; +} + +- (UIColor *)subtitleColorForOutgoing:(bool)outgoing +{ + if (outgoing) + return [UIColor whiteColor]; + else + return [UIColor blackColor]; +} + +- (CGSize)contentSizeWithContainerSize:(CGSize)containerSize +{ + CGSize forwardedSize = [_forwardedModel contentSizeWithContainerSize:containerSize]; + CGSize nameSize = [_authorNameModel contentSizeWithContainerSize:containerSize]; + + return CGSizeMake(MIN(MAX(forwardedSize.width, nameSize.width), containerSize.width), TGNeoForwardHeaderHeight); +} + +- (void)setFrame:(CGRect)frame +{ + [super setFrame:frame]; + + _forwardedModel.frame = CGRectMake(0, 0, frame.size.width, 20); + _authorNameModel.frame = CGRectMake(0, 14.5f, frame.size.width, 20); +} + +@end diff --git a/Watch/Extension/TGNeoImageViewModel.h b/Watch/Extension/TGNeoImageViewModel.h new file mode 100644 index 0000000000..62242f01a0 --- /dev/null +++ b/Watch/Extension/TGNeoImageViewModel.h @@ -0,0 +1,12 @@ +#import "TGNeoViewModel.h" +#import + +@interface TGNeoImageViewModel : TGNeoViewModel + +@property (nonatomic, strong) UIImage *image; +@property (nonatomic, strong) UIColor *tintColor; + +- (instancetype)initWithImage:(UIImage *)image; +- (instancetype)initWithImage:(UIImage *)image tintColor:(UIColor *)tintColor; + +@end diff --git a/Watch/Extension/TGNeoImageViewModel.m b/Watch/Extension/TGNeoImageViewModel.m new file mode 100644 index 0000000000..f8d2888354 --- /dev/null +++ b/Watch/Extension/TGNeoImageViewModel.m @@ -0,0 +1,42 @@ +#import "TGNeoImageViewModel.h" + +@implementation TGNeoImageViewModel + +- (instancetype)initWithImage:(UIImage *)image +{ + self = [super init]; + if (self != nil) + { + _image = image; + } + return self; +} + +- (instancetype)initWithImage:(UIImage *)image tintColor:(UIColor *)tintColor +{ + self = [super init]; + if (self != nil) + { + _image = [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; + _tintColor = tintColor; + } + return self; +} + +- (void)drawInContext:(CGContextRef)context +{ + UIGraphicsPushContext(context); + if (_tintColor != nil) + { + CGContextSaveGState(context); + CGContextSetFillColorWithColor(context, _tintColor.CGColor); + } + + [self.image drawInRect:CGRectMake((self.frame.size.width - self.image.size.width) / 2, (self.frame.size.height - self.image.size.height) / 2, self.image.size.width, self.image.size.height)]; + + if (_tintColor) + CGContextRestoreGState(context); + UIGraphicsPopContext(); +} + +@end diff --git a/Watch/Extension/TGNeoLabelViewModel.h b/Watch/Extension/TGNeoLabelViewModel.h new file mode 100644 index 0000000000..e92a902ac7 --- /dev/null +++ b/Watch/Extension/TGNeoLabelViewModel.h @@ -0,0 +1,15 @@ +#import "TGNeoViewModel.h" +#import + +@interface TGNeoLabelViewModel : TGNeoViewModel + +@property (nonatomic, strong) NSString *text; +@property (nonatomic, strong) NSAttributedString *attributedText; +@property (nonatomic, strong) NSDictionary *attributes; +@property (nonatomic, assign) CGFloat maxWidth; +@property (nonatomic, assign) bool multiline; + +- (instancetype)initWithText:(NSString *)text font:(UIFont *)font color:(UIColor *)color attributes:(NSDictionary *)attributes; +- (instancetype)initWithAttributedText:(NSAttributedString *)attributedText; + +@end diff --git a/Watch/Extension/TGNeoLabelViewModel.m b/Watch/Extension/TGNeoLabelViewModel.m new file mode 100644 index 0000000000..dd7fdb2adc --- /dev/null +++ b/Watch/Extension/TGNeoLabelViewModel.m @@ -0,0 +1,72 @@ +#import "TGNeoLabelViewModel.h" + +@implementation TGNeoLabelViewModel + +- (instancetype)initWithText:(NSString *)text font:(UIFont *)font color:(UIColor *)color attributes:(NSDictionary *)attributes +{ + self = [super init]; + if (self != nil) + { + _text = text; + _multiline = true; + + NSMutableDictionary *finalAttributes = [NSMutableDictionary dictionaryWithDictionary:attributes]; + finalAttributes[NSFontAttributeName] = font; + finalAttributes[NSForegroundColorAttributeName] = color; + _attributes = finalAttributes; + } + return self; +} + +- (instancetype)initWithAttributedText:(NSAttributedString *)attributedText +{ + self = [super init]; + if (self != nil) + { + _attributedText = attributedText; + } + return self; +} + +- (CGSize)contentSizeWithContainerSize:(CGSize)containerSize +{ + NSAttributedString *string = nil; + + if (_attributedText != nil) + string = _attributedText; + else if (self.text.length > 0) + string = [[NSAttributedString alloc] initWithString:self.text attributes:_attributes]; + else + string = [[NSAttributedString alloc] initWithString:@" "]; + + CGSize contentSize = [string boundingRectWithSize:containerSize options:[self _stringDrawingOptionsForMetrics:true] context:nil].size; + contentSize.width = ceilf(contentSize.width); + contentSize.height = ceilf(contentSize.height); + + return contentSize; +} + +- (void)drawInContext:(CGContextRef)context +{ + UIGraphicsPushContext(context); + NSStringDrawingOptions options = [self _stringDrawingOptionsForMetrics:false]; + if (self.attributedText.length > 0) + [self.attributedText drawWithRect:self.bounds options:options context:nil]; + else if (self.text.length > 0) + [self.text drawWithRect:self.bounds options:options attributes:self.attributes context:nil]; + UIGraphicsPopContext(); +} + +- (NSStringDrawingOptions)_stringDrawingOptionsForMetrics:(bool)forMetrics +{ + NSStringDrawingOptions options = kNilOptions; + if (self.multiline || !forMetrics) + options |= NSStringDrawingUsesLineFragmentOrigin; + + if (!self.multiline) + options |= NSStringDrawingTruncatesLastVisibleLine; + + return options; +} + +@end diff --git a/Watch/Extension/TGNeoMediaMessageViewModel.h b/Watch/Extension/TGNeoMediaMessageViewModel.h new file mode 100644 index 0000000000..9a025eaf90 --- /dev/null +++ b/Watch/Extension/TGNeoMediaMessageViewModel.h @@ -0,0 +1,5 @@ +#import "TGNeoBubbleMessageViewModel.h" + +@interface TGNeoMediaMessageViewModel : TGNeoBubbleMessageViewModel + +@end diff --git a/Watch/Extension/TGNeoMediaMessageViewModel.m b/Watch/Extension/TGNeoMediaMessageViewModel.m new file mode 100644 index 0000000000..17a048a7c3 --- /dev/null +++ b/Watch/Extension/TGNeoMediaMessageViewModel.m @@ -0,0 +1,194 @@ +#import "TGNeoMediaMessageViewModel.h" +#import "TGWatchCommon.h" +#import "TGBridgeMessage.h" + +#import "TGGeometry.h" + +#import "TGMessageViewModel.h" + +const UIEdgeInsets TGNeoMediaMessageViewModelInsets = { 1.5, 1.5, 5.0, 1.5 }; +const CGFloat TGNeoMediaCaptionSpacing = 3.0f; + +@interface TGNeoMediaMessageViewModel () +{ + TGNeoLabelViewModel *_textModel; + + int64_t _peerId; + int32_t _messageId; + TGBridgeImageMediaAttachment *_imageAttachment; + TGBridgeVideoMediaAttachment *_videoAttachment; + TGBridgeLocationMediaAttachment *_locationAttachment; +} +@end + +@implementation TGNeoMediaMessageViewModel + +- (instancetype)initWithMessage:(TGBridgeMessage *)message type:(TGNeoMessageType)type users:(NSDictionary *)users context:(TGBridgeContext *)context +{ + self = [super initWithMessage:message type:type users:users context:context]; + if (self != nil) + { + _peerId = message.cid; + _messageId = message.identifier; + + bool hasHeader = (self.forwardHeaderModel != nil || self.replyHeaderModel != nil); + for (TGBridgeMediaAttachment *attachment in message.media) + { + if ([attachment isKindOfClass:[TGBridgeImageMediaAttachment class]]) + { + _imageAttachment = (TGBridgeImageMediaAttachment *)attachment; + } + else if ([attachment isKindOfClass:[TGBridgeVideoMediaAttachment class]]) + { + _videoAttachment = (TGBridgeVideoMediaAttachment *)attachment; + } + else if ([attachment isKindOfClass:[TGBridgeLocationMediaAttachment class]]) + { + _locationAttachment = (TGBridgeLocationMediaAttachment *)attachment; + } + } + + if (message.text.length > 0) + { + _textModel = [[TGNeoLabelViewModel alloc] initWithText:message.text font:[UIFont systemFontOfSize:[TGNeoBubbleMessageViewModel bodyTextFontSize]] color:[self normalColorForMessage:message type:type] attributes:nil]; + [self addSubmodel:_textModel]; + } + + self.showBubble = (_textModel != nil) || hasHeader; + } + return self; +} + +- (CGSize)contentContainerSizeWithImageSize:(CGSize)imageSize +{ + return CGSizeMake(imageSize.width + TGNeoMediaMessageViewModelInsets.left + TGNeoMediaMessageViewModelInsets.right - TGNeoBubbleMessageViewModelInsets.left - TGNeoBubbleMessageViewModelInsets.right, FLT_MAX); +} + +- (CGSize)layoutWithContainerSize:(CGSize)containerSize +{ + CGSize imageSize; + if (_imageAttachment != nil) + { + imageSize = [self imageSizeForAttachment:_imageAttachment containerSize:containerSize]; + } + else if (_videoAttachment != nil) + { + imageSize = [self imageSizeForAttachment:_videoAttachment containerSize:containerSize]; + } + else + { + switch (TGWatchScreenType()) + { + case TGScreenType42mm: + imageSize = CGSizeMake(142, 92); + break; + + default: + imageSize = CGSizeMake(125, 92); + break; + } + } + + CGSize contentContainerSize = self.showBubble ? [self contentContainerSizeWithImageSize:imageSize] : containerSize; + + CGSize headerSize = [self layoutHeaderModelsWithContainerSize:contentContainerSize]; + CGFloat textTopOffset = headerSize.height; + CGSize contentSize = CGSizeZero; + + if (self.forwardHeaderModel == nil && self.replyHeaderModel == nil && self.authorNameModel == nil) + { + textTopOffset = TGNeoMediaMessageViewModelInsets.top; + } + else if (self.showBubble) + { + textTopOffset += TGNeoBubbleHeaderSpacing; + } + else if (!self.showBubble) + { + if (self.authorNameModel != nil) + textTopOffset += TGNeoBubbleHeaderSpacing; + else + textTopOffset = 0; + } + + UIEdgeInsets contentInsets = self.showBubble ? TGNeoMediaMessageViewModelInsets : UIEdgeInsetsZero; + + UIEdgeInsets inset = UIEdgeInsetsMake(textTopOffset, contentInsets.left, 0, 0); + if (_imageAttachment != nil || _videoAttachment != nil) + { + TGBridgeMediaAttachment *attachment = _imageAttachment ?: _videoAttachment; + + [self addAdditionalLayout:@ + { + TGNeoContentInset: [NSValue valueWithUIEdgeInsets:inset], + TGNeoMessageMediaImage: @ + { + TGNeoMessageMediaPeerId: @(_peerId), + TGNeoMessageMediaMessageId: @(_messageId), + TGNeoMessageMediaImageSpinner: @true, + TGNeoMessageMediaPlayButton: @(_videoAttachment != nil), + TGNeoMessageMediaImageAttachment: attachment, + TGNeoMessageMediaSize: [NSValue valueWithCGSize:imageSize] + } + } withKey:TGNeoMessageMediaGroup]; + } + else if (_locationAttachment != nil) + { + [self addAdditionalLayout:@ + { + TGNeoContentInset: [NSValue valueWithUIEdgeInsets:inset], + TGNeoMessageMediaMap: @ + { + TGNeoMessageMediaMapCoordinate: [NSValue valueWithMKCoordinate:CLLocationCoordinate2DMake(_locationAttachment.latitude, _locationAttachment.longitude)], + TGNeoMessageMediaSize: [NSValue valueWithCGSize:imageSize] + } + } withKey:TGNeoMessageMediaGroup]; + } + + contentSize.width = imageSize.width + contentInsets.left + contentInsets.right; + + if (_textModel != nil) + { + CGSize textSize = [_textModel contentSizeWithContainerSize:contentContainerSize]; + _textModel.frame = CGRectMake(TGNeoBubbleMessageViewModelInsets.left, textTopOffset + imageSize.height + TGNeoMediaCaptionSpacing, textSize.width, textSize.height); + + contentSize.height = CGRectGetMaxY(_textModel.frame) + TGNeoBubbleMessageViewModelInsets.bottom; + } + else + { + contentSize.height = textTopOffset + imageSize.height + contentInsets.bottom; + } + + if (!self.showBubble) + { + //contentSize.width = containerSize.width; + contentSize.height += 3.5f; + } + + [super layoutWithContainerSize:contentSize]; + + return contentSize; +} + +- (CGSize)imageSizeForAttachment:(TGBridgeMediaAttachment *)attachment containerSize:(CGSize)containerSize +{ + CGSize targetImageSize = CGSizeZero; + + if ([attachment isKindOfClass:[TGBridgeImageMediaAttachment class]]) + targetImageSize = ((TGBridgeImageMediaAttachment *)attachment).dimensions; + else if ([attachment isKindOfClass:[TGBridgeVideoMediaAttachment class]]) + targetImageSize = ((TGBridgeVideoMediaAttachment *)attachment).dimensions; + + CGSize screenSize = containerSize; + + CGSize mediaGroupSize = CGSizeZero; + CGSize maxSize = CGSizeMake(screenSize.width - 15, screenSize.width); + CGSize minSize = CGSizeMake(screenSize.width / 1.25f, screenSize.width / 2); + [TGMessageViewModel imageBubbleSizeForImageSize:targetImageSize minSize:minSize maxSize:maxSize thumbnailSize:&mediaGroupSize renderSize:NULL]; + + mediaGroupSize = CGSizeMake(ceilf(mediaGroupSize.width), ceilf(mediaGroupSize.height)); + + return mediaGroupSize; +} + +@end diff --git a/Watch/Extension/TGNeoMessageViewModel.h b/Watch/Extension/TGNeoMessageViewModel.h new file mode 100644 index 0000000000..2921a064de --- /dev/null +++ b/Watch/Extension/TGNeoMessageViewModel.h @@ -0,0 +1,59 @@ +#import "TGNeoRenderableViewModel.h" + +@class TGBridgeMessage; +@class TGBridgeUser; +@class TGBridgeContext; + +typedef enum +{ + TGNeoMessageTypeGeneric, + TGNeoMessageTypeGroup, + TGNeoMessageTypeChannel +} TGNeoMessageType; + +@interface TGNeoMessageViewModel : TGNeoRenderableViewModel + +@property (nonatomic, readonly) int32_t identifier; +@property (nonatomic, readonly) TGNeoMessageType type; +@property (nonatomic, readonly) NSDictionary *additionalLayout; +@property (nonatomic, assign) bool showBubble; + +- (instancetype)initWithMessage:(TGBridgeMessage *)message type:(TGNeoMessageType)type users:(NSDictionary *)users context:(TGBridgeContext *)context; + +- (void)addAdditionalLayout:(NSDictionary *)layout withKey:(NSString *)key; + ++ (TGNeoMessageViewModel *)viewModelForMessage:(TGBridgeMessage *)message type:(TGNeoMessageType)type context:(TGBridgeContext *)context additionalPeers:(NSDictionary *)additionalPeers; + +@end + +extern NSString *const TGNeoContentInset; + +extern NSString *const TGNeoMessageHeaderGroup; +extern NSString *const TGNeoMessageReplyImageGroup; +extern NSString *const TGNeoMessageReplyMediaAttachment; + +extern NSString *const TGNeoMessageMediaGroup; +extern NSString *const TGNeoMessageMediaPeerId; +extern NSString *const TGNeoMessageMediaMessageId; +extern NSString *const TGNeoMessageMediaImage; +extern NSString *const TGNeoMessageMediaImageAttachment; +extern NSString *const TGNeoMessageMediaImageSpinner; +extern NSString *const TGNeoMessageMediaPlayButton; +extern NSString *const TGNeoMessageMediaSize; +extern NSString *const TGNeoMessageMediaMap; +extern NSString *const TGNeoMessageMediaMapSize; +extern NSString *const TGNeoMessageMediaMapCoordinate; + +extern NSString *const TGNeoMessageMetaGroup; +extern NSString *const TGNeoMessageAvatarGroup; +extern NSString *const TGNeoMessageAvatarIdentifier; +extern NSString *const TGNeoMessageAvatarUrl; +extern NSString *const TGNeoMessageAvatarColor; +extern NSString *const TGNeoMessageAvatarInitials; + +extern NSString *const TGNeoMessageAudioButton; +extern NSString *const TGNeoMessageAudioButtonHasBackground; +extern NSString *const TGNeoMessageAudioBackgroundColor; +extern NSString *const TGNeoMessageAudioIcon; +extern NSString *const TGNeoMessageAudioIconTint; +extern NSString *const TGNeoMessageAudioAnimatedIcon; diff --git a/Watch/Extension/TGNeoMessageViewModel.m b/Watch/Extension/TGNeoMessageViewModel.m new file mode 100644 index 0000000000..fa4f83e79c --- /dev/null +++ b/Watch/Extension/TGNeoMessageViewModel.m @@ -0,0 +1,151 @@ +#import "TGNeoMessageViewModel.h" +#import "TGNeoTextMessageViewModel.h" +#import "TGNeoSmiliesMessageViewModel.h" +#import "TGNeoMediaMessageViewModel.h" +#import "TGNeoAudioMessageViewModel.h" +#import "TGNeoFileMessageViewModel.h" +#import "TGNeoContactMessageViewModel.h" +#import "TGNeoVenueMessageViewModel.h" +#import "TGNeoStickerMessageViewModel.h" +#import "TGNeoServiceMessageViewModel.h" +#import "TGNeoUnsupportedMessageViewModel.h" + +#import "TGNeoConversationRowController.h" +#import "TGNeoConversationMediaRowController.h" +#import "TGNeoConversationStaticRowController.h" + +#import "TGStringUtils.h" + +#import "TGBridgePeerIdAdapter.h" +#import "TGBridgeMessage.h" +#import "TGBridgeUserCache.h" + +NSString *const TGNeoContentInset = @"contentInset"; + +NSString *const TGNeoMessageHeaderGroup = @"header"; +NSString *const TGNeoMessageReplyImageGroup = @"replyImage"; +NSString *const TGNeoMessageReplyMediaAttachment = @"attachment"; + +NSString *const TGNeoMessageMediaGroup = @"media"; +NSString *const TGNeoMessageMediaPeerId = @"peerId"; +NSString *const TGNeoMessageMediaMessageId = @"mid"; +NSString *const TGNeoMessageMediaImage = @"image"; +NSString *const TGNeoMessageMediaImageAttachment = @"attachment"; +NSString *const TGNeoMessageMediaImageSpinner = @"spinner"; +NSString *const TGNeoMessageMediaPlayButton = @"button"; +NSString *const TGNeoMessageMediaSize = @"size"; +NSString *const TGNeoMessageMediaMap = @"map"; +NSString *const TGNeoMessageMediaMapSize = @"size"; +NSString *const TGNeoMessageMediaMapCoordinate = @"coordinate"; + +NSString *const TGNeoMessageMetaGroup = @"meta"; +NSString *const TGNeoMessageAvatarGroup = @"avatar"; +NSString *const TGNeoMessageAvatarIdentifier = @"identifier"; +NSString *const TGNeoMessageAvatarUrl = @"url"; +NSString *const TGNeoMessageAvatarColor = @"color"; +NSString *const TGNeoMessageAvatarInitials = @"initials"; + +NSString *const TGNeoMessageAudioButton = @"audio"; +NSString *const TGNeoMessageAudioButtonHasBackground = @"hasBackground"; +NSString *const TGNeoMessageAudioBackgroundColor = @"color"; +NSString *const TGNeoMessageAudioIcon = @"icon"; +NSString *const TGNeoMessageAudioIconTint = @"tint"; +NSString *const TGNeoMessageAudioAnimatedIcon = @"animatedIcon"; + +@implementation TGNeoMessageViewModel + +- (instancetype)initWithMessage:(TGBridgeMessage *)message type:(TGNeoMessageType)type users:(NSDictionary *)users context:(TGBridgeContext *)context +{ + self = [super init]; + if (self != nil) + { + _type = type; + _identifier = message.identifier; + } + return self; +} + +- (void)addAdditionalLayout:(NSDictionary *)layout withKey:(NSString *)key +{ + if (_additionalLayout != nil) + [_additionalLayout.mutableCopy addEntriesFromDictionary:@{ key: layout }]; + else + _additionalLayout = @{ key: layout }; +} + ++ (TGNeoMessageViewModel *)viewModelForMessage:(TGBridgeMessage *)message type:(TGNeoMessageType)type context:(TGBridgeContext *)context additionalPeers:(NSDictionary *)additionalPeers +{ + Class viewModelClass = [TGNeoTextMessageViewModel class]; + + bool hasReplyHeader = false; + bool hasForwardHeader = false; + + for (TGBridgeMediaAttachment *attachment in message.media) + { + if ([attachment isKindOfClass:[TGBridgeReplyMessageMediaAttachment class]]) + hasReplyHeader = true; + else if ([attachment isKindOfClass:[TGBridgeForwardedMessageMediaAttachment class]]) + hasForwardHeader = true; + } + + for (TGBridgeMediaAttachment *attachment in message.media) + { + if ([attachment isKindOfClass:[TGBridgeImageMediaAttachment class]]) + { + viewModelClass = [TGNeoMediaMessageViewModel class]; + } + else if ([attachment isKindOfClass:[TGBridgeVideoMediaAttachment class]]) + { + viewModelClass = [TGNeoMediaMessageViewModel class]; + } + else if ([attachment isKindOfClass:[TGBridgeAudioMediaAttachment class]]) + { + viewModelClass = [TGNeoAudioMessageViewModel class]; + } + else if ([attachment isKindOfClass:[TGBridgeDocumentMediaAttachment class]]) + { + TGBridgeDocumentMediaAttachment *documentAttachment = (TGBridgeDocumentMediaAttachment *)attachment; + if (documentAttachment.isSticker) + viewModelClass = [TGNeoStickerMessageViewModel class]; + else if (documentAttachment.isAudio) + viewModelClass = [TGNeoAudioMessageViewModel class]; + else + viewModelClass = [TGNeoFileMessageViewModel class];; + } + else if ([attachment isKindOfClass:[TGBridgeLocationMediaAttachment class]]) + { + TGBridgeLocationMediaAttachment *locationAttachment = (TGBridgeLocationMediaAttachment *)attachment; + if (locationAttachment.venue != nil) + viewModelClass = [TGNeoVenueMessageViewModel class]; + else + viewModelClass = [TGNeoMediaMessageViewModel class]; + } + else if ([attachment isKindOfClass:[TGBridgeContactMediaAttachment class]]) + { + viewModelClass = [TGNeoContactMessageViewModel class]; + } + else if ([attachment isKindOfClass:[TGBridgeActionMediaAttachment class]]) + { + viewModelClass = [TGNeoServiceMessageViewModel class]; + } + else if ([attachment isKindOfClass:[TGBridgeUnsupportedMediaAttachment class]]) + { + viewModelClass = [TGNeoUnsupportedMessageViewModel class]; + } + } + + if (viewModelClass == [TGNeoTextMessageViewModel class] && !hasForwardHeader && !hasReplyHeader && message.text.length > 0) + { + NSUInteger length = 0; + bool emojiOnly = [TGStringUtils stringContainsEmojiOnly:message.text length:&length]; + if (emojiOnly && length <= 3) + viewModelClass = [TGNeoSmiliesMessageViewModel class]; + } + + NSMutableDictionary *users = [NSMutableDictionary dictionaryWithDictionary:additionalPeers]; + [users addEntriesFromDictionary:[[TGBridgeUserCache instance] usersWithIndexSet:[message involvedUserIds]]]; + + return [[viewModelClass alloc] initWithMessage:message type:type users:users context:context]; +} + +@end diff --git a/Watch/Extension/TGNeoRenderableViewModel.h b/Watch/Extension/TGNeoRenderableViewModel.h new file mode 100644 index 0000000000..171149b21d --- /dev/null +++ b/Watch/Extension/TGNeoRenderableViewModel.h @@ -0,0 +1,12 @@ +#import "TGNeoViewModel.h" +#import + +@interface TGNeoRenderableViewModel : TGNeoViewModel + +@property (nonatomic, assign) CGSize contentSize; +@property (nonatomic, strong) UIImage *cachedImage; + +- (CGSize)layoutWithContainerSize:(CGSize)containerSize; ++ (SSignal *)renderSignalForViewModel:(TGNeoRenderableViewModel *)viewModel; + +@end diff --git a/Watch/Extension/TGNeoRenderableViewModel.m b/Watch/Extension/TGNeoRenderableViewModel.m new file mode 100644 index 0000000000..5a83b58f09 --- /dev/null +++ b/Watch/Extension/TGNeoRenderableViewModel.m @@ -0,0 +1,53 @@ +#import "TGNeoRenderableViewModel.h" + +@implementation TGNeoRenderableViewModel + +- (CGSize)layoutWithContainerSize:(CGSize)containerSize +{ + return CGSizeZero; +} + ++ (SSignal *)renderSignalForViewModel:(TGNeoRenderableViewModel *)viewModel +{ + return [[SSignal alloc] initWithGenerator:^id(SSubscriber *subscriber) + { + CGFloat scale = 2.0f; + CGSize size = viewModel.contentSize; + CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); + + CGContextRef context = CGBitmapContextCreate(NULL, + size.width * scale, size.height * scale, + 8, size.width * scale * 4, colorSpace, + kCGImageAlphaPremultipliedFirst); + CFRelease(colorSpace); + + if (context == nil) + { + [subscriber putError:nil]; + return nil; + } + + CGContextScaleCTM(context, scale, -scale); + CGContextTranslateCTM(context, 0, -size.height); + + [viewModel drawInContext:context]; + + CGImageRef imgRef = CGBitmapContextCreateImage(context); + if (imgRef == nil) + { + CFRelease(context); + return nil; + } + + UIImage *image = [UIImage imageWithCGImage:imgRef]; + CFRelease(imgRef); + CFRelease(context); + + [subscriber putNext:image]; + [subscriber putCompletion]; + + return nil; + }]; +} + +@end diff --git a/Watch/Extension/TGNeoReplyHeaderViewModel.h b/Watch/Extension/TGNeoReplyHeaderViewModel.h new file mode 100644 index 0000000000..d2f327cb98 --- /dev/null +++ b/Watch/Extension/TGNeoReplyHeaderViewModel.h @@ -0,0 +1,18 @@ +#import "TGNeoViewModel.h" + +@class TGBridgeReplyMessageMediaAttachment; +@class TGBridgeMediaAttachment; +@class TGBridgeMessage; + +@interface TGNeoReplyHeaderViewModel : TGNeoViewModel + +@property (nonatomic, readonly) TGBridgeMediaAttachment *mediaAttachment; +@property (nonatomic, readonly) TGBridgeMessage *replyMessage; + +- (instancetype)initWithReplyAttachment:(TGBridgeReplyMessageMediaAttachment *)attachment users:(NSDictionary *)users outgoing:(bool)outgoing; + +@end + +extern const CGFloat TGNeoReplyHeaderHeight; +extern const CGFloat TGNeoReplyHeaderLineWidth; +extern const CGFloat TGNeoReplyHeaderSpacing; diff --git a/Watch/Extension/TGNeoReplyHeaderViewModel.m b/Watch/Extension/TGNeoReplyHeaderViewModel.m new file mode 100644 index 0000000000..8eb5b98bbf --- /dev/null +++ b/Watch/Extension/TGNeoReplyHeaderViewModel.m @@ -0,0 +1,132 @@ +#import "TGNeoReplyHeaderViewModel.h" +#import "TGWatchCommon.h" +#import "TGNeoLabelViewModel.h" +#import "TGNeoAttachmentViewModel.h" + +#import "TGBridgeMessage.h" +#import "TGBridgeChat.h" +#import "TGBridgeUser.h" +#import "TGBridgeReplyMessageMediaAttachment.h" +#import "TGBridgeVideoMediaAttachment.h" + +const CGFloat TGNeoReplyHeaderHeight = 29.0f; +const CGFloat TGNeoReplyHeaderLineWidth = 2.0f; +const CGFloat TGNeoReplyHeaderSpacing = 4.0f; +const CGFloat TGNeoReplyHeaderImageWidth = 26.0f; + +@interface TGNeoReplyHeaderViewModel () +{ + TGNeoLabelViewModel *_authorNameModel; + TGNeoLabelViewModel *_textNameModel; + TGNeoAttachmentViewModel *_attachmentModel; + + bool _outgoing; +} +@end + +@implementation TGNeoReplyHeaderViewModel + +- (instancetype)initWithReplyAttachment:(TGBridgeReplyMessageMediaAttachment *)attachment users:(NSDictionary *)users outgoing:(bool)outgoing +{ + self = [super init]; + if (self != nil) + { + _outgoing = outgoing; + _replyMessage = attachment.message; + + NSString *name = nil; + id peer = users[@(attachment.message.fromUid)]; + if ([peer isKindOfClass:[TGBridgeUser class]]) + name = [peer displayName]; + else if ([peer isKindOfClass:[TGBridgeChat class]]) + name = [peer groupTitle]; + + _authorNameModel = [[TGNeoLabelViewModel alloc] initWithText:name font:[UIFont systemFontOfSize:12 weight:UIFontWeightMedium] color:[self normalColorForOutgoing:outgoing] attributes:nil]; + _authorNameModel.multiline = false; + [self addSubmodel:_authorNameModel]; + + _attachmentModel = [[TGNeoAttachmentViewModel alloc] initWithAttachments:attachment.message.media author:nil forChannel:false users:nil font:[UIFont systemFontOfSize:12] subTitleColor:[self subtitleColorForOutgoing:outgoing] normalColor:[self textColorForOutgoing:outgoing] compact:true caption:attachment.message.text]; + + for (TGBridgeMediaAttachment *media in attachment.message.media) + { + if ([media isKindOfClass:[TGBridgeImageMediaAttachment class]] + || [media isKindOfClass:[TGBridgeVideoMediaAttachment class]]) + { + _mediaAttachment = media; + } + } + + if (_attachmentModel != nil) + { + [self addSubmodel:_attachmentModel]; + } + else + { + _textNameModel = [[TGNeoLabelViewModel alloc] initWithText:attachment.message.text font:[UIFont systemFontOfSize:12] color:[self textColorForOutgoing:outgoing] attributes:nil]; + _textNameModel.multiline = false; + [self addSubmodel:_textNameModel]; + } + } + return self; +} + +- (UIColor *)normalColorForOutgoing:(bool)outgoing +{ + if (outgoing) + return [UIColor whiteColor]; + else + return [UIColor hexColor:0x1f97f8]; +} + +- (UIColor *)textColorForOutgoing:(bool)outgoing +{ + if (outgoing) + return [UIColor whiteColor]; + else + return [UIColor blackColor]; +} + +- (UIColor *)subtitleColorForOutgoing:(bool)outgoing +{ + if (outgoing) + return [UIColor hexColor:0xbeddf6]; + else + return [UIColor hexColor:0x7e7e81]; +} + +- (CGSize)contentSizeWithContainerSize:(CGSize)containerSize +{ + CGSize nameSize = [_authorNameModel contentSizeWithContainerSize:containerSize]; + CGSize textSize = [_textNameModel contentSizeWithContainerSize:containerSize]; + + CGFloat maxWidth = MAX(textSize.width, nameSize.width); + maxWidth += TGNeoReplyHeaderLineWidth + TGNeoReplyHeaderSpacing; + + if (_mediaAttachment != nil) + maxWidth += TGNeoReplyHeaderImageWidth + TGNeoReplyHeaderSpacing; + + return CGSizeMake(MIN(maxWidth, containerSize.width), TGNeoReplyHeaderHeight); +} + +- (void)setFrame:(CGRect)frame +{ + [super setFrame:frame]; + + CGFloat xOffset = TGNeoReplyHeaderLineWidth + TGNeoReplyHeaderSpacing; + if (_mediaAttachment != nil) + xOffset += TGNeoReplyHeaderImageWidth + TGNeoReplyHeaderSpacing; + + _authorNameModel.frame = CGRectMake(xOffset, 0, frame.size.width - xOffset, 20); + _textNameModel.frame = CGRectMake(xOffset, 14.5f, frame.size.width - xOffset, 20); + _attachmentModel.frame = CGRectMake(xOffset, 14.5f, frame.size.width - xOffset, 20); +} + +- (void)drawInContext:(CGContextRef)context +{ + [super drawInContext:context]; + + CGContextSetFillColorWithColor(context, [self normalColorForOutgoing:_outgoing].CGColor); + CGContextFillRect(context, CGRectMake(0, 0, TGNeoReplyHeaderLineWidth, self.frame.size.height)); +} + +@end diff --git a/Watch/Extension/TGNeoRowController.h b/Watch/Extension/TGNeoRowController.h new file mode 100644 index 0000000000..550188d428 --- /dev/null +++ b/Watch/Extension/TGNeoRowController.h @@ -0,0 +1,49 @@ +#import "WKInterfaceTable+TGDataDrivenTable.h" +#import "TGNeoMessageViewModel.h" + +@class TGBridgeMessage; +@class TGNeoMessageViewModel; +@class TGBridgeContext; + +@interface TGNeoRowController : TGTableRowController + +@property (nonatomic, copy) bool (^shouldRenderContent)(void); +@property (nonatomic, copy) bool (^shouldRenderOnMainThread)(void); +@property (nonatomic, copy) void (^animate)(void (^)(void)); + +@property (nonatomic, strong) NSDictionary *additionalPeers; + +@property (nonatomic, weak) IBOutlet WKInterfaceGroup *bubbleGroup; +@property (nonatomic, weak) IBOutlet WKInterfaceGroup *contentGroup; + +@property (nonatomic, weak) IBOutlet WKInterfaceGroup *headerWrapperGroup; +@property (nonatomic, weak) IBOutlet WKInterfaceGroup *replyImageGroup; + +@property (nonatomic, weak) IBOutlet WKInterfaceGroup *mediaWrapperGroup; +@property (nonatomic, weak) IBOutlet WKInterfaceGroup *imageGroup; +@property (nonatomic, weak) IBOutlet WKInterfaceImage *spinnerImage; +@property (nonatomic, weak) IBOutlet WKInterfaceButton *videoButton; +@property (nonatomic, weak) IBOutlet WKInterfaceMap *map; + +@property (nonatomic, weak) IBOutlet WKInterfaceGroup *metaWrapperGroup; +@property (nonatomic, weak) IBOutlet WKInterfaceGroup *avatarGroup; +@property (nonatomic, weak) IBOutlet WKInterfaceLabel *avatarLabel; +@property (nonatomic, weak) IBOutlet WKInterfaceButton *audioButton; +@property (nonatomic, weak) IBOutlet WKInterfaceGroup *audioButtonGroup; +@property (nonatomic, weak) IBOutlet WKInterfaceImage *audioIcon; + +@property (nonatomic, weak) IBOutlet WKInterfaceGroup *statusGroup; +@property (nonatomic, weak) IBOutlet WKInterfaceImage *statusIcon; + +@property (nonatomic, copy) void (^buttonPressed)(void); + +- (void)updateWithMessage:(TGBridgeMessage *)message context:(TGBridgeContext *)context index:(NSInteger)index type:(TGNeoMessageType)type; +- (void)applyAdditionalLayoutForViewModel:(TGNeoMessageViewModel *)viewModel; + +- (void)setProcessingState:(bool)processing; + +- (IBAction)remotePressedAction; + ++ (Class)rowControllerClassForMessage:(TGBridgeMessage *)message; + +@end diff --git a/Watch/Extension/TGNeoRowController.m b/Watch/Extension/TGNeoRowController.m new file mode 100644 index 0000000000..0fcc0433f9 --- /dev/null +++ b/Watch/Extension/TGNeoRowController.m @@ -0,0 +1,467 @@ +#import "TGNeoRowController.h" +#import "TGWatchCommon.h" +#import "TGNeoConversationRowController.h" +#import "TGNeoConversationSimpleRowController.h" +#import "TGNeoConversationMediaRowController.h" +#import "TGNeoConversationStaticRowController.h" + +#import "TGStringUtils.h" +#import "TGLocationUtils.h" + +#import "TGNeoMessageViewModel.h" +#import "TGNeoBubbleMessageViewModel.h" +#import "TGNeoStickerMessageViewModel.h" + +#import "TGBridgeMessage.h" + +#import "WKInterfaceGroup+Signals.h" +#import "TGBridgeMediaSignals.h" + +@interface TGNeoRowController () +{ + TGNeoMessageViewModel *_viewModel; + SMetaDisposable *_renderDisposable; + + bool _pendingRendering; + + bool _processing; + NSString *_normalIconName; + NSString *_processingIconName; +} +@end + +@implementation TGNeoRowController + +- (instancetype)init +{ + self = [super init]; + if (self != nil) + { + _renderDisposable = [[SMetaDisposable alloc] init]; + } + return self; +} + +- (void)dealloc +{ + [_renderDisposable dispose]; +} + ++ (CGSize)containerSizeForMessage:(TGBridgeMessage *)message +{ + if (message.outgoing) + { + static dispatch_once_t onceToken; + static CGSize containerSize; + dispatch_once(&onceToken, ^ + { + CGSize screenSize = [[WKInterfaceDevice currentDevice] screenBounds].size; + containerSize = CGSizeMake(screenSize.width - 5, screenSize.height); + }); + return containerSize; + } + else + { + static dispatch_once_t onceToken; + static CGSize containerSize; + dispatch_once(&onceToken, ^ + { + containerSize = [[WKInterfaceDevice currentDevice] screenBounds].size; + }); + return containerSize; + } +} + +- (void)updateWithMessage:(TGBridgeMessage *)message context:(TGBridgeContext *)context index:(NSInteger)index type:(TGNeoMessageType)type +{ + bool isChannelMessage = (type == TGNeoMessageTypeChannel); + + if (!isChannelMessage) + [self updateStatusWithMessage:message]; + + if ([self renderIfNeeded]) + return; + + if (_viewModel != nil) + return; + + _viewModel = [TGNeoMessageViewModel viewModelForMessage:message type:type context:context additionalPeers:self.additionalPeers]; + + CGSize containerSize = [TGNeoRowController containerSizeForMessage:message]; + CGSize contentSize = [_viewModel layoutWithContainerSize:containerSize]; + + if (_viewModel.showBubble) + { + if (isChannelMessage) + { + [self.bubbleGroup setBackgroundImageNamed:@"ChatBubbleChannel"]; + } + else + { + if (message.outgoing) + [self.bubbleGroup setBackgroundImageNamed:@"ChatBubbleOutgoing"]; + else + [self.bubbleGroup setBackgroundImageNamed:@"ChatBubbleIncoming"]; + } + } + + if (!isChannelMessage && message.outgoing) + { + [self.bubbleGroup setHorizontalAlignment:WKInterfaceObjectHorizontalAlignmentRight]; + + if (!_viewModel.showBubble) + [self.statusGroup setContentInset:UIEdgeInsetsMake(4, 0, 0, 0)]; + } + + self.bubbleGroup.width = contentSize.width; + self.bubbleGroup.height = contentSize.height; + + self.contentGroup.width = contentSize.width; + self.contentGroup.height = contentSize.height; + + [self applyAdditionalLayoutForViewModel:_viewModel]; + + bool shouldRender = true; + if (self.shouldRenderContent != nil) + shouldRender = self.shouldRenderContent(); + + if (shouldRender) + [self _render]; + else + _pendingRendering = true; +} + +- (void)_render +{ + _pendingRendering = false; + + bool onMainThread = true; + SSignal *signal = [TGNeoRenderableViewModel renderSignalForViewModel:_viewModel]; + if (!onMainThread) + signal = [[signal startOn:[SQueue concurrentDefaultQueue]] deliverOn:[SQueue mainQueue]]; + + __weak TGNeoRowController *weakSelf = self; + [_renderDisposable setDisposable:[signal startWithNext:^(UIImage *image) + { + __strong TGNeoRowController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf.contentGroup setBackgroundImage:image]; + }]]; +} + +- (bool)renderIfNeeded +{ + if (!_pendingRendering) + return false; + + [self _render]; + + return true; +} + +- (void)updateStatusWithMessage:(TGBridgeMessage *)message +{ + if (message.outgoing) + { + bool failed = (message.deliveryState == TGBridgeMessageDeliveryStateFailed); + bool unread = (message.deliveryState == TGBridgeMessageDeliveryStatePending || (message.deliveryState == TGBridgeMessageDeliveryStateDelivered && message.unread)); + + bool dotHidden = !failed && !unread; + self.statusGroup.hidden = dotHidden; + + if (!dotHidden) + { + if (failed) + [self.statusIcon setTintColor:[UIColor hexColor:0xff4a5c]]; + else if (unread) + [self.statusIcon setTintColor:[UIColor hexColor:0x2ba2e7]]; + } + } +} + +- (void)applyAdditionalLayoutForViewModel:(TGNeoMessageViewModel *)viewModel +{ + NSDictionary *layout = viewModel.additionalLayout; + + NSDictionary *headerGroupLayout = layout[TGNeoMessageHeaderGroup]; + if (headerGroupLayout != nil) + { + self.headerWrapperGroup.hidden = false; + + NSDictionary *imageLayout = headerGroupLayout[TGNeoMessageReplyImageGroup]; + if (imageLayout != nil) + { + TGBridgeMediaAttachment *attachment = imageLayout[TGNeoMessageReplyMediaAttachment]; + if (attachment != nil) + { + int64_t peerId = [imageLayout[TGNeoMessageMediaPeerId] int64Value]; + int32_t messageId = [imageLayout[TGNeoMessageMediaMessageId] int32Value]; + + CGSize imageSize = CGSizeMake(26, 26); + [self.replyImageGroup setBackgroundImageSignal:[TGBridgeMediaSignals thumbnailWithPeerId:peerId messageId:messageId size:imageSize notification:false] isVisible:self.isVisible]; + } + } + + NSValue *insetValue = headerGroupLayout[TGNeoContentInset]; + if (insetValue != nil) + { + UIEdgeInsets inset = insetValue.UIEdgeInsetsValue; + [self.headerWrapperGroup setContentInset:inset]; + } + } + + NSDictionary *mediaGroupLayout = layout[TGNeoMessageMediaGroup]; + if (mediaGroupLayout != nil) + { + self.mediaWrapperGroup.hidden = false; + + if (![viewModel isKindOfClass:[TGNeoStickerMessageViewModel class]]) + [self.imageGroup setCornerRadius:viewModel.showBubble ? 12 : 13]; + + NSDictionary *imageGroup = mediaGroupLayout[TGNeoMessageMediaImage]; + NSDictionary *mapGroup = mediaGroupLayout[TGNeoMessageMediaMap]; + if (imageGroup != nil) + { + self.imageGroup.hidden = false; + TGBridgeMediaAttachment *attachment = imageGroup[TGNeoMessageMediaImageAttachment]; + int64_t peerId = [imageGroup[TGNeoMessageMediaPeerId] int64Value]; + int32_t messageId = [imageGroup[TGNeoMessageMediaMessageId] int32Value]; + + CGSize size = CGSizeMake(100, 100); + NSValue *imageSizeValue = imageGroup[TGNeoMessageMediaSize]; + if (imageSizeValue != nil) + { + size = imageSizeValue.CGSizeValue; + self.imageGroup.width = size.width; + self.imageGroup.height = size.height; + + if ([attachment isKindOfClass:[TGBridgeVideoMediaAttachment class]] && ((TGBridgeVideoMediaAttachment *)attachment).round) { + self.imageGroup.cornerRadius = size.width / 2.0f; + } + } + + bool hasPlayButton = [imageGroup[TGNeoMessageMediaPlayButton] boolValue]; + bool hasSpinner = [imageGroup[TGNeoMessageMediaImageSpinner] boolValue]; + + if (hasSpinner) + self.spinnerImage.hidden = false; + + __weak TGNeoRowController *weakSelf = self; + if ([attachment isKindOfClass:[TGBridgeImageMediaAttachment class]]) + { + [self.imageGroup setBackgroundImageSignal:[[TGBridgeMediaSignals thumbnailWithPeerId:peerId messageId:messageId size:size notification:false] onNext:^(id next) + { + __strong TGNeoRowController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + strongSelf.spinnerImage.hidden = true; + }] isVisible:self.isVisible]; + } + else if ([attachment isKindOfClass:[TGBridgeVideoMediaAttachment class]]) + { + [self.imageGroup setBackgroundImageSignal:[[TGBridgeMediaSignals thumbnailWithPeerId:peerId messageId:messageId size:size notification:false] onNext:^(id next) + { + __strong TGNeoRowController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + strongSelf.spinnerImage.hidden = true; + if (hasPlayButton) + strongSelf.videoButton.hidden = false; + }] isVisible:self.isVisible]; + } + else if ([attachment isKindOfClass:[TGBridgeDocumentMediaAttachment class]]) + { + TGBridgeDocumentMediaAttachment *document = (TGBridgeDocumentMediaAttachment *)attachment; + [self.imageGroup setBackgroundImageSignal:[TGBridgeMediaSignals stickerWithDocumentId:document.documentId peerId:peerId messageId:messageId type:TGMediaStickerImageTypeNormal notification:false] isVisible:self.isVisible]; + } + } + else if (mapGroup != nil) + { + self.map.hidden = false; + + CGSize size; + NSValue *imageSizeValue = mapGroup[TGNeoMessageMediaSize]; + if (imageSizeValue != nil) + { + size = imageSizeValue.CGSizeValue; + self.map.width = size.width; + self.map.height = size.height; + } + + NSValue *coordinateValue = mapGroup[TGNeoMessageMediaMapCoordinate]; + if (coordinateValue != nil) + { + CLLocationCoordinate2D coordinate = [coordinateValue MKCoordinateValue]; + CLLocationCoordinate2D regionCoordinate = CLLocationCoordinate2DMake([TGLocationUtils adjustGMapLatitude:coordinate.latitude withPixelOffset:-10 zoom:15], coordinate.longitude); + + self.map.region = MKCoordinateRegionMake(regionCoordinate, MKCoordinateSpanMake(0.003, 0.003)); + self.map.centerPinCoordinate = coordinate; + } + } + + NSValue *insetValue = mediaGroupLayout[TGNeoContentInset]; + if (insetValue != nil) + { + UIEdgeInsets inset = insetValue.UIEdgeInsetsValue; + [self.mediaWrapperGroup setContentInset:inset]; + } + } + + NSDictionary *metaGroupLayout = layout[TGNeoMessageMetaGroup]; + if (metaGroupLayout != nil) + { + self.metaWrapperGroup.hidden = false; + + NSDictionary *audioLayout = metaGroupLayout[TGNeoMessageAudioButton]; + if (audioLayout != nil) + { + self.audioButton.hidden = false; + + NSNumber *hasBackground = audioLayout[TGNeoMessageAudioButtonHasBackground] ?: @true; + if (hasBackground.boolValue) { + UIColor *color = audioLayout[TGNeoMessageAudioBackgroundColor]; + if (color != nil) { + [self.audioButtonGroup setBackgroundColor:color]; + } else { + [self.audioButtonGroup setBackgroundColor:[UIColor hexColor:0x6bbeee]]; + } + } + else + [self.audioButtonGroup setBackgroundColor:[UIColor clearColor]]; + + NSString *audioIcon = audioLayout[TGNeoMessageAudioIcon]; + if (audioIcon != nil) + { + _normalIconName = audioIcon; + [self.audioIcon setImageNamed:audioIcon]; + } + + NSString *audioAnimatedIcon = audioLayout[TGNeoMessageAudioAnimatedIcon]; + if (audioAnimatedIcon != nil) + _processingIconName = audioAnimatedIcon; + + UIColor *iconColor = audioLayout[TGNeoMessageAudioIconTint]; + if (iconColor != nil) + [self.audioIcon setTintColor:iconColor]; + } + + NSDictionary *avatarLayout = metaGroupLayout[TGNeoMessageAvatarGroup]; + if (avatarLayout != nil) + { + self.avatarGroup.hidden = false; + + int64_t identifier = [avatarLayout[TGNeoMessageAvatarIdentifier] int64Value]; + NSString *avatarUrl = avatarLayout[TGNeoMessageAvatarUrl]; + if (avatarUrl.length > 0) + { + [self.avatarGroup setBackgroundImageSignal:[TGBridgeMediaSignals avatarWithPeerId:identifier url:avatarUrl type:TGBridgeMediaAvatarTypeSmall] isVisible:self.isVisible]; + } + else + { + NSString *initials = avatarLayout[TGNeoMessageAvatarInitials]; + UIColor *color = avatarLayout[TGNeoMessageAvatarColor]; + + self.avatarLabel.hidden = false; + self.avatarLabel.text = initials; + [self.avatarGroup setBackgroundColor:color]; + } + } + + NSValue *insetValue = metaGroupLayout[TGNeoContentInset]; + if (insetValue != nil) + { + UIEdgeInsets inset = insetValue.UIEdgeInsetsValue; + [self.metaWrapperGroup setContentInset:inset]; + } + } +} + +- (IBAction)remotePressedAction +{ + if (self.buttonPressed != nil) + self.buttonPressed(); +} + +- (void)setProcessingState:(bool)processing +{ + if (processing == _processing) + return; + + _processing = processing; + + if (processing) + { + [self.audioIcon setImageNamed:_processingIconName]; + [self.audioIcon startAnimatingWithImagesInRange:NSMakeRange(0, 39) duration:0.65 repeatCount:0]; + } + else + { + [self.audioIcon stopAnimating]; + [self.audioIcon setImageNamed:_normalIconName]; + } +} + ++ (Class)rowControllerClassForMessage:(TGBridgeMessage *)message +{ + Class class = [TGNeoConversationRowController class]; + + bool hasReplyHeader = false; + bool hasAttachments = false; + + for (TGBridgeMediaAttachment *attachment in message.media) + { + if ([attachment isKindOfClass:[TGBridgeImageMediaAttachment class]]) + { + hasAttachments = true; + class = [TGNeoConversationMediaRowController class]; + } + else if ([attachment isKindOfClass:[TGBridgeVideoMediaAttachment class]]) + { + hasAttachments = true; + class = [TGNeoConversationMediaRowController class]; + } + else if ([attachment isKindOfClass:[TGBridgeDocumentMediaAttachment class]]) + { + hasAttachments = true; + TGBridgeDocumentMediaAttachment *documentAttachment = (TGBridgeDocumentMediaAttachment *)attachment; + if (documentAttachment.isSticker) + class = [TGNeoConversationMediaRowController class]; + } + else if ([attachment isKindOfClass:[TGBridgeLocationMediaAttachment class]]) + { + hasAttachments = true; + if (((TGBridgeLocationMediaAttachment *)attachment).venue == nil) + class = [TGNeoConversationMediaRowController class]; + } + else if ([attachment isKindOfClass:[TGBridgeAudioMediaAttachment class]]) + { + hasAttachments = true; + } + else if ([attachment isKindOfClass:[TGBridgeUnsupportedMediaAttachment class]]) + { + hasAttachments = true; + } + else if ([attachment isKindOfClass:[TGBridgeActionMediaAttachment class]]) + { + class = [TGNeoConversationStaticRowController class]; + } + else if ([attachment isKindOfClass:[TGBridgeForwardedMessageMediaAttachment class]]) + { + } + else if ([attachment isKindOfClass:[TGBridgeReplyMessageMediaAttachment class]]) + { + hasReplyHeader = true; + } + } + + if (class == [TGNeoConversationRowController class] && !hasReplyHeader && !hasAttachments) + class = [TGNeoConversationSimpleRowController class]; + + return class; +} + +@end diff --git a/Watch/Extension/TGNeoServiceMessageViewModel.h b/Watch/Extension/TGNeoServiceMessageViewModel.h new file mode 100644 index 0000000000..c73d8d2f17 --- /dev/null +++ b/Watch/Extension/TGNeoServiceMessageViewModel.h @@ -0,0 +1,9 @@ +#import "TGNeoMessageViewModel.h" + +@class TGChatInfo; + +@interface TGNeoServiceMessageViewModel : TGNeoMessageViewModel + +- (instancetype)initWithChatInfo:(TGChatInfo *)chatInfo; + +@end diff --git a/Watch/Extension/TGNeoServiceMessageViewModel.m b/Watch/Extension/TGNeoServiceMessageViewModel.m new file mode 100644 index 0000000000..71f0d88372 --- /dev/null +++ b/Watch/Extension/TGNeoServiceMessageViewModel.m @@ -0,0 +1,303 @@ +#import "TGNeoServiceMessageViewModel.h" +#import "TGWatchCommon.h" +#import "TGNeoLabelViewModel.h" + +#import "TGBridgeMessage.h" +#import "TGBridgeUser.h" +#import "TGChatInfo.h" + +#import "TGBridgePeerIdAdapter.h" + +const UIEdgeInsets TGNeoServiceMessageInsets = { 2, 0, 6, 0 }; +const UIEdgeInsets TGNeoChatInfoInsets = { 12, 0, 12, 0 }; + +@interface TGNeoServiceMessageViewModel () +{ + TGNeoLabelViewModel *_titleModel; + TGNeoLabelViewModel *_textModel; + + bool _chatInfo; +} +@end + +@implementation TGNeoServiceMessageViewModel + +- (instancetype)initWithMessage:(TGBridgeMessage *)message type:(TGNeoMessageType)type users:(NSDictionary *)users context:(TGBridgeContext *)context +{ + self = [super initWithMessage:message type:type users:users context:context]; + if (self != nil) + { + NSString *actionText = nil; + NSArray *additionalAttributes = nil; + + bool isChannel = type == TGNeoMessageTypeChannel; + + TGBridgeUser *author = users[@(message.fromUid)]; + + for (TGBridgeMediaAttachment *attachment in message.media) + { + if ([attachment isKindOfClass:[TGBridgeActionMediaAttachment class]]) + { + TGBridgeActionMediaAttachment *actionAttachment = (TGBridgeActionMediaAttachment *)attachment; + + switch (actionAttachment.actionType) + { + case TGBridgeMessageActionChatEditTitle: + { + if (isChannel) + { + NSString *formatString = TGLocalized(@"Notification.ChannelFullTitleUpdated"); + actionText = [NSString stringWithFormat:formatString, actionAttachment.actionData[@"title"]]; + } + else + { + NSString *authorName = author.displayName; + NSString *formatString = TGLocalized(@"Notification.ChangedGroupName"); + actionText = [NSString stringWithFormat:formatString, authorName, actionAttachment.actionData[@"title"]]; + + NSRange formatNameRange = [formatString rangeOfString:@"%@"]; + if (formatNameRange.location != NSNotFound) + { + additionalAttributes = [TGNeoServiceMessageViewModel _mediumFontAttributeForRange:NSMakeRange(formatNameRange.location, authorName.length)]; + } + } + } + break; + + case TGBridgeMessageActionChatEditPhoto: + { + NSString *authorName = author.displayName; + bool changed = actionAttachment.actionData[@"photo"]; + + if (isChannel) + { + actionText = changed ? TGLocalized(@"Notification.ChannelPhotoUpdated") : TGLocalized(@"Notification.ChannelPhotoRemoved"); + } + else + { + NSString *formatString = changed ? TGLocalized(@"Notification.ChangedGroupPhoto") : TGLocalized(@"Notification.RemovedGroupPhoto"); + + actionText = [NSString stringWithFormat:formatString, authorName]; + + NSRange formatNameRange = [formatString rangeOfString:@"%@"]; + if (formatNameRange.location != NSNotFound) + { + additionalAttributes = [TGNeoServiceMessageViewModel _mediumFontAttributeForRange:NSMakeRange(formatNameRange.location, authorName.length)]; + } + } + } + break; + + case TGBridgeMessageActionUserChangedPhoto: + + break; + + case TGBridgeMessageActionChatAddMember: + case TGBridgeMessageActionChatDeleteMember: + { + NSString *authorName = author.displayName; + TGBridgeUser *user = users[@([actionAttachment.actionData[@"uid"] int32Value])]; + + if (user.identifier == author.identifier) + { + NSString *formatString = (actionAttachment.actionType == TGBridgeMessageActionChatAddMember) ? TGLocalized(@"Notification.JoinedChat") : TGLocalized(@"Notification.LeftChat"); + actionText = [[NSString alloc] initWithFormat:formatString, authorName]; + + NSRange formatNameRange = [formatString rangeOfString:@"%@"]; + if (formatNameRange.location != NSNotFound) + { + additionalAttributes = [TGNeoServiceMessageViewModel _mediumFontAttributeForRange:NSMakeRange(formatNameRange.location, authorName.length)]; + } + } + else + { + NSString *userName = user.displayName; + NSString *formatString = (actionAttachment.actionType == TGBridgeMessageActionChatAddMember) ? TGLocalized(@"Notification.Invited") : TGLocalized(@"Notification.Kicked"); + actionText = [[NSString alloc] initWithFormat:formatString, authorName, userName]; + + NSRange formatNameRangeFirst = [formatString rangeOfString:@"%@"]; + NSRange formatNameRangeSecond = formatNameRangeFirst.location != NSNotFound ? [formatString rangeOfString:@"%@" options:0 range:NSMakeRange(formatNameRangeFirst.location + formatNameRangeFirst.length, formatString.length - (formatNameRangeFirst.location + formatNameRangeFirst.length))] : NSMakeRange(NSNotFound, 0); + + if (formatNameRangeFirst.location != NSNotFound && formatNameRangeSecond.location != NSNotFound) + { + NSMutableArray *array = [[NSMutableArray alloc] init]; + + NSRange rangeFirst = NSMakeRange(formatNameRangeFirst.location, authorName.length); + [array addObjectsFromArray:[TGNeoServiceMessageViewModel _mediumFontAttributeForRange:rangeFirst]]; + [array addObjectsFromArray:[TGNeoServiceMessageViewModel _mediumFontAttributeForRange:NSMakeRange(rangeFirst.length - formatNameRangeFirst.length + formatNameRangeSecond.location, userName.length)]]; + + additionalAttributes = array; + } + } + } + break; + + case TGBridgeMessageActionJoinedByLink: + { + NSString *authorName = author.displayName; + NSString *formatString = TGLocalized(@"Notification.JoinedGroupByLink"); + actionText = [[NSString alloc] initWithFormat:formatString, authorName, actionAttachment.actionData[@"title"]]; + + NSRange formatNameRange = [formatString rangeOfString:@"%@"]; + if (formatNameRange.location != NSNotFound) + { + additionalAttributes = [TGNeoServiceMessageViewModel _mediumFontAttributeForRange:NSMakeRange(formatNameRange.location, authorName.length)]; + } + } + break; + + case TGBridgeMessageActionCreateChat: + { + NSString *authorName = author.displayName; + NSString *formatString = TGLocalized(@"Notification.CreatedChatWithTitle"); + actionText = [[NSString alloc] initWithFormat:formatString, authorName, actionAttachment.actionData[@"title"]]; + + NSRange formatNameRange = [formatString rangeOfString:@"%@"]; + if (formatNameRange.location != NSNotFound) + { + additionalAttributes = [TGNeoServiceMessageViewModel _mediumFontAttributeForRange:NSMakeRange(formatNameRange.location, authorName.length)]; + } + } + break; + + case TGBridgeMessageActionContactRegistered: + { + actionText = TGLocalized(@"Notification.Joined"); + } + break; + + case TGBridgeMessageActionChannelCreated: + { + actionText = TGLocalized(@"Notification.CreatedChannel"); + } + break; + + case TGBridgeMessageActionChannelInviter: + { + TGBridgeUser *user = users[@([actionAttachment.actionData[@"uid"] int32Value])]; + NSString *authorName = user.displayName; + NSString *formatString = TGLocalized(@"Notification.ChannelInviter"); + actionText = [[NSString alloc] initWithFormat:formatString, authorName]; + + NSRange formatNameRange = [formatString rangeOfString:@"%@"]; + if (formatNameRange.location != NSNotFound) + { + additionalAttributes = [TGNeoServiceMessageViewModel _mediumFontAttributeForRange:NSMakeRange(formatNameRange.location, authorName.length)]; + } + } + break; + + case TGBridgeMessageActionGroupMigratedTo: + { + actionText = TGLocalized(@"Notification.ChannelMigratedFrom"); + } + break; + + case TGBridgeMessageActionGroupActivated: + { + actionText = TGLocalized(@"Notification.GroupActivated"); + } + break; + + case TGBridgeMessageActionGroupDeactivated: + { + actionText = TGLocalized(@"Notification.GroupDeactivated"); + } + break; + + case TGBridgeMessageActionChannelMigratedFrom: + { + actionText = TGLocalized(@"Notification.ChannelMigratedFrom"); + } + break; + + default: + break; + } + } + } + + if (actionText == nil) + actionText = @""; + + NSMutableParagraphStyle *style = [[NSMutableParagraphStyle alloc] init]; + style.alignment = NSTextAlignmentCenter; + + NSMutableAttributedString *attributedText = [[NSMutableAttributedString alloc] initWithString:actionText attributes:@ + { NSFontAttributeName: [UIFont systemFontOfSize:12], + NSForegroundColorAttributeName: [UIColor whiteColor], + NSParagraphStyleAttributeName: style + }]; + + if (additionalAttributes != nil) + { + NSUInteger count = additionalAttributes.count; + for (NSUInteger i = 0; i < count; i += 2) + { + NSRange range = NSMakeRange(0, 0); + [(NSValue *)[additionalAttributes objectAtIndex:i] getValue:&range]; + NSDictionary *attributes = [additionalAttributes objectAtIndex:i + 1]; + + if (range.location + range.length <= attributedText.length) + [attributedText addAttributes:attributes range:range]; + } + } + + _textModel = [[TGNeoLabelViewModel alloc] initWithAttributedText:attributedText]; + _textModel.multiline = true; + [self addSubmodel:_textModel]; + } + return self; +} + +- (instancetype)initWithChatInfo:(TGChatInfo *)chatInfo +{ + self = [super init]; + if (self != nil) + { + _chatInfo = true; + + NSMutableParagraphStyle *style = [[NSMutableParagraphStyle alloc] init]; + style.alignment = NSTextAlignmentCenter; + NSDictionary *attributes = @{ NSParagraphStyleAttributeName: style }; + + _titleModel = [[TGNeoLabelViewModel alloc] initWithText:chatInfo.title font:[UIFont systemFontOfSize:12 weight:UIFontWeightSemibold] color:[UIColor whiteColor] attributes:attributes]; + [self addSubmodel:_titleModel]; + + _textModel = [[TGNeoLabelViewModel alloc] initWithText:chatInfo.text font:[UIFont systemFontOfSize:12 weight:UIFontWeightMedium] color:[UIColor whiteColor] attributes:attributes]; + [self addSubmodel:_textModel]; + } + return self; +} + +- (CGSize)layoutWithContainerSize:(CGSize)containerSize +{ + CGSize titleSize = CGSizeZero; + UIEdgeInsets inset = _chatInfo ? TGNeoChatInfoInsets : TGNeoServiceMessageInsets; + CGFloat textTopOffset = inset.top; + + if (_titleModel != nil) + { + titleSize = [_titleModel contentSizeWithContainerSize:CGSizeMake(containerSize.width, FLT_MAX)]; + _titleModel.frame = CGRectMake((containerSize.width - titleSize.width) / 2, textTopOffset, titleSize.width, titleSize.height); + + textTopOffset = CGRectGetMaxY(_titleModel.frame) + 1; + } + + CGSize textSize = [_textModel contentSizeWithContainerSize:CGSizeMake(containerSize.width, FLT_MAX)]; + _textModel.frame = CGRectMake((containerSize.width - textSize.width) / 2, textTopOffset, textSize.width, textSize.height); + + CGSize contentSize = CGSizeMake(containerSize.width, CGRectGetMaxY(_textModel.frame) + inset.bottom); + + self.contentSize = contentSize; + + return contentSize; +} + ++ (NSArray *)_mediumFontAttributeForRange:(NSRange)range +{ + NSDictionary *fontAttributes = @{ NSFontAttributeName: [UIFont systemFontOfSize:12.0f weight:UIFontWeightMedium], NSForegroundColorAttributeName: [UIColor whiteColor] }; + return [[NSArray alloc] initWithObjects:[[NSValue alloc] initWithBytes:&range objCType:@encode(NSRange)], fontAttributes, nil]; +} + +@end diff --git a/Watch/Extension/TGNeoSmiliesMessageViewModel.h b/Watch/Extension/TGNeoSmiliesMessageViewModel.h new file mode 100644 index 0000000000..b34a61f30d --- /dev/null +++ b/Watch/Extension/TGNeoSmiliesMessageViewModel.h @@ -0,0 +1,5 @@ +#import "TGNeoMessageViewModel.h" + +@interface TGNeoSmiliesMessageViewModel : TGNeoMessageViewModel + +@end diff --git a/Watch/Extension/TGNeoSmiliesMessageViewModel.m b/Watch/Extension/TGNeoSmiliesMessageViewModel.m new file mode 100644 index 0000000000..fd97837015 --- /dev/null +++ b/Watch/Extension/TGNeoSmiliesMessageViewModel.m @@ -0,0 +1,74 @@ +#import "TGNeoSmiliesMessageViewModel.h" +#import "TGNeoBubbleMessageViewModel.h" +#import "TGNeoLabelViewModel.h" + +#import "TGBridgeContext.h" +#import "TGBridgeMessage.h" + +#import "TGWatchColor.h" + +#import "TGBridgePeerIdAdapter.h" + +const CGFloat TGNeoSmiliesMessageHeight = 39; + +@interface TGNeoSmiliesMessageViewModel () +{ + TGNeoLabelViewModel *_authorNameModel; + TGNeoLabelViewModel *_textModel; + bool _outgoing; +} +@end + +@implementation TGNeoSmiliesMessageViewModel + +- (instancetype)initWithMessage:(TGBridgeMessage *)message type:(TGNeoMessageType)type users:(NSDictionary *)users context:(TGBridgeContext *)context +{ + self = [super initWithMessage:message type:type users:users context:context]; + if (self != nil) + { + _outgoing = message.outgoing; + + if (message.cid < 0 && type != TGNeoMessageTypeChannel && !message.outgoing) + { + _authorNameModel = [[TGNeoLabelViewModel alloc] initWithText:[users[@(message.fromUid)] displayName] font:[UIFont systemFontOfSize:14] color:[TGColor colorForUserId:(int32_t)message.fromUid myUserId:context.userId] attributes:nil]; + [self addSubmodel:_authorNameModel]; + } + + _textModel = [[TGNeoLabelViewModel alloc] initWithText:message.text font:[UIFont systemFontOfSize:35] color:[UIColor whiteColor] attributes:nil]; + _textModel.multiline = false; + [self addSubmodel:_textModel]; + } + return self; +} + +- (void)drawInContext:(CGContextRef)context +{ + CGContextSetFillColorWithColor(context, [UIColor grayColor].CGColor); + CGContextFillRect(context, self.bounds); + + [super drawInContext:context]; +} + +- (CGSize)layoutWithContainerSize:(CGSize)containerSize +{ + CGFloat textTopOffset = 0; + if (_authorNameModel != nil) + { + CGSize nameSize = [_authorNameModel contentSizeWithContainerSize:CGSizeMake(containerSize.width - TGNeoBubbleMessageViewModelInsets.left - TGNeoBubbleMessageViewModelInsets.right, FLT_MAX)]; + _authorNameModel.frame = CGRectMake(TGNeoBubbleMessageViewModelInsets.left, floor(TGNeoBubbleMessageViewModelInsets.top / 2.0), nameSize.width, 16.5f); + textTopOffset += CGRectGetMaxY(_authorNameModel.frame) + TGNeoBubbleHeaderSpacing; + } + + CGSize size = [_textModel contentSizeWithContainerSize:containerSize]; + CGFloat inset = 0; //TGNeoBubbleMessageViewModelInsets.left + if (_outgoing) + size.width += inset; + + _textModel.frame = CGRectMake(_outgoing ? 0 : inset, textTopOffset, size.width, size.height); + + self.contentSize = CGSizeMake(MAX(CGRectGetMaxX(_authorNameModel.frame), size.width), TGNeoSmiliesMessageHeight + textTopOffset); + + return self.contentSize; +} + +@end diff --git a/Watch/Extension/TGNeoStickerMessageViewModel.h b/Watch/Extension/TGNeoStickerMessageViewModel.h new file mode 100644 index 0000000000..72fb6cd894 --- /dev/null +++ b/Watch/Extension/TGNeoStickerMessageViewModel.h @@ -0,0 +1,5 @@ +#import "TGNeoMessageViewModel.h" + +@interface TGNeoStickerMessageViewModel : TGNeoMessageViewModel + +@end diff --git a/Watch/Extension/TGNeoStickerMessageViewModel.m b/Watch/Extension/TGNeoStickerMessageViewModel.m new file mode 100644 index 0000000000..56bf7ddfc6 --- /dev/null +++ b/Watch/Extension/TGNeoStickerMessageViewModel.m @@ -0,0 +1,95 @@ +#import "TGNeoStickerMessageViewModel.h" +#import "TGWatchCommon.h" +#import "TGNeoLabelViewModel.h" + +#import "TGNeoBubbleMessageViewModel.h" + +#import "TGGeometry.h" + +#import "TGBridgeContext.h" +#import "TGBridgeMessage.h" + +#import "../Extension/TGStringUtils.h" +#import "TGBridgePeerIdAdapter.h" + +@interface TGNeoStickerMessageViewModel () +{ + int64_t _peerId; + int32_t _messageId; + + TGNeoLabelViewModel *_authorNameModel; + + TGBridgeDocumentMediaAttachment *_documentAttachment; + bool _outgoing; +} +@end + +@implementation TGNeoStickerMessageViewModel + +- (instancetype)initWithMessage:(TGBridgeMessage *)message type:(TGNeoMessageType)type users:(NSDictionary *)users context:(TGBridgeContext *)context +{ + self = [super initWithMessage:message type:type users:users context:context]; + if (self != nil) + { + _peerId = message.cid; + _messageId = message.identifier; + + self.showBubble = false; + + TGBridgeDocumentMediaAttachment *documentAttachment = nil; + for (TGBridgeMediaAttachment *attachment in message.media) + { + if ([attachment isKindOfClass:[TGBridgeDocumentMediaAttachment class]]) + { + documentAttachment = (TGBridgeDocumentMediaAttachment *)attachment; + break; + } + } + + _documentAttachment = documentAttachment; + _outgoing = message.outgoing; + + if (message.cid < 0 && !TGPeerIdIsChannel(message.cid) && !message.outgoing) + { + _authorNameModel = [[TGNeoLabelViewModel alloc] initWithText:[users[@(message.fromUid)] displayName] font:[UIFont systemFontOfSize:14] color:[TGColor colorForUserId:(int32_t)message.fromUid myUserId:context.userId] attributes:nil]; + [self addSubmodel:_authorNameModel]; + } + } + return self; +} + +- (CGSize)layoutWithContainerSize:(CGSize)containerSize +{ + CGFloat textTopOffset = 0; + if (_authorNameModel != nil) + { + CGSize nameSize = [_authorNameModel contentSizeWithContainerSize:CGSizeMake(containerSize.width - TGNeoBubbleMessageViewModelInsets.left - TGNeoBubbleMessageViewModelInsets.right, FLT_MAX)]; + _authorNameModel.frame = CGRectMake(TGNeoBubbleMessageViewModelInsets.left, floor(TGNeoBubbleMessageViewModelInsets.top / 2.0), nameSize.width, 16.5f); + textTopOffset += CGRectGetMaxY(_authorNameModel.frame) + TGNeoBubbleHeaderSpacing; + } + + CGFloat stickerHeight = TGWatchStickerSizeForScreen(TGWatchScreenType()).height; + CGSize imageSize = TGFitSize(_documentAttachment.imageSize.CGSizeValue, CGSizeMake(containerSize.width / 2, stickerHeight)); + + UIEdgeInsets inset = UIEdgeInsetsMake(textTopOffset, 0, 0, 0); + if (_documentAttachment != nil) + { + [self addAdditionalLayout:@ + { + TGNeoContentInset: [NSValue valueWithUIEdgeInsets:inset], + TGNeoMessageMediaImage: @ + { + TGNeoMessageMediaPeerId: @(_peerId), + TGNeoMessageMediaMessageId: @(_messageId), + TGNeoMessageMediaImageAttachment: _documentAttachment, + TGNeoMessageMediaSize: [NSValue valueWithCGSize:imageSize] + } + } withKey:TGNeoMessageMediaGroup]; + } + + self.contentSize = CGSizeMake(MAX(CGRectGetMaxX(_authorNameModel.frame), imageSize.width), ceilf(imageSize.height) + textTopOffset + 2); + + return self.contentSize; +} + +@end diff --git a/Watch/Extension/TGNeoTextMessageViewModel.h b/Watch/Extension/TGNeoTextMessageViewModel.h new file mode 100644 index 0000000000..4ef72c5ab2 --- /dev/null +++ b/Watch/Extension/TGNeoTextMessageViewModel.h @@ -0,0 +1,5 @@ +#import "TGNeoBubbleMessageViewModel.h" + +@interface TGNeoTextMessageViewModel : TGNeoBubbleMessageViewModel + +@end diff --git a/Watch/Extension/TGNeoTextMessageViewModel.m b/Watch/Extension/TGNeoTextMessageViewModel.m new file mode 100644 index 0000000000..2efe822100 --- /dev/null +++ b/Watch/Extension/TGNeoTextMessageViewModel.m @@ -0,0 +1,63 @@ +#import "TGNeoTextMessageViewModel.h" +#import "TGNeoLabelViewModel.h" +#import "TGMessageViewModel.h" + +#import "TGBridgeMessage.h" + +@interface TGNeoTextMessageViewModel () +{ + TGNeoLabelViewModel *_textModel; +} +@end + +@implementation TGNeoTextMessageViewModel + +- (instancetype)initWithMessage:(TGBridgeMessage *)message type:(TGNeoMessageType)type users:(NSDictionary *)users context:(TGBridgeContext *)context +{ + self = [super initWithMessage:message type:type users:users context:context]; + if (self != nil) + { + NSString *text = [message.text stringByReplacingOccurrencesOfString:@"/" withString:@"/\u2060"]; + CGFloat fontSize = [TGNeoBubbleMessageViewModel bodyTextFontSize]; + UIColor *textColor = [self normalColorForMessage:message type:type]; + + if (message.textCheckingResults.count > 0) + { + _textModel = [[TGNeoLabelViewModel alloc] initWithAttributedText:[TGMessageViewModel attributedTextForMessage:message fontSize:fontSize textColor:textColor]]; + } + else + { + _textModel = [[TGNeoLabelViewModel alloc] initWithText:text font:[UIFont systemFontOfSize:fontSize] color:textColor attributes:nil]; + } + + _textModel.multiline = true; + + [self addSubmodel:_textModel]; + } + return self; +} + +- (CGSize)layoutWithContainerSize:(CGSize)containerSize +{ + CGSize contentContainerSize = [self contentContainerSizeWithContainerSize:containerSize]; + + CGSize headerSize = [self layoutHeaderModelsWithContainerSize:contentContainerSize]; + CGFloat maxContentWidth = headerSize.width; + CGFloat textTopOffset = headerSize.height; + + CGSize textSize = [_textModel contentSizeWithContainerSize:contentContainerSize]; + _textModel.frame = CGRectMake(TGNeoBubbleMessageViewModelInsets.left, textTopOffset, textSize.width, textSize.height); + + if (textSize.width > maxContentWidth) + maxContentWidth = textSize.width; + + CGSize contentSize = CGSizeZero; + contentSize.width = maxContentWidth + TGNeoBubbleMessageViewModelInsets.left + TGNeoBubbleMessageViewModelInsets.right; + contentSize.height = CGRectGetMaxY(_textModel.frame) + TGNeoBubbleMessageViewModelInsets.bottom; + + [super layoutWithContainerSize:contentSize]; + + return contentSize; +} + +@end diff --git a/Watch/Extension/TGNeoUnsupportedMessageViewModel.h b/Watch/Extension/TGNeoUnsupportedMessageViewModel.h new file mode 100644 index 0000000000..41cafc4abe --- /dev/null +++ b/Watch/Extension/TGNeoUnsupportedMessageViewModel.h @@ -0,0 +1,5 @@ +#import "TGNeoBubbleMessageViewModel.h" + +@interface TGNeoUnsupportedMessageViewModel : TGNeoBubbleMessageViewModel + +@end diff --git a/Watch/Extension/TGNeoUnsupportedMessageViewModel.m b/Watch/Extension/TGNeoUnsupportedMessageViewModel.m new file mode 100644 index 0000000000..01d19f2676 --- /dev/null +++ b/Watch/Extension/TGNeoUnsupportedMessageViewModel.m @@ -0,0 +1,95 @@ +#import "TGNeoUnsupportedMessageViewModel.h" +#import "TGWatchCommon.h" +#import "TGBridgeMessage.h" + +@interface TGNeoUnsupportedMessageViewModel () +{ + TGNeoLabelViewModel *_titleModel; + TGNeoLabelViewModel *_subtitleModel; + + UIColor *_buttonTint; + UIColor *_iconTint; +} +@end + +@implementation TGNeoUnsupportedMessageViewModel + +- (instancetype)initWithMessage:(TGBridgeMessage *)message type:(TGNeoMessageType)type users:(NSDictionary *)users context:(TGBridgeContext *)context +{ + self = [super initWithMessage:message type:type users:users context:context]; + if (self != nil) + { + TGBridgeUnsupportedMediaAttachment *unsupportedAttachment = nil; + + for (TGBridgeMediaAttachment *attachment in message.media) + { + if ([attachment isKindOfClass:[TGBridgeUnsupportedMediaAttachment class]]) + { + unsupportedAttachment = (TGBridgeUnsupportedMediaAttachment *)attachment; + break; + } + } + + NSString *title = unsupportedAttachment.title; + NSString *subtitle = unsupportedAttachment.subtitle; + + _titleModel = [[TGNeoLabelViewModel alloc] initWithText:title font:[UIFont systemFontOfSize:12 weight:UIFontWeightMedium] color:[self normalColorForMessage:message type:type] attributes:nil]; + _titleModel.multiline = false; + [self addSubmodel:_titleModel]; + + _subtitleModel = [[TGNeoLabelViewModel alloc] initWithText:subtitle font:[UIFont systemFontOfSize:12] color:[self subtitleColorForMessage:message type:type] attributes:nil]; + _subtitleModel.multiline = false; + [self addSubmodel:_subtitleModel]; + + _buttonTint = [self accentColorForMessage:message type:type]; + _iconTint = [self contrastAccentColorForMessage:message type:type]; + } + return self; +} + +- (CGSize)layoutWithContainerSize:(CGSize)containerSize +{ + CGSize contentContainerSize = [self contentContainerSizeWithContainerSize:containerSize]; + + CGSize headerSize = [self layoutHeaderModelsWithContainerSize:contentContainerSize]; + CGFloat maxContentWidth = headerSize.width; + CGFloat textTopOffset = headerSize.height; + + CGFloat leftOffset = 26 + TGNeoBubbleMessageMetaSpacing; + + UIEdgeInsets inset = UIEdgeInsetsMake(textTopOffset + 1.5f, TGNeoBubbleMessageViewModelInsets.left, 0, 0); + NSDictionary *openButtonDictionary = _titleModel.text.length == 0 ? @{} : @{ + TGNeoMessageAudioIcon: @"RemotePhone", + TGNeoMessageAudioIconTint: _iconTint, + TGNeoMessageAudioBackgroundColor: _buttonTint, + TGNeoMessageAudioButtonHasBackground: @true }; + + if (openButtonDictionary.count > 0) + { + inset.left -= 4; + leftOffset -= 5; + } + else + { + + } + + contentContainerSize = CGSizeMake(containerSize.width - TGNeoBubbleMessageViewModelInsets.left - TGNeoBubbleMessageViewModelInsets.right - leftOffset, FLT_MAX); + + CGSize nameSize = [_titleModel contentSizeWithContainerSize:contentContainerSize]; + CGSize durationSize = [_subtitleModel contentSizeWithContainerSize:contentContainerSize]; + maxContentWidth = MAX(maxContentWidth, MAX(nameSize.width, durationSize.width) + leftOffset); + + _titleModel.frame = CGRectMake(TGNeoBubbleMessageViewModelInsets.left + leftOffset, textTopOffset, nameSize.width, 14); + _subtitleModel.frame = CGRectMake(TGNeoBubbleMessageViewModelInsets.left + leftOffset, CGRectGetMaxY(_titleModel.frame), durationSize.width, 14); + + [self addAdditionalLayout:@{ TGNeoContentInset: [NSValue valueWithUIEdgeInsets:inset], TGNeoMessageAudioButton: openButtonDictionary } withKey:TGNeoMessageMetaGroup]; + + CGSize contentSize = CGSizeMake(inset.left + TGNeoBubbleMessageViewModelInsets.right + maxContentWidth, CGRectGetMaxY(_subtitleModel.frame) + TGNeoBubbleMessageViewModelInsets.bottom); + + [super layoutWithContainerSize:contentSize]; + + return contentSize; +} + +@end diff --git a/Watch/Extension/TGNeoVenueMessageViewModel.h b/Watch/Extension/TGNeoVenueMessageViewModel.h new file mode 100644 index 0000000000..3b4376d130 --- /dev/null +++ b/Watch/Extension/TGNeoVenueMessageViewModel.h @@ -0,0 +1,5 @@ +#import "TGNeoBubbleMessageViewModel.h" + +@interface TGNeoVenueMessageViewModel : TGNeoBubbleMessageViewModel + +@end diff --git a/Watch/Extension/TGNeoVenueMessageViewModel.m b/Watch/Extension/TGNeoVenueMessageViewModel.m new file mode 100644 index 0000000000..8cb4cc3bdf --- /dev/null +++ b/Watch/Extension/TGNeoVenueMessageViewModel.m @@ -0,0 +1,72 @@ +#import "TGNeoVenueMessageViewModel.h" +#import "TGNeoImageViewModel.h" +#import "TGBridgeMessage.h" + +@interface TGNeoVenueMessageViewModel () +{ + TGNeoImageViewModel *_iconModel; + TGNeoLabelViewModel *_nameModel; + TGNeoLabelViewModel *_addressModel; +} +@end + +@implementation TGNeoVenueMessageViewModel + +- (instancetype)initWithMessage:(TGBridgeMessage *)message type:(TGNeoMessageType)type users:(NSDictionary *)users context:(TGBridgeContext *)context +{ + self = [super initWithMessage:message type:type users:users context:context]; + if (self != nil) + { + TGBridgeLocationMediaAttachment *locationAttachment = nil; + for (TGBridgeMediaAttachment *attachment in message.media) + { + if ([attachment isKindOfClass:[TGBridgeLocationMediaAttachment class]]) + { + locationAttachment = (TGBridgeLocationMediaAttachment *)attachment; + break; + } + } + + _iconModel = [[TGNeoImageViewModel alloc] initWithImage:[UIImage imageNamed:@"Location"] tintColor:[self accentColorForMessage:message type:type]]; + [self addSubmodel:_iconModel]; + + TGBridgeVenueAttachment *venue = locationAttachment.venue; + + _nameModel = [[TGNeoLabelViewModel alloc] initWithText:venue.title font:[UIFont systemFontOfSize:12 weight:UIFontWeightMedium] color:[self normalColorForMessage:message type:type] attributes:nil]; + _nameModel.multiline = false; + [self addSubmodel:_nameModel]; + + _addressModel = [[TGNeoLabelViewModel alloc] initWithText:venue.address font:[UIFont systemFontOfSize:12] color:[self subtitleColorForMessage:message type:type] attributes:nil]; + _addressModel.multiline = false; + [self addSubmodel:_addressModel]; + } + return self; +} + +- (CGSize)layoutWithContainerSize:(CGSize)containerSize +{ + CGSize contentContainerSize = [self contentContainerSizeWithContainerSize:containerSize]; + + CGSize headerSize = [self layoutHeaderModelsWithContainerSize:contentContainerSize]; + CGFloat maxContentWidth = headerSize.width; + CGFloat textTopOffset = headerSize.height; + + CGFloat leftOffset = 20 + TGNeoBubbleMessageMetaSpacing; + contentContainerSize = CGSizeMake(containerSize.width - TGNeoBubbleMessageViewModelInsets.left - TGNeoBubbleMessageViewModelInsets.right - leftOffset, FLT_MAX); + + CGSize nameSize = [_nameModel contentSizeWithContainerSize:contentContainerSize]; + CGSize addressSize = [_addressModel contentSizeWithContainerSize:contentContainerSize]; + maxContentWidth = MAX(maxContentWidth, MAX(nameSize.width, addressSize.width) + leftOffset); + + _iconModel.frame = CGRectMake(TGNeoBubbleMessageViewModelInsets.left - 3, textTopOffset + 1.5f, 26, 26); + _nameModel.frame = CGRectMake(TGNeoBubbleMessageViewModelInsets.left + leftOffset, textTopOffset, nameSize.width, 14); + _addressModel.frame = CGRectMake(TGNeoBubbleMessageViewModelInsets.left + leftOffset, CGRectGetMaxY(_nameModel.frame), addressSize.width, 14); + + CGSize contentSize = CGSizeMake(TGNeoBubbleMessageViewModelInsets.left + TGNeoBubbleMessageViewModelInsets.right + maxContentWidth, CGRectGetMaxY(_addressModel.frame) + TGNeoBubbleMessageViewModelInsets.bottom); + + [super layoutWithContainerSize:contentSize]; + + return contentSize; +} + +@end diff --git a/Watch/Extension/TGNeoViewModel.h b/Watch/Extension/TGNeoViewModel.h new file mode 100644 index 0000000000..de0467fa69 --- /dev/null +++ b/Watch/Extension/TGNeoViewModel.h @@ -0,0 +1,19 @@ +#import +#import + +@interface TGNeoViewModel : NSObject + +@property (nonatomic, assign) CGRect frame; +@property (nonatomic, readonly) CGRect bounds; +@property (nonatomic, assign) bool hidden; + +@property (nonatomic, readonly) NSArray *submodels; +- (void)addSubmodel:(TGNeoViewModel *)viewModel; +- (void)removeSubmodel:(TGNeoViewModel *)viewModel; + +- (void)drawInContext:(CGContextRef)context; +- (void)drawSubmodelsInContext:(CGContextRef)context; + +- (CGSize)contentSizeWithContainerSize:(CGSize)containerSize; + +@end diff --git a/Watch/Extension/TGNeoViewModel.m b/Watch/Extension/TGNeoViewModel.m new file mode 100644 index 0000000000..2ccae06e43 --- /dev/null +++ b/Watch/Extension/TGNeoViewModel.m @@ -0,0 +1,61 @@ +#import "TGNeoViewModel.h" + +@interface TGNeoViewModel () +{ + NSMutableArray *_submodels; +} +@end + +@implementation TGNeoViewModel + +- (instancetype)init +{ + self = [super init]; + if (self != nil) + { + _submodels = [[NSMutableArray alloc] init]; + } + return self; +} + +- (CGRect)bounds +{ + return CGRectMake(0, 0, self.frame.size.width, self.frame.size.height); +} + +- (NSArray *)submodels +{ + return _submodels; +} + +- (void)addSubmodel:(TGNeoViewModel *)viewModel +{ + [_submodels addObject:viewModel]; +} + +- (void)removeSubmodel:(TGNeoViewModel *)viewModel +{ + [_submodels removeObject:viewModel]; +} + +- (void)drawInContext:(CGContextRef)context +{ + [self drawSubmodelsInContext:context]; +} + +- (void)drawSubmodelsInContext:(CGContextRef)context +{ + for (TGNeoViewModel *submodel in self.submodels) + { + CGContextTranslateCTM(context, submodel.frame.origin.x, submodel.frame.origin.y); + [submodel drawInContext:context]; + CGContextTranslateCTM(context, -submodel.frame.origin.x, -submodel.frame.origin.y); + } +} + +- (CGSize)contentSizeWithContainerSize:(CGSize)containerSize +{ + return CGSizeZero; +} + +@end diff --git a/Watch/Extension/TGNotificationController.h b/Watch/Extension/TGNotificationController.h new file mode 100644 index 0000000000..84d102f4de --- /dev/null +++ b/Watch/Extension/TGNotificationController.h @@ -0,0 +1,42 @@ +#import +#import + +@interface TGNotificationController : WKUserNotificationInterfaceController + +@property (nonatomic, weak) IBOutlet WKInterfaceGroup *forwardHeaderGroup; +@property (nonatomic, weak) IBOutlet WKInterfaceLabel *forwardTitleLabel; +@property (nonatomic, weak) IBOutlet WKInterfaceLabel *forwardFromLabel; + +@property (nonatomic, weak) IBOutlet WKInterfaceGroup *replyHeaderGroup; +@property (nonatomic, weak) IBOutlet WKInterfaceGroup *replyHeaderImageGroup; +@property (nonatomic, weak) IBOutlet WKInterfaceLabel *replyAuthorNameLabel; +@property (nonatomic, weak) IBOutlet WKInterfaceLabel *replyMessageTextLabel; + +@property (nonatomic, weak) IBOutlet WKInterfaceLabel *nameLabel; +@property (nonatomic, weak) IBOutlet WKInterfaceLabel *messageTextLabel; +@property (nonatomic, weak) IBOutlet WKInterfaceLabel *chatTitleLabel; +@property (nonatomic, weak) IBOutlet WKInterfaceGroup *mediaGroup; +@property (nonatomic, weak) IBOutlet WKInterfaceGroup *captionGroup; +@property (nonatomic, weak) IBOutlet WKInterfaceLabel *captionLabel; + +@property (nonatomic, weak) IBOutlet WKInterfaceGroup *wrapperGroup; + +@property (nonatomic, weak) IBOutlet WKInterfaceGroup *mapGroup; +@property (nonatomic, weak) IBOutlet WKInterfaceMap *map; + +@property (nonatomic, weak) IBOutlet WKInterfaceGroup *durationGroup; +@property (nonatomic, weak) IBOutlet WKInterfaceLabel *durationLabel; + +@property (nonatomic, weak) IBOutlet WKInterfaceLabel *titleLabel; +@property (nonatomic, weak) IBOutlet WKInterfaceLabel *subtitleLabel; + +@property (nonatomic, weak) IBOutlet WKInterfaceGroup *audioGroup; + +@property (nonatomic, weak) IBOutlet WKInterfaceGroup *fileGroup; +@property (nonatomic, weak) IBOutlet WKInterfaceGroup *fileIconGroup; +@property (nonatomic, weak) IBOutlet WKInterfaceImage *venueIcon; + +@property (nonatomic, weak) IBOutlet WKInterfaceGroup *stickerWrapperGroup; +@property (nonatomic, weak) IBOutlet WKInterfaceGroup *stickerGroup; + +@end diff --git a/Watch/Extension/TGNotificationController.m b/Watch/Extension/TGNotificationController.m new file mode 100644 index 0000000000..7514cb053c --- /dev/null +++ b/Watch/Extension/TGNotificationController.m @@ -0,0 +1,385 @@ +#import "TGNotificationController.h" +#import "TGWatchCommon.h" +#import "TGStringUtils.h" +#import "TGLocationUtils.h" +#import "WKInterfaceImage+Signals.h" + +#import "TGInputController.h" + +#import "TGMessageViewModel.h" + +#import "TGBridgeMediaSignals.h" +#import "TGBridgeClient.h" +#import "TGBridgeSubscriptions.h" +#import "TGBridgeChatMessages.h" +#import "TGBridgeMessage.h" +#import "TGBridgeChat.h" +#import "TGBridgeUser.h" +#import "TGBridgeUserCache.h" + +#import "TGBridgePeerIdAdapter.h" + +#import +#import + +@interface TGNotificationController() +{ + NSString *_currentAvatarPhoto; + SMetaDisposable *_disposable; +} +@end + +@implementation TGNotificationController + +- (instancetype)init +{ + self = [super init]; + if (self != nil) + { + _disposable = [[SMetaDisposable alloc] init]; + } + return self; +} + +- (void)dealloc +{ + [_disposable dispose]; +} + +- (void)didReceiveNotification:(UNNotification *)notification +{ + UNNotificationContent *content = notification.request.content; + NSString *titleText = content.title; + NSString *bodyText = content.body; + + if (titleText > 0){ + self.nameLabel.hidden = false; + self.nameLabel.text = titleText; + } + self.messageTextLabel.text = bodyText; + + [self processMessageWithUserInfo:content.userInfo defaultTitle:titleText defaultBody:bodyText completion:nil]; +} + +- (void)didReceiveLocalNotification:(UILocalNotification *)localNotification withCompletion:(void (^)(WKUserNotificationInterfaceType))completionHandler +{ + [self processMessageWithUserInfo:localNotification.userInfo defaultTitle:localNotification.alertTitle defaultBody:localNotification.alertBody completion:completionHandler]; +} + +- (void)didReceiveRemoteNotification:(NSDictionary *)remoteNotification withCompletion:(void (^)(WKUserNotificationInterfaceType))completionHandler +{ + NSString *titleText = remoteNotification[@"aps"][@"alert"][@"title"]; + NSString *bodyText = remoteNotification[@"aps"][@"alert"][@"body"]; + [self processMessageWithUserInfo:remoteNotification defaultTitle:titleText defaultBody:bodyText completion:completionHandler]; +} + +- (void)processMessageWithUserInfo:(NSDictionary *)userInfo defaultTitle:(NSString *)defaultTitle defaultBody:(NSString *)defaultBody completion:(void (^)(WKUserNotificationInterfaceType))completionHandler +{ + NSString *fromId = userInfo[@"from_id"]; + NSString *chatId = userInfo[@"chat_id"]; + NSString *channelId = userInfo[@"channel_id"]; + NSString *mid = userInfo[@"msg_id"]; + + int64_t peerId = 0; + if (fromId != nil) { + peerId = [fromId longLongValue]; + } else if (chatId != nil) { + peerId = TGPeerIdFromGroupId([chatId integerValue]); + } else if (channelId != nil) { + peerId = TGPeerIdFromChannelId([channelId integerValue]); + } + int32_t messageId = [mid intValue]; + + if (peerId == 0 || messageId == 0) + { + if (defaultTitle.length > 0){ + self.nameLabel.hidden = false; + self.nameLabel.text = defaultTitle; + } + self.messageTextLabel.text = defaultBody; + if (completionHandler != nil) + completionHandler(WKUserNotificationInterfaceTypeCustom); + return; + } + + NSLog(@"[Notification] processing message peerId: %lld mid: %d", peerId, messageId); + TGBridgeChatMessageSubscription *subscription = [[TGBridgeChatMessageSubscription alloc] initWithPeerId:peerId messageId:messageId]; + NSData *data = [NSKeyedArchiver archivedDataWithRootObject:subscription]; + + __weak TGNotificationController *weakSelf = self; + SSignal *signal = [[TGBridgeClient instance] sendMessageData:data]; + [_disposable setDisposable:[[signal timeout:4.5 onQueue:[SQueue mainQueue] orSignal:[SSignal single:@0]] startWithNext:^(NSData *messageData) { + __strong TGNotificationController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + if ([messageData isKindOfClass:[NSData class]]) { + NSLog(@"[Notification] Received message data, applying"); + + TGBridgeResponse *response = [NSKeyedUnarchiver unarchiveObjectWithData:messageData]; + NSDictionary *message = response.next; + [strongSelf updateWithMessage:message[TGBridgeMessageKey] users:message[TGBridgeUsersDictionaryKey] chat:message[TGBridgeChatKey] completion:completionHandler]; + } + else { + NSLog(@"[Notification] 4.5 sec timeout, fallback to apns data"); + + strongSelf.nameLabel.hidden = false; + strongSelf.nameLabel.text = defaultTitle; + strongSelf.messageTextLabel.text = defaultBody; + if (completionHandler != nil) + completionHandler(WKUserNotificationInterfaceTypeCustom); + } + } error:^(id error) + { + __strong TGNotificationController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + NSLog(@"[Notification] getMessage error, fallback to apns data"); + + strongSelf.nameLabel.hidden = false; + strongSelf.nameLabel.text = defaultTitle; + strongSelf.messageTextLabel.text = defaultBody; + if (completionHandler != nil) + completionHandler(WKUserNotificationInterfaceTypeCustom); + } completed:nil]]; +} + +- (void)updateWithMessage:(TGBridgeMessage *)message users:(NSDictionary *)users chat:(TGBridgeChat *)chat completion:(void (^)(WKUserNotificationInterfaceType))completionHandler +{ + [[TGBridgeUserCache instance] storeUsers:[users allValues]]; + + bool mediaGroupHidden = true; + bool mapGroupHidden = true; + bool fileGroupHidden = true; + bool stickerGroupHidden = true; + bool captionGroupHidden = true; + + TGBridgeForwardedMessageMediaAttachment *forwardAttachment = nil; + TGBridgeReplyMessageMediaAttachment *replyAttachment = nil; + NSString *messageText = nil; + + __block NSInteger completionCount = 1; + void (^completionBlock)(void) = ^ + { + completionCount--; + if (completionCount == 0 && completionHandler != nil) + completionHandler(WKUserNotificationInterfaceTypeCustom); + }; + + for (TGBridgeMediaAttachment *attachment in message.media) + { + if ([attachment isKindOfClass:[TGBridgeForwardedMessageMediaAttachment class]]) + { + forwardAttachment = (TGBridgeForwardedMessageMediaAttachment *)attachment; + } + else if ([attachment isKindOfClass:[TGBridgeReplyMessageMediaAttachment class]]) + { + replyAttachment = (TGBridgeReplyMessageMediaAttachment *)attachment; + } + else if ([attachment isKindOfClass:[TGBridgeImageMediaAttachment class]]) + { + mediaGroupHidden = false; + + TGBridgeImageMediaAttachment *imageAttachment = (TGBridgeImageMediaAttachment *)attachment; + + completionCount++; + + CGSize imageSize = CGSizeZero; + [TGMessageViewModel updateMediaGroup:self.mediaGroup activityIndicator:nil attachment:imageAttachment message:message notification:true currentPhoto:NULL standalone:true margin:1.5f imageSize:&imageSize isVisible:nil completion:completionBlock]; + + self.mediaGroup.width = imageSize.width; + self.mediaGroup.height = imageSize.height; + + self.durationGroup.hidden = true; + } + else if ([attachment isKindOfClass:[TGBridgeVideoMediaAttachment class]]) + { + mediaGroupHidden = false; + + TGBridgeVideoMediaAttachment *videoAttachment = (TGBridgeVideoMediaAttachment *)attachment; + + completionCount++; + + CGSize imageSize = CGSizeZero; + [TGMessageViewModel updateMediaGroup:self.mediaGroup activityIndicator:nil attachment:videoAttachment message:message notification:true currentPhoto:NULL standalone:true margin:1.5f imageSize:&imageSize isVisible:nil completion:completionBlock]; + + self.mediaGroup.width = imageSize.width; + self.mediaGroup.height = imageSize.height; + if (videoAttachment.round) + self.mediaGroup.cornerRadius = imageSize.width / 2.0f; + + self.durationGroup.hidden = false; + + NSInteger durationMinutes = floor(videoAttachment.duration / 60.0); + NSInteger durationSeconds = videoAttachment.duration % 60; + self.durationLabel.text = [NSString stringWithFormat:@"%ld:%02ld", (long)durationMinutes, (long)durationSeconds]; + } + else if ([attachment isKindOfClass:[TGBridgeDocumentMediaAttachment class]]) + { + TGBridgeDocumentMediaAttachment *documentAttachment = (TGBridgeDocumentMediaAttachment *)attachment; + + if (documentAttachment.isSticker) + { + stickerGroupHidden = false; + + completionCount++; + + [TGStickerViewModel updateWithMessage:message notification:true isGroup:false context:nil currentDocumentId:NULL authorLabel:nil imageGroup:self.stickerGroup isVisible:nil completion:completionBlock]; + } + else if (documentAttachment.isAudio && documentAttachment.isVoice) + { + fileGroupHidden = false; + + self.titleLabel.text = TGLocalized(@"Message.Audio"); + + NSInteger durationMinutes = floor(documentAttachment.duration / 60.0); + NSInteger durationSeconds = documentAttachment.duration % 60; + self.subtitleLabel.text = [NSString stringWithFormat:@"%ld:%02ld", (long)durationMinutes, (long)durationSeconds]; + + self.audioGroup.hidden = false; + self.fileIconGroup.hidden = true; + self.venueIcon.hidden = true; + } + else + { + fileGroupHidden = false; + + self.titleLabel.text = documentAttachment.fileName; + self.subtitleLabel.text = [TGStringUtils stringForFileSize:documentAttachment.fileSize precision:2]; + + self.fileIconGroup.hidden = false; + self.audioGroup.hidden = true; + self.venueIcon.hidden = true; + } + } + else if ([attachment isKindOfClass:[TGBridgeAudioMediaAttachment class]]) + { + fileGroupHidden = false; + + TGBridgeAudioMediaAttachment *audioAttachment = (TGBridgeAudioMediaAttachment *)attachment; + + self.titleLabel.text = TGLocalized(@"Message.Audio"); + + NSInteger durationMinutes = floor(audioAttachment.duration / 60.0); + NSInteger durationSeconds = audioAttachment.duration % 60; + self.subtitleLabel.text = [NSString stringWithFormat:@"%ld:%02ld", (long)durationMinutes, (long)durationSeconds]; + + self.audioGroup.hidden = false; + self.fileIconGroup.hidden = true; + self.venueIcon.hidden = true; + } + else if ([attachment isKindOfClass:[TGBridgeLocationMediaAttachment class]]) + { + mapGroupHidden = false; + + TGBridgeLocationMediaAttachment *locationAttachment = (TGBridgeLocationMediaAttachment *)attachment; + + CLLocationCoordinate2D coordinate = CLLocationCoordinate2DMake([TGLocationUtils adjustGMapLatitude:locationAttachment.latitude withPixelOffset:-10 zoom:15], locationAttachment.longitude); + self.map.region = MKCoordinateRegionMake(coordinate, MKCoordinateSpanMake(0.003, 0.003)); + self.map.centerPinCoordinate = CLLocationCoordinate2DMake(locationAttachment.latitude, locationAttachment.longitude); + + if (locationAttachment.venue != nil) + { + fileGroupHidden = false; + + self.titleLabel.text = locationAttachment.venue.title; + self.subtitleLabel.text = locationAttachment.venue.address; + } + + self.audioGroup.hidden = true; + self.fileIconGroup.hidden = true; + self.venueIcon.hidden = false; + } + else if ([attachment isKindOfClass:[TGBridgeContactMediaAttachment class]]) + { + fileGroupHidden = false; + + TGBridgeContactMediaAttachment *contactAttachment = (TGBridgeContactMediaAttachment *)attachment; + + self.audioGroup.hidden = true; + self.fileIconGroup.hidden = true; + self.venueIcon.hidden = true; + + self.titleLabel.text = [contactAttachment displayName]; + self.subtitleLabel.text = contactAttachment.prettyPhoneNumber; + } + else if ([attachment isKindOfClass:[TGBridgeActionMediaAttachment class]]) + { + messageText = [TGMessageViewModel stringForActionAttachment:(TGBridgeActionMediaAttachment *)attachment message:message users:users forChannel:(chat.isChannel && !chat.isChannelGroup)]; + } + else if ([attachment isKindOfClass:[TGBridgeUnsupportedMediaAttachment class]]) + { + fileGroupHidden = false; + + TGBridgeUnsupportedMediaAttachment *unsupportedAttachment = (TGBridgeUnsupportedMediaAttachment *)attachment; + + self.titleLabel.text = unsupportedAttachment.title; + self.subtitleLabel.text = unsupportedAttachment.subtitle; + + self.fileIconGroup.hidden = true; + self.audioGroup.hidden = true; + self.venueIcon.hidden = true; + } + } + + if (messageText == nil) + messageText = message.text; + + id forwardPeer = nil; + if (forwardAttachment != nil) + { + if (TGPeerIdIsChannel(forwardAttachment.peerId)) + forwardPeer = users[@(forwardAttachment.peerId)]; + else + forwardPeer = [[TGBridgeUserCache instance] userWithId:(int32_t)forwardAttachment.peerId]; + } + [TGMessageViewModel updateForwardHeaderGroup:self.forwardHeaderGroup titleLabel:self.forwardTitleLabel fromLabel:self.forwardFromLabel forwardAttachment:forwardAttachment forwardPeer:forwardPeer textColor:[UIColor blackColor]]; + + if (replyAttachment != nil) + { + self.replyHeaderImageGroup.hidden = true; + completionCount++; + } + + [TGMessageViewModel updateReplyHeaderGroup:self.replyHeaderGroup authorLabel:self.replyAuthorNameLabel imageGroup:nil textLabel:self.replyMessageTextLabel titleColor:[UIColor blackColor] subtitleColor:[UIColor hexColor:0x7e7e81] replyAttachment:replyAttachment currentReplyPhoto:NULL isVisible:nil completion:completionBlock]; + + self.mediaGroup.hidden = mediaGroupHidden; + self.mapGroup.hidden = mapGroupHidden; + self.fileGroup.hidden = fileGroupHidden; + self.captionGroup.hidden = captionGroupHidden; + self.stickerGroup.hidden = stickerGroupHidden; + self.stickerWrapperGroup.hidden = stickerGroupHidden; + + self.wrapperGroup.hidden = (self.mediaGroup.hidden && self.mapGroup.hidden && self.fileGroup.hidden && self.stickerGroup.hidden); + + if (chat.isGroup || chat.isChannelGroup) + { + self.chatTitleLabel.text = chat.groupTitle; + self.chatTitleLabel.hidden = false; + } + + self.nameLabel.hidden = false; + if (chat.isChannel && !chat.isChannelGroup) + self.nameLabel.text = chat.groupTitle; + else + self.nameLabel.text = [users[@(message.fromUid)] displayName]; + + self.messageTextLabel.hidden = (messageText.length == 0); + if (!self.messageTextLabel.hidden) + self.messageTextLabel.text = messageText; + + completionBlock(); +} + +- (NSArray *)suggestionsForResponseToActionWithIdentifier:(NSString *)identifier forLocalNotification:(UILocalNotification *)localNotification inputLanguage:(NSString *)inputLanguage +{ + return [TGInputController suggestionsForText:nil]; +} + +- (NSArray *)suggestionsForResponseToActionWithIdentifier:(NSString *)identifier forRemoteNotification:(NSDictionary *)remoteNotification inputLanguage:(NSString *)inputLanguage +{ + return [TGInputController suggestionsForText:nil]; +} + +@end diff --git a/Watch/Extension/TGProfilePhotoController.h b/Watch/Extension/TGProfilePhotoController.h new file mode 100644 index 0000000000..25db4f64cc --- /dev/null +++ b/Watch/Extension/TGProfilePhotoController.h @@ -0,0 +1,17 @@ +#import "TGInterfaceController.h" + +@interface TGProfilePhotoControllerContext : NSObject + +@property (nonatomic, readonly) int64_t identifier; +@property (nonatomic, readonly) NSString *imageUrl; + +- (instancetype)initWithIdentifier:(int64_t)identifier imageUrl:(NSString *)imageUrl; + +@end + +@interface TGProfilePhotoController : TGInterfaceController + +@property (nonatomic, weak) IBOutlet WKInterfaceGroup *imageGroup; +@property (nonatomic, weak) IBOutlet WKInterfaceImage *activityIndicator; + +@end diff --git a/Watch/Extension/TGProfilePhotoController.m b/Watch/Extension/TGProfilePhotoController.m new file mode 100644 index 0000000000..71a16105de --- /dev/null +++ b/Watch/Extension/TGProfilePhotoController.m @@ -0,0 +1,90 @@ +#import "TGProfilePhotoController.h" +#import "TGWatchCommon.h" +#import "WKInterfaceGroup+Signals.h" +#import "TGBridgeMediaSignals.h" + +NSString *const TGProfilePhotoControllerIdentifier = @"TGProfilePhotoController"; + +@implementation TGProfilePhotoControllerContext + +- (instancetype)initWithIdentifier:(int64_t)identifier imageUrl:(NSString *)imageUrl +{ + self = [super init]; + if (self != nil) + { + _identifier = identifier; + _imageUrl = imageUrl; + } + return self; +} + +@end + +@interface TGProfilePhotoController () +{ + TGProfilePhotoControllerContext *_context; +} +@end + +@implementation TGProfilePhotoController + +- (instancetype)init +{ + self = [super init]; + if (self != nil) + { + + } + return self; +} + +- (void)configureWithContext:(TGProfilePhotoControllerContext *)context +{ + _context = context; + + self.title = TGLocalized(@"Watch.PhotoView.Title"); + + __weak TGProfilePhotoController *weakSelf = self; + [self.imageGroup setBackgroundImageSignal:[[TGBridgeMediaSignals avatarWithPeerId:_context.identifier url:_context.imageUrl type:TGBridgeMediaAvatarTypeLarge] onNext:^(id next) + { + __strong TGProfilePhotoController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + if (next != nil) + { + strongSelf.imageGroup.alpha = 0.0f; + strongSelf.activityIndicator.hidden = true; + [strongSelf animateWithDuration:0.25f animations:^ + { + strongSelf.imageGroup.alpha = 1.0f; + }]; + } + }] isVisible:^bool + { + __strong TGProfilePhotoController *strongSelf = weakSelf; + if (strongSelf == nil) + return false; + + return strongSelf.isVisible; + }]; +} + +- (void)willActivate +{ + [super willActivate]; + + [self.imageGroup updateIfNeeded]; +} + +- (void)didDeactivate +{ + [super didDeactivate]; +} + ++ (NSString *)identifier +{ + return TGProfilePhotoControllerIdentifier; +} + +@end diff --git a/Watch/Extension/TGStickerPackRowController.h b/Watch/Extension/TGStickerPackRowController.h new file mode 100644 index 0000000000..a53bb3b3ea --- /dev/null +++ b/Watch/Extension/TGStickerPackRowController.h @@ -0,0 +1,13 @@ +#import "WKInterfaceTable+TGDataDrivenTable.h" + +@class TGBridgeStickerPack; + +@interface TGStickerPackRowController : TGTableRowController + +@property (nonatomic, weak) IBOutlet WKInterfaceImage *image; +@property (nonatomic, weak) IBOutlet WKInterfaceLabel *nameLabel; +@property (nonatomic, weak) IBOutlet WKInterfaceLabel *countLabel; + +- (void)updateWithStickerPack:(TGBridgeStickerPack *)stickerPack; + +@end diff --git a/Watch/Extension/TGStickerPackRowController.m b/Watch/Extension/TGStickerPackRowController.m new file mode 100644 index 0000000000..3e5ac43eb1 --- /dev/null +++ b/Watch/Extension/TGStickerPackRowController.m @@ -0,0 +1,31 @@ +#import "TGStickerPackRowController.h" +#import "TGWatchCommon.h" +#import "TGStringUtils.h" + +#import "TGBridgeStickerPack.h" + +#import "WKInterfaceImage+Signals.h" +#import "TGBridgeMediaSignals.h" + +NSString *const TGStickerPackRowIdentifier = @"TGStickerPackRow"; + +@implementation TGStickerPackRowController + +- (void)updateWithStickerPack:(TGBridgeStickerPack *)stickerPack +{ + [self.image setSignal:[TGBridgeMediaSignals stickerWithDocumentId:0 packId:0 accessHash:0 type:TGMediaStickerImageTypeList] isVisible:self.isVisible]; + self.nameLabel.text = stickerPack.title; + self.countLabel.text = [[NSString alloc] initWithFormat:TGLocalized([TGStringUtils integerValueFormat:@"StickerPack.StickerCount_" value:stickerPack.documents.count]), [[NSString alloc] initWithFormat:@"%d", (int)stickerPack.documents.count]]; +} + +- (void)notifyVisiblityChange +{ + [self.image updateIfNeeded]; +} + ++ (NSString *)identifier +{ + return TGStickerPackRowIdentifier; +} + +@end diff --git a/Watch/Extension/TGStickerPacksController.h b/Watch/Extension/TGStickerPacksController.h new file mode 100644 index 0000000000..b755c3c86a --- /dev/null +++ b/Watch/Extension/TGStickerPacksController.h @@ -0,0 +1,19 @@ +#import "TGInterfaceController.h" + +@class TGBridgeStickerPack; + +@interface TGStickerPacksControllerContext : NSObject + +@property (nonatomic, readonly) NSArray *stickerPacks; +@property (nonatomic, copy) void (^completionBlock)(TGBridgeStickerPack *stickerPack); + +- (instancetype)initWithStickerPacks:(NSArray *)stickerPacks; + +@end + +@interface TGStickerPacksController : TGInterfaceController + +@property (nonatomic, weak) IBOutlet WKInterfaceTable *table; +@property (nonatomic, weak) IBOutlet WKInterfaceImage *activityIndicator; + +@end diff --git a/Watch/Extension/TGStickerPacksController.m b/Watch/Extension/TGStickerPacksController.m new file mode 100644 index 0000000000..815817e748 --- /dev/null +++ b/Watch/Extension/TGStickerPacksController.m @@ -0,0 +1,115 @@ +#import "TGStickerPacksController.h" +#import "TGWatchCommon.h" +#import "TGBridgeStickersSignals.h" +#import "TGBridgeStickerPack.h" + +#import "WKInterfaceTable+TGDataDrivenTable.h" + +#import "TGStickerPackRowController.h" + +#import "TGStickersController.h" + +NSString *const TGStickerPacksControllerIdentifier = @"TGStickerPacksController"; + +@implementation TGStickerPacksControllerContext + +- (instancetype)initWithStickerPacks:(NSArray *)stickerPacks +{ + self = [super init]; + if (self != nil) + { + _stickerPacks = stickerPacks; + } + return self; +} + +@end + +@interface TGStickerPacksController () +{ + TGStickerPacksControllerContext *_context; + + NSArray *_stickerPackModels; +} +@end + +@implementation TGStickerPacksController + +- (instancetype)init +{ + self = [super init]; + if (self != nil) + { + [self.table _setInitialHidden:true]; + self.table.tableDataSource = self; + } + return self; +} + +- (void)configureWithContext:(TGStickerPacksControllerContext *)context +{ + _context = context; + + _stickerPackModels = context.stickerPacks; + + __weak TGStickerPacksController *weakSelf = self; + [self performInterfaceUpdate:^(bool animated) + { + __strong TGStickerPacksController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + strongSelf.activityIndicator.hidden = true; + [strongSelf.table reloadData]; + }]; +} + +- (void)willActivate +{ + [super willActivate]; + + [self.table notifyVisiblityChange]; +} + +#pragma mark - + +- (Class)table:(WKInterfaceTable *)table rowControllerClassAtIndexPath:(NSIndexPath *)indexPath +{ + return [TGStickerPackRowController class]; +} + +- (NSUInteger)numberOfRowsInTable:(WKInterfaceTable *)table section:(NSUInteger)section +{ + return _stickerPackModels.count; +} + +- (void)table:(WKInterfaceTable *)table updateRowController:(TGStickerPackRowController *)controller forIndexPath:(TGIndexPath *)indexPath +{ + __weak TGStickerPacksController *weakSelf = self; + controller.isVisible = ^bool + { + __strong TGStickerPacksController *strongSelf = weakSelf; + if (strongSelf == nil) + return false; + + return strongSelf.isVisible; + }; + [controller updateWithStickerPack:_stickerPackModels[indexPath.row]]; +} + +- (void)table:(WKInterfaceTable *)table didSelectRowAtIndexPath:(TGIndexPath *)indexPath +{ + [self dismissController]; + + if (_context.completionBlock != nil) + _context.completionBlock(_stickerPackModels[indexPath.row]); +} + +#pragma mark - + ++ (NSString *)identifier +{ + return TGStickerPacksControllerIdentifier; +} + +@end diff --git a/Watch/Extension/TGStickersController.h b/Watch/Extension/TGStickersController.h new file mode 100644 index 0000000000..4f1c6eea82 --- /dev/null +++ b/Watch/Extension/TGStickersController.h @@ -0,0 +1,21 @@ +#import "TGInterfaceController.h" + +@class TGBridgeContext; +@class TGBridgeStickerPack; +@class TGBridgeDocumentMediaAttachment; + +@interface TGStickersControllerContext : NSObject + +@property (nonatomic, copy) void (^completionBlock)(TGBridgeDocumentMediaAttachment *sticker); + +@end + +@interface TGStickersController : TGInterfaceController + +@property (nonatomic, weak) IBOutlet WKInterfaceTable *table; +@property (nonatomic, weak) IBOutlet WKInterfaceImage *activityIndicator; + +@property (nonatomic, weak) IBOutlet WKInterfaceGroup *alertGroup; +@property (nonatomic, weak) IBOutlet WKInterfaceLabel *alertLabel; + +@end diff --git a/Watch/Extension/TGStickersController.m b/Watch/Extension/TGStickersController.m new file mode 100644 index 0000000000..90d0f45125 --- /dev/null +++ b/Watch/Extension/TGStickersController.m @@ -0,0 +1,230 @@ +#import "TGStickersController.h" +#import "TGWatchCommon.h" +#import "TGBridgeStickersSignals.h" +#import "TGBridgeStickerPack.h" + +#import "WKInterfaceTable+TGDataDrivenTable.h" +#import "TGTableDeltaUpdater.h" + +#import "TGStickersHeaderController.h" +#import "TGStickersSectionHeaderController.h" +#import "TGStickersRowController.h" + +#import "TGStickerPacksController.h" + +NSString *const TGStickersControllerIdentifier = @"TGStickersController"; + +@implementation TGStickersControllerContext + +@end + + +@interface TGStickersController () +{ + TGStickersControllerContext *_context; + + TGBridgeStickerPack *_stickerPack; + + SMetaDisposable *_stickerPacksDisposable; + SMetaDisposable *_recentStickersDisposable; + NSArray *_stickerPackModels; + NSArray *_stickerModels; +} +@end + +@implementation TGStickersController + +- (instancetype)init +{ + self = [super init]; + if (self != nil) + { + _stickerPacksDisposable = [[SMetaDisposable alloc] init]; + _recentStickersDisposable = [[SMetaDisposable alloc] init]; + + [self.alertGroup _setInitialHidden:true]; + [self.table _setInitialHidden:true]; + self.table.tableDataSource = self; + } + return self; +} + +- (void)dealloc +{ + [_stickerPacksDisposable dispose]; + [_recentStickersDisposable dispose]; +} + +- (void)configureWithContext:(TGStickersControllerContext *)context +{ + _context = context; + + [self reloadData]; +} + +- (void)reloadData +{ + __weak TGStickersController *weakSelf = self; + void (^updateInteface)(bool, NSArray *, bool) = ^(bool initial, NSArray *oldData, bool recent) + { + __strong TGStickersController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf performInterfaceUpdate:^(bool animated) + { + __strong TGStickersController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + if (recent && strongSelf->_stickerModels.count == 0) + { + strongSelf.alertGroup.hidden = false; + strongSelf.alertLabel.text = TGLocalized(@"Watch.Stickers.RecentPlaceholder"); + } + else + { + strongSelf.alertGroup.hidden = true; + } + + strongSelf.activityIndicator.hidden = true; + strongSelf.table.hidden = false; + + [strongSelf.table reloadData]; + }]; + }; + + if (_stickerPack == nil) + { +// [_stickerPacksDisposable setDisposable:[[[TGBridgeStickersSignals stickerPacks] deliverOn:[SQueue mainQueue]] startWithNext:^(NSArray *stickerPacks) +// { +// __strong TGStickersController *strongSelf = weakSelf; +// if (strongSelf == nil) +// return; +// +// strongSelf->_stickerPackModels = stickerPacks; +// }]]; + [_recentStickersDisposable setDisposable:[[[TGBridgeStickersSignals recentStickersWithLimit:24] deliverOn:[SQueue mainQueue]] startWithNext:^(NSArray *recent) + { + __strong TGStickersController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + NSArray *currentStickerModels = strongSelf->_stickerModels; + bool initial = currentStickerModels == nil; + if (![currentStickerModels isEqual:recent]) { + strongSelf->_stickerModels = recent; + updateInteface(initial, currentStickerModels, true); + } + }]]; + } + else + { + [_recentStickersDisposable setDisposable:nil]; + + _stickerModels = _stickerPack.documents; + updateInteface(true, nil, false); + } +} + +- (void)willActivate +{ + [super willActivate]; + + [self.table notifyVisiblityChange]; +} + +#pragma mark - + +- (NSUInteger)numberOfSectionsInTable:(WKInterfaceTable *)table +{ + return 1; +} + +- (NSUInteger)numberOfRowsInTable:(WKInterfaceTable *)table section:(NSUInteger)section +{ + return (NSInteger)ceilf(_stickerModels.count / 2.0f); +} + +- (Class)table:(WKInterfaceTable *)table rowControllerClassAtIndexPath:(TGIndexPath *)indexPath +{ + return [TGStickersRowController class]; +} + +- (void)table:(WKInterfaceTable *)table updateRowController:(TGStickersRowController *)controller forIndexPath:(TGIndexPath *)indexPath +{ + NSInteger leftIndex = indexPath.row * 2; + NSInteger rightIndex = indexPath.row * 2 + 1; + + TGBridgeDocumentMediaAttachment *leftSticker = nil; + if (leftIndex < _stickerModels.count) + leftSticker = _stickerModels[leftIndex]; + + TGBridgeDocumentMediaAttachment *rightSticker = nil; + if (rightIndex < _stickerModels.count) + rightSticker = _stickerModels[rightIndex]; + + __weak TGStickersController *weakSelf = self; + controller.isVisible = ^bool + { + __strong TGStickersController *strongSelf = weakSelf; + if (strongSelf == nil) + return false; + + return strongSelf.isVisible; + }; + + [controller updateWithLeftSticker:leftSticker rightSticker:rightSticker]; + + if (leftSticker != nil) + { + controller.leftStickerPressed = ^ + { + __strong TGStickersController *strongSelf = weakSelf; + if (strongSelf != nil) + [strongSelf completeWithSticker:leftSticker]; + }; + } + + if (rightSticker != nil) + { + controller.rightStickerPressed = ^ + { + __strong TGStickersController *strongSelf = weakSelf; + if (strongSelf != nil) + [strongSelf completeWithSticker:rightSticker]; + }; + } +} + +- (id)contextForSegueWithIdentifer:(NSString *)segueIdentifier table:(WKInterfaceTable *)table indexPath:(TGIndexPath *)indexPath +{ + __weak TGStickersController *weakSelf = self; + TGStickerPacksControllerContext *context = [[TGStickerPacksControllerContext alloc] initWithStickerPacks:_stickerPackModels]; + context.completionBlock = ^(TGBridgeStickerPack *stickerPack) + { + __strong TGStickersController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + strongSelf->_stickerPack = stickerPack; + [strongSelf reloadData]; + }; + + return context; +} + +- (void)completeWithSticker:(TGBridgeDocumentMediaAttachment *)sticker +{ + if (_context.completionBlock != nil) + _context.completionBlock(sticker); + + [self dismissController]; +} + ++ (NSString *)identifier +{ + return TGStickersControllerIdentifier; +} + +@end diff --git a/Watch/Extension/TGStickersHeaderController.h b/Watch/Extension/TGStickersHeaderController.h new file mode 100644 index 0000000000..ca76e48b4b --- /dev/null +++ b/Watch/Extension/TGStickersHeaderController.h @@ -0,0 +1,9 @@ +#import "WKInterfaceTable+TGDataDrivenTable.h" + +@interface TGStickersHeaderController : TGTableRowController + +@property (nonatomic, weak) IBOutlet WKInterfaceLabel *nameLabel; + +- (void)update; + +@end diff --git a/Watch/Extension/TGStickersHeaderController.m b/Watch/Extension/TGStickersHeaderController.m new file mode 100644 index 0000000000..01ddd91514 --- /dev/null +++ b/Watch/Extension/TGStickersHeaderController.m @@ -0,0 +1,18 @@ +#import "TGStickersHeaderController.h" +#import "TGWatchCommon.h" + +NSString *const TGStickersHeaderIdentifier = @"TGStickersHeader"; + +@implementation TGStickersHeaderController + +- (void)update +{ + self.nameLabel.text = TGLocalized(@"Watch.Stickers.StickerPacks"); +} + ++ (NSString *)identifier +{ + return TGStickersHeaderIdentifier; +} + +@end diff --git a/Watch/Extension/TGStickersRowController.h b/Watch/Extension/TGStickersRowController.h new file mode 100644 index 0000000000..7656c31c52 --- /dev/null +++ b/Watch/Extension/TGStickersRowController.h @@ -0,0 +1,18 @@ +#import "WKInterfaceTable+TGDataDrivenTable.h" + +@class TGBridgeDocumentMediaAttachment; + +@interface TGStickersRowController : TGTableRowController + +@property (nonatomic, weak) IBOutlet WKInterfaceGroup *leftStickerImageGroup; +@property (nonatomic, weak) IBOutlet WKInterfaceGroup *rightStickerImageGroup; + +- (IBAction)leftStickerPressedAction; +- (IBAction)rightStickerPressedAction; + +@property (nonatomic, copy) void (^leftStickerPressed)(void); +@property (nonatomic, copy) void (^rightStickerPressed)(void); + +- (void)updateWithLeftSticker:(TGBridgeDocumentMediaAttachment *)leftSticker rightSticker:(TGBridgeDocumentMediaAttachment *)rightSticker; + +@end diff --git a/Watch/Extension/TGStickersRowController.m b/Watch/Extension/TGStickersRowController.m new file mode 100644 index 0000000000..ca206876eb --- /dev/null +++ b/Watch/Extension/TGStickersRowController.m @@ -0,0 +1,40 @@ +#import "TGStickersRowController.h" + +#import "WKInterfaceGroup+Signals.h" +#import "TGBridgeMediaSignals.h" +#import "TGBridgeDocumentMediaAttachment.h" + +NSString *const TGStickersRowIdentifier = @"TGStickersRow"; + +@implementation TGStickersRowController + +- (IBAction)leftStickerPressedAction +{ + if (self.leftStickerPressed != nil) + self.leftStickerPressed(); +} + +- (IBAction)rightStickerPressedAction +{ + if (self.rightStickerPressed != nil) + self.rightStickerPressed(); +} + +- (void)updateWithLeftSticker:(TGBridgeDocumentMediaAttachment *)leftSticker rightSticker:(TGBridgeDocumentMediaAttachment *)rightSticker +{ + [self.leftStickerImageGroup setBackgroundImageSignal:[TGBridgeMediaSignals stickerWithDocumentId:leftSticker.documentId packId:leftSticker.stickerPackId accessHash:leftSticker.stickerPackAccessHash type:TGMediaStickerImageTypeNormal] isVisible:self.isVisible]; + [self.rightStickerImageGroup setBackgroundImageSignal:[TGBridgeMediaSignals stickerWithDocumentId:rightSticker.documentId packId:rightSticker.stickerPackId accessHash:rightSticker.stickerPackAccessHash type:TGMediaStickerImageTypeNormal] isVisible:self.isVisible]; +} + +- (void)notifyVisiblityChange +{ + [self.leftStickerImageGroup updateIfNeeded]; + [self.rightStickerImageGroup updateIfNeeded]; +} + ++ (NSString *)identifier +{ + return TGStickersRowIdentifier; +} + +@end diff --git a/Watch/Extension/TGStickersSectionHeaderController.h b/Watch/Extension/TGStickersSectionHeaderController.h new file mode 100644 index 0000000000..507acf8c2c --- /dev/null +++ b/Watch/Extension/TGStickersSectionHeaderController.h @@ -0,0 +1,9 @@ +#import "WKInterfaceTable+TGDataDrivenTable.h" + +@interface TGStickersSectionHeaderController : TGTableRowController + +@property (nonatomic, weak) IBOutlet WKInterfaceLabel *titleLabel; + +@property (nonatomic, strong) NSString *title; + +@end diff --git a/Watch/Extension/TGStickersSectionHeaderController.m b/Watch/Extension/TGStickersSectionHeaderController.m new file mode 100644 index 0000000000..34cc40e2bb --- /dev/null +++ b/Watch/Extension/TGStickersSectionHeaderController.m @@ -0,0 +1,23 @@ +#import "TGStickersSectionHeaderController.h" +#import "TGWatchCommon.h" + +NSString *const TGStickersSectionHeaderIdentifier = @"TGStickersSectionHeader"; + +@implementation TGStickersSectionHeaderController + +- (NSString *)title +{ + return self.titleLabel.text; +} + +- (void)setTitle:(NSString *)title +{ + self.titleLabel.text = title; +} + ++ (NSString *)identifier +{ + return TGStickersSectionHeaderIdentifier; +} + +@end diff --git a/Watch/Extension/TGStringUtils.h b/Watch/Extension/TGStringUtils.h new file mode 100644 index 0000000000..4b250c7cb0 --- /dev/null +++ b/Watch/Extension/TGStringUtils.h @@ -0,0 +1,42 @@ +#import + +#ifdef __cplusplus +extern "C" { +#endif + + int32_t murMurHash32(NSString *string); + int32_t murMurHashBytes32(void *bytes, int length); + + bool TGIsRTL(); + bool TGIsArabic(); + bool TGIsKorean(); + bool TGIsLocaleArabic(); + +#ifdef __cplusplus +} +#endif + +@interface TGStringUtils : NSObject + ++ (bool)stringContainsEmojiOnly:(NSString *)string length:(NSUInteger *)length; + ++ (NSString *)stringWithLocalizedNumber:(NSInteger)number; ++ (NSString *)stringWithLocalizedNumberCharacters:(NSString *)string; + ++ (NSString *)stringForFileSize:(NSUInteger)size precision:(NSInteger)precision; + ++ (NSString *)initialsForFirstName:(NSString *)firstName lastName:(NSString *)lastName single:(bool)single; ++ (NSString *)initialForGroupName:(NSString *)groupName; + ++ (NSString *)integerValueFormat:(NSString *)prefix value:(NSInteger)value; + ++ (NSString *)md5WithString:(NSString *)string; + +@end + + +@interface NSString (NSArrayFormatExtension) + ++ (id)stringWithFormat:(NSString *)format array:(NSArray*) arguments; + +@end diff --git a/Watch/Extension/TGStringUtils.m b/Watch/Extension/TGStringUtils.m new file mode 100644 index 0000000000..49041590e9 --- /dev/null +++ b/Watch/Extension/TGStringUtils.m @@ -0,0 +1,231 @@ +#import "TGStringUtils.h" +#import "TGWatchCommon.h" +#import + +bool TGIsRTL() +{ + static bool value = false; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^ + { + value = ([NSLocale characterDirectionForLanguage:[[NSLocale preferredLanguages] objectAtIndex:0]] == NSLocaleLanguageDirectionRightToLeft); + }); + + return value; +} + +bool TGIsArabic() +{ + static bool value = false; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^ + { + NSString *language = [[NSLocale preferredLanguages] objectAtIndex:0]; + value = [language isEqualToString:@"ar"]; + }); + return value; +} + +bool TGIsKorean() +{ + static bool value = false; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^ + { + NSString *language = [[NSLocale preferredLanguages] objectAtIndex:0]; + value = [language isEqualToString:@"ko"]; + }); + return value; +} + +bool TGIsLocaleArabic() +{ + static bool value = false; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^ + { + NSString *identifier = [[NSLocale currentLocale] localeIdentifier]; + value = [identifier isEqualToString:@"ar"] || [identifier hasPrefix:@"ar_"]; + }); + return value; +} + +@implementation TGStringUtils + ++ (NSString *)stringWithLocalizedNumber:(NSInteger)number +{ + return [self stringWithLocalizedNumberCharacters:[[NSString alloc] initWithFormat:@"%d", (int)number]]; +} + ++ (NSString *)stringWithLocalizedNumberCharacters:(NSString *)string +{ + NSString *resultString = string; + + if (TGIsArabic()) + { + static NSString *arabicNumbers = @"٠١٢٣٤٥٦٧٨٩"; + NSMutableString *mutableString = [[NSMutableString alloc] init]; + for (int i = 0; i < (int)string.length; i++) + { + unichar c = [string characterAtIndex:i]; + if (c >= '0' && c <= '9') + [mutableString replaceCharactersInRange:NSMakeRange(mutableString.length, 0) withString:[arabicNumbers substringWithRange:NSMakeRange(c - '0', 1)]]; + else + [mutableString replaceCharactersInRange:NSMakeRange(mutableString.length, 0) withString:[string substringWithRange:NSMakeRange(i, 1)]]; + } + resultString = mutableString; + } + + return resultString; +} + ++ (NSString *)stringForFileSize:(NSUInteger)size precision:(NSInteger)precision +{ + NSString *string = @""; + if (size < 1024) + { + string = [[NSString alloc] initWithFormat:TGLocalized(@"FileSize.B"), [[NSString alloc] initWithFormat:@"%d", (int)size]];} + else if (size < 1024 * 1024) + { + string = [[NSString alloc] initWithFormat:TGLocalized(@"FileSize.KB"), [[NSString alloc] initWithFormat:@"%d", (int)(size / 1024)]]; + } + else + { + NSString *format = [NSString stringWithFormat:@"%%0.%df", (int)precision]; + string = [[NSString alloc] initWithFormat:TGLocalized(@"FileSize.MB"), [[NSString alloc] initWithFormat:format, (CGFloat)(size / 1024.0f / 1024.0f)]]; + } + + return string; +} + +static bool isEmojiCharacter(NSString *singleChar) +{ + const unichar high = [singleChar characterAtIndex:0]; + + if (0xd800 <= high && high <= 0xdbff && singleChar.length >= 2) + { + const unichar low = [singleChar characterAtIndex:1]; + const int codepoint = ((high - 0xd800) * 0x400) + (low - 0xdc00) + 0x10000; + + return (0x1d000 <= codepoint && codepoint <= 0x1f77f); + } + + return (0x2100 <= high && high <= 0x27bf); +} + ++ (NSString *)_cleanedUpString:(NSString *)string +{ + NSMutableString *__block buffer = [NSMutableString stringWithCapacity:string.length]; + + [string enumerateSubstringsInRange:NSMakeRange(0, string.length) + options:NSStringEnumerationByComposedCharacterSequences + usingBlock: ^(NSString* substring, __unused NSRange substringRange, __unused NSRange enclosingRange, __unused BOOL* stop) + { + [buffer appendString:isEmojiCharacter(substring) ? @"" : substring]; + }]; + + return buffer; +} + ++ (NSString *)initialsForFirstName:(NSString *)firstName lastName:(NSString *)lastName single:(bool)single +{ + NSString *initials = @""; + + NSString *cleanFirstName = [self _cleanedUpString:firstName]; + NSString *cleanLastName = [self _cleanedUpString:lastName]; + + if (!single && cleanFirstName.length != 0 && cleanLastName.length != 0) + initials = [[NSString alloc] initWithFormat:@"%@\u200B%@", [cleanFirstName substringToIndex:1], [cleanLastName substringToIndex:1]]; //\u200B is not rendering properly + else if (cleanFirstName.length != 0) + initials = [cleanFirstName substringToIndex:1]; + else if (cleanLastName.length != 0) + initials = [cleanLastName substringToIndex:1]; + + return [initials uppercaseString]; +} + ++ (NSString *)initialForGroupName:(NSString *)groupName +{ + NSString *initial = @" "; + NSString *cleanGroupName = [self _cleanedUpString:groupName]; + if (cleanGroupName.length > 0) + initial = [cleanGroupName substringToIndex:1]; + + return [initial uppercaseString]; +} + ++ (NSString *)integerValueFormat:(NSString *)prefix value:(NSInteger)value +{ + if (value == 1) + return [prefix stringByAppendingString:@"1"]; + else if (value == 2) + return [prefix stringByAppendingString:@"2"]; + else if (value >= 3 && value <= 10) + return [prefix stringByAppendingString:@"3_10"]; + else + return [prefix stringByAppendingString:@"any"]; +} + ++ (NSString *)md5WithString:(NSString *)string +{ + const char *ptr = [string UTF8String]; + unsigned char md5Buffer[16]; + CC_MD5(ptr, (CC_LONG)[string lengthOfBytesUsingEncoding:NSUTF8StringEncoding], md5Buffer); + NSString *output = [[NSString alloc] initWithFormat:@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x", md5Buffer[0], md5Buffer[1], md5Buffer[2], md5Buffer[3], md5Buffer[4], md5Buffer[5], md5Buffer[6], md5Buffer[7], md5Buffer[8], md5Buffer[9], md5Buffer[10], md5Buffer[11], md5Buffer[12], md5Buffer[13], md5Buffer[14], md5Buffer[15]]; + + return output; +} + ++ (bool)stringContainsEmojiOnly:(NSString *)string length:(NSUInteger *)length +{ + if (string.length == 0) + return false; + + __block bool result = true; + + __block NSUInteger count = 0; + [string enumerateSubstringsInRange:NSMakeRange(0, string.length) + options:NSStringEnumerationByComposedCharacterSequences + usingBlock: ^(NSString *substring, __unused NSRange substringRange, __unused NSRange enclosingRange, BOOL *stop) + { + if (!isEmojiCharacter(substring)) + { + result = false; + *stop = true; + } + count++; + }]; + + if (length != NULL) + *length = count; + + return result; +} + +@end + + +@implementation NSString (NSArrayFormatExtension) + ++ (instancetype)stringWithFormat:(NSString *)format array:(NSArray *)arguments +{ + switch (arguments.count) + { + case 1: + return [NSString stringWithFormat:TGLocalized(format), arguments[0]]; + + case 2: + return [NSString stringWithFormat:TGLocalized(format), arguments[0], arguments[1]]; + + case 3: + return [NSString stringWithFormat:TGLocalized(format), arguments[0], arguments[1], arguments[2]]; + + case 4: + return [NSString stringWithFormat:TGLocalized(format), arguments[0], arguments[1], arguments[2], arguments[3]]; + + default: + return TGLocalized(format); + } +} + +@end diff --git a/Watch/Extension/TGTableDeltaUpdater.h b/Watch/Extension/TGTableDeltaUpdater.h new file mode 100644 index 0000000000..f36de642f8 --- /dev/null +++ b/Watch/Extension/TGTableDeltaUpdater.h @@ -0,0 +1,17 @@ +#import + +@class TGIndexPath; + +@interface TGTableAlignment : NSObject + +@property (nonatomic, assign) bool deletion; +@property (nonatomic, assign) NSInteger pos; +@property (nonatomic, assign) NSInteger len; + +@end + +@interface TGTableDeltaUpdater : NSObject + ++ (void)updateTable:(WKInterfaceTable *)table oldData:(NSArray *)oldData newData:(NSArray *)newData controllerClassForIndexPath:(Class (^)(TGIndexPath *indexPath))controllerClassForIndexPath; + +@end diff --git a/Watch/Extension/TGTableDeltaUpdater.m b/Watch/Extension/TGTableDeltaUpdater.m new file mode 100644 index 0000000000..9018e00aef --- /dev/null +++ b/Watch/Extension/TGTableDeltaUpdater.m @@ -0,0 +1,176 @@ +#import "TGTableDeltaUpdater.h" +#import "WKInterfaceTable+TGDataDrivenTable.h" + +@implementation TGTableAlignment + ++ (instancetype)insertionWithPos:(NSInteger)pos len:(NSInteger)len +{ + TGTableAlignment *alignment = [[TGTableAlignment alloc] init]; + alignment.pos = pos; + alignment.len = len; + return alignment; +} + ++ (instancetype)deletionWithPos:(NSInteger)pos len:(NSInteger)len +{ + TGTableAlignment *alignment = [[TGTableAlignment alloc] init]; + alignment.deletion = true; + alignment.pos = pos; + alignment.len = len; + return alignment; +} + +@end + +@implementation TGTableDeltaUpdater + ++ (NSArray *)longestCommonSubsequenceForOldData:(NSArray *)oldData newData:(NSArray *)newData +{ + NSUInteger x = oldData.count; + NSUInteger y = newData.count; + + NSInteger lens[x + 1][y + 1]; + for (NSUInteger i = 0; i < (x + 1); i++) + { + for (NSUInteger j = 0; j < (y + 1); j++) + { + lens[i][j] = 0; + } + } + + NSMutableArray *result = [[NSMutableArray alloc] init]; + + for (NSUInteger i = 0; i < x; i++) + { + for (NSUInteger j = 0; j < y; j++) + { + NSObject *oldItem = oldData[i]; + NSObject *newItem = newData[j]; + + if ([[oldItem uniqueIdentifier] isEqual:[newItem uniqueIdentifier]]) + lens[i + 1][j + 1] = lens[i][j] + 1; + else + lens[i + 1][j + 1] = MAX(lens[i + 1][j], lens[i][j + 1]); + } + } + + while (x != 0 && y != 0) + { + if (lens[x][y] == lens[x - 1][y]) + { + --x; + } + else if (lens[x][y] == lens[x][y - 1]) + { + --y; + } + else + { + [result insertObject:oldData[x - 1] atIndex:0]; + --x; + --y; + } + } + + return result; +} + ++ (NSArray *)differenceForOldData:(NSArray *)left newData:(NSArray *)right +{ + NSArray *lcs = [self longestCommonSubsequenceForOldData:left newData:right]; + + NSInteger left_i = 0; + NSInteger right_i = 0; + + NSInteger totalOffset = 0; + + NSMutableArray *changes = [[NSMutableArray alloc] init]; + + for (NSObject *element in lcs) + { + NSInteger leftOffset = 0; + NSInteger rightOffset = 0; + + while (true) + { + if ([[left[left_i] uniqueIdentifier] isEqual:[element uniqueIdentifier]]) + { + break; + } + else + { + left_i++; + leftOffset++; + } + } + + while (true) + { + if ([[right[right_i] uniqueIdentifier] isEqual:[element uniqueIdentifier]]) + { + break; + } + else + { + right_i++; + rightOffset++; + } + } + + if (rightOffset > leftOffset) + { + NSInteger insertions = rightOffset - leftOffset; + NSInteger pos = left_i + totalOffset; + [changes addObject:[TGTableAlignment insertionWithPos:pos len:insertions]]; + totalOffset += insertions; + } + else if (leftOffset > rightOffset) + { + NSInteger deletions = leftOffset - rightOffset; + NSInteger pos = left_i - deletions + totalOffset; + [changes addObject:[TGTableAlignment deletionWithPos:pos len:deletions]]; + totalOffset -= deletions; + } + + left_i++; + right_i++; + } + + NSInteger afterLastInLeft = left.count - left_i; + NSInteger afterLastInRight = right.count - right_i; + + if (afterLastInRight > afterLastInLeft) + { + NSInteger insertions = afterLastInRight - afterLastInLeft; + NSInteger pos = left_i + totalOffset; + [changes addObject:[TGTableAlignment insertionWithPos:pos len:insertions]]; + } + else if (afterLastInLeft > afterLastInRight) + { + NSInteger deletions = afterLastInLeft - afterLastInRight; + NSInteger pos = left_i + totalOffset; + [changes addObject:[TGTableAlignment deletionWithPos:pos len:deletions]]; + } + + return changes; +} + ++ (void)updateTable:(WKInterfaceTable *)table oldData:(NSArray *)oldData newData:(NSArray *)newData controllerClassForIndexPath:(Class (^)(TGIndexPath *indexPath))controllerClassForIndexPath +{ + if (table.numberOfRows == 0) + { + [table reloadData]; + return; + } + + NSArray *changes = [self differenceForOldData:oldData newData:newData]; + [table applyBatchChanges:changes]; + + NSMutableArray *reloads = [[NSMutableArray alloc] init]; + for (NSInteger i = 0; i < newData.count; i++) + [reloads addObject:[TGIndexPath indexPathForRow:i inSection:0]]; + + [table reloadRowsAtIndexPaths:reloads]; +} + +@end diff --git a/Watch/Extension/TGUserHandle.h b/Watch/Extension/TGUserHandle.h new file mode 100644 index 0000000000..6ff157b271 --- /dev/null +++ b/Watch/Extension/TGUserHandle.h @@ -0,0 +1,18 @@ +#import "WKInterfaceTable+TGDataDrivenTable.h" + +typedef NS_ENUM(NSUInteger, TGUserHandleType) { + TGUserHandleTypeUndefined, + TGUserHandleTypePhone, + TGUserHandleTypeDescription +}; + +@interface TGUserHandle : NSObject + +@property (nonatomic, readonly) NSString *handle; +@property (nonatomic, readonly) NSString *type; +@property (nonatomic, readonly) TGUserHandleType handleType; +@property (nonatomic, readonly) NSString *data; + +- (instancetype)initWithHandle:(NSString *)handle type:(NSString *)type handleType:(TGUserHandleType)handleType data:(NSString *)data; + +@end \ No newline at end of file diff --git a/Watch/Extension/TGUserHandle.m b/Watch/Extension/TGUserHandle.m new file mode 100644 index 0000000000..ff0cb2a3d1 --- /dev/null +++ b/Watch/Extension/TGUserHandle.m @@ -0,0 +1,24 @@ +#import "TGUserHandle.h" +#import "TGStringUtils.h" + +@implementation TGUserHandle + +- (instancetype)initWithHandle:(NSString *)handle type:(NSString *)type handleType:(TGUserHandleType)handleType data:(NSString *)data +{ + self = [super init]; + if (self != nil) + { + _handle = handle; + _type = type; + _handleType = handleType; + _data = data; + } + return self; +} + +- (NSString *)uniqueIdentifier +{ + return [TGStringUtils md5WithString:[NSString stringWithFormat:@"%@,%@,%zu", self.handle, self.type, (unsigned long)self.handleType]]; +} + +@end \ No newline at end of file diff --git a/Watch/Extension/TGUserHandleRowController.h b/Watch/Extension/TGUserHandleRowController.h new file mode 100644 index 0000000000..ac8bb02aab --- /dev/null +++ b/Watch/Extension/TGUserHandleRowController.h @@ -0,0 +1,16 @@ +#import "WKInterfaceTable+TGDataDrivenTable.h" + +@class TGUserHandle; + +@interface TGUserHandleRowController : TGTableRowController + +@property (nonatomic, weak) IBOutlet WKInterfaceLabel *handleLabel; +@property (nonatomic, weak) IBOutlet WKInterfaceLabel *typeLabel; + +- (void)updateWithUserHandle:(TGUserHandle *)userHandle; + +@end + +@interface TGUserHandleActiveRowController : TGUserHandleRowController + +@end \ No newline at end of file diff --git a/Watch/Extension/TGUserHandleRowController.m b/Watch/Extension/TGUserHandleRowController.m new file mode 100644 index 0000000000..ecea79333f --- /dev/null +++ b/Watch/Extension/TGUserHandleRowController.m @@ -0,0 +1,47 @@ +#import "TGUserHandleRowController.h" +#import "TGUserHandle.h" + +NSString *const TGUserHandleRowIdentifier = @"TGUserHandleRow"; + +@implementation TGUserHandleRowController + +- (void)updateWithUserHandle:(TGUserHandle *)userHandle +{ + bool useRegularFont = (userHandle.handleType == TGUserHandleTypeDescription); + + NSMutableDictionary *attributes = [[NSMutableDictionary alloc] init]; + attributes[NSFontAttributeName] = useRegularFont ? [UIFont systemFontOfSize:16.0f weight:UIFontWeightRegular] : [UIFont systemFontOfSize:16.0f weight:UIFontWeightMedium]; + attributes[NSForegroundColorAttributeName] = [UIColor whiteColor]; + + if (useRegularFont) + { + NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init]; + paragraphStyle.hyphenationFactor = 1.0f; + attributes[NSParagraphStyleAttributeName] = paragraphStyle; + } + + NSString *handle = userHandle.handle; + if (handle == nil) + handle = @""; + + self.handleLabel.attributedText = [[NSAttributedString alloc] initWithString:handle attributes:attributes]; + self.typeLabel.text = userHandle.type; +} + ++ (NSString *)identifier +{ + return TGUserHandleRowIdentifier; +} + +@end + +NSString *const TGUserHandleActiveRowIdentifier = @"TGUserHandleActiveRow"; + +@implementation TGUserHandleActiveRowController + ++ (NSString *)identifier +{ + return TGUserHandleActiveRowIdentifier; +} + +@end diff --git a/Watch/Extension/TGUserInfoController.h b/Watch/Extension/TGUserInfoController.h new file mode 100644 index 0000000000..788a4c3e08 --- /dev/null +++ b/Watch/Extension/TGUserInfoController.h @@ -0,0 +1,29 @@ +#import "TGInterfaceController.h" + +@class TGBridgeContext; +@class TGBridgeUser; +@class TGBridgeChat; + +@interface TGUserInfoControllerContext : NSObject + +@property (nonatomic, strong) TGBridgeContext *context; +@property (nonatomic, readonly) TGBridgeUser *user; +@property (nonatomic, readonly) int32_t userId; + +@property (nonatomic, readonly) TGBridgeChat *channel; + +@property (nonatomic, assign) bool disallowCompose; + +- (instancetype)initWithUser:(TGBridgeUser *)user; +- (instancetype)initWithUserId:(int32_t)userId; + +- (instancetype)initWithChannel:(TGBridgeChat *)channel; + +@end + +@interface TGUserInfoController : TGInterfaceController + +@property (nonatomic, weak) IBOutlet WKInterfaceTable *table; +@property (nonatomic, weak) IBOutlet WKInterfaceImage *activityIndicator; + +@end diff --git a/Watch/Extension/TGUserInfoController.m b/Watch/Extension/TGUserInfoController.m new file mode 100644 index 0000000000..9d3e3b398f --- /dev/null +++ b/Watch/Extension/TGUserInfoController.m @@ -0,0 +1,508 @@ +#import "TGUserInfoController.h" +#import "TGWatchCommon.h" +#import "TGStringUtils.h" + +#import "TGBridgeBotSignals.h" +#import "TGBridgeUserInfoSignals.h" +#import "TGBridgePeerSettingsSignals.h" +#import "TGBridgeUser.h" +#import "TGBridgeBotInfo.h" +#import "TGBridgeUserCache.h" +#import "TGUserHandle.h" + +#import "TGBridgeChat.h" + +#import "TGTableDeltaUpdater.h" + +#import "WKInterfaceTable+TGDataDrivenTable.h" +#import "TGInterfaceMenu.h" + +#import "TGUserInfoHeaderController.h" +#import "TGUserHandleRowController.h" + +#import "TGProfilePhotoController.h" +#import "TGNeoConversationController.h" + +NSString *const TGUserInfoControllerIdentifier = @"TGUserInfoController"; + +@implementation TGUserInfoControllerContext + +- (instancetype)initWithUser:(TGBridgeUser *)user +{ + self = [super init]; + if (self != nil) + { + _user = user; + } + return self; +} + +- (instancetype)initWithUserId:(int32_t)userId +{ + self = [super init]; + if (self != nil) + { + _userId = userId; + } + return self; +} + +- (instancetype)initWithChannel:(TGBridgeChat *)channel +{ + self = [super init]; + if (self != nil) + { + _channel = channel; + } + return self; +} + +@end + +@interface TGUserInfoController () +{ + SMetaDisposable *_userDisposable; + SMetaDisposable *_botInfoDisposable; + SMetaDisposable *_peerSettingsDisposable; + SMetaDisposable *_updateSettingsDisposable; + + TGUserInfoControllerContext *_context; + TGBridgeUser *_userModel; + TGBridgeBotInfo *_botInfo; + bool _muted; + bool _blocked; + + TGBridgeChat *_channelModel; + + NSArray *_handleModels; + NSArray *_currentHandleModels; + + TGInterfaceMenu *_menu; +} +@end + +@implementation TGUserInfoController + +- (instancetype)init +{ + self = [super init]; + if (self != nil) + { + _userDisposable = [[SMetaDisposable alloc] init]; + _peerSettingsDisposable = [[SMetaDisposable alloc] init]; + _updateSettingsDisposable = [[SMetaDisposable alloc] init]; + + [self.table _setInitialHidden:true]; + self.table.tableDataSource = self; + } + return self; +} + +- (void)dealloc +{ + [_userDisposable dispose]; + [_botInfoDisposable dispose]; + [_peerSettingsDisposable dispose]; + [_updateSettingsDisposable dispose]; +} + +- (void)configureWithContext:(TGUserInfoControllerContext *)context +{ + _context = context; + + if (context.channel != nil) + [self configureWithChannelContext:context]; + else + [self configureWithUserContext:context]; +} + +- (void)configureWithChannelContext:(TGUserInfoControllerContext *)context +{ + _channelModel = context.channel; + + self.title = _channelModel.isChannelGroup ? TGLocalized(@"Watch.GroupInfo.Title") : TGLocalized(@"Watch.ChannelInfo.Title"); + + [self updateChannelHandles]; + + __weak TGUserInfoController *weakSelf = self; + [self performInterfaceUpdate:^(bool animated) + { + __strong TGUserInfoController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf reloadData]; + }]; + + [self setupPeerSettingsWithId:_channelModel.identifier]; +} + +- (void)configureWithUserContext:(TGUserInfoControllerContext *)context +{ + self.title = TGLocalized(@"Watch.UserInfo.Title"); + + int32_t userId = (_context.user != nil) ? (int32_t)_context.user.identifier : _context.userId; + SSignal *remoteUserSignal = [TGBridgeUserInfoSignals userInfoWithUserId:userId]; + + SSignal *userSignal = nil; + + TGBridgeUser *cachedUser = [[TGBridgeUserCache instance] userWithId:userId]; + if (cachedUser == nil) + cachedUser = _context.user; + + if (cachedUser != nil) + userSignal = [[SSignal single:cachedUser] then:remoteUserSignal]; + else + userSignal = remoteUserSignal; + + __weak TGUserInfoController *weakSelf = self; + [_userDisposable setDisposable:[[userSignal deliverOn:[SQueue mainQueue]] startWithNext:^(TGBridgeUser *user) + { + __strong TGUserInfoController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + strongSelf->_userModel = user; + [[TGBridgeUserCache instance] storeUser:user]; + + if ([strongSelf _userIsBot] && strongSelf->_botInfo == nil) + { + strongSelf->_botInfoDisposable = [[SMetaDisposable alloc] init]; + [strongSelf->_botInfoDisposable setDisposable:[[TGBridgeBotSignals botInfoForUserId:user.identifier] startWithNext:^(TGBridgeBotInfo *botInfo) + { + __strong TGUserInfoController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + strongSelf->_botInfo = botInfo; + + [strongSelf updateUserHandles]; + + [strongSelf performInterfaceUpdate:^(bool animated) + { + __strong TGUserInfoController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf reloadData]; + }]; + }]]; + } + + [strongSelf updateUserHandles]; + + [strongSelf performInterfaceUpdate:^(bool animated) + { + __strong TGUserInfoController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf reloadData]; + }]; + }]]; + + [self setupPeerSettingsWithId:userId]; +} + +- (void)setupPeerSettingsWithId:(int64_t)peerId +{ + __weak TGUserInfoController *weakSelf = self; + [_peerSettingsDisposable setDisposable:[[[TGBridgePeerSettingsSignals peerSettingsWithPeerId:peerId] deliverOn:[SQueue mainQueue]] startWithNext:^(NSDictionary *next) + { + __strong TGUserInfoController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + bool blocked = [next[@"blocked"] boolValue]; + bool muted = [next[@"muted"] boolValue]; + + strongSelf->_blocked = blocked; + strongSelf->_muted = muted; + + [strongSelf performInterfaceUpdate:^(bool animated) + { + __strong TGUserInfoController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf reloadData]; + }]; + }]]; +} + +- (void)updateUserHandles +{ + NSMutableArray *handles = [[NSMutableArray alloc] init]; + if (_userModel.phoneNumber.length > 0) + { + [handles addObject:[[TGUserHandle alloc] initWithHandle:_userModel.prettyPhoneNumber type:TGLocalized(@"UserInfo.GenericPhoneLabel") handleType:TGUserHandleTypePhone data:_userModel.phoneNumber]]; + } + if (_userModel.userName.length > 0) + { + [handles addObject:[[TGUserHandle alloc] initWithHandle:[NSString stringWithFormat:@"@%@", self->_userModel.userName] type:TGLocalized(@"Profile.Username") handleType:TGUserHandleTypeUndefined data:nil]]; + } + if (_userModel.about.length > 0) + { + [handles addObject:[[TGUserHandle alloc] initWithHandle:_userModel.about type:TGLocalized(@"Profile.BotInfo") handleType:TGUserHandleTypeDescription data:nil]]; + } + + _handleModels = handles; +} + +- (void)updateChannelHandles +{ + NSMutableArray *handles = [[NSMutableArray alloc] init]; + if (_channelModel.userName.length > 0) + { + [handles addObject:[[TGUserHandle alloc] initWithHandle:[NSString stringWithFormat:@"t.me/%@", self->_channelModel.userName] type:TGLocalized(@"Channel.LinkItem") handleType:TGUserHandleTypeUndefined data:nil]]; + } + if (_channelModel.about.length > 0) + { + [handles addObject:[[TGUserHandle alloc] initWithHandle:_channelModel.about type:TGLocalized(@"Channel.AboutItem") handleType:TGUserHandleTypeDescription data:nil]]; + } + + _handleModels = handles; +} + +- (void)reloadData +{ + NSArray *currentHandles = _currentHandleModels; + _currentHandleModels = _handleModels; + + self.activityIndicator.hidden = true; + + NSInteger numberOfRows = [self numberOfRowsInTable:self.table section:0]; + if (self.table.numberOfRows == numberOfRows + 1) + { + [self.table reloadHeader]; + + NSMutableArray *indexPaths = [[NSMutableArray alloc] init]; + for (NSInteger i = 0; i < numberOfRows; i++) + [indexPaths addObject:[TGIndexPath indexPathForRow:i inSection:0]]; + + [self.table reloadRowsAtIndexPaths:indexPaths]; + } + else + { + if (self.table.hidden) + { + [self.table reloadData]; + } + else + { + [TGTableDeltaUpdater updateTable:self.table oldData:currentHandles newData:_currentHandleModels controllerClassForIndexPath:^Class(TGIndexPath *indexPath) + { + return nil; + }]; + } + } + + [self updateMenuItems]; + + self.table.hidden = false; +} + +- (void)willActivate +{ + [super willActivate]; + + [self.table notifyVisiblityChange]; +} + +- (void)didDeactivate +{ + [super didDeactivate]; +} + +#pragma mark - + +- (void)updateMenuItems +{ + [_menu clearItems]; + + if (_menu == nil) + _menu = [[TGInterfaceMenu alloc] initForInterfaceController:self]; + + __weak TGUserInfoController *weakSelf = self; + + NSMutableArray *menuItems = [[NSMutableArray alloc] init]; + + bool muted = _muted; + bool blocked = _blocked; + + bool muteForever = _channelModel.isChannelGroup; + int32_t muteFor = muteForever ? INT_MAX : 1; + NSString *muteTitle = muteForever ? TGLocalized(@"Watch.UserInfo.MuteTitle") : [NSString stringWithFormat:TGLocalized([TGStringUtils integerValueFormat:@"Watch.UserInfo.Mute_" value:muteFor]), muteFor]; + + TGInterfaceMenuItem *muteItem = [[TGInterfaceMenuItem alloc] initWithItemIcon:muted ? WKMenuItemIconSpeaker : WKMenuItemIconMute title:muted ? TGLocalized(@"Watch.UserInfo.Unmute") : muteTitle actionBlock:^(TGInterfaceController *controller, TGInterfaceMenuItem *sender) + { + __strong TGUserInfoController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf->_updateSettingsDisposable setDisposable:[[TGBridgePeerSettingsSignals toggleMutedWithPeerId:strongSelf->_userModel.identifier] startWithNext:nil completed:^ + { + __strong TGUserInfoController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + strongSelf->_muted = !muted; + [strongSelf updateMenuItems]; + }]]; + }]; + [menuItems addObject:muteItem]; + + if (_channelModel == nil) + { + TGInterfaceMenuItem *blockItem = [[TGInterfaceMenuItem alloc] initWithItemIcon:WKMenuItemIconBlock title:blocked ? TGLocalized(@"Watch.UserInfo.Unblock") : TGLocalized(@"Watch.UserInfo.Block") actionBlock:^(TGInterfaceController *controller, TGInterfaceMenuItem *sender) + { + __strong TGUserInfoController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf->_updateSettingsDisposable setDisposable:[[TGBridgePeerSettingsSignals updateBlockStatusWithPeerId:strongSelf->_userModel.identifier blocked:!blocked] startWithNext:nil completed:^ + { + __strong TGUserInfoController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + strongSelf->_blocked = !blocked; + [strongSelf updateMenuItems]; + }]]; + }]; + [menuItems addObject:blockItem]; + } + + if (!_context.disallowCompose) + { + TGInterfaceMenuItem *composeItem = [[TGInterfaceMenuItem alloc] initWithImageNamed:@"Compose" title:TGLocalized(@"Watch.UserInfo.SendMessage") actionBlock:^(TGInterfaceController *controller, TGInterfaceMenuItem *sender) + { + __strong TGUserInfoController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + TGNeoConversationControllerContext *context = [[TGNeoConversationControllerContext alloc] initWithPeerId:strongSelf->_userModel.identifier]; + + [strongSelf pushControllerWithClass:[TGNeoConversationController class] context:context]; + }]; + [menuItems addObject:composeItem]; + } + + [_menu addItems:menuItems]; +} + +#pragma mark - + +- (Class)headerControllerClassForTable:(WKInterfaceTable *)table +{ + return [TGUserInfoHeaderController class]; +} + +- (void)table:(WKInterfaceTable *)table updateHeaderController:(TGUserInfoHeaderController *)controller +{ + __weak TGUserInfoController *weakSelf = self; + controller.isVisible = ^bool + { + __strong TGUserInfoController *strongSelf = weakSelf; + if (strongSelf == nil) + return false; + + return strongSelf.isVisible; + }; + controller.avatarPressed = ^ + { + __strong TGUserInfoController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + int64_t identifier = 0; + NSString *url = nil; + if (strongSelf->_userModel != nil) { + identifier = strongSelf->_userModel.identifier; + url = strongSelf->_userModel.photoSmall; + } + else if (strongSelf->_channelModel != nil) { + identifier = strongSelf->_channelModel.identifier; + url = strongSelf->_channelModel.groupPhotoSmall; + } + + if (url != nil) + { + TGProfilePhotoControllerContext *context = [[TGProfilePhotoControllerContext alloc] initWithIdentifier:identifier imageUrl:url]; + [strongSelf pushControllerWithClass:[TGProfilePhotoController class] context:context]; + } + }; + + if (_userModel != nil) + [controller updateWithUser:_userModel context:_context.context]; + else + [controller updateWithChannel:_channelModel]; +} + +- (NSUInteger)numberOfRowsInTable:(WKInterfaceTable *)table section:(NSUInteger)section +{ + return _currentHandleModels.count; +} + +- (Class)table:(WKInterfaceTable *)table rowControllerClassAtIndexPath:(TGIndexPath *)indexPath +{ + TGUserHandle *userHandle = _currentHandleModels[indexPath.row]; + switch (userHandle.handleType) + { + case TGUserHandleTypePhone: + return [TGUserHandleActiveRowController class]; + break; + + default: + return [TGUserHandleRowController class]; + } +} + +- (void)table:(WKInterfaceTable *)table updateRowController:(TGUserHandleRowController *)controller forIndexPath:(TGIndexPath *)indexPath +{ + TGUserHandle *userHandle = _currentHandleModels[indexPath.row]; + [controller updateWithUserHandle:userHandle]; +} + +- (void)table:(WKInterfaceTable *)table didSelectRowAtIndexPath:(TGIndexPath *)indexPath +{ + TGUserHandle *userHandle = _currentHandleModels[indexPath.row]; + switch (userHandle.handleType) + { + case TGUserHandleTypePhone: + [[WKExtension sharedExtension] openSystemURL:[NSURL URLWithString:[NSString stringWithFormat:@"tel://%@", userHandle.data]]]; + break; + + default: + break; + } +} + +- (bool)_userHasPhone +{ + return (_userModel.phoneNumber.length > 0); +} + +- (bool)_userHasUsername +{ + return (_userModel.userName.length > 0); +} + +- (bool)_userIsBot +{ + return (_userModel.kind != TGBridgeUserKindGeneric); +} + +- (bool)_botHasDescription +{ + return (_botInfo.shortDescription.length > 0); +} + +#pragma mark - + ++ (NSString *)identifier +{ + return TGUserInfoControllerIdentifier; +} + +@end diff --git a/Watch/Extension/TGUserInfoHeaderController.h b/Watch/Extension/TGUserInfoHeaderController.h new file mode 100644 index 0000000000..a19b1c61e0 --- /dev/null +++ b/Watch/Extension/TGUserInfoHeaderController.h @@ -0,0 +1,22 @@ +#import "WKInterfaceTable+TGDataDrivenTable.h" + +@class TGBridgeContext; +@class TGBridgeUser; +@class TGBridgeChat; + +@interface TGUserInfoHeaderController : TGTableRowController + +@property (nonatomic, weak) IBOutlet WKInterfaceButton *avatarButton; +@property (nonatomic, weak) IBOutlet WKInterfaceGroup *avatarGroup; +@property (nonatomic, weak) IBOutlet WKInterfaceLabel *avatarInitialsLabel; +@property (nonatomic, weak) IBOutlet WKInterfaceImage *avatarVerified; +@property (nonatomic, weak) IBOutlet WKInterfaceLabel *nameLabel; +@property (nonatomic, weak) IBOutlet WKInterfaceLabel *lastSeenLabel; +- (IBAction)avatarPressedAction; + +@property (nonatomic, copy) void (^avatarPressed)(void); + +- (void)updateWithUser:(TGBridgeUser *)user context:(TGBridgeContext *)context; +- (void)updateWithChannel:(TGBridgeChat *)channel; + +@end diff --git a/Watch/Extension/TGUserInfoHeaderController.m b/Watch/Extension/TGUserInfoHeaderController.m new file mode 100644 index 0000000000..05b72ae420 --- /dev/null +++ b/Watch/Extension/TGUserInfoHeaderController.m @@ -0,0 +1,177 @@ +#import "TGUserInfoHeaderController.h" +#import "TGWatchCommon.h" +#import "TGDateUtils.h" +#import "TGStringUtils.h" + +#import "WKInterfaceGroup+Signals.h" + +#import "TGBridgeMediaSignals.h" + +#import "TGBridgeUser.h" +#import "TGBridgeChat.h" + +#import "TGBridgeContext.h" + +NSString *const TGUserInfoHeaderIdentifier = @"TGUserInfoHeader"; + +@interface TGUserInfoHeaderController () +{ + NSString *_currentAvatarPhoto; +} +@end + +@implementation TGUserInfoHeaderController + +- (void)updateWithUser:(TGBridgeUser *)user context:(TGBridgeContext *)context +{ + self.nameLabel.text = user.displayName; + + if (user.photoSmall.length > 0) + { + self.avatarButton.enabled = true; + self.avatarInitialsLabel.hidden = true; + + if (user.verified) + { + self.avatarGroup.backgroundColor = [UIColor clearColor]; + [self.avatarGroup setCornerRadius:0]; + self.avatarVerified.hidden = false; + } + else + { + self.avatarGroup.backgroundColor = [UIColor hexColor:0x1a1a1a]; + [self.avatarGroup setCornerRadius:22]; + self.avatarVerified.hidden = true; + } + + if (![_currentAvatarPhoto isEqualToString:user.photoSmall]) + { + _currentAvatarPhoto = user.photoSmall; + + __weak TGUserInfoHeaderController *weakSelf = self; + [self.avatarGroup setBackgroundImageSignal:[[TGBridgeMediaSignals avatarWithPeerId:user.identifier url:_currentAvatarPhoto type:TGBridgeMediaAvatarTypeProfile] onError:^(id error) + { + __strong TGUserInfoHeaderController *strongSelf = weakSelf; + if (strongSelf != nil) + strongSelf->_currentAvatarPhoto = nil; + }] isVisible:self.isVisible]; + } + } + else + { + self.avatarButton.enabled = false; + self.avatarInitialsLabel.hidden = false; + self.avatarGroup.backgroundColor = [TGColor colorForUserId:user.identifier myUserId:context.userId]; + self.avatarInitialsLabel.text = [TGStringUtils initialsForFirstName:user.firstName lastName:user.lastName single:true]; + + _currentAvatarPhoto = nil; + [self.avatarGroup setBackgroundImageSignal:nil isVisible:self.isVisible]; + } + + if (user.identifier == 777000) + { + self.lastSeenLabel.textColor = [TGColor subtitleColor]; + self.lastSeenLabel.text = TGLocalized(@"Watch.UserInfo.Service"); + } + else if (user.kind != TGBridgeUserKindGeneric) + { + self.lastSeenLabel.textColor = [TGColor subtitleColor]; + self.lastSeenLabel.text = TGLocalized(@"Bot.GenericBotStatus"); + } + else if (user.online || user.identifier == context.userId) + { + self.lastSeenLabel.textColor = [TGColor accentColor]; + self.lastSeenLabel.text = TGLocalized(@"Presence.online"); + } + else + { + self.lastSeenLabel.textColor = [TGColor subtitleColor]; + self.lastSeenLabel.text = [TGDateUtils stringForRelativeLastSeen:user.lastSeen]; + } +} + +- (void)updateWithChannel:(TGBridgeChat *)channel +{ + self.nameLabel.text = channel.groupTitle; + self.lastSeenLabel.textColor = [TGColor subtitleColor]; + + if (!channel.isChannelGroup) + { + self.lastSeenLabel.text = TGLocalized(@"Channel.Status"); + } + else + { + if (channel.participantsCount == 0) + { + self.lastSeenLabel.text = @""; + } + else + { + self.lastSeenLabel.text = [NSString stringWithFormat:TGLocalized([TGStringUtils integerValueFormat:@"Conversation.StatusMembers_" value:channel.participantsCount]), [NSString stringWithFormat:@"%d", (int32_t)channel.participantsCount]]; + } + } + + if (channel.groupPhotoSmall.length > 0) + { + self.avatarButton.enabled = true; + self.avatarInitialsLabel.hidden = true; + + if (channel.verified) + { + self.avatarGroup.backgroundColor = [UIColor clearColor]; + [self.avatarGroup setCornerRadius:0]; + self.avatarVerified.hidden = false; + } + else + { + self.avatarGroup.backgroundColor = [UIColor hexColor:0x1a1a1a]; + [self.avatarGroup setCornerRadius:22]; + self.avatarVerified.hidden = true; + } + + if (![_currentAvatarPhoto isEqualToString:channel.groupPhotoSmall]) + { + _currentAvatarPhoto = channel.groupPhotoSmall; + + __weak TGUserInfoHeaderController *weakSelf = self; + [self.avatarGroup setBackgroundImageSignal:[[TGBridgeMediaSignals avatarWithPeerId:channel.identifier url:_currentAvatarPhoto type:TGBridgeMediaAvatarTypeProfile] onError:^(id error) + { + __strong TGUserInfoHeaderController *strongSelf = weakSelf; + if (strongSelf != nil) + strongSelf->_currentAvatarPhoto = nil; + }] isVisible:self.isVisible]; + } + } + else + { + self.avatarButton.enabled = false; + self.avatarInitialsLabel.hidden = false; + self.avatarGroup.backgroundColor = [TGColor colorForGroupId:channel.identifier]; + [self.avatarGroup setCornerRadius:22]; + self.avatarVerified.hidden = true; + self.avatarInitialsLabel.text = [TGStringUtils initialForGroupName:channel.groupTitle]; + + _currentAvatarPhoto = nil; + [self.avatarGroup setBackgroundImageSignal:nil isVisible:self.isVisible]; + } +} + +- (void)avatarPressedAction +{ + if (self.avatarPressed != nil) + self.avatarPressed(); +} + +- (void)notifyVisiblityChange +{ + [self.avatarGroup updateIfNeeded]; +} + +#pragma mark - + ++ (NSString *)identifier +{ + return TGUserInfoHeaderIdentifier; +} + +@end diff --git a/Watch/Extension/TGUserRowController.h b/Watch/Extension/TGUserRowController.h new file mode 100644 index 0000000000..ea48d2a724 --- /dev/null +++ b/Watch/Extension/TGUserRowController.h @@ -0,0 +1,19 @@ +#import "WKInterfaceTable+TGDataDrivenTable.h" + +@class TGBridgeUser; +@class TGBridgeChat; +@class TGBridgeBotCommandInfo; +@class TGBridgeContext; + +@interface TGUserRowController : TGTableRowController + +@property (nonatomic, weak) IBOutlet WKInterfaceGroup *avatarGroup; +@property (nonatomic, weak) IBOutlet WKInterfaceLabel *avatarInitialsLabel; +@property (nonatomic, weak) IBOutlet WKInterfaceLabel *nameLabel; +@property (nonatomic, weak) IBOutlet WKInterfaceLabel *lastSeenLabel; + +- (void)updateWithUser:(TGBridgeUser *)user context:(TGBridgeContext *)context; +- (void)updateWithChannel:(TGBridgeChat *)channel context:(TGBridgeContext *)context; +- (void)updateWithBotCommandInfo:(TGBridgeBotCommandInfo *)commandInfo botUser:(TGBridgeUser *)botUser context:(TGBridgeContext *)context; + +@end diff --git a/Watch/Extension/TGUserRowController.m b/Watch/Extension/TGUserRowController.m new file mode 100644 index 0000000000..4fdd5e6a61 --- /dev/null +++ b/Watch/Extension/TGUserRowController.m @@ -0,0 +1,132 @@ +#import "TGUserRowController.h" +#import "TGWatchCommon.h" +#import "TGDateUtils.h" +#import "TGStringUtils.h" + +#import "WKInterfaceGroup+Signals.h" + +#import "TGBridgeMediaSignals.h" + +#import "TGBridgeUser.h" +#import "TGBridgeChat.h" +#import "TGBridgeBotCommandInfo.h" +#import "TGBridgeContext.h" + +NSString *const TGUserRowIdentifier = @"TGUserRow"; + +@interface TGUserRowController () +{ + NSString *_currentAvatarPhoto; +} +@end + +@implementation TGUserRowController + +- (void)updateWithUser:(TGBridgeUser *)user context:(TGBridgeContext *)context +{ + self.nameLabel.text = user.displayName; + + [self updateAvatarWithUser:user context:context]; + + if (user.kind != TGBridgeUserKindGeneric) + { + self.lastSeenLabel.textColor = [TGColor subtitleColor]; + self.lastSeenLabel.text = TGLocalized(@"Bot.GenericBotStatus"); + } + else if (user.online || user.identifier == context.userId) + { + self.lastSeenLabel.textColor = [TGColor accentColor]; + self.lastSeenLabel.text = TGLocalized(@"Presence.online"); + } + else + { + self.lastSeenLabel.textColor = [TGColor subtitleColor]; + self.lastSeenLabel.text = [TGDateUtils stringForRelativeLastSeen:user.lastSeen]; + } +} + +- (void)updateWithChannel:(TGBridgeChat *)channel context:(TGBridgeContext *)context +{ + self.nameLabel.text = channel.groupTitle; + [self updateAvatarWithChat:channel context:context]; +} + +- (void)updateWithBotCommandInfo:(TGBridgeBotCommandInfo *)commandInfo botUser:(TGBridgeUser *)botUser context:(TGBridgeContext *)context +{ + self.nameLabel.text = [NSString stringWithFormat:@"/%@", commandInfo.command]; + self.lastSeenLabel.textColor = [TGColor subtitleColor]; + self.lastSeenLabel.text = commandInfo.commandDescription.length > 0 ? commandInfo.commandDescription : nil; + [self updateAvatarWithUser:botUser context:context]; +} + +- (void)updateAvatarWithUser:(TGBridgeUser *)user context:(TGBridgeContext *)context +{ + if (user.photoSmall.length > 0) + { + self.avatarInitialsLabel.hidden = true; + self.avatarGroup.backgroundColor = [UIColor hexColor:0x222223]; + if (![_currentAvatarPhoto isEqualToString:user.photoSmall]) + { + _currentAvatarPhoto = user.photoSmall; + + __weak TGUserRowController *weakSelf = self; + [self.avatarGroup setBackgroundImageSignal:[[TGBridgeMediaSignals avatarWithPeerId:user.identifier url:_currentAvatarPhoto type:TGBridgeMediaAvatarTypeSmall] onNext:^(id next) + { + __strong TGUserRowController *strongSelf = weakSelf; + if (strongSelf != nil) + strongSelf->_currentAvatarPhoto = nil; + }] isVisible:self.isVisible]; + } + } + else + { + self.avatarInitialsLabel.hidden = false; + self.avatarGroup.backgroundColor = [TGColor colorForUserId:user.identifier myUserId:context.userId]; + self.avatarInitialsLabel.text = [TGStringUtils initialsForFirstName:user.firstName lastName:user.lastName single:true]; + + [self.avatarGroup setBackgroundImageSignal:nil isVisible:self.isVisible]; + _currentAvatarPhoto = nil; + } +} + +- (void)updateAvatarWithChat:(TGBridgeChat *)chat context:(TGBridgeContext *)context +{ + if (chat.groupPhotoSmall.length > 0) + { + self.avatarInitialsLabel.hidden = true; + self.avatarGroup.backgroundColor = [UIColor hexColor:0x222223]; + if (![_currentAvatarPhoto isEqualToString:chat.groupPhotoSmall]) + { + _currentAvatarPhoto = chat.groupPhotoSmall; + + __weak TGUserRowController *weakSelf = self; + [self.avatarGroup setBackgroundImageSignal:[[TGBridgeMediaSignals avatarWithPeerId:chat.identifier url:_currentAvatarPhoto type:TGBridgeMediaAvatarTypeSmall] onNext:^(id next) + { + __strong TGUserRowController *strongSelf = weakSelf; + if (strongSelf != nil) + strongSelf->_currentAvatarPhoto = nil; + }] isVisible:self.isVisible]; + } + } + else + { + self.avatarInitialsLabel.hidden = false; + self.avatarGroup.backgroundColor = [TGColor colorForGroupId:chat.identifier]; + self.avatarInitialsLabel.text = [TGStringUtils initialForGroupName:chat.groupTitle]; + + [self.avatarGroup setBackgroundImageSignal:nil isVisible:self.isVisible]; + _currentAvatarPhoto = nil; + } +} + +- (void)notifyVisiblityChange +{ + [self.avatarGroup updateIfNeeded]; +} + ++ (NSString *)identifier +{ + return TGUserRowIdentifier; +} + +@end diff --git a/Watch/Extension/TGWatchColor.h b/Watch/Extension/TGWatchColor.h new file mode 100644 index 0000000000..c16035780f --- /dev/null +++ b/Watch/Extension/TGWatchColor.h @@ -0,0 +1,18 @@ +#import + +@interface UIColor (TGColor) + ++ (UIColor *)hexColor:(NSInteger)hex; ++ (UIColor *)hexColor:(NSInteger)hex withAlpha:(CGFloat)alpha; + +@end + +@interface TGColor : NSObject + ++ (UIColor *)colorForUserId:(int32_t)userId myUserId:(int32_t)myUserId; ++ (UIColor *)colorForGroupId:(int64_t)groupId; + ++ (UIColor *)accentColor; ++ (UIColor *)subtitleColor; + +@end diff --git a/Watch/Extension/TGWatchColor.m b/Watch/Extension/TGWatchColor.m new file mode 100644 index 0000000000..515141dbb3 --- /dev/null +++ b/Watch/Extension/TGWatchColor.m @@ -0,0 +1,65 @@ +#import "TGWatchColor.h" +#import "TGBridgePeerIdAdapter.h" +#import + +@implementation UIColor (TGColor) + ++ (UIColor *)hexColor:(NSInteger)hex +{ + return [[UIColor alloc] initWithRed:(((hex >> 16) & 0xff) / 255.0f) green:(((hex >> 8) & 0xff) / 255.0f) blue:(((hex) & 0xff) / 255.0f) alpha:1.0f]; +} + ++ (UIColor *)hexColor:(NSInteger)hex withAlpha:(CGFloat)alpha +{ + return [[UIColor alloc] initWithRed:(((hex >> 16) & 0xff) / 255.0f) green:(((hex >> 8) & 0xff) / 255.0f) blue:(((hex) & 0xff) / 255.0f) alpha:alpha]; +} + +@end + +@implementation TGColor + ++ (NSArray *)placeholderColors +{ + static NSArray *colors; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^ + { + colors = @[ [UIColor hexColor:0xff516a], + [UIColor hexColor:0xffa85c], + [UIColor hexColor:0x665fff], + [UIColor hexColor:0x54cb68], + [UIColor hexColor:0x28c9b7], + [UIColor hexColor:0x2a9ef1], + [UIColor hexColor:0xd669ed]]; + }); + + return colors; +} + ++ (UIColor *)colorForUserId:(int32_t)userId myUserId:(int32_t)myUserId +{ + return [self placeholderColors][userId % 7]; +} + ++ (UIColor *)colorForGroupId:(int64_t)groupId +{ + int32_t peerId = 0; + if (TGPeerIdIsGroup(groupId)) { + peerId = TGGroupIdFromPeerId(groupId); + } else if (TGPeerIdIsChannel(groupId)) { + peerId = TGChannelIdFromPeerId(groupId); + } + return [self placeholderColors][peerId % 7]; +} + ++ (UIColor *)accentColor +{ + return [UIColor hexColor:0x2ea4e5]; +} + ++ (UIColor *)subtitleColor +{ + return [UIColor hexColor:0x8f8f8f]; +} + +@end diff --git a/Watch/Extension/TGWatchCommon.h b/Watch/Extension/TGWatchCommon.h new file mode 100644 index 0000000000..80636c3608 --- /dev/null +++ b/Watch/Extension/TGWatchCommon.h @@ -0,0 +1,58 @@ +#import +#import "WKInterface+TGInterface.h" +#import "TGWatchColor.h" + +#define TGTick NSDate *startTime = [NSDate date] +#define TGTock NSLog(@"%s Time: %f", __func__, -[startTime timeIntervalSinceNow]) + +#define TGLog(s) NSLog(s) + +#ifdef __cplusplus +extern "C" { +#endif + +extern int TGLocalizedStaticVersion; + +void TGSetLocalizationFromFile(NSURL *fileUrl); +bool TGIsCustomLocalizationActive(); +void TGResetLocalization(); +NSString *TGLocalized(NSString *s); + +static inline void TGDispatchOnMainThread(dispatch_block_t block) +{ + if ([NSThread isMainThread]) + block(); + else + dispatch_async(dispatch_get_main_queue(), block); +} + +static inline void TGDispatchAfter(double delay, dispatch_queue_t queue, dispatch_block_t block) +{ + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)((delay) * NSEC_PER_SEC)), queue, block); +} + +void TGSwizzleMethodImplementation(Class clazz, SEL originalMethod, SEL modifiedMethod); + +CGSize TGWatchScreenSize(); + +typedef NS_ENUM(NSUInteger, TGScreenType) +{ + TGScreenType38mm, + TGScreenType40mm, + TGScreenType42mm, + TGScreenType44mm, +}; + +TGScreenType TGWatchScreenType(); +CGSize TGWatchStickerSizeForScreen(TGScreenType screenType); + +#ifdef __cplusplus +} +#endif + +@interface NSNumber (IntegerTypes) + +- (int32_t)int32Value; +- (int64_t)int64Value; + +@end diff --git a/Watch/Extension/TGWatchCommon.m b/Watch/Extension/TGWatchCommon.m new file mode 100644 index 0000000000..34fe8f47f6 --- /dev/null +++ b/Watch/Extension/TGWatchCommon.m @@ -0,0 +1,200 @@ +#import "TGWatchCommon.h" +#import "TGExtensionDelegate.h" + +#import +#import + +void TGSwizzleMethodImplementation(Class class, SEL originalSelector, SEL modifiedSelector) +{ + Method originalMethod = class_getInstanceMethod(class, originalSelector); + Method modifiedMethod = class_getInstanceMethod(class, modifiedSelector); + + if (class_addMethod(class, originalSelector, method_getImplementation(modifiedMethod), method_getTypeEncoding(modifiedMethod))) + { + class_replaceMethod(class, modifiedSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); + } + else + { + method_exchangeImplementations(originalMethod, modifiedMethod); + } +} + +CGSize TGWatchScreenSize() +{ + static CGSize size; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^ + { + size = [[WKInterfaceDevice currentDevice] screenBounds].size; + }); + + return size; +} + +TGScreenType TGWatchScreenType() +{ + static TGScreenType type; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^ + { + int width = (int)TGWatchScreenSize().width; + switch (width) { + case 136: + type = TGScreenType38mm; + break; + case 156: + type = TGScreenType42mm; + break; + case 162: + type = TGScreenType40mm; + break; + case 184: + type = TGScreenType44mm; + break; + default: + type = TGScreenType42mm; + break; + } + }); + + return type; +} + +CGSize TGWatchStickerSizeForScreen(TGScreenType screenType) +{ + switch (screenType) { + case TGScreenType38mm: + return CGSizeMake(72, 72); + case TGScreenType42mm: + return CGSizeMake(84, 84); + case TGScreenType40mm: + return CGSizeMake(88, 88); + case TGScreenType44mm: + return CGSizeMake(100, 100); + default: + return CGSizeMake(84, 84); + } +} + +@implementation NSNumber (IntegerTypes) + +- (int32_t)int32Value +{ + return (int32_t)[self intValue]; +} + +- (int64_t)int64Value +{ + return (int64_t)[self longLongValue]; +} + +@end + + +int TGLocalizedStaticVersion = 0; + +static NSBundle *customLocalizationBundle = nil; + +static NSString *customLocalizationBundlePath() +{ + return [[TGExtensionDelegate documentsPath] stringByAppendingPathComponent:@"CustomLocalization.bundle"]; +} + +void TGSetLocalizationFromFile(NSURL *fileUrl) +{ + TGResetLocalization(); + + [[NSFileManager defaultManager] createDirectoryAtPath:customLocalizationBundlePath() withIntermediateDirectories:true attributes:nil error:nil]; + + NSString *stringsFilePath = [customLocalizationBundlePath() stringByAppendingPathComponent:@"Localizable.strings"]; + [[NSFileManager defaultManager] removeItemAtPath:stringsFilePath error:nil]; + + if ([[NSFileManager defaultManager] copyItemAtURL:fileUrl toURL:[NSURL fileURLWithPath:stringsFilePath] error:nil]) + { + NSString *tempPath = [NSTemporaryDirectory() stringByAppendingPathComponent:[[NSString alloc] initWithFormat:@"localiation-%d", (int)arc4random()]]; + [[NSFileManager defaultManager] copyItemAtPath:customLocalizationBundlePath() toPath:tempPath error:nil]; + customLocalizationBundle = [NSBundle bundleWithPath:tempPath]; + } +} + +bool TGIsCustomLocalizationActive() +{ + return customLocalizationBundle != nil; +} + +void TGResetLocalization() +{ + customLocalizationBundle = nil; + [[NSFileManager defaultManager] removeItemAtPath:customLocalizationBundlePath() error:nil]; + + TGLocalizedStaticVersion++; +} + +NSString *TGLocalized(NSString *s) +{ + static NSString *untranslatedString = nil; + + static dispatch_once_t onceToken1; + dispatch_once(&onceToken1, ^ + { + untranslatedString = [[NSString alloc] initWithFormat:@"UNTRANSLATED_%x", (int)arc4random()]; + + if ([[NSFileManager defaultManager] fileExistsAtPath:customLocalizationBundlePath()]) + customLocalizationBundle = [NSBundle bundleWithPath:customLocalizationBundlePath()]; + }); + + if (customLocalizationBundle != nil) + { + NSString *string = [customLocalizationBundle localizedStringForKey:s value:untranslatedString table:nil]; + if (string != nil && ![string isEqualToString:untranslatedString]) + return string; + } + + static NSBundle *localizationBundle = nil; + static NSBundle *fallbackBundle = nil; + + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^ + { + fallbackBundle = [NSBundle bundleWithPath:[[NSBundle mainBundle] pathForResource:@"en" ofType:@"lproj"]]; + + NSString *language = [[NSLocale preferredLanguages] objectAtIndex:0]; + + if (![[[NSBundle mainBundle] localizations] containsObject:language]) + { + localizationBundle = fallbackBundle; + + if ([language rangeOfString:@"-"].location != NSNotFound) + { + NSString *languageWithoutRegion = [language substringToIndex:[language rangeOfString:@"-"].location]; + + for (NSString *localization in [[NSBundle mainBundle] localizations]) + { + if ([languageWithoutRegion isEqualToString:localization]) + { + NSBundle *candidateBundle = [NSBundle bundleWithPath:[[NSBundle mainBundle] pathForResource:localization ofType:@"lproj"]]; + if (candidateBundle != nil) + localizationBundle = candidateBundle; + + break; + } + } + } + } + else + localizationBundle = [NSBundle mainBundle]; + }); + + NSString *string = [localizationBundle localizedStringForKey:s value:untranslatedString table:nil]; + if (string != nil && ![string isEqualToString:untranslatedString]) + return string; + + if (localizationBundle != fallbackBundle) + { + NSString *string = [fallbackBundle localizedStringForKey:s value:untranslatedString table:nil]; + if (string != nil && ![string isEqualToString:untranslatedString]) + return string; + } + + return s; +} diff --git a/Watch/Extension/WKInterface+TGInterface.h b/Watch/Extension/WKInterface+TGInterface.h new file mode 100644 index 0000000000..81bc4df48e --- /dev/null +++ b/Watch/Extension/WKInterface+TGInterface.h @@ -0,0 +1,47 @@ +#import + +@interface WKInterfaceObject (TGInterface) + +@property (nonatomic, assign) CGFloat alpha; +@property (nonatomic, assign, getter=isHidden) bool hidden; + +@property (nonatomic, assign) CGFloat width; +@property (nonatomic, assign) CGFloat height; + +- (void)_setInitialHidden:(bool)hidden; + +@end + +@interface WKInterfaceGroup (TGInterface) + +@property (nonatomic, strong) UIColor *backgroundColor; +@property (nonatomic, assign) CGFloat cornerRadius; + +@end + +@interface WKInterfaceLabel (TGInterface) + +@property (nonatomic, strong) NSString *text; +@property (nonatomic, strong) UIColor *textColor; + +@property (nonatomic, strong) NSString *hyphenatedText; + +@property (nonatomic, strong) NSAttributedString *attributedText; + +@end + +@interface WKInterfaceButton (TGInterface) + +@property (nonatomic, strong) NSString *title; +@property (nonatomic, strong) NSAttributedString *attributedTitle; + +@property (nonatomic, assign, getter=isEnabled) bool enabled; + +@end + +@interface WKInterfaceMap (TGInterface) + +@property (nonatomic, assign) MKCoordinateRegion region; +@property (nonatomic, assign) CLLocationCoordinate2D centerPinCoordinate; + +@end diff --git a/Watch/Extension/WKInterface+TGInterface.m b/Watch/Extension/WKInterface+TGInterface.m new file mode 100644 index 0000000000..f0efe043b3 --- /dev/null +++ b/Watch/Extension/WKInterface+TGInterface.m @@ -0,0 +1,256 @@ +#import "WKInterface+TGInterface.h" +#import "TGWatchCommon.h" +#import + +@implementation WKInterfaceObject (TGInterface) + +@dynamic alpha, hidden; + ++ (void)load +{ + TGSwizzleMethodImplementation(self.class, @selector(setAlpha:), @selector(tg_setAlpha:)); + TGSwizzleMethodImplementation(self.class, @selector(setHidden:), @selector(tg_setHidden:)); +} + +- (CGFloat)alpha +{ + return [objc_getAssociatedObject(self, @selector(alpha)) floatValue]; +} + +- (void)tg_setAlpha:(CGFloat)alpha +{ + objc_setAssociatedObject(self, @selector(alpha), @(alpha), OBJC_ASSOCIATION_RETAIN_NONATOMIC); + [self tg_setAlpha:alpha]; +} + +- (bool)isHidden +{ + return [objc_getAssociatedObject(self, @selector(isHidden)) boolValue]; +} + +- (void)tg_setHidden:(BOOL)hidden +{ + objc_setAssociatedObject(self, @selector(isHidden), @(hidden), OBJC_ASSOCIATION_RETAIN_NONATOMIC); + [self tg_setHidden:hidden]; +} + +- (void)_setInitialHidden:(bool)hidden +{ + objc_setAssociatedObject(self, @selector(isHidden), @(hidden), OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} + +- (CGFloat)width +{ + return [objc_getAssociatedObject(self, @selector(width)) floatValue]; +} + +- (void)tg_setWidth:(CGFloat)width +{ + objc_setAssociatedObject(self, @selector(width), @(width), OBJC_ASSOCIATION_RETAIN_NONATOMIC); + [self tg_setWidth:width]; +} + +- (CGFloat)height +{ + return [objc_getAssociatedObject(self, @selector(height)) floatValue]; +} + +- (void)tg_setHeight:(CGFloat)height +{ + objc_setAssociatedObject(self, @selector(height), @(height), OBJC_ASSOCIATION_RETAIN_NONATOMIC); + [self tg_setHeight:height]; +} + +@end + + +@implementation WKInterfaceGroup (TGInterface) + ++ (void)load +{ + TGSwizzleMethodImplementation(self.class, @selector(setBackgroundColor:), @selector(tg_setBackgroundColor:)); + TGSwizzleMethodImplementation(self.class, @selector(setCornerRadius:), @selector(tg_setCornerRadius:)); +} + +- (UIColor *)backgroundColor +{ + return objc_getAssociatedObject(self, @selector(backgroundColor)); +} + +- (void)tg_setBackgroundColor:(UIColor *)backgroundColor +{ + objc_setAssociatedObject(self, @selector(backgroundColor), backgroundColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + [self tg_setBackgroundColor:backgroundColor]; +} + +- (CGFloat)cornerRadius +{ + return [objc_getAssociatedObject(self, @selector(alpha)) floatValue]; +} + +- (void)tg_setCornerRadius:(CGFloat)cornerRadius +{ + objc_setAssociatedObject(self, @selector(cornerRadius), @(cornerRadius), OBJC_ASSOCIATION_RETAIN_NONATOMIC); + [self tg_setCornerRadius:cornerRadius]; +} + +@end + + +@implementation WKInterfaceLabel (TGInterface) + +@dynamic text, textColor, attributedText; + ++ (void)load +{ + TGSwizzleMethodImplementation(self.class, @selector(setText:), @selector(tg_setText:)); + TGSwizzleMethodImplementation(self.class, @selector(setTextColor:), @selector(tg_setTextColor:)); + TGSwizzleMethodImplementation(self.class, @selector(setAttributedText:), @selector(tg_setAttributedText:)); +} + +- (NSString *)text +{ + return objc_getAssociatedObject(self, @selector(text)); +} + +- (void)tg_setText:(NSString *)text +{ + objc_setAssociatedObject(self, @selector(text), text, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + [self tg_setText:text]; +} + +- (UIColor *)textColor +{ + return objc_getAssociatedObject(self, @selector(textColor)); +} + +- (void)tg_setTextColor:(UIColor *)textColor +{ + objc_setAssociatedObject(self, @selector(textColor), textColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + [self tg_setTextColor:textColor]; +} + +- (NSAttributedString *)attributedText +{ + return objc_getAssociatedObject(self, @selector(attributedText)); +} + +- (void)tg_setAttributedText:(NSAttributedString *)attributedText +{ + objc_setAssociatedObject(self, @selector(attributedText), attributedText, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + [self tg_setAttributedText:attributedText]; +} + +- (NSString *)hyphenatedText +{ + return self.attributedText.string; +} + +- (void)setHyphenatedText:(NSString *)hyphenatedText +{ + NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init]; + paragraphStyle.hyphenationFactor = 1.0f; + + self.attributedText = [[NSAttributedString alloc] initWithString:hyphenatedText attributes:@{ NSParagraphStyleAttributeName:paragraphStyle }]; +} + +@end + + +@implementation WKInterfaceButton (TGInterface) + ++ (void)load +{ + TGSwizzleMethodImplementation(self.class, @selector(setTitle:), @selector(tg_setTitle:)); + TGSwizzleMethodImplementation(self.class, @selector(setAttributedTitle:), @selector(tg_setAttributedTitle:)); + TGSwizzleMethodImplementation(self.class, @selector(setTextColor:), @selector(tg_setTextColor:)); +} + +- (NSString *)title +{ + return objc_getAssociatedObject(self, @selector(title)); +} + +- (void)tg_setTitle:(NSString *)title +{ + objc_setAssociatedObject(self, @selector(title), title, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + [self tg_setTitle:title]; +} + +- (NSAttributedString *)attributedTitle +{ + return objc_getAssociatedObject(self, @selector(attributedTitle)); +} + +- (void)tg_setAttributedTitle:(NSAttributedString *)attributedTitle +{ + objc_setAssociatedObject(self, @selector(attributedTitle), attributedTitle, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + [self tg_setAttributedTitle:attributedTitle]; +} + +- (bool)isEnabled +{ + return [objc_getAssociatedObject(self, @selector(isEnabled)) boolValue]; +} + +- (void)tg_setEnabled:(BOOL)enabled +{ + objc_setAssociatedObject(self, @selector(isEnabled), @(enabled), OBJC_ASSOCIATION_RETAIN_NONATOMIC); + [self tg_setEnabled:enabled]; +} + +@end + + +@implementation WKInterfaceMap (TGInterface) + ++ (void)load +{ + TGSwizzleMethodImplementation(self.class, @selector(setRegion:), @selector(tg_setRegion:)); +} + +- (MKCoordinateRegion)region +{ + MKCoordinateRegion region = MKCoordinateRegionMake(CLLocationCoordinate2DMake(0.0, 0.0), MKCoordinateSpanMake(0.0, 0.0)); + + NSArray *values = objc_getAssociatedObject(self, @selector(region)); + if (values != nil) + region = MKCoordinateRegionMake([values.firstObject MKCoordinateValue], [values.lastObject MKCoordinateSpanValue]); + + return region; +} + +- (void)tg_setRegion:(MKCoordinateRegion)region +{ + MKCoordinateRegion currentRegion = self.region; + + if (fabs(currentRegion.center.latitude - region.center.latitude) < DBL_EPSILON && fabs(currentRegion.center.longitude - region.center.longitude) < DBL_EPSILON && fabs(currentRegion.span.latitudeDelta - region.span.latitudeDelta) < DBL_EPSILON && fabs(currentRegion.span.longitudeDelta - region.span.longitudeDelta) < DBL_EPSILON) + { + return; + } + + objc_setAssociatedObject(self, @selector(region), @[ [NSValue valueWithMKCoordinate:region.center], [NSValue valueWithMKCoordinateSpan:region.span] ], OBJC_ASSOCIATION_RETAIN_NONATOMIC); + [self tg_setRegion:region]; +} + +- (CLLocationCoordinate2D)centerPinCoordinate +{ + return [objc_getAssociatedObject(self, @selector(centerPinCoordinate)) MKCoordinateValue]; +} + +- (void)setCenterPinCoordinate:(CLLocationCoordinate2D)centerPinCoordinate +{ + CLLocationCoordinate2D currentCoordinate = self.centerPinCoordinate; + + if (fabs(currentCoordinate.latitude - centerPinCoordinate.latitude) < DBL_EPSILON && fabs(currentCoordinate.longitude - centerPinCoordinate.longitude) < DBL_EPSILON) + { + return; + } + + [self removeAllAnnotations]; + + if (fabs(centerPinCoordinate.latitude) > 0 || fabs(centerPinCoordinate.longitude) > 0) + [self addAnnotation:centerPinCoordinate withPinColor:WKInterfaceMapPinColorRed]; +} + +@end diff --git a/Watch/Extension/WKInterfaceGroup+Signals.h b/Watch/Extension/WKInterfaceGroup+Signals.h new file mode 100644 index 0000000000..ec11390c9e --- /dev/null +++ b/Watch/Extension/WKInterfaceGroup+Signals.h @@ -0,0 +1,9 @@ +#import +#import + +@interface WKInterfaceGroup (Signals) + +- (void)setBackgroundImageSignal:(SSignal *)signal isVisible:(bool (^)(void))isVisible; +- (void)updateIfNeeded; + +@end diff --git a/Watch/Extension/WKInterfaceGroup+Signals.m b/Watch/Extension/WKInterfaceGroup+Signals.m new file mode 100644 index 0000000000..e2d480c03a --- /dev/null +++ b/Watch/Extension/WKInterfaceGroup+Signals.m @@ -0,0 +1,146 @@ +#import "WKInterfaceGroup+Signals.h" +#import "TGWatchCommon.h" +#import + +@interface WKInterfaceGroup (Signals_Private) + +@property (nonatomic, strong) SMetaDisposable *disposable; +@property (nonatomic, strong) id postponedImage; + +@property (nonatomic, assign) bool isEmpty; + +@end + +@implementation WKInterfaceGroup (Signals) + ++ (void)load +{ + TGSwizzleMethodImplementation(self.class, NSSelectorFromString(@"dealloc"), @selector(tg_dealloc)); +} + +- (void)tg_dealloc +{ + [self.disposable dispose]; + + [self tg_dealloc]; +} + +- (id)postponedImage +{ + return objc_getAssociatedObject(self, @selector(postponedImage)); +} + +- (void)setPostponedImage:(id)image +{ + objc_setAssociatedObject(self, @selector(postponedImage), image, OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} + +- (NSInteger)isEmpty +{ + return [objc_getAssociatedObject(self, @selector(isEmpty)) boolValue]; +} + +- (void)setIsEmpty:(bool)empty +{ + objc_setAssociatedObject(self, @selector(isEmpty), @(empty), OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} + +- (NSInteger)version +{ + return [objc_getAssociatedObject(self, @selector(version)) integerValue]; +} + +- (void)setVersion:(NSInteger)version +{ + objc_setAssociatedObject(self, @selector(version), @(version), OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} + +- (SMetaDisposable *)disposable +{ + return objc_getAssociatedObject(self, @selector(disposable)); +} + +- (void)setDisposable:(SMetaDisposable *)disposable +{ + objc_setAssociatedObject(self, @selector(disposable), disposable, OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} + +- (void)setBackgroundImageSignal:(SSignal *)signal isVisible:(bool (^)(void))isVisible +{ + if (self.disposable == nil) + self.disposable = [[SMetaDisposable alloc] init]; + + NSInteger version = ++self.version; + + __weak WKInterfaceGroup *weakSelf = self; + [self.disposable setDisposable:[[signal deliverOn:[SQueue mainQueue]] startWithNext:^(id next) + { + __strong WKInterfaceGroup *strongSelf = weakSelf; + if (strongSelf == nil || strongSelf.version != version) + return; + + bool shouldUpdate = true; + if (isVisible != nil) + shouldUpdate = isVisible(); + + if (shouldUpdate) + [strongSelf _setBackgroundImageWithNext:next]; + else + strongSelf.postponedImage = next; + } error:^(id error) + { + } completed:^ + { + }]]; + + if (signal == nil && !self.isEmpty) + { + bool shouldUpdate = true; + if (isVisible != nil) + shouldUpdate = isVisible(); + + if (shouldUpdate) + { + self.isEmpty = true; + [self setBackgroundImage:nil]; + } + else + { + self.postponedImage = [NSNull null]; + } + } +} + +- (void)updateIfNeeded +{ + if (self.postponedImage == nil) + return; + + UIImage *image = self.postponedImage; + self.postponedImage = nil; + + [self _setBackgroundImageWithNext:image]; +} + +- (void)_setBackgroundImageWithNext:(id)next +{ + if ([next isKindOfClass:[NSString class]]) + { + [self setBackgroundImageNamed:(NSString *)next]; + } + else if ([next isKindOfClass:[NSData class]]) + { + [self setBackgroundImageData:(NSData *)next]; + } + else if ([next isKindOfClass:[UIImage class]]) + { + [self setBackgroundImage:(UIImage *)next]; + } + else if ([next isKindOfClass:[NSNull class]]) + { + self.isEmpty = true; + [self setBackgroundImage:nil]; + } +} + +@end diff --git a/Watch/Extension/WKInterfaceImage+Signals.h b/Watch/Extension/WKInterfaceImage+Signals.h new file mode 100644 index 0000000000..8647d90bee --- /dev/null +++ b/Watch/Extension/WKInterfaceImage+Signals.h @@ -0,0 +1,9 @@ +#import +#import + +@interface WKInterfaceImage (Signals) + +- (void)setSignal:(SSignal *)signal isVisible:(bool (^)(void))isVisible; +- (void)updateIfNeeded; + +@end diff --git a/Watch/Extension/WKInterfaceImage+Signals.m b/Watch/Extension/WKInterfaceImage+Signals.m new file mode 100644 index 0000000000..70aad97f0c --- /dev/null +++ b/Watch/Extension/WKInterfaceImage+Signals.m @@ -0,0 +1,111 @@ +#import "WKInterfaceImage+Signals.h" +#import "TGWatchCommon.h" +#import + +@interface WKInterfaceImage (Signals_Private) + +@property (nonatomic, strong) SMetaDisposable *disposable; +@property (nonatomic, strong) id postponedImage; + +@end + +@implementation WKInterfaceImage (Signals) + ++ (void)load +{ + TGSwizzleMethodImplementation(self.class, NSSelectorFromString(@"dealloc"), @selector(tg_dealloc)); +} + +- (void)tg_dealloc +{ + [self.disposable dispose]; + + [self tg_dealloc]; +} + +- (id)postponedImage +{ + return objc_getAssociatedObject(self, @selector(postponedImage)); +} + +- (void)setPostponedImage:(id)image +{ + objc_setAssociatedObject(self, @selector(postponedImage), image, OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} + +- (NSInteger)version +{ + return [objc_getAssociatedObject(self, @selector(version)) integerValue]; +} + +- (void)setVersion:(NSInteger)version +{ + objc_setAssociatedObject(self, @selector(version), @(version), OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} + +- (SMetaDisposable *)disposable +{ + return objc_getAssociatedObject(self, @selector(disposable)); +} + +- (void)setDisposable:(SMetaDisposable *)disposable +{ + objc_setAssociatedObject(self, @selector(disposable), disposable, OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} + +- (void)setSignal:(SSignal *)signal isVisible:(bool (^)(void))isVisible +{ + if (self.disposable == nil) + self.disposable = [[SMetaDisposable alloc] init]; + + NSInteger version = ++self.version; + + __weak WKInterfaceImage *weakSelf = self; + [self.disposable setDisposable:[[signal deliverOn:[SQueue mainQueue]] startWithNext:^(id next) + { + __strong WKInterfaceImage *strongSelf = weakSelf; + if (strongSelf == nil || strongSelf.version != version) + return; + + bool shouldUpdate = true; + if (isVisible != nil) + shouldUpdate = isVisible(); + + if (shouldUpdate) + [strongSelf _setImageWithNext:next]; + else + strongSelf.postponedImage = next; + } error:^(id error) + { + } completed:^ + { + }]]; + + if (signal == nil) + [self setImage:nil]; +} + +- (void)updateIfNeeded +{ + if (self.postponedImage == nil) + return; + + UIImage *image = self.postponedImage; + self.postponedImage = nil; + + [self _setImageWithNext:image]; +} + +- (void)_setImageWithNext:(id)next +{ + if ([next isKindOfClass:[NSString class]]) + [self setImageNamed:(NSString *)next]; + else if ([next isKindOfClass:[NSData class]]) + [self setImageData:(NSData *)next]; + else if ([next isKindOfClass:[UIImage class]]) + [self setImage:(UIImage *)next]; + else if ([next isKindOfClass:[NSNull class]]) + [self setImage:nil]; +} + +@end diff --git a/Watch/Extension/WKInterfaceTable+TGDataDrivenTable.h b/Watch/Extension/WKInterfaceTable+TGDataDrivenTable.h new file mode 100644 index 0000000000..e27297606a --- /dev/null +++ b/Watch/Extension/WKInterfaceTable+TGDataDrivenTable.h @@ -0,0 +1,96 @@ +#import +#import "TGIndexPath.h" + +@protocol TGTableItem + +- (NSString *)uniqueIdentifier; + +@end + +@interface TGTableRowController : NSObject + +@property (nonatomic, readonly) bool initialized; +@property (nonatomic, copy) bool (^isVisible)(void); +- (void)notifyVisiblityChange; +- (bool)_isVisible; + +- (void)setupInterface; + ++ (NSString *)identifier; + +@end + +@protocol TGTableDataSource + +- (NSUInteger)numberOfRowsInTable:(WKInterfaceTable *)table section:(NSUInteger)section; +- (Class)table:(WKInterfaceTable *)table rowControllerClassAtIndexPath:(TGIndexPath *)indexPath; + +@optional + +- (NSUInteger)numberOfSectionsInTable:(WKInterfaceTable *)table; + +- (Class)headerControllerClassForTable:(WKInterfaceTable *)table; +- (Class)footerControllerClassForTable:(WKInterfaceTable *)table; + +- (Class)table:(WKInterfaceTable *)table controllerClassForSection:(NSUInteger)section; + +- (void)table:(WKInterfaceTable *)table updateHeaderController:(TGTableRowController *)controller; +- (void)table:(WKInterfaceTable *)table updateFooterController:(TGTableRowController *)controller; + +- (void)table:(WKInterfaceTable *)table updateSectionController:(TGTableRowController *)controller forSection:(NSUInteger)section; + +- (void)table:(WKInterfaceTable *)table updateRowController:(TGTableRowController *)controller forIndexPath:(TGIndexPath *)indexPath; + +@end + + +@interface WKInterfaceTable (TGDataDrivenTable) + +@property (nonatomic, weak) id tableDataSource; + +@property (nonatomic, readonly) TGTableRowController *headerController; +@property (nonatomic, readonly) TGTableRowController *footerController; + +@property (nonatomic, assign) bool reloadDataReversed; + +- (TGTableRowController *)controllerForRowAtIndexPath:(TGIndexPath *)indexPath; +- (TGIndexPath *)indexPathForRowWithController:(TGTableRowController *)controller; + +- (void)reloadData; + +- (void)reloadHeader; +- (void)reloadFooter; +- (void)reloadSectionHeader:(NSUInteger)section; + +- (void)beginUpdates; +- (void)endUpdates; + +- (void)scrollToSection:(NSUInteger)section; +- (void)scrollToRowAtIndexPath:(TGIndexPath *)indexPath; +- (void)scrollToBottom; + +- (void)insertSections:(NSIndexSet *)sections withSectionControllerClass:(Class)controllerClass; +- (void)removeSections:(NSIndexSet *)sections; + +- (void)insertRowsAtIndexPaths:(NSArray *)indexPaths withRowControllerClass:(Class)controllerClass; +- (void)removeRowsAtIndexPaths:(NSArray *)indexPaths; +- (void)reloadRowsAtIndexPaths:(NSArray *)indexPaths; +- (void)reloadAllRows; + +- (void)applyBatchChanges:(NSArray *)changes; + +- (void)notifyVisiblityChange; + +- (TGIndexPath *)indexPathForRowIndex:(NSUInteger)rowIndex; + +@end + + +@interface WKInterfaceController (TGDataDrivenTable) + +- (void)tableDidSelectHeader:(WKInterfaceTable *)table; +- (void)tableDidSelectFooter:(WKInterfaceTable *)table; +- (void)table:(WKInterfaceTable *)table didSelectSection:(NSUInteger)section; +- (void)table:(WKInterfaceTable *)table didSelectRowAtIndexPath:(TGIndexPath *)indexPath; + +@end diff --git a/Watch/Extension/WKInterfaceTable+TGDataDrivenTable.m b/Watch/Extension/WKInterfaceTable+TGDataDrivenTable.m new file mode 100644 index 0000000000..2812922f6c --- /dev/null +++ b/Watch/Extension/WKInterfaceTable+TGDataDrivenTable.m @@ -0,0 +1,793 @@ +#import "WKInterfaceTable+TGDataDrivenTable.h" +#import +#import "TGWatchCommon.h" + +#import "TGTableDeltaUpdater.h" + +typedef enum +{ + TGTableDataEntryTypeRow, + TGTableDataEntryTypeSection, + TGTableDataEntryTypeHeader, + TGTableDataEntryTypeFooter +} TGTableDataEntryType; + +@interface TGTableDataEntry : NSObject + +@property (nonatomic, readonly) TGTableDataEntryType type; +@property (nonatomic, readonly) Class controllerClass; +@property (nonatomic, assign) NSUInteger section; +@property (nonatomic, assign) NSUInteger row; + +@property (nonatomic, readonly) TGIndexPath *indexPath; +@property (nonatomic, readonly) NSString *controllerIdentifier; + +@end + +@implementation TGTableDataEntry + +- (instancetype)initWithControllerClass:(Class)controllerClass type:(TGTableDataEntryType)type +{ + NSParameterAssert(type == TGTableDataEntryTypeHeader || type == TGTableDataEntryTypeFooter); + return [self initWithControllerClass:controllerClass section:NSNotFound row:NSNotFound type:type]; +} + +- (instancetype)initWithControllerClass:(Class)controllerClass section:(NSInteger)section +{ + return [self initWithControllerClass:controllerClass section:section row:NSNotFound type:TGTableDataEntryTypeSection]; +} + +- (instancetype)initWithControllerClass:(Class)controllerClass section:(NSUInteger)section row:(NSUInteger)row +{ + return [self initWithControllerClass:controllerClass section:section row:row type:TGTableDataEntryTypeRow]; +} + +- (instancetype)initWithControllerClass:(Class)controllerClass section:(NSUInteger)section row:(NSUInteger)row type:(TGTableDataEntryType) type +{ + self = [super init]; + if (self != nil) + { + _controllerClass = controllerClass; + _section = section; + _row = row; + _type = type; + } + return self; +} + +- (TGIndexPath *)indexPath +{ + if (self.section == NSNotFound || self.row == NSNotFound) + return nil; + + return [TGIndexPath indexPathForRow:self.row inSection:self.section]; +} + +- (NSString *)controllerIdentifier +{ + return [self.controllerClass identifier]; +} + +- (NSUInteger)hash +{ + return self.controllerIdentifier.hash ^ self.section ^ self.row; +} + +- (BOOL)isEqual:(id)object +{ + if (object == self) + return YES; + + if (!object || ![object isKindOfClass:[self class]]) + return NO; + + TGTableDataEntry *entry = (TGTableDataEntry *)object; + return (self.controllerClass == entry.controllerClass && self.section == entry.section && self.row == entry.row); +} + +@end + + +@implementation TGTableRowController + +- (bool)_isVisible +{ + if (self.isVisible == nil) + return true; + + return self.isVisible(); +} + +- (void)setupInterface +{ + _initialized = true; +} + +- (void)notifyVisiblityChange +{ + +} + ++ (NSString *)identifier +{ + return nil; +} + +@end + + +@implementation WKInterfaceTable (TGDataDrivenTable) + +@dynamic tableDataSource; + +- (void)reloadData +{ + NSArray *tableData = [self fetchDataFromDataSource:self.tableDataSource]; + NSArray *rowControllerIdentifiers = [tableData valueForKey:@"controllerIdentifier"]; + [self setRowTypes:rowControllerIdentifiers]; + [self updateRowControllersWithData:tableData]; + [self setTableData:tableData]; +} + +- (void)reloadHeader +{ + if ([self _hasHeader] && self.numberOfRows > 0) + [self updateRowAtIndex:0]; +} + +- (void)reloadFooter +{ + if ([self _hasFooter] && self.numberOfRows > 0) + [self updateRowAtIndex:self.numberOfRows - 1]; +} + +- (void)reloadSectionHeader:(NSUInteger)section +{ + NSUInteger rowIndex = [self rowIndexForSection:section]; + if (rowIndex != NSNotFound) + [self updateRowAtIndex:rowIndex]; +} + +- (NSArray *)fetchDataFromDataSource:(id)dataSource +{ + NSParameterAssert(dataSource); + + NSMutableArray *tableData = [[NSMutableArray alloc] init]; + + if ([dataSource respondsToSelector:@selector(headerControllerClassForTable:)]) + { + Class controllerClass = [dataSource headerControllerClassForTable:self]; + if (controllerClass != nil) + [tableData addObject:[[TGTableDataEntry alloc] initWithControllerClass:controllerClass type:TGTableDataEntryTypeHeader]]; + } + + NSUInteger sectionsCount = 1; + if ([dataSource respondsToSelector:@selector(numberOfSectionsInTable:)]) + sectionsCount = [dataSource numberOfSectionsInTable:self]; + + bool mayHaveSectionHeader = [dataSource respondsToSelector:@selector(table:controllerClassForSection:)]; + + for (NSUInteger section = 0; section < sectionsCount; section++) + { + if (mayHaveSectionHeader) + { + Class controllerClass = [dataSource table:self controllerClassForSection:section]; + if (controllerClass != nil) + [tableData addObject:[[TGTableDataEntry alloc] initWithControllerClass:controllerClass section:section]]; + } + + NSUInteger rowsCount = [dataSource numberOfRowsInTable:self section:section]; + for (NSUInteger row = 0; row < rowsCount; row++) + { + TGIndexPath *indexPath = [TGIndexPath indexPathForRow:row inSection:section]; + Class controllerClass = [dataSource table:self rowControllerClassAtIndexPath:indexPath]; + NSAssert(controllerClass, @"Row controller class is a must"); + [tableData addObject:[[TGTableDataEntry alloc] initWithControllerClass:controllerClass section:section row:row]]; + } + } + + if ([dataSource respondsToSelector:@selector(footerControllerClassForTable:)]) + { + Class controllerClass = [dataSource footerControllerClassForTable:self]; + if (controllerClass != nil) + [tableData addObject:[[TGTableDataEntry alloc] initWithControllerClass:controllerClass type:TGTableDataEntryTypeFooter]]; + } + + return tableData; +} + +- (void)updateRowControllersWithData:(NSArray *)tableData +{ + NSEnumerationOptions options = kNilOptions; + if (self.reloadDataReversed) + options = NSEnumerationReverse; + + [tableData enumerateObjectsWithOptions:options usingBlock:^(TGTableDataEntry *tableEntry, NSUInteger index, BOOL *stop) + { + [self updateRowAtIndex:index withTableData:tableData]; + }]; +} + +#pragma mark - + +- (bool)_hasHeader +{ + return ([self.tableDataSource respondsToSelector:@selector(headerControllerClassForTable:)] && [self.tableDataSource headerControllerClassForTable:self] != nil); +} + +- (TGTableRowController *)headerController +{ + if ([self _hasHeader]) + return [self rowControllerAtIndex:0]; + + return nil; +} + +- (bool)_hasFooter +{ + return ([self.tableDataSource respondsToSelector:@selector(footerControllerClassForTable:)] && [self.tableDataSource footerControllerClassForTable:self] != nil); +} + +- (TGTableRowController *)footerController +{ + if ([self _hasFooter]) + return [self rowControllerAtIndex:self.numberOfRows - 1]; + + return nil; +} + +- (TGTableRowController *)controllerForRowAtIndexPath:(TGIndexPath *)indexPath +{ + NSInteger rowIndex = [self rowIndexForIndexPath:indexPath]; + if (rowIndex != NSNotFound) + return [self rowControllerAtIndex:rowIndex]; + + return nil; +} + +- (TGIndexPath *)indexPathForRowWithController:(TGTableRowController *)controller +{ + for (NSInteger i = 0; i < self.numberOfRows; i++) + { + TGTableRowController *rowController = [self rowControllerAtIndex:i]; + if (rowController == controller) + return [self indexPathForRowIndex:i]; + } + + return nil; +} + +#pragma mark - + +- (void)beginUpdates +{ + self.isUpdating = true; + self.rowIndexesToAdd = [[NSMutableArray alloc] init]; + self.rowClassesToAdd = [[NSMutableDictionary alloc] init]; +} + +- (void)endUpdates +{ + NSAssert(self.isUpdating, @"Call beginUpdates first"); + + if (!self.isUpdating) + return; + + [self.rowIndexesToAdd sortUsingComparator:^NSComparisonResult(TGIndexPath *obj1, TGIndexPath *obj2) + { + NSInteger r1 = obj1.row; + NSInteger r2 = obj2.row; + + if (r1 > r2) + return NSOrderedDescending; + if (r1 < r2) + return NSOrderedAscending; + + return NSOrderedSame; + }]; + + self.isUpdating = false; + + [self.rowIndexesToAdd enumerateObjectsUsingBlock:^(TGIndexPath *indexPath, NSUInteger idx, BOOL *stop) + { + [self insertRowsAtIndexPaths:@[ indexPath ] withRowControllerClass:self.rowClassesToAdd[indexPath]]; + }]; + + self.rowIndexesToAdd = nil; + self.rowClassesToAdd = nil; +} + +#pragma mark - + +- (void)insertSections:(NSIndexSet *)sections withSectionControllerClass:(Class)controllerClass +{ + NSMutableIndexSet *rowIndexes = [[NSMutableIndexSet alloc] init]; + NSMutableArray *insertedSections = [[NSMutableArray alloc] init]; + + [sections enumerateIndexesUsingBlock:^(NSUInteger section, BOOL *stop) + { + NSUInteger rowIndex = [self rowIndexForSection:section]; + if (rowIndex != NSNotFound) + { + [rowIndexes addIndex:rowIndex]; + [insertedSections addObject:@(section)]; + } + }]; + + [self insertRowsAtIndexes:rowIndexes withRowType:[controllerClass identifier]]; + + if ([self.tableDataSource respondsToSelector:@selector(table:updateSectionController:forSection:)]) + { + [rowIndexes enumerateIndexesUsingBlock:^(NSUInteger index, BOOL *stop) + { + TGTableRowController *controller = [self rowControllerAtIndex:index]; + NSUInteger section = [insertedSections[index] integerValue]; + [self.tableDataSource table:self updateSectionController:controller forSection:section]; + }]; + } + + [self _updateTableData]; +} + +- (void)removeSections:(NSIndexSet *)sections +{ + NSMutableIndexSet *rowIndexes = [[NSMutableIndexSet alloc] init]; + NSArray *tableData = [self tableData]; + NSUInteger count = tableData.count; + + [sections enumerateIndexesUsingBlock:^(NSUInteger section, BOOL *stop) + { + NSUInteger rowIndex = [self rowIndexForSection:section]; + if (rowIndex != NSNotFound) + { + [rowIndexes addIndex:rowIndex]; + + if (rowIndex < tableData.count - 1) + { + NSUInteger subArrayStart = rowIndex + 1; + NSArray *subRowData = [tableData subarrayWithRange:NSMakeRange(subArrayStart, count - subArrayStart)]; + [subRowData enumerateObjectsUsingBlock:^(TGTableDataEntry *row, NSUInteger index, BOOL *stop2) + { + if (row.section == section) + [rowIndexes addIndex:subArrayStart + index]; + else + *stop2 = true; + }]; + } + } + }]; + + [self removeRowsAtIndexes:rowIndexes]; + + [self _updateTableData]; +} + +- (void)insertRowsAtIndexPaths:(NSArray *)indexPaths withRowControllerClass:(Class)controllerClass +{ + if (indexPaths.count == 0) + return; + + if (self.isUpdating) + { + [self.rowIndexesToAdd addObjectsFromArray:indexPaths]; + [indexPaths enumerateObjectsUsingBlock:^(TGIndexPath *indexPath, NSUInteger idx, BOOL *stop) + { + [self.rowClassesToAdd setObject:controllerClass forKey:indexPath]; + }]; + + return; + } + + NSArray *tableData = [self fetchDataFromDataSource:self.tableDataSource]; + [self setTableData:tableData]; + + NSMutableIndexSet *rowIndexes = [[NSMutableIndexSet alloc] init]; + + for (TGIndexPath *indexPath in indexPaths) + { + NSUInteger rowIndex = [self rowIndexForIndexPath:indexPath]; + if (rowIndex != NSNotFound) + [rowIndexes addIndex:rowIndex]; + } + + [self insertRowsAtIndexes:rowIndexes withRowType:[controllerClass identifier]]; + + if ([self.tableDataSource respondsToSelector:@selector(table:updateRowController:forIndexPath:)]) + { + [rowIndexes enumerateIndexesUsingBlock:^(NSUInteger index, BOOL *stop) + { + TGTableRowController *controller = [self rowControllerAtIndex:index]; + TGTableDataEntry *tableEntry = tableData[index]; + TGIndexPath *indexPath = [TGIndexPath indexPathForRow:tableEntry.row inSection:tableEntry.section]; + [self.tableDataSource table:self updateRowController:controller forIndexPath:indexPath]; + }]; + } +} + +- (void)removeRowsAtIndexPaths:(NSArray *)indexPaths +{ + if (indexPaths.count == 0) + return; + + NSMutableIndexSet *rowIndexes = [[NSMutableIndexSet alloc] init]; + + for (TGIndexPath *indexPath in indexPaths) + { + NSUInteger rowIndex = [self rowIndexForIndexPath:indexPath]; + [rowIndexes addIndex:rowIndex]; + } + + [self removeRowsAtIndexes:rowIndexes]; + + NSArray *tableData = [self fetchDataFromDataSource:self.tableDataSource]; + [self setTableData:tableData]; +} + +- (void)reloadAllRows +{ + NSMutableArray *reloads = [[NSMutableArray alloc] init]; + NSInteger count = [self.tableDataSource numberOfRowsInTable:self section:0]; + + for (NSInteger i = 0; i < count; i++) + [reloads addObject:[TGIndexPath indexPathForRow:i inSection:0]]; + + [self reloadRowsAtIndexPaths:reloads]; +} + +- (void)applyBatchChanges:(NSArray *)changes +{ + NSArray *tableData = [self fetchDataFromDataSource:self.tableDataSource]; + [self setTableData:tableData]; + + NSInteger indexOffset = [self rowIndexForIndexPath:[TGIndexPath indexPathForRow:0 inSection:0]]; + if (indexOffset == NSNotFound) + indexOffset = 0; + + for (TGTableAlignment *alignment in changes) + { + if (alignment.pos < 0 || alignment.pos > 1000) + { + [self reloadData]; + return; + } + + if (!alignment.deletion) + { + for (NSInteger i = alignment.pos; i < alignment.pos + alignment.len; i++) + { + NSInteger index = i + indexOffset; + Class controllerClass = [self.tableDataSource table:self rowControllerClassAtIndexPath:[TGIndexPath indexPathForRow:i inSection:0]]; + [self insertRowsAtIndexes:[NSIndexSet indexSetWithIndex:index] withRowType:[controllerClass identifier]]; + } + } + else + { + NSMutableIndexSet *indexSet = [[NSMutableIndexSet alloc] init]; + for (NSInteger i = alignment.pos; i < alignment.pos + alignment.len; i++) + { + [indexSet addIndex:i + indexOffset]; + } + [self removeRowsAtIndexes:indexSet]; + } + } +} + +- (NSInteger)_indexForIndexPath:(TGIndexPath *)indexPath firstIndex:(NSInteger)firstIndex +{ + return indexPath.row + firstIndex; +} + +- (void)reloadRowsAtIndexPaths:(NSArray *)indexPaths +{ + if (indexPaths.count == 0) + return; + + NSMutableIndexSet *rowIndexes = [[NSMutableIndexSet alloc] init]; + + for (TGIndexPath *indexPath in indexPaths) + { + NSUInteger rowIndex = [self rowIndexForIndexPath:indexPath]; + if (rowIndex != NSNotFound) + [rowIndexes addIndex:rowIndex]; + } + + [rowIndexes enumerateIndexesUsingBlock:^(NSUInteger index, BOOL *stop) + { + [self updateRowAtIndex:index]; + }]; +} + +- (void)notifyVisiblityChange +{ + for (NSInteger i = 0; i < self.numberOfRows; i++) + { + TGTableRowController *controller = [self rowControllerAtIndex:i]; + [controller notifyVisiblityChange]; + } +} + +- (void)updateRowAtIndex:(NSUInteger)index +{ + [self updateRowAtIndex:index withTableData:self.tableData]; +} + +- (void)updateRowAtIndex:(NSUInteger)index withTableData:(NSArray *)tableData +{ + id dataSource = self.tableDataSource; + + TGTableRowController *controller = [self rowControllerAtIndex:index]; + TGTableDataEntry *tableEntry = tableData[index]; + + if (!controller.initialized) + [controller setupInterface]; + + switch (tableEntry.type) + { + case TGTableDataEntryTypeHeader: + { + if ([dataSource respondsToSelector:@selector(table:updateHeaderController:)]) + [dataSource table:self updateHeaderController:controller]; + } + break; + + case TGTableDataEntryTypeFooter: + { + if ([dataSource respondsToSelector:@selector(table:updateFooterController:)]) + [dataSource table:self updateFooterController:controller]; + } + break; + + case TGTableDataEntryTypeSection: + { + if ([dataSource respondsToSelector:@selector(table:updateSectionController:forSection:)]) + [dataSource table:self updateSectionController:controller forSection:tableEntry.section]; + } + break; + + case TGTableDataEntryTypeRow: + { + if ([dataSource respondsToSelector:@selector(table:updateRowController:forIndexPath:)]) + [dataSource table:self updateRowController:controller forIndexPath:tableEntry.indexPath]; + } + break; + + default: + break; + } +} + +- (void)_updateTableData +{ + [self setTableData:[self fetchDataFromDataSource:self.tableDataSource]]; +} + +- (NSArray *)smoothedTableData:(NSArray *)tableData +{ + NSMutableArray *newTableData = [[NSMutableArray alloc] initWithCapacity:tableData.count]; + NSUInteger runningSection = 0, previousSection = 0, runningRow = 0, previousRow = 0; + + TGTableDataEntry *firstRowData = [tableData firstObject]; + previousSection = firstRowData.section; + + for (TGTableDataEntry *tableEntry in tableData) + { + NSUInteger section = tableEntry.section; + NSUInteger row = tableEntry.row; + + if (section < NSNotFound) + { + if (section != previousSection && previousSection != NSNotFound) + { + runningSection++; + runningRow = 0; + } + + section = runningSection; + + if (row < NSNotFound) + { + row = runningRow; + runningRow++; + } + } + + previousSection = tableEntry.section; + + TGTableDataEntry *newTableEntry = [[TGTableDataEntry alloc] initWithControllerClass:tableEntry.controllerClass section:section row:row]; + [newTableData addObject:newTableEntry]; + } + + return newTableData; +} + +#pragma mark - + +- (void)scrollToSection:(NSUInteger)section +{ + NSUInteger rowIndex = [self rowIndexForSection:section]; + if (rowIndex != NSNotFound) + [self scrollToRowAtIndex:rowIndex]; + else + [self scrollToRowAtIndexPath:[TGIndexPath indexPathForRow:0 inSection:section]]; +} + +- (void)scrollToRowAtIndexPath:(TGIndexPath *)indexPath +{ + NSUInteger rowIndex = [self rowIndexForIndexPath:indexPath]; + if (rowIndex != NSNotFound) + [self scrollToRowAtIndex:rowIndex]; +} + +- (void)scrollToBottom +{ + [self scrollToRowAtIndex:[self numberOfRows] + 1]; +} + +#pragma mark - + +- (bool)isUpdating +{ + return [objc_getAssociatedObject(self, @selector(isUpdating)) boolValue]; +} + +- (void)setIsUpdating:(bool)updating +{ + objc_setAssociatedObject(self, @selector(isUpdating), @(updating), OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} + +- (NSMutableArray *)rowIndexesToAdd +{ + return objc_getAssociatedObject(self, @selector(rowIndexesToAdd)); +} + +- (void)setRowIndexesToAdd:(NSMutableArray *)rowIndexesToAdd +{ + objc_setAssociatedObject(self, @selector(rowIndexesToAdd), rowIndexesToAdd, OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} + +- (NSMutableDictionary *)rowClassesToAdd +{ + return objc_getAssociatedObject(self, @selector(rowClassesToAdd)); +} + +- (void)setRowClassesToAdd:(NSMutableDictionary *)rowClassesToAdd +{ + objc_setAssociatedObject(self, @selector(rowClassesToAdd), rowClassesToAdd, OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} + +- (id)tableDataSource +{ + return objc_getAssociatedObject(self, @selector(tableDataSource)); +} + +- (void)setTableDataSource:(id)dataSource +{ + objc_setAssociatedObject(self, @selector(tableDataSource), dataSource, OBJC_ASSOCIATION_ASSIGN); +} + +- (bool)reloadDataReversed +{ + return [objc_getAssociatedObject(self, @selector(reloadDataReversed)) boolValue]; +} + +- (void)setReloadDataReversed:(bool)reloadDataReversed +{ + objc_setAssociatedObject(self, @selector(reloadDataReversed), @(reloadDataReversed), OBJC_ASSOCIATION_ASSIGN); +} + +- (NSArray *)tableData +{ + return objc_getAssociatedObject(self, @selector(tableData)); +} + +- (void)setTableData:(NSArray *)tableData +{ + objc_setAssociatedObject(self, @selector(tableData), tableData, OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} + +#pragma mark - + +- (TGIndexPath *)indexPathForRowIndex:(NSUInteger)rowIndex +{ + if (rowIndex >= [self tableData].count) + return nil; + + return [[self tableData][rowIndex] indexPath]; +} + +- (NSUInteger)rowIndexForIndexPath:(TGIndexPath *)indexPath +{ + NSParameterAssert(indexPath); + + __block NSUInteger rowIndex = NSNotFound; + + [[self tableData] enumerateObjectsUsingBlock:^(TGTableDataEntry *tableEntry, NSUInteger index, BOOL *stop) + { + if (tableEntry.section == indexPath.section && tableEntry.row == indexPath.row) + { + rowIndex = index; + *stop = true; + } + }]; + + return rowIndex; +} + +- (NSUInteger)rowIndexForSection:(NSUInteger)section +{ + return [self rowIndexForIndexPath:[TGIndexPath indexPathForRow:NSNotFound inSection:section]]; +} + +- (NSUInteger)sectionForRowIndex:(NSUInteger)rowIndex +{ + return [[self tableData][rowIndex] section]; +} + +@end + + +@implementation WKInterfaceController (TGDataDrivenTable) + ++ (void)load +{ + TGSwizzleMethodImplementation(self.class, @selector(table:didSelectRowAtIndex:), @selector(tg_table:didSelectRowAtIndex:)); +} + +- (void)tg_table:(WKInterfaceTable *)table didSelectRowAtIndex:(NSInteger)rowIndex +{ + [self tg_table:table didSelectRowAtIndex:rowIndex]; + + TGTableDataEntry *tableEntry = [table tableData][rowIndex]; + + switch (tableEntry.type) + { + case TGTableDataEntryTypeHeader: + { + [self tableDidSelectHeader:table]; + } + break; + + case TGTableDataEntryTypeFooter: + { + [self tableDidSelectFooter:table]; + } + break; + + case TGTableDataEntryTypeSection: + { + [self table:table didSelectSection:tableEntry.section]; + } + break; + + case TGTableDataEntryTypeRow: + { + [self table:table didSelectRowAtIndexPath:tableEntry.indexPath]; + } + break; + + default: + break; + } +} + +- (void)tableDidSelectHeader:(WKInterfaceTable *)table +{ + +} + +- (void)tableDidSelectFooter:(WKInterfaceTable *)table +{ + +} + +- (void)table:(WKInterfaceTable *)table didSelectSection:(NSUInteger)section +{ + +} + +- (void)table:(WKInterfaceTable *)table didSelectRowAtIndexPath:(TGIndexPath *)indexPath +{ + +} + +@end diff --git a/Watch/Extension/WatchExtension-Prefix.pch b/Watch/Extension/WatchExtension-Prefix.pch new file mode 100644 index 0000000000..93663d0687 --- /dev/null +++ b/Watch/Extension/WatchExtension-Prefix.pch @@ -0,0 +1,9 @@ +#ifndef Telegraph_WatchExtension_Prefix_pch +#define Telegraph_WatchExtension_Prefix_pch + +#import +#import +#import +#import "TGWatchCommon.h" + +#endif diff --git a/Widget/Base.lproj/MainInterface.storyboard b/Widget/Base.lproj/MainInterface.storyboard new file mode 100644 index 0000000000..adfc32d812 --- /dev/null +++ b/Widget/Base.lproj/MainInterface.storyboard @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Widget/Info.plist b/Widget/Info.plist new file mode 100644 index 0000000000..7f78a3fce4 --- /dev/null +++ b/Widget/Info.plist @@ -0,0 +1,31 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + People + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + XPC! + CFBundleShortVersionString + 5.0.17 + CFBundleVersion + 624 + NSExtension + + NSExtensionMainStoryboard + MainInterface + NSExtensionPointIdentifier + com.apple.widget-extension + + + diff --git a/Widget/PeerNode.swift b/Widget/PeerNode.swift new file mode 100644 index 0000000000..9c61ac0703 --- /dev/null +++ b/Widget/PeerNode.swift @@ -0,0 +1,149 @@ +import Foundation +import Postbox +import TelegramCore +import UIKit + +private extension UIColor { + convenience init(rgb: UInt32) { + self.init(red: CGFloat((rgb >> 16) & 0xff) / 255.0, green: CGFloat((rgb >> 8) & 0xff) / 255.0, blue: CGFloat(rgb & 0xff) / 255.0, alpha: 1.0) + } +} + +private let UIScreenScale = UIScreen.main.scale +private func floorToScreenPixels(_ value: CGFloat) -> CGFloat { + return floor(value * UIScreenScale) / UIScreenScale +} + +private let avatarFont: UIFont = UIFont(name: ".SFCompactRounded-Semibold", size: 18.0) ?? UIFont.systemFont(ofSize: 18.0) + +private let gradientColors: [NSArray] = [ + [UIColor(rgb: 0xff516a).cgColor, UIColor(rgb: 0xff885e).cgColor], + [UIColor(rgb: 0xffa85c).cgColor, UIColor(rgb: 0xffcd6a).cgColor], + [UIColor(rgb: 0x665fff).cgColor, UIColor(rgb: 0x82b1ff).cgColor], + [UIColor(rgb: 0x54cb68).cgColor, UIColor(rgb: 0xa0de7e).cgColor], + [UIColor(rgb: 0x4acccd).cgColor, UIColor(rgb: 0x00fcfd).cgColor], + [UIColor(rgb: 0x2a9ef1).cgColor, UIColor(rgb: 0x72d5fd).cgColor], + [UIColor(rgb: 0xd669ed).cgColor, UIColor(rgb: 0xe0a2f3).cgColor], +] + +private func avatarRoundImage(size: CGSize, source: UIImage) -> UIImage? { + UIGraphicsBeginImageContextWithOptions(size, false, 0.0) + let context = UIGraphicsGetCurrentContext() + + context?.beginPath() + context?.addEllipse(in: CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height)) + context?.clip() + + source.draw(in: CGRect(origin: CGPoint(), size: size)) + + let image = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + return image +} + +private func avatarViewLettersImage(size: CGSize, peerId: PeerId, accountPeerId: PeerId, letters: [String]) -> UIImage? { + UIGraphicsBeginImageContextWithOptions(size, false, 0.0) + let context = UIGraphicsGetCurrentContext() + + context?.beginPath() + context?.addEllipse(in: CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height)) + context?.clip() + + let colorIndex = abs(Int(accountPeerId.id + peerId.id)) + + let colorsArray = gradientColors[colorIndex % gradientColors.count] + var locations: [CGFloat] = [1.0, 0.0] + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: colorsArray, locations: &locations)! + + context?.drawLinearGradient(gradient, start: CGPoint(), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) + + context?.setBlendMode(.normal) + + let string = letters.count == 0 ? "" : (letters[0] + (letters.count == 1 ? "" : letters[1])) + let attributedString = NSAttributedString(string: string, attributes: [NSAttributedStringKey.font: UIFont.systemFont(ofSize: 20.0), NSAttributedStringKey.foregroundColor: UIColor.white]) + + let line = CTLineCreateWithAttributedString(attributedString) + let lineBounds = CTLineGetBoundsWithOptions(line, .useGlyphPathBounds) + + let lineOffset = CGPoint(x: string == "B" ? 1.0 : 0.0, y: 0.0) + let lineOrigin = CGPoint(x: floorToScreenPixels(-lineBounds.origin.x + (size.width - lineBounds.size.width) / 2.0) + lineOffset.x, y: floorToScreenPixels(-lineBounds.origin.y + (size.height - lineBounds.size.height) / 2.0)) + + context?.translateBy(x: size.width / 2.0, y: size.height / 2.0) + context?.scaleBy(x: 1.0, y: -1.0) + context?.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) + + context?.translateBy(x: lineOrigin.x, y: lineOrigin.y) + if let context = context { + CTLineDraw(line, context) + } + context?.translateBy(x: -lineOrigin.x, y: -lineOrigin.y) + + let image = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + return image +} + +private let avatarSize = CGSize(width: 50.0, height: 50.0) + +private final class AvatarView: UIImageView { + init(account: Account, peer: Peer, size: CGSize) { + super.init(frame: CGRect()) + + if let resource = peer.smallProfileImage?.resource, let path = account.postbox.mediaBox.completedResourcePath(resource), let image = UIImage(contentsOfFile: path), let roundImage = avatarRoundImage(size: size, source: image) { + self.image = roundImage + } else { + self.image = avatarViewLettersImage(size: size, peerId: peer.id, accountPeerId: account.peerId, letters: peer.displayLetters) + } + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +final class PeerView: UIView { + let peer: Peer + private let avatarView: AvatarView + private let titleLabel: UILabel + + private let tapped: () -> Void + + init(account: Account, peer: Peer, tapped: @escaping () -> Void) { + self.peer = peer + self.tapped = tapped + self.avatarView = AvatarView(account: account, peer: peer, size: avatarSize) + + self.titleLabel = UILabel() + self.titleLabel.text = peer.compactDisplayTitle + self.titleLabel.textColor = .black + self.titleLabel.font = UIFont.systemFont(ofSize: 11.0) + self.titleLabel.lineBreakMode = .byTruncatingTail + + super.init(frame: CGRect()) + + self.addSubview(self.avatarView) + self.addSubview(self.titleLabel) + + self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func updateLayout(size: CGSize) { + self.avatarView.frame = CGRect(origin: CGPoint(x: floor((size.width - avatarSize.width) / 2.0), y: 0.0), size: avatarSize) + + var titleSize = self.titleLabel.sizeThatFits(size) + titleSize.width = min(size.width - 6.0, ceil(titleSize.width)) + titleSize.height = ceil(titleSize.height) + self.titleLabel.frame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: avatarSize.height + 5.0), size: titleSize) + } + + @objc func tapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.tapped() + } + } +} diff --git a/Widget/TodayViewController.swift b/Widget/TodayViewController.swift new file mode 100644 index 0000000000..4f05d81189 --- /dev/null +++ b/Widget/TodayViewController.swift @@ -0,0 +1,257 @@ +import UIKit +import TelegramCore +import SwiftSignalKit +import Postbox +import NotificationCenter + +private var accountCache: Account? + +private var installedSharedLogger = false + +private func setupSharedLogger(_ path: String) { + if !installedSharedLogger { + installedSharedLogger = true + Logger.setSharedLogger(Logger(basePath: path)) + } +} + +private let auxiliaryMethods = AccountAuxiliaryMethods(updatePeerChatInputState: { _, _ in + return nil +}, fetchResource: { _, _, _, _ in + return nil +}, fetchResourceMediaReferenceHash: { _ in + return .single(nil) +}) + +class TodayViewController: UIViewController, NCWidgetProviding { + private var initializedInterface = false + + private let disposable = MetaDisposable() + + deinit { + self.disposable.dispose() + } + + private var snapshotView: UIImageView? + + override func viewDidLoad() { + super.viewDidLoad() + + self.snapshotView?.removeFromSuperview() + let snapshotView = UIImageView() + if let path = self.getSnapshotPath(), let image = UIImage(contentsOfFile: path) { + snapshotView.image = image + } + self.snapshotView = snapshotView + self.view.addSubview(snapshotView) + + if self.initializedInterface { + return + } + self.initializedInterface = true + 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[.. + 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 in + return currentAccount(allocateIfNotExists: false, networkArguments: NetworkInitializationArguments(apiId: apiId, languagesCategory: languagesCategory, appVersion: appVersion), supplementary: true, manager: accountManager, rootPath: rootPath, beginWithTestingEnvironment: false, auxiliaryMethods: auxiliaryMethods) + |> mapToSignal { account -> Signal in + if let account = account { + switch account { + case .upgrading: + return .complete() + case let .authorized(account): + accountCache = account + return .single(account) + case .unauthorized: + return .complete() + } + } else { + return .complete() + } + } + } + |> take(1) + } + + let applicationInterface = account |> afterNext { account in + setupAccount(account) + } |> deliverOnMainQueue |> afterNext { [weak self] account in + account.resetStateManagement() + + let _ = (recentPeers(account: account) + |> deliverOnMainQueue).start(next: { peers in + if let strongSelf = self { + switch peers { + case let .peers(peers): + strongSelf.setPeers(account: account, peers: peers.filter { !$0.isDeleted }) + case .disabled: + break + } + } + }) + //account.network.shouldKeepConnection.set(shouldBeMaster.get() |> map({ $0 })) + } + + self.disposable.set(applicationInterface.start()) + } + + func widgetPerformUpdate(completionHandler: (@escaping (NCUpdateResult) -> Void)) { + completionHandler(.newData) + } + + @available(iOSApplicationExtension 10.0, *) + func widgetActiveDisplayModeDidChange(_ activeDisplayMode: NCWidgetDisplayMode, withMaximumSize maxSize: CGSize) { + + } + + private var peers: [Peer]? + + private func setPeers(account: Account, peers: [Peer]) { + self.peers = peers + self.peerViews.forEach { + $0.removeFromSuperview() + } + self.peerViews = [] + for peer in peers { + let peerView = PeerView(account: account, peer: peer, tapped: { [weak self] in + if let strongSelf = self { + if let url = URL(string: "tg://localpeer?id=\(peer.id.toInt64())") { + strongSelf.extensionContext?.open(url, completionHandler: nil) + } + } + }) + self.view.addSubview(peerView) + self.peerViews.append(peerView) + } + + self.validSnapshotSize = nil + if let size = self.validLayout { + self.updateLayout(size: size) + } + } + + private var validLayout: CGSize? + private var validSnapshotSize: CGSize? + + private var peerViews: [PeerView] = [] + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + self.updateLayout(size: self.view.bounds.size) + } + + private func updateLayout(size: CGSize) { + self.validLayout = size + + if let image = self.snapshotView?.image { + let scale = UIScreen.main.scale + self.snapshotView?.frame = CGRect(origin: CGPoint(), size: CGSize(width: image.size.width / scale, height: image.size.height / scale)) + } + + let peerSize = CGSize(width: 70.0, height: 100.0) + + var peerFrames: [CGRect] = [] + + var offset: CGFloat = 0.0 + for _ in self.peerViews { + let peerFrame = CGRect(origin: CGPoint(x: offset, y: 10.0), size: peerSize) + offset += peerFrame.size.width + if peerFrame.maxX > size.width { + break + } + peerFrames.append(peerFrame) + } + + var totalSize: CGFloat = 0.0 + for i in 0 ..< peerFrames.count { + totalSize += peerFrames[i].width + } + + let spacing: CGFloat = floor((size.width - totalSize) / CGFloat(peerFrames.count)) + offset = floor(spacing / 2.0) + for i in 0 ..< peerFrames.count { + let peerView = self.peerViews[i] + peerView.frame = CGRect(origin: CGPoint(x: offset, y: 20.0), size: peerFrames[i].size) + peerView.updateLayout(size: peerFrames[i].size) + offset += peerFrames[i].width + spacing + } + + if self.peers != nil { + self.snapshotView?.removeFromSuperview() + if self.validSnapshotSize != size { + self.validSnapshotSize = size + self.updateSnapshot() + } + } + } + + private func updateSnapshot() { + if let path = self.getSnapshotPath() { + DispatchQueue.main.async { + UIGraphicsBeginImageContextWithOptions(self.view.bounds.size, false, 0.0) + /*let context = UIGraphicsGetCurrentContext() + context?.setFillColor(UIColor.blue.cgColor) + context?.fill(CGRect(origin: CGPoint(), size: self.view.bounds.size))*/ + self.view.drawHierarchy(in: self.view.bounds, afterScreenUpdates: false) + let image = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + if let image = image, let data = UIImagePNGRepresentation(image) { + let _ = try? FileManager.default.removeItem(atPath: path) + do { + try data.write(to: URL(fileURLWithPath: path)) + } catch let e { + print("\(e)") + } + } + } + } + } + + private func getSnapshotPath() -> String? { + let appBundleIdentifier = Bundle.main.bundleIdentifier! + guard let lastDotRange = appBundleIdentifier.range(of: ".", options: [.backwards]) else { + return nil + } + + let appGroupName = "group.\(appBundleIdentifier[.. + + + + com.apple.security.application-groups + + group.org.telegram.TelegramHD + + + diff --git a/Widget/Widget-AppStoreLLC.entitlements b/Widget/Widget-AppStoreLLC.entitlements new file mode 100644 index 0000000000..c9a9054223 --- /dev/null +++ b/Widget/Widget-AppStoreLLC.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.ph.telegra.Telegraph + + + diff --git a/Widget/Widget-Bridging-Header.h b/Widget/Widget-Bridging-Header.h new file mode 100644 index 0000000000..fd2b66c5aa --- /dev/null +++ b/Widget/Widget-Bridging-Header.h @@ -0,0 +1,6 @@ +#ifndef Widget_Bridging_Header_h +#define Widget_Bridging_Header_h + +#import "../Telegram-iOS/BuildConfig.h" + +#endif diff --git a/Widget/Widget-HockeyApp.entitlements b/Widget/Widget-HockeyApp.entitlements new file mode 100644 index 0000000000..65f2a19d32 --- /dev/null +++ b/Widget/Widget-HockeyApp.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.org.telegram.Telegram-iOS + + + diff --git a/Widget/ar.lproj/InfoPlist.strings b/Widget/ar.lproj/InfoPlist.strings new file mode 100644 index 0000000000..07394dd6c9 --- /dev/null +++ b/Widget/ar.lproj/InfoPlist.strings @@ -0,0 +1 @@ +"CFBundleDisplayName" = "الأشخاص"; diff --git a/Widget/de.lproj/InfoPlist.strings b/Widget/de.lproj/InfoPlist.strings new file mode 100644 index 0000000000..1ad24433c2 --- /dev/null +++ b/Widget/de.lproj/InfoPlist.strings @@ -0,0 +1 @@ +"CFBundleDisplayName" = "Leute"; diff --git a/Widget/en.lproj/InfoPlist.strings b/Widget/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..b1ddd1ac39 --- /dev/null +++ b/Widget/en.lproj/InfoPlist.strings @@ -0,0 +1 @@ +"CFBundleDisplayName" = "People"; diff --git a/Widget/en.lproj/Localizable.strings b/Widget/en.lproj/Localizable.strings new file mode 100644 index 0000000000..c90696d0fb --- /dev/null +++ b/Widget/en.lproj/Localizable.strings @@ -0,0 +1,2 @@ +"Widget.NoUsers" = "No users here yet..."; +"Widget.AuthRequired" = "Open Telegram and log in."; diff --git a/Widget/es.lproj/InfoPlist.strings b/Widget/es.lproj/InfoPlist.strings new file mode 100644 index 0000000000..3d5094963a --- /dev/null +++ b/Widget/es.lproj/InfoPlist.strings @@ -0,0 +1 @@ +"CFBundleDisplayName" = "Personas"; diff --git a/Widget/it.lproj/InfoPlist.strings b/Widget/it.lproj/InfoPlist.strings new file mode 100644 index 0000000000..f118d25a4d --- /dev/null +++ b/Widget/it.lproj/InfoPlist.strings @@ -0,0 +1 @@ +"CFBundleDisplayName" = "Persone"; diff --git a/Widget/ko.lproj/InfoPlist.strings b/Widget/ko.lproj/InfoPlist.strings new file mode 100644 index 0000000000..e1bc831c53 --- /dev/null +++ b/Widget/ko.lproj/InfoPlist.strings @@ -0,0 +1 @@ +"CFBundleDisplayName" = "사람"; diff --git a/Widget/nl.lproj/InfoPlist.strings b/Widget/nl.lproj/InfoPlist.strings new file mode 100644 index 0000000000..a23cbfc4a2 --- /dev/null +++ b/Widget/nl.lproj/InfoPlist.strings @@ -0,0 +1 @@ +"CFBundleDisplayName" = "Mensen"; diff --git a/Widget/pt.lproj/InfoPlist.strings b/Widget/pt.lproj/InfoPlist.strings new file mode 100644 index 0000000000..a6c032d0ed --- /dev/null +++ b/Widget/pt.lproj/InfoPlist.strings @@ -0,0 +1 @@ +"CFBundleDisplayName" = "Pessoas"; diff --git a/Widget/ru.lproj/InfoPlist.strings b/Widget/ru.lproj/InfoPlist.strings new file mode 100644 index 0000000000..689e714f47 --- /dev/null +++ b/Widget/ru.lproj/InfoPlist.strings @@ -0,0 +1 @@ +"CFBundleDisplayName" = "Люди"; diff --git a/fastlane/Fastfile b/fastlane/Fastfile new file mode 100644 index 0000000000..180868f26d --- /dev/null +++ b/fastlane/Fastfile @@ -0,0 +1 @@ +import "../../Telegram-iOS-Shared/fastlane/Fastfile" diff --git a/submodules/AsyncDisplayKit b/submodules/AsyncDisplayKit new file mode 160000 index 0000000000..e6b29d1196 --- /dev/null +++ b/submodules/AsyncDisplayKit @@ -0,0 +1 @@ +Subproject commit e6b29d1196cbd78592a238e51932d281091b4d14 diff --git a/submodules/Display b/submodules/Display new file mode 160000 index 0000000000..e7712fcbe8 --- /dev/null +++ b/submodules/Display @@ -0,0 +1 @@ +Subproject commit e7712fcbe81b1df34c553da02571bd5907767f1a diff --git a/submodules/HockeySDK-iOS b/submodules/HockeySDK-iOS new file mode 160000 index 0000000000..cc23a1b293 --- /dev/null +++ b/submodules/HockeySDK-iOS @@ -0,0 +1 @@ +Subproject commit cc23a1b2931faf23d5ebff65d64e1842a5d6bedf diff --git a/submodules/LegacyComponents b/submodules/LegacyComponents new file mode 160000 index 0000000000..67590c671e --- /dev/null +++ b/submodules/LegacyComponents @@ -0,0 +1 @@ +Subproject commit 67590c671e544f6310f618218d2f58b9052a2740 diff --git a/submodules/MtProtoKit b/submodules/MtProtoKit new file mode 160000 index 0000000000..49b13a53c8 --- /dev/null +++ b/submodules/MtProtoKit @@ -0,0 +1 @@ +Subproject commit 49b13a53c8784adaee80813fabf03703d6bd16a5 diff --git a/submodules/Postbox b/submodules/Postbox new file mode 160000 index 0000000000..44be243c0e --- /dev/null +++ b/submodules/Postbox @@ -0,0 +1 @@ +Subproject commit 44be243c0e5ef3999b1116b8bbc3f77405c873d4 diff --git a/submodules/SSignalKit b/submodules/SSignalKit new file mode 160000 index 0000000000..2c2da8f611 --- /dev/null +++ b/submodules/SSignalKit @@ -0,0 +1 @@ +Subproject commit 2c2da8f6113fef7f69eb929980f332c35bf5d877 diff --git a/submodules/TelegramCore b/submodules/TelegramCore new file mode 160000 index 0000000000..4f6575d65d --- /dev/null +++ b/submodules/TelegramCore @@ -0,0 +1 @@ +Subproject commit 4f6575d65d92df3dd733d033cf068f51f461cbd3 diff --git a/submodules/TelegramUI b/submodules/TelegramUI new file mode 160000 index 0000000000..6fbe46be7a --- /dev/null +++ b/submodules/TelegramUI @@ -0,0 +1 @@ +Subproject commit 6fbe46be7afc50e463e8d5982ce11a0eaa54c51a diff --git a/submodules/libtgvoip b/submodules/libtgvoip new file mode 160000 index 0000000000..5948b290d0 --- /dev/null +++ b/submodules/libtgvoip @@ -0,0 +1 @@ +Subproject commit 5948b290d037d2086379c9347ee9dbdf546069f3 diff --git a/submodules/lottie-ios b/submodules/lottie-ios new file mode 160000 index 0000000000..5b0545a807 --- /dev/null +++ b/submodules/lottie-ios @@ -0,0 +1 @@ +Subproject commit 5b0545a8073984070e4dbf942a3dbd7b7ce61fd1 diff --git a/tools/GenerateLocalization.swift b/tools/GenerateLocalization.swift new file mode 100644 index 0000000000..446ed5bedb --- /dev/null +++ b/tools/GenerateLocalization.swift @@ -0,0 +1,524 @@ +import Foundation + +struct Entry { + let key: String + let value: String +} + +enum ArgumentType { + case any + case integer(decimalNumbers: Int) + case float + + init(_ control: String, decimalNumbers: Int) { + switch control { + case "d": + self = .integer(decimalNumbers: decimalNumbers) + case "f": + self = .float + case "@": + self = .any + default: + preconditionFailure() + } + } +} + +struct Argument { + let index: Int + let type: ArgumentType +} + +func escapedIdentifier(_ value: String) -> String { + return value.replacingOccurrences(of: ".", with: "_").replacingOccurrences(of: "#", with: "_").replacingOccurrences(of: " ", with: "_").replacingOccurrences(of: "'", with: "_") +} + +func functionArguments(_ arguments: [Argument]) -> String { + var result = "" + var existingIndices = Set() + for argument in arguments.sorted(by: { $0.index < $1.index }) { + if existingIndices.contains(argument.index) { + continue + } + existingIndices.insert(argument.index) + if !result.isEmpty { + result += ", " + } + result += "_ _\(argument.index): " + switch argument.type { + case .any: + result += "String" + case .float: + result += "Float" + case .integer: + result += "Int" + } + } + return result +} + +func formatArguments(_ arguments: [Argument]) -> String { + var result = "" + for argument in arguments.sorted(by: { $0.index < $1.index }) { + if !result.isEmpty { + result += ", " + } + switch argument.type { + case .any: + result += "_\(argument.index)" + case .float: + result += "\"\\(_\(argument.index))\"" + case let .integer(decimalNumbers): + if decimalNumbers == 0 { + result += "\"\\(_\(argument.index))\"" + } else { + result += "String(format: \"%.\(decimalNumbers)d\", _\(argument.index))" + } + } + } + return result +} + +let argumentRegex = try! NSRegularExpression(pattern: "%((\\.(\\d+))?)(((\\d+)\\$)?)([@df])", options: []) + +func parseArguments(_ value: String) -> [Argument] { + let string = value as NSString + + let matches = argumentRegex.matches(in: string as String, options: [], range: NSRange(location: 0, length: string.length)) + + var arguments: [Argument] = [] + var index = 0 + if value.range(of: ".2d") != nil { + print(value) + } + for match in matches { + var currentIndex = index + var decimalNumbers = 0 + if match.range(at: 3).location != NSNotFound { + decimalNumbers = Int(string.substring(with: match.range(at: 3)))! + } + if match.range(at: 6).location != NSNotFound { + currentIndex = Int(string.substring(with: match.range(at: 6)))! + } + arguments.append(Argument(index: currentIndex, type: ArgumentType(string.substring(with: match.range(at: 7)), decimalNumbers: decimalNumbers))) + index += 1 + } + + return arguments +} + +func addCode(_ lines: [String]) -> String { + var result: String = "" + for line in lines { + result += line + result += "\n" + } + return result +} + +enum PluralizationForm: Int32 { + case zero = 0 + case one = 1 + case two = 2 + case few = 3 + case many = 4 + case other = 5 + + static var formCount = Int(PluralizationForm.other.rawValue + 1) + static var all: [PluralizationForm] = [.zero, .one, .two, .few, .many, .other] + + var name: String { + switch self { + case .zero: + return "zero" + case .one: + return "one" + case .two: + return "two" + case .few: + return "few" + case .many: + return "many" + case .other: + return "other" + } + } +} + +let pluralizationFormRegex = try! NSRegularExpression(pattern: "(.*?)_(0|zero|1|one|2|two|3_10|few|many|any|other)$", options: []) + +func pluralizationForm(_ key: String) -> (String, PluralizationForm)? { + let string = key as NSString + let matches = pluralizationFormRegex.matches(in: string as String, options: [], range: NSRange(location: 0, length: string.length)) + + for match in matches { + if match.range(at: 1).location != NSNotFound && match.range(at: 2).location != NSNotFound { + let base = string.substring(with: match.range(at: 1)) + let value = string.substring(with: match.range(at: 2)) + let form: PluralizationForm + switch value { + case "0", "zero": + form = .zero + case "1", "one": + form = .one + case "2", "two": + form = .two + case "3_10", "few": + form = .few + case "many": + form = .many + case "any", "other": + form = .other + default: + return nil + } + return (base, form) + } + } + + return nil +} + +final class WriteBuffer { + var data = Data() + + init() { + } + + func append(_ value: Int32) { + let ptr = self.data.count + self.data.count += 4 + self.data.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer) -> Void in + var value = value + memcpy(bytes.advanced(by: ptr), &value, 4) + } + } + + func append(_ string: String) { + let bytes = string.data(using: .utf8)! + self.append(Int32(bytes.count)) + self.data.append(bytes) + } +} + +if CommandLine.arguments.count != 4 { + print("Usage: swift GenerateLocalization.swift Localizable.strings Strings.swift Strings.mapping") +} else { + if let rawDict = NSDictionary(contentsOfFile: CommandLine.arguments[1]) { + var result = "import Foundation\n\n" + + result += +""" +private let fallbackDict: [String: String] = { + guard let mainPath = Bundle.main.path(forResource: \"en\", ofType: \"lproj\"), let bundle = Bundle(path: mainPath) else { + return [:] + } + guard let path = bundle.path(forResource: \"Localizable\", ofType: \"strings\") else { + return [:] + } + guard let dict = NSDictionary(contentsOf: URL(fileURLWithPath: path)) as? [String: String] else { + return [:] + } + return dict +}() + +private extension PluralizationForm { + var canonicalSuffix: String { + switch self { + case .zero: + return \"_0\" + case .one: + return \"_1\" + case .two: + return \"_2\" + case .few: + return \"_3_10\" + case .many: + return \"_many\" + case .other: + return \"_any\" + } + } +} + +public final class PresentationStringsComponent { + public let languageCode: String + public let localizedName: String + public let pluralizationRulesCode: String? + public let dict: [String: String] + + public init(languageCode: String, localizedName: String, pluralizationRulesCode: String?, dict: [String: String]) { + self.languageCode = languageCode + self.localizedName = localizedName + self.pluralizationRulesCode = pluralizationRulesCode + self.dict = dict + } +} + +private func getValue(_ primaryComponent: PresentationStringsComponent, _ secondaryComponent: PresentationStringsComponent?, _ key: String) -> String { + if let value = primaryComponent.dict[key] { + return value + } else if let secondaryComponent = secondaryComponent, let value = secondaryComponent.dict[key] { + return value + } else if let value = fallbackDict[key] { + return value + } else { + return key + } +} + +private func getValueWithForm(_ primaryComponent: PresentationStringsComponent, _ secondaryComponent: PresentationStringsComponent?, _ key: String, _ form: PluralizationForm) -> String { + let builtKey = key + form.canonicalSuffix + if let value = primaryComponent.dict[builtKey] { + return value + } else if let secondaryComponent = secondaryComponent, let value = secondaryComponent.dict[builtKey] { + return value + } else if let value = fallbackDict[builtKey] { + return value + } + return key +} + +private let argumentRegex = try! NSRegularExpression(pattern: \"%(((\\\\d+)\\\\$)?)([@df])\", options: []) +private func extractArgumentRanges(_ value: String) -> [(Int, NSRange)] { + var result: [(Int, NSRange)] = [] + let string = value as NSString + let matches = argumentRegex.matches(in: string as String, options: [], range: NSRange(location: 0, length: string.length)) + var index = 0 + for match in matches { + var currentIndex = index + if match.range(at: 3).location != NSNotFound { + currentIndex = Int(string.substring(with: match.range(at: 3)))! - 1 + } + result.append((currentIndex, match.range(at: 0))) + index += 1 + } + result.sort(by: { $0.1.location < $1.1.location }) + return result +} + +func formatWithArgumentRanges(_ value: String, _ ranges: [(Int, NSRange)], _ arguments: [String]) -> (String, [(Int, NSRange)]) { + let string = value as NSString + + var resultingRanges: [(Int, NSRange)] = [] + + var currentLocation = 0 + + let result = NSMutableString() + for (index, range) in ranges { + if currentLocation < range.location { + result.append(string.substring(with: NSRange(location: currentLocation, length: range.location - currentLocation))) + } + resultingRanges.append((index, NSRange(location: result.length, length: (arguments[index] as NSString).length))) + result.append(arguments[index]) + currentLocation = range.location + range.length + } + if currentLocation != string.length { + result.append(string.substring(with: NSRange(location: currentLocation, length: string.length - currentLocation))) + } + return (result as String, resultingRanges) +} + +private final class DataReader { + private let data: Data + private var ptr: Int = 0 + + init(_ data: Data) { + self.data = data + } + + func readInt32() -> Int32 { + assert(self.ptr + 4 <= self.data.count) + let result = self.data.withUnsafeBytes { (bytes: UnsafePointer) -> Int32 in + var value: Int32 = 0 + memcpy(&value, bytes.advanced(by: self.ptr), 4) + return value + } + self.ptr += 4 + return result + } + + func readString() -> String { + let length = Int(self.readInt32()) + assert(self.ptr + length <= self.data.count) + let value = String(data: self.data.subdata(in: self.ptr ..< self.ptr + length), encoding: .utf8)! + self.ptr += length + return value + } +} + +private func loadMapping() -> ([Int], [String], [Int], [Int], [String]) { + guard let filePath = frameworkBundle.path(forResource: "PresentationStrings", ofType: "mapping") else { + fatalError() + } + guard let data = try? Data(contentsOf: URL(fileURLWithPath: filePath)) else { + fatalError() + } + + let reader = DataReader(data) + + let idCount = Int(reader.readInt32()) + var sIdList: [Int] = [] + var sKeyList: [String] = [] + var sArgIdList: [Int] = [] + for _ in 0 ..< idCount { + let id = Int(reader.readInt32()) + sIdList.append(id) + sKeyList.append(reader.readString()) + if reader.readInt32() != 0 { + sArgIdList.append(id) + } + } + + let pCount = Int(reader.readInt32()) + var pIdList: [Int] = [] + var pKeyList: [String] = [] + for _ in 0 ..< Int(pCount) { + pIdList.append(Int(reader.readInt32())) + pKeyList.append(reader.readString()) + } + + return (sIdList, sKeyList, sArgIdList, pIdList, pKeyList) +} + +private let keyMapping: ([Int], [String], [Int], [Int], [String]) = loadMapping() + +public final class PresentationStrings { + public let lc: UInt32 + + public let primaryComponent: PresentationStringsComponent + public let secondaryComponent: PresentationStringsComponent? + public let baseLanguageCode: String + + private let _s: [Int: String] + private let _r: [Int: [(Int, NSRange)]] + private let _ps: [Int: String] + +""" + let rawKeyPairs = rawDict.map({ ($0 as! String, $1 as! String) }) + let idKeyPairs = zip(rawKeyPairs, 0 ..< rawKeyPairs.count).map({ pair, index in (pair.0, pair.1, index) }) + + var pluralizationKeys = Set() + var pluralizationBaseKeys = Set() + for (key, _, _) in idKeyPairs { + if let (base, _) = pluralizationForm(key) { + pluralizationKeys.insert(key) + pluralizationBaseKeys.insert(base) + } + } + let pluralizationKeyPairs = zip(pluralizationBaseKeys, 0 ..< pluralizationBaseKeys.count).map({ ($0, $1) }) + + for (key, value, id) in idKeyPairs { + if pluralizationKeys.contains(key) { + continue + } + + let arguments = parseArguments(value) + if !arguments.isEmpty { + result += " public func \(escapedIdentifier(key))(\(functionArguments(arguments))) -> (String, [(Int, NSRange)]) {\n" + result += " return formatWithArgumentRanges(self._s[\(id)]!, self._r[\(id)]!, [\(formatArguments(arguments))])\n" + result += " }\n" + } else { + result += " public var \(escapedIdentifier(key)): String { return self._s[\(id)]! }\n" + } + } + + for (key, id) in pluralizationKeyPairs { + result += +""" + public func \(escapedIdentifier(key))(_ value: Int32) -> String { + let form = presentationStringsPluralizationForm(self.lc, value) + return String(format: self._ps[\(id) * \(PluralizationForm.formCount) + Int(form.rawValue)]!, \"\\(value)\") + } + +""" + } + + result += +""" + + init(primaryComponent: PresentationStringsComponent, secondaryComponent: PresentationStringsComponent?) { + self.primaryComponent = primaryComponent + self.secondaryComponent = secondaryComponent + + self.baseLanguageCode = secondaryComponent?.languageCode ?? primaryComponent.languageCode + + let languageCode = primaryComponent.pluralizationRulesCode ?? primaryComponent.languageCode + var rawCode = languageCode as NSString + var range = rawCode.range(of: \"_\") + if range.location != NSNotFound { + rawCode = rawCode.substring(to: range.location) as NSString + } + range = rawCode.range(of: \"-\") + if range.location != NSNotFound { + rawCode = rawCode.substring(to: range.location) as NSString + } + rawCode = rawCode.lowercased as NSString + var lc: UInt32 = 0 + for i in 0 ..< rawCode.length { + lc = (lc << 8) + UInt32(rawCode.character(at: i)) + } + self.lc = lc + + var _s: [Int: String] = [:] + var _r: [Int: [(Int, NSRange)]] = [:] + + let loadedKeyMapping = keyMapping + + let sIdList: [Int] = loadedKeyMapping.0 + let sKeyList: [String] = loadedKeyMapping.1 + let sArgIdList: [Int] = loadedKeyMapping.2 + +""" + let mappingResult = WriteBuffer() + let mappingKeyPairs = idKeyPairs.filter({ !pluralizationKeys.contains($0.0) }) + mappingResult.append(Int32(mappingKeyPairs.count)) + for (key, value, id) in mappingKeyPairs { + mappingResult.append(Int32(id)) + mappingResult.append(key) + let arguments = parseArguments(value) + mappingResult.append(arguments.isEmpty ? 0 : 1) + } + + result += +""" + for i in 0 ..< sIdList.count { + _s[sIdList[i]] = getValue(primaryComponent, secondaryComponent, sKeyList[i]) + } + for i in 0 ..< sArgIdList.count { + _r[sArgIdList[i]] = extractArgumentRanges(_s[sArgIdList[i]]!) + } + self._s = _s + self._r = _r + + var _ps: [Int: String] = [:] + let pIdList: [Int] = loadedKeyMapping.3 + let pKeyList: [String] = loadedKeyMapping.4 + +""" + mappingResult.append(Int32(pluralizationKeyPairs.count)) + for (key, id) in pluralizationKeyPairs { + mappingResult.append(Int32(id)) + mappingResult.append(key) + } + result += +""" + for i in 0 ..< pIdList.count { + for form in 0 ..< \(PluralizationForm.formCount) { + _ps[pIdList[i] * \(PluralizationForm.formCount) + form] = getValueWithForm(primaryComponent, secondaryComponent, pKeyList[i], PluralizationForm(rawValue: Int32(form))!) + } + } + self._ps = _ps + +""" + result += " }\n" + result += "}\n\n" + let _ = try? FileManager.default.removeItem(atPath: CommandLine.arguments[2]) + let _ = try? FileManager.default.removeItem(atPath: CommandLine.arguments[3]) + let _ = try? result.write(toFile: CommandLine.arguments[2], atomically: true, encoding: .utf8) + let _ = try? mappingResult.data.write(to: URL(fileURLWithPath: CommandLine.arguments[3])) + } else { + print("Couldn't read file") + exit(1) + } +}